Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -29,3 +29,6 @@ fastlane/report.xml
.expo
.vercel
.env*.local

# Claude
.claude
1 change: 1 addition & 0 deletions Gemfile.lock
Original file line number Diff line number Diff line change
Expand Up @@ -296,6 +296,7 @@ PLATFORMS
arm64-darwin-22
arm64-darwin-23
arm64-darwin-24
arm64-darwin-25

DEPENDENCIES
cocoapods (= 1.14.3)
Expand Down
59 changes: 57 additions & 2 deletions dapps/poc-pos-app/app/scan.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import QRCode from "@/components/qr-code";
import { ThemedText } from "@/components/themed-text";
import { WalletConnectLoading } from "@/components/walletconnect-loading";
import { Spacing } from "@/constants/spacing";
import { useNfcPayment } from "@/hooks/use-nfc-payment";
import { useTheme } from "@/hooks/use-theme-color";
import { useLogsStore } from "@/store/useLogsStore";
import { useSettingsStore } from "@/store/useSettingsStore";
Expand Down Expand Up @@ -33,12 +34,28 @@ export default function ScanScreen() {
const [qrUri, setQrUri] = useState("");
const [paymentId, setPaymentId] = useState<string | null>(null);

const { deviceId, merchantId } = useSettingsStore((state) => state);
const { deviceId, merchantId, nfcEnabled } = useSettingsStore((state) => state);
const addLog = useLogsStore((state) => state.addLog);
const Theme = useTheme();

const { amount } = params;

// NFC integration - Android HCE or iOS VAS
const { isNfcActive, nfcMode } = useNfcPayment({
paymentUrl: qrUri,
enabled: !!qrUri && nfcEnabled,
onNfcReady: () => {
const modeLabel = nfcMode === "hce" ? "HCE" : nfcMode === "vas" ? "VAS" : "NFC";
addLog("info", `NFC ${modeLabel} activated`, "scan", "useNfcPayment", {
paymentUrl: qrUri,
nfcMode,
});
},
onNfcError: (error) => {
addLog("error", error.message, "scan", "useNfcPayment", { error, nfcMode });
},
});

const onSuccess = useCallback(
(data: PaymentStatusResponse) => {
const {
Expand Down Expand Up @@ -167,6 +184,18 @@ export default function ScanScreen() {

const isProcessing = paymentStatusData?.status === "processing";

// Determine instruction text based on NFC availability
// Show "Tap or scan" when NFC is active (HCE on Android or VAS on iOS)
const showNfcOption = nfcMode !== "none" && isNfcActive;
const instructionText = showNfcOption ? "Tap or scan to pay" : "Scan to pay";

// Get NFC mode label for display
const getNfcModeLabel = () => {
if (nfcMode === "hce") return "NFC Ready (HCE)";
if (nfcMode === "vas") return "NFC Ready (VAS)";
return "NFC Ready";
};

return (
<View style={styles.container}>
{isProcessing ? (
Expand All @@ -186,8 +215,19 @@ export default function ScanScreen() {
<ThemedText
style={[styles.amountText, { color: Theme["text-tertiary"] }]}
>
Scan to pay
{instructionText}
</ThemedText>
{/* NFC Status Indicator (when HCE or VAS is active) */}
{showNfcOption && (
<View style={styles.nfcIndicator}>
<View style={[styles.nfcDot, { backgroundColor: "#4CAF50" }]} />
<ThemedText
style={[styles.nfcText, { color: Theme["text-tertiary"] }]}
>
{getNfcModeLabel()}
</ThemedText>
</View>
)}
<ThemedText
style={[
styles.amountValue,
Expand Down Expand Up @@ -259,4 +299,19 @@ const styles = StyleSheet.create({
position: "absolute",
alignSelf: "center",
},
nfcIndicator: {
flexDirection: "row",
alignItems: "center",
gap: Spacing["spacing-2"],
marginTop: Spacing["spacing-2"],
},
nfcDot: {
width: 8,
height: 8,
borderRadius: 4,
},
nfcText: {
fontSize: 12,
fontWeight: "500",
},
});
64 changes: 64 additions & 0 deletions dapps/poc-pos-app/app/settings.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import { BorderRadius, Spacing } from "@/constants/spacing";
import { VariantList, VariantName } from "@/constants/variants";
import { useBiometricAuth } from "@/hooks/use-biometric-auth";
import { useMerchantFlow } from "@/hooks/use-merchant-flow";
import { useNfcCapabilities } from "@/hooks/use-nfc-capabilities";
import { useTheme } from "@/hooks/use-theme-color";
import { useLogsStore } from "@/store/useLogsStore";
import { useSettingsStore } from "@/store/useSettingsStore";
Expand Down Expand Up @@ -38,9 +39,14 @@ export default function SettingsScreen() {
const getVariantPrinterLogo = useSettingsStore(
(state) => state.getVariantPrinterLogo,
);
const nfcEnabled = useSettingsStore((state) => state.nfcEnabled);
const setNfcEnabled = useSettingsStore((state) => state.setNfcEnabled);
const addLog = useLogsStore((state) => state.addLog);
const theme = useTheme();

// NFC capabilities for toggle
const nfcCapabilities = useNfcCapabilities();

// Custom hooks for biometrics and merchant flow
const {
biometricStatus,
Expand Down Expand Up @@ -99,6 +105,42 @@ export default function SettingsScreen() {
setVariant(value);
};

const handleNfcToggle = (value: boolean) => {
// Only check NFC support when trying to enable
if (value) {
// Check platform-specific NFC support
if (Platform.OS === "android" && !nfcCapabilities.isHceSupported) {
showErrorToast("NFC not supported on this device");
return;
}
if (Platform.OS === "ios" && !nfcCapabilities.isVasAvailable) {
showErrorToast(nfcCapabilities.vasReason || "NFC not available");
return;
}
}
setNfcEnabled(value);
};

// Show NFC toggle on both platforms when mode is available
const isNfcModeAvailable = nfcCapabilities.nfcMode !== "none";
const showNfcToggle = Platform.OS === "android" || Platform.OS === "ios";

// Get NFC support description
const getNfcDescription = () => {
if (nfcCapabilities.isLoading) return "Checking NFC support...";
if (Platform.OS === "android") {
return nfcCapabilities.isHceSupported
? "Tap to pay via NFC (HCE)"
: "HCE not supported";
}
if (Platform.OS === "ios") {
return nfcCapabilities.isVasAvailable
? "Tap to pay via NFC (VAS)"
: "VAS not available";
}
return "NFC not supported";
};

const handleTestPrinterPress = async () => {
try {
const isBluetoothPermissionGranted = await requestBluetoothPermission();
Expand Down Expand Up @@ -196,6 +238,28 @@ export default function SettingsScreen() {
/>
</Card>

{/* NFC Support toggle - Android HCE / iOS VAS */}
{showNfcToggle && (
<Card style={styles.card}>
<View style={styles.biometricRow}>
<View style={styles.biometricLabel}>
<ThemedText fontSize={16} lineHeight={18}>
NFC Support
</ThemedText>
<ThemedText fontSize={12} lineHeight={14} color="text-tertiary">
{getNfcDescription()}
</ThemedText>
</View>
<Switch
style={styles.switch}
value={nfcEnabled && isNfcModeAvailable}
onValueChange={handleNfcToggle}
disabled={nfcCapabilities.isLoading}
/>
</View>
</Card>
)}

<Card style={styles.merchantCard}>
<ThemedText fontSize={16} lineHeight={18}>
Merchant ID
Expand Down
190 changes: 190 additions & 0 deletions dapps/poc-pos-app/hooks/use-nfc-capabilities.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,190 @@
import { useEffect, useState } from "react";
import { NativeModules, Platform } from "react-native";

const { HceModule, VasModule } = NativeModules;

export type NfcMode = "hce" | "vas" | "none";

export interface NfcCapabilities {
/** Device has NFC hardware */
isNfcSupported: boolean;
/** NFC is turned on in settings */
isNfcEnabled: boolean;
/** Device supports Host Card Emulation (Android only) */
isHceSupported: boolean;
/** Device supports Apple VAS via Tap to Pay (iOS only) */
isVasSupported: boolean;
/** VAS is available and ready to use (entitlement configured) */
isVasAvailable: boolean;
/** The active NFC mode for this device */
nfcMode: NfcMode;
/** Still checking capabilities */
isLoading: boolean;
/** Error message if check failed */
error: string | null;
/** Reason for VAS unavailability (iOS only) */
vasReason: string | null;
}

/**
* Hook to check NFC capabilities of the device
*
* Returns platform-specific NFC capabilities:
* - Android: HCE (Host Card Emulation) for NDEF tag emulation
* - iOS: VAS (Value Added Services) via Tap to Pay on iPhone
*
* The `nfcMode` indicates which mode is available:
* - "hce": Android HCE mode
* - "vas": iOS VAS mode (Tap to Pay)
* - "none": No NFC transmission available
*/
export function useNfcCapabilities(): NfcCapabilities {
const [capabilities, setCapabilities] = useState<NfcCapabilities>({
isNfcSupported: false,
isNfcEnabled: false,
isHceSupported: false,
isVasSupported: false,
isVasAvailable: false,
nfcMode: "none",
isLoading: true,
error: null,
vasReason: null,
});

useEffect(() => {
async function checkCapabilities() {
try {
// iOS: Check VAS support via Tap to Pay
if (Platform.OS === "ios") {
if (!VasModule) {
// VasModule not available - gracefully disable NFC features
setCapabilities({
isNfcSupported: true, // Modern iOS has NFC
isNfcEnabled: true,
isHceSupported: false, // iOS never supports HCE
isVasSupported: false,
isVasAvailable: false,
nfcMode: "none",
isLoading: false,
error: null,
vasReason: "VAS module not available",
});
return;
}

try {
const result = await VasModule.getVasCapabilities();
const isVasAvailable = result.isVasAvailable ?? false;

setCapabilities({
isNfcSupported: true,
isNfcEnabled: true,
isHceSupported: false,
isVasSupported: result.isVasSupported ?? false,
isVasAvailable,
nfcMode: isVasAvailable ? "vas" : "none",
isLoading: false,
error: null,
vasReason: result.reason ?? null,
});
} catch (error) {
console.warn("VAS capabilities check failed:", error);
setCapabilities({
isNfcSupported: true,
isNfcEnabled: true,
isHceSupported: false,
isVasSupported: false,
isVasAvailable: false,
nfcMode: "none",
isLoading: false,
error: null,
vasReason: "Failed to check VAS capabilities",
});
}
return;
}

// Android: Check HCE support
if (Platform.OS === "android") {
if (!HceModule) {
// HceModule not available - gracefully disable NFC features
setCapabilities({
isNfcSupported: false,
isNfcEnabled: false,
isHceSupported: false,
isVasSupported: false,
isVasAvailable: false,
nfcMode: "none",
isLoading: false,
error: null,
vasReason: null,
});
return;
}

try {
const result = await HceModule.getNfcCapabilities();
const isHceSupported = result.isHceSupported ?? false;

setCapabilities({
isNfcSupported: result.isNfcSupported ?? false,
isNfcEnabled: result.isNfcEnabled ?? false,
isHceSupported,
isVasSupported: false, // Android doesn't support VAS
isVasAvailable: false,
nfcMode: isHceSupported ? "hce" : "none",
isLoading: false,
error: null,
vasReason: null,
});
} catch (error) {
console.warn("NFC capabilities check failed:", error);
setCapabilities({
isNfcSupported: false,
isNfcEnabled: false,
isHceSupported: false,
isVasSupported: false,
isVasAvailable: false,
nfcMode: "none",
isLoading: false,
error: null,
vasReason: null,
});
}
return;
}

// Other platforms (web, etc.)
setCapabilities({
isNfcSupported: false,
isNfcEnabled: false,
isHceSupported: false,
isVasSupported: false,
isVasAvailable: false,
nfcMode: "none",
isLoading: false,
error: null,
vasReason: null,
});
} catch (error) {
// Catch-all for any unexpected errors
console.warn("Unexpected error checking NFC capabilities:", error);
setCapabilities({
isNfcSupported: false,
isNfcEnabled: false,
isHceSupported: false,
isVasSupported: false,
isVasAvailable: false,
nfcMode: "none",
isLoading: false,
error: null,
vasReason: null,
});
}
}

checkCapabilities();
}, []);

return capabilities;
}
Loading