diff --git a/wallets/rn_cli_wallet/android/app/build.gradle b/wallets/rn_cli_wallet/android/app/build.gradle
index 7c7e2966..6ca3fa9a 100644
--- a/wallets/rn_cli_wallet/android/app/build.gradle
+++ b/wallets/rn_cli_wallet/android/app/build.gradle
@@ -85,7 +85,7 @@ android {
applicationId "com.walletconnect.web3wallet.rnsample"
minSdkVersion rootProject.ext.minSdkVersion
targetSdkVersion rootProject.ext.targetSdkVersion
- versionCode 59
+ versionCode 60
versionName "1.0"
resValue "string", "build_config_package", "com.walletconnect.web3wallet.rnsample"
}
diff --git a/wallets/rn_cli_wallet/android/app/src/main/AndroidManifest.xml b/wallets/rn_cli_wallet/android/app/src/main/AndroidManifest.xml
index 41dff387..7d9d31da 100644
--- a/wallets/rn_cli_wallet/android/app/src/main/AndroidManifest.xml
+++ b/wallets/rn_cli_wallet/android/app/src/main/AndroidManifest.xml
@@ -4,6 +4,8 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/wallets/rn_cli_wallet/ios/Podfile.lock b/wallets/rn_cli_wallet/ios/Podfile.lock
index 3ec67777..167127b5 100644
--- a/wallets/rn_cli_wallet/ios/Podfile.lock
+++ b/wallets/rn_cli_wallet/ios/Podfile.lock
@@ -1933,6 +1933,8 @@ PODS:
- Yoga
- react-native-netinfo (11.4.1):
- React-Core
+ - react-native-nfc-manager (3.17.2):
+ - React-Core
- react-native-quick-base64 (2.2.2):
- React-Core
- react-native-quick-crypto (0.7.17):
@@ -3120,6 +3122,7 @@ DEPENDENCIES:
- react-native-keyboard-controller (from `../node_modules/react-native-keyboard-controller`)
- react-native-mmkv (from `../node_modules/react-native-mmkv`)
- "react-native-netinfo (from `../node_modules/@react-native-community/netinfo`)"
+ - react-native-nfc-manager (from `../node_modules/react-native-nfc-manager`)
- react-native-quick-base64 (from `../node_modules/react-native-quick-base64`)
- react-native-quick-crypto (from `../node_modules/react-native-quick-crypto`)
- react-native-safe-area-context (from `../node_modules/react-native-safe-area-context`)
@@ -3273,6 +3276,8 @@ EXTERNAL SOURCES:
:path: "../node_modules/react-native-mmkv"
react-native-netinfo:
:path: "../node_modules/@react-native-community/netinfo"
+ react-native-nfc-manager:
+ :path: "../node_modules/react-native-nfc-manager"
react-native-quick-base64:
:path: "../node_modules/react-native-quick-base64"
react-native-quick-crypto:
@@ -3419,6 +3424,7 @@ SPEC CHECKSUMS:
react-native-keyboard-controller: f2ed31d12d9d8cb8ad2f9110c18fa5df499b66a3
react-native-mmkv: ac7507625cd74bac0eb5333604a7cd7b08fe9e3e
react-native-netinfo: cec9c4e86083cb5b6aba0e0711f563e2fbbff187
+ react-native-nfc-manager: c8891e460b4943b695d63f7f4effc6345bbefc83
react-native-quick-base64: 6568199bb2ac8e72ecdfdc73a230fbc5c1d3aac4
react-native-quick-crypto: dac9db2adb0a61b4881909b6db7c51eaaada66a8
react-native-safe-area-context: c6e2edd1c1da07bdce287fa9d9e60c5f7b514616
diff --git a/wallets/rn_cli_wallet/ios/RNWeb3Wallet.xcodeproj/project.pbxproj b/wallets/rn_cli_wallet/ios/RNWeb3Wallet.xcodeproj/project.pbxproj
index d7c4a6ab..25a95de6 100644
--- a/wallets/rn_cli_wallet/ios/RNWeb3Wallet.xcodeproj/project.pbxproj
+++ b/wallets/rn_cli_wallet/ios/RNWeb3Wallet.xcodeproj/project.pbxproj
@@ -33,7 +33,7 @@
13B07F961A680F5B00A75B9A /* RNWallet.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = RNWallet.app; sourceTree = BUILT_PRODUCTS_DIR; };
13B07FB51A68108700A75B9A /* Images.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; name = Images.xcassets; path = RNWeb3Wallet/Images.xcassets; sourceTree = ""; };
13B07FB61A68108700A75B9A /* Info.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; name = Info.plist; path = RNWeb3Wallet/Info.plist; sourceTree = ""; };
- 18BB39FF004742E9BA98CBB1 /* KHTeka-Light.otf */ = {isa = PBXFileReference; explicitFileType = undefined; fileEncoding = undefined; includeInIndex = 0; lastKnownFileType = unknown; name = "KHTeka-Light.otf"; path = "../assets/fonts/KHTeka-Light.otf"; sourceTree = ""; };
+ 18BB39FF004742E9BA98CBB1 /* KHTeka-Light.otf */ = {isa = PBXFileReference; explicitFileType = undefined; fileEncoding = 9; includeInIndex = 0; lastKnownFileType = unknown; name = "KHTeka-Light.otf"; path = "../assets/fonts/KHTeka-Light.otf"; sourceTree = ""; };
1DE6EA8716545F6CED9BF875 /* Pods-App-RNWallet.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-App-RNWallet.debug.xcconfig"; path = "Target Support Files/Pods-App-RNWallet/Pods-App-RNWallet.debug.xcconfig"; sourceTree = ""; };
1FCDA2A22B9B8E6200E0BF0C /* RNWallet Debug.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = "RNWallet Debug.app"; sourceTree = BUILT_PRODUCTS_DIR; };
1FCDA2BA2B9B8E6800E0BF0C /* RNWallet Internal.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = "RNWallet Internal.app"; sourceTree = BUILT_PRODUCTS_DIR; };
@@ -44,13 +44,13 @@
2C50482F22BB4C56ACE24EA8 /* BootSplash.storyboard */ = {isa = PBXFileReference; explicitFileType = undefined; fileEncoding = 9; includeInIndex = 0; lastKnownFileType = unknown; name = BootSplash.storyboard; path = RNWeb3Wallet/BootSplash.storyboard; sourceTree = ""; };
3616329FA2535E44622E8399 /* libPods-App-RNWallet Debug.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; includeInIndex = 0; path = "libPods-App-RNWallet Debug.a"; sourceTree = BUILT_PRODUCTS_DIR; };
3F16776DD2AD1459C80F97BA /* PrivacyInfo.xcprivacy */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xml; name = PrivacyInfo.xcprivacy; path = RNWeb3Wallet/PrivacyInfo.xcprivacy; sourceTree = ""; };
- 4FBAB7122BF343DBAF0BB7F7 /* KHTeka-Medium.otf */ = {isa = PBXFileReference; explicitFileType = undefined; fileEncoding = undefined; includeInIndex = 0; lastKnownFileType = unknown; name = "KHTeka-Medium.otf"; path = "../assets/fonts/KHTeka-Medium.otf"; sourceTree = ""; };
+ 4FBAB7122BF343DBAF0BB7F7 /* KHTeka-Medium.otf */ = {isa = PBXFileReference; explicitFileType = undefined; fileEncoding = 9; includeInIndex = 0; lastKnownFileType = unknown; name = "KHTeka-Medium.otf"; path = "../assets/fonts/KHTeka-Medium.otf"; sourceTree = ""; };
540B49B18EE9B4B3211A85F1 /* Pods-App-RNWallet Debug.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-App-RNWallet Debug.debug.xcconfig"; path = "Target Support Files/Pods-App-RNWallet Debug/Pods-App-RNWallet Debug.debug.xcconfig"; sourceTree = ""; };
- 7AE83AB305C04F529AD8275C /* Colors.xcassets */ = {isa = PBXFileReference; explicitFileType = undefined; fileEncoding = undefined; includeInIndex = 0; lastKnownFileType = folder.assetcatalog; name = Colors.xcassets; path = RNWeb3Wallet/Colors.xcassets; sourceTree = ""; };
+ 7AE83AB305C04F529AD8275C /* Colors.xcassets */ = {isa = PBXFileReference; explicitFileType = undefined; fileEncoding = 9; includeInIndex = 0; lastKnownFileType = folder.assetcatalog; name = Colors.xcassets; path = RNWeb3Wallet/Colors.xcassets; sourceTree = ""; };
7F0ADF9FBB4DFB43D9B42D14 /* libPods-App-RNWallet.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; includeInIndex = 0; path = "libPods-App-RNWallet.a"; sourceTree = BUILT_PRODUCTS_DIR; };
8987A5832B077AEB1770930B /* Pods-App-RNWallet Internal.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-App-RNWallet Internal.debug.xcconfig"; path = "Target Support Files/Pods-App-RNWallet Internal/Pods-App-RNWallet Internal.debug.xcconfig"; sourceTree = ""; };
936277491F1F3293104F8AD2 /* libPods-App-RNWallet Internal.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; includeInIndex = 0; path = "libPods-App-RNWallet Internal.a"; sourceTree = BUILT_PRODUCTS_DIR; };
- ABEB231DA62E483992083356 /* KHTeka-Regular.otf */ = {isa = PBXFileReference; explicitFileType = undefined; fileEncoding = undefined; includeInIndex = 0; lastKnownFileType = unknown; name = "KHTeka-Regular.otf"; path = "../assets/fonts/KHTeka-Regular.otf"; sourceTree = ""; };
+ ABEB231DA62E483992083356 /* KHTeka-Regular.otf */ = {isa = PBXFileReference; explicitFileType = undefined; fileEncoding = 9; includeInIndex = 0; lastKnownFileType = unknown; name = "KHTeka-Regular.otf"; path = "../assets/fonts/KHTeka-Regular.otf"; sourceTree = ""; };
BC0E53642B7E95F80075EFC3 /* RNWeb3Wallet.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; name = RNWeb3Wallet.entitlements; path = RNWeb3Wallet/RNWeb3Wallet.entitlements; sourceTree = ""; };
BC0E53652B7EAFF70075EFC3 /* RNWeb3WalletDebug.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; name = RNWeb3WalletDebug.entitlements; path = RNWeb3Wallet/RNWeb3WalletDebug.entitlements; sourceTree = ""; };
C3A29D35A12B15C6680651C1 /* PrivacyInfo.xcprivacy */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xml; name = PrivacyInfo.xcprivacy; path = RNWeb3Wallet/PrivacyInfo.xcprivacy; sourceTree = ""; };
@@ -346,10 +346,14 @@
inputFileListPaths = (
"${PODS_ROOT}/Target Support Files/Pods-App-RNWallet Debug/Pods-App-RNWallet Debug-resources-${CONFIGURATION}-input-files.xcfilelist",
);
+ inputPaths = (
+ );
name = "[CP] Copy Pods Resources";
outputFileListPaths = (
"${PODS_ROOT}/Target Support Files/Pods-App-RNWallet Debug/Pods-App-RNWallet Debug-resources-${CONFIGURATION}-output-files.xcfilelist",
);
+ outputPaths = (
+ );
runOnlyForDeploymentPostprocessing = 0;
shellPath = /bin/sh;
shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-App-RNWallet Debug/Pods-App-RNWallet Debug-resources.sh\"\n";
@@ -505,10 +509,14 @@
inputFileListPaths = (
"${PODS_ROOT}/Target Support Files/Pods-App-RNWallet Debug/Pods-App-RNWallet Debug-frameworks-${CONFIGURATION}-input-files.xcfilelist",
);
+ inputPaths = (
+ );
name = "[CP] Embed Pods Frameworks";
outputFileListPaths = (
"${PODS_ROOT}/Target Support Files/Pods-App-RNWallet Debug/Pods-App-RNWallet Debug-frameworks-${CONFIGURATION}-output-files.xcfilelist",
);
+ outputPaths = (
+ );
runOnlyForDeploymentPostprocessing = 0;
shellPath = /bin/sh;
shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-App-RNWallet Debug/Pods-App-RNWallet Debug-frameworks.sh\"\n";
@@ -522,10 +530,14 @@
inputFileListPaths = (
"${PODS_ROOT}/Target Support Files/Pods-App-RNWallet/Pods-App-RNWallet-frameworks-${CONFIGURATION}-input-files.xcfilelist",
);
+ inputPaths = (
+ );
name = "[CP] Embed Pods Frameworks";
outputFileListPaths = (
"${PODS_ROOT}/Target Support Files/Pods-App-RNWallet/Pods-App-RNWallet-frameworks-${CONFIGURATION}-output-files.xcfilelist",
);
+ outputPaths = (
+ );
runOnlyForDeploymentPostprocessing = 0;
shellPath = /bin/sh;
shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-App-RNWallet/Pods-App-RNWallet-frameworks.sh\"\n";
@@ -539,10 +551,14 @@
inputFileListPaths = (
"${PODS_ROOT}/Target Support Files/Pods-App-RNWallet/Pods-App-RNWallet-resources-${CONFIGURATION}-input-files.xcfilelist",
);
+ inputPaths = (
+ );
name = "[CP] Copy Pods Resources";
outputFileListPaths = (
"${PODS_ROOT}/Target Support Files/Pods-App-RNWallet/Pods-App-RNWallet-resources-${CONFIGURATION}-output-files.xcfilelist",
);
+ outputPaths = (
+ );
runOnlyForDeploymentPostprocessing = 0;
shellPath = /bin/sh;
shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-App-RNWallet/Pods-App-RNWallet-resources.sh\"\n";
@@ -600,10 +616,14 @@
inputFileListPaths = (
"${PODS_ROOT}/Target Support Files/Pods-App-RNWallet Internal/Pods-App-RNWallet Internal-resources-${CONFIGURATION}-input-files.xcfilelist",
);
+ inputPaths = (
+ );
name = "[CP] Copy Pods Resources";
outputFileListPaths = (
"${PODS_ROOT}/Target Support Files/Pods-App-RNWallet Internal/Pods-App-RNWallet Internal-resources-${CONFIGURATION}-output-files.xcfilelist",
);
+ outputPaths = (
+ );
runOnlyForDeploymentPostprocessing = 0;
shellPath = /bin/sh;
shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-App-RNWallet Internal/Pods-App-RNWallet Internal-resources.sh\"\n";
@@ -617,10 +637,14 @@
inputFileListPaths = (
"${PODS_ROOT}/Target Support Files/Pods-App-RNWallet Internal/Pods-App-RNWallet Internal-frameworks-${CONFIGURATION}-input-files.xcfilelist",
);
+ inputPaths = (
+ );
name = "[CP] Embed Pods Frameworks";
outputFileListPaths = (
"${PODS_ROOT}/Target Support Files/Pods-App-RNWallet Internal/Pods-App-RNWallet Internal-frameworks-${CONFIGURATION}-output-files.xcfilelist",
);
+ outputPaths = (
+ );
runOnlyForDeploymentPostprocessing = 0;
shellPath = /bin/sh;
shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-App-RNWallet Internal/Pods-App-RNWallet Internal-frameworks.sh\"\n";
@@ -756,7 +780,7 @@
PRODUCT_BUNDLE_IDENTIFIER = com.walletconnect.web3wallet.rnsample;
PRODUCT_NAME = RNWallet;
PROVISIONING_PROFILE_SPECIFIER = "";
- "PROVISIONING_PROFILE_SPECIFIER[sdk=iphoneos*]" = "match AppStore com.walletconnect.web3wallet.rnsample 1757428941";
+ "PROVISIONING_PROFILE_SPECIFIER[sdk=iphoneos*]" = "match AppStore com.walletconnect.web3wallet.rnsample";
SUPPORTED_PLATFORMS = "iphoneos iphonesimulator";
SUPPORTS_MACCATALYST = NO;
SUPPORTS_MAC_DESIGNED_FOR_IPHONE_IPAD = NO;
diff --git a/wallets/rn_cli_wallet/ios/RNWeb3Wallet/RNWeb3Wallet.entitlements b/wallets/rn_cli_wallet/ios/RNWeb3Wallet/RNWeb3Wallet.entitlements
index fde45392..246e4c52 100644
--- a/wallets/rn_cli_wallet/ios/RNWeb3Wallet/RNWeb3Wallet.entitlements
+++ b/wallets/rn_cli_wallet/ios/RNWeb3Wallet/RNWeb3Wallet.entitlements
@@ -7,9 +7,13 @@
com.apple.developer.associated-domains
applinks:lab.reown.com
+ applinks:pay.walletconnect.com
+ applinks:staging.pay.walletconnect.com
+ applinks:dev.pay.walletconnect.com
com.apple.developer.nfc.readersession.formats
+ NDEF
TAG
diff --git a/wallets/rn_cli_wallet/ios/RNWeb3Wallet/RNWeb3WalletDebug.entitlements b/wallets/rn_cli_wallet/ios/RNWeb3Wallet/RNWeb3WalletDebug.entitlements
index fde45392..246e4c52 100644
--- a/wallets/rn_cli_wallet/ios/RNWeb3Wallet/RNWeb3WalletDebug.entitlements
+++ b/wallets/rn_cli_wallet/ios/RNWeb3Wallet/RNWeb3WalletDebug.entitlements
@@ -7,9 +7,13 @@
com.apple.developer.associated-domains
applinks:lab.reown.com
+ applinks:pay.walletconnect.com
+ applinks:staging.pay.walletconnect.com
+ applinks:dev.pay.walletconnect.com
com.apple.developer.nfc.readersession.formats
+ NDEF
TAG
diff --git a/wallets/rn_cli_wallet/package.json b/wallets/rn_cli_wallet/package.json
index 4467dce5..6babe651 100644
--- a/wallets/rn_cli_wallet/package.json
+++ b/wallets/rn_cli_wallet/package.json
@@ -59,6 +59,7 @@
"react-native-keyboard-controller": "^1.20.6",
"react-native-mmkv": "3.3.3",
"react-native-modal": "14.0.0-rc.1",
+ "react-native-nfc-manager": "^3.15.1",
"react-native-quick-base64": "2.2.2",
"react-native-quick-crypto": "0.7.17",
"react-native-reanimated": "^4.2.1",
diff --git a/wallets/rn_cli_wallet/src/assets/NfcTap.tsx b/wallets/rn_cli_wallet/src/assets/NfcTap.tsx
new file mode 100644
index 00000000..c7b2ea5a
--- /dev/null
+++ b/wallets/rn_cli_wallet/src/assets/NfcTap.tsx
@@ -0,0 +1,11 @@
+import Svg, { Path, type SvgProps } from 'react-native-svg';
+
+const SvgNfcTap = (props: SvgProps) => (
+
+);
+export default SvgNfcTap;
diff --git a/wallets/rn_cli_wallet/src/components/Header.tsx b/wallets/rn_cli_wallet/src/components/Header.tsx
index ccc133c6..cb1b130e 100644
--- a/wallets/rn_cli_wallet/src/components/Header.tsx
+++ b/wallets/rn_cli_wallet/src/components/Header.tsx
@@ -1,21 +1,53 @@
import { View, StyleSheet, Image, Platform } from 'react-native';
import { useSafeAreaInsets } from 'react-native-safe-area-context';
+import Toast from 'react-native-toast-message';
import { useTheme } from '@/hooks/useTheme';
import { BorderRadius, Spacing } from '@/utils/ThemeUtil';
+import { useNfc, isAllowedNfcUri } from '@/hooks/useNfc';
+import { usePairing } from '@/hooks/usePairing';
import ModalStore from '@/store/ModalStore';
import BarcodeSvg from '@/assets/Barcode';
+import NfcTapSvg from '@/assets/NfcTap';
import { Button } from '@/components/Button';
export function Header() {
const { top } = useSafeAreaInsets();
const Theme = useTheme();
+ const { isNfcSupported, scanNfcTag } = useNfc();
+ const { handleUriOrPaymentLink } = usePairing();
const onScannerPress = () => {
ModalStore.open('ScannerOptionsModal', {});
};
+ const onNfcPress = async () => {
+ try {
+ const uri = await scanNfcTag();
+ if (uri) {
+ if (!isAllowedNfcUri(uri)) {
+ Toast.show({
+ type: 'error',
+ text1: 'Unrecognized NFC tag',
+ });
+ return;
+ }
+ handleUriOrPaymentLink(uri);
+ } else {
+ Toast.show({
+ type: 'info',
+ text1: 'No data found on NFC tag',
+ });
+ }
+ } catch {
+ Toast.show({
+ type: 'error',
+ text1: 'NFC scan failed',
+ });
+ }
+ };
+
return (
-
+
+ {Platform.OS === 'ios' && isNfcSupported && (
+
+ )}
+
+
);
}
@@ -67,13 +113,20 @@ const styles = StyleSheet.create({
width: 28,
height: 18,
},
- scanButton: {
+ actions: {
+ flexDirection: 'row',
+ gap: Spacing[2],
+ },
+ actionButton: {
height: 38,
width: 38,
borderRadius: BorderRadius[3],
alignItems: 'center',
justifyContent: 'center',
},
+ nfcButton: {
+ borderWidth: 1,
+ },
scanIcon: {
height: 20,
width: 20,
diff --git a/wallets/rn_cli_wallet/src/hooks/useNfc.ts b/wallets/rn_cli_wallet/src/hooks/useNfc.ts
new file mode 100644
index 00000000..3ced5cb1
--- /dev/null
+++ b/wallets/rn_cli_wallet/src/hooks/useNfc.ts
@@ -0,0 +1,283 @@
+import { useCallback, useEffect, useRef, useState } from 'react';
+import { AppState, Platform } from 'react-native';
+import NfcManager, {
+ NfcTech,
+ NfcEvents,
+ NfcAdapter,
+ Ndef,
+} from 'react-native-nfc-manager';
+
+import LogStore from '@/store/LogStore';
+
+const ALLOWED_NFC_HOSTS = [
+ 'pay.walletconnect.com',
+ 'staging.pay.walletconnect.com',
+ 'dev.pay.walletconnect.com',
+];
+
+export function isAllowedNfcUri(uri: string): boolean {
+ try {
+ const { protocol, hostname } = new URL(uri);
+ return (
+ protocol === 'wc:' ||
+ (protocol === 'https:' && ALLOWED_NFC_HOSTS.includes(hostname))
+ );
+ } catch {
+ return false;
+ }
+}
+
+let nfcStarted = false;
+let foregroundDispatchPaused = false;
+let foregroundRegistered = false;
+let resumeForegroundDispatch: (() => void) | null = null;
+
+async function ensureNfcStarted() {
+ if (!nfcStarted) {
+ await NfcManager.start();
+ nfcStarted = true;
+ }
+}
+
+function pauseForegroundDispatch() {
+ foregroundDispatchPaused = true;
+}
+
+function unpauseForegroundDispatch() {
+ foregroundDispatchPaused = false;
+ foregroundRegistered = false;
+ resumeForegroundDispatch?.();
+}
+
+export function useNfc() {
+ const [isNfcSupported, setIsNfcSupported] = useState(false);
+
+ useEffect(() => {
+ ensureNfcStarted()
+ .then(() => NfcManager.isSupported())
+ .then(supported => {
+ setIsNfcSupported(supported);
+ LogStore.log(`NFC supported: ${supported}`, 'useNfc', 'init');
+ })
+ .catch(() => {
+ setIsNfcSupported(false);
+ });
+ }, []);
+
+ const scanNfcTag = useCallback(async (): Promise => {
+ pauseForegroundDispatch();
+ try {
+ await NfcManager.cancelTechnologyRequest().catch(() => {});
+ await NfcManager.requestTechnology(NfcTech.Ndef);
+ const tag = await NfcManager.getTag();
+
+ if (!tag?.ndefMessage?.length) {
+ LogStore.log('No NDEF message on tag', 'useNfc', 'scanNfcTag');
+ return null;
+ }
+
+ for (const record of tag.ndefMessage) {
+ const uri = extractUri(record);
+ if (uri) {
+ LogStore.log(`NFC URI found: ${uri}`, 'useNfc', 'scanNfcTag');
+ return uri;
+ }
+ }
+
+ LogStore.log('No URI found in NDEF records', 'useNfc', 'scanNfcTag');
+ return null;
+ } catch (error: any) {
+ if (error?.message?.includes('cancelled')) {
+ return null;
+ }
+ LogStore.log(`NFC scan error: ${error?.message}`, 'useNfc', 'scanNfcTag');
+ throw error;
+ } finally {
+ NfcManager.cancelTechnologyRequest().catch(() => {});
+ unpauseForegroundDispatch();
+ }
+ }, []);
+
+ return {
+ isNfcSupported,
+ scanNfcTag,
+ };
+}
+
+/**
+ * Android-only: registers NFC foreground dispatch so tags are automatically
+ * intercepted while the app is visible. Calls `onUri` with the parsed URI.
+ */
+export function useNfcForegroundDispatch(onUri: (uri: string) => void) {
+ const onUriRef = useRef(onUri);
+ onUriRef.current = onUri;
+
+ useEffect(() => {
+ if (Platform.OS !== 'android') {
+ return;
+ }
+
+ let lastHandledUri = '';
+ let lastHandledTime = 0;
+
+ const register = async () => {
+ if (foregroundRegistered || foregroundDispatchPaused) {
+ return;
+ }
+ try {
+ await ensureNfcStarted();
+ NfcManager.setEventListener(NfcEvents.DiscoverTag, (tag: any) => {
+ LogStore.log(
+ 'Foreground dispatch: tag discovered',
+ 'useNfc',
+ 'foregroundDispatch',
+ );
+
+ if (!tag?.ndefMessage?.length) {
+ LogStore.log(
+ 'Foreground dispatch: no NDEF message',
+ 'useNfc',
+ 'foregroundDispatch',
+ );
+ return;
+ }
+
+ for (const record of tag.ndefMessage) {
+ const uri = extractUri(record);
+ if (uri) {
+ // Debounce: skip duplicate tag reads within 3 seconds
+ const now = Date.now();
+ if (uri === lastHandledUri && now - lastHandledTime < 3000) {
+ LogStore.log(
+ 'Foreground dispatch: duplicate tag ignored',
+ 'useNfc',
+ 'foregroundDispatch',
+ );
+ return;
+ }
+ lastHandledUri = uri;
+ lastHandledTime = now;
+
+ LogStore.log(
+ `Foreground dispatch URI: ${uri}`,
+ 'useNfc',
+ 'foregroundDispatch',
+ );
+ onUriRef.current(uri);
+ return;
+ }
+ }
+
+ LogStore.log(
+ 'Foreground dispatch: no URI in NDEF records',
+ 'useNfc',
+ 'foregroundDispatch',
+ );
+ });
+
+ /* eslint-disable no-bitwise */
+ const readerModeFlags =
+ NfcAdapter.FLAG_READER_NFC_A |
+ NfcAdapter.FLAG_READER_NFC_B |
+ NfcAdapter.FLAG_READER_NFC_V;
+ /* eslint-enable no-bitwise */
+ await NfcManager.registerTagEvent({
+ isReaderModeEnabled: true,
+ readerModeFlags,
+ });
+ foregroundRegistered = true;
+ LogStore.log(
+ 'NFC reader mode registered',
+ 'useNfc',
+ 'foregroundDispatch',
+ );
+ } catch (error: any) {
+ LogStore.log(
+ `Failed to register foreground dispatch: ${error?.message}`,
+ 'useNfc',
+ 'foregroundDispatch',
+ );
+ }
+ };
+
+ const unregister = async () => {
+ if (!foregroundRegistered) {
+ return;
+ }
+ try {
+ await NfcManager.unregisterTagEvent();
+ NfcManager.setEventListener(NfcEvents.DiscoverTag, null);
+ foregroundRegistered = false;
+ } catch {
+ // ignore
+ }
+ };
+
+ register();
+
+ resumeForegroundDispatch = () => register();
+
+ const subscription = AppState.addEventListener('change', state => {
+ if (state === 'active') {
+ register();
+ } else {
+ unregister();
+ }
+ });
+
+ return () => {
+ subscription.remove();
+ resumeForegroundDispatch = null;
+ unregister();
+ };
+ }, []);
+}
+
+function extractUri(record: {
+ tnf: number;
+ type: number[];
+ payload: number[];
+}): string | null {
+ // TNF 3 = Absolute URI — the type field IS the URI
+ if (record.tnf === 3 && record.type?.length) {
+ return bytesToString(record.type);
+ }
+
+ // TNF 1 = Well-known type
+ if (record.tnf === 1) {
+ const typeStr = bytesToString(record.type);
+
+ // RTD_URI (type = "U" / 0x55)
+ if (typeStr === 'U' && record.payload?.length > 1) {
+ return Ndef.uri.decodePayload(new Uint8Array(record.payload) as any);
+ }
+
+ // RTD_SMART_POSTER (type = "Sp") — contains nested NDEF with URI
+ if (typeStr === 'Sp' && record.payload?.length) {
+ try {
+ const innerBytes = new Uint8Array(record.payload);
+ const innerRecords = Ndef.decodeMessage(innerBytes as any);
+ if (innerRecords) {
+ for (const inner of innerRecords) {
+ const uri = extractUri(inner as any);
+ if (uri) {
+ return uri;
+ }
+ }
+ }
+ } catch {
+ // Failed to parse smart poster, skip
+ }
+ }
+ }
+
+ return null;
+}
+
+function bytesToString(bytes: number[]): string {
+ let result = '';
+ for (const byte of bytes) {
+ result += String.fromCharCode(byte);
+ }
+ return result;
+}
diff --git a/wallets/rn_cli_wallet/src/screens/App.tsx b/wallets/rn_cli_wallet/src/screens/App.tsx
index ae93edee..8d43712a 100644
--- a/wallets/rn_cli_wallet/src/screens/App.tsx
+++ b/wallets/rn_cli_wallet/src/screens/App.tsx
@@ -16,6 +16,7 @@ import { RootStackNavigator } from '@/navigators/RootStackNavigator';
import useInitializeWalletKit from '@/hooks/useInitializeWalletKit';
import useWalletKitEventsManager from '@/hooks/useWalletKitEventsManager';
import { usePairing } from '@/hooks/usePairing';
+import { useNfcForegroundDispatch, isAllowedNfcUri } from '@/hooks/useNfc';
import { walletKit } from '@/utils/WalletKitUtil';
import SettingsStore from '@/store/SettingsStore';
import ModalStore from '@/store/ModalStore';
@@ -63,6 +64,19 @@ const App = () => {
// Get centralized URI/payment link handler
const { handleUriOrPaymentLink } = usePairing();
+ // Android: automatically intercept NFC tags while app is in foreground
+ const handleNfcUri = useCallback(
+ (uri: string) => {
+ if (isAllowedNfcUri(uri)) {
+ handleUriOrPaymentLink(uri);
+ } else {
+ LogStore.log(`NFC URI rejected: ${uri}`, 'App', 'handleNfcUri');
+ }
+ },
+ [handleUriOrPaymentLink],
+ );
+ useNfcForegroundDispatch(handleNfcUri);
+
// Hide splash screen once wallets are initialized, addresses are loaded and theme mode is set
useEffect(() => {
if (initialized && eip155Address && themeMode) {
@@ -88,7 +102,7 @@ const App = () => {
const deeplinkHandler = useCallback(
({ url }: { url: string }) => {
LogStore.log('Deep link received', 'App', 'deeplinkHandler', {
- url
+ url,
});
// 1. Link mode (wc_ev) - SDK handles it, just set the flag
@@ -98,7 +112,21 @@ const App = () => {
return;
}
- // 2. Redirection from app with encoded URI (wc?uri=)
+ // 2. Payment link from NFC tag or App Link (pay.walletconnect.com)
+ try {
+ const { hostname } = new URL(url);
+ if (
+ hostname.endsWith('.pay.walletconnect.com') ||
+ hostname === 'pay.walletconnect.com'
+ ) {
+ handleUriOrPaymentLink(url);
+ return;
+ }
+ } catch {
+ // Not a valid URL, continue to other handlers
+ }
+
+ // 3. Redirection from app with encoded URI (wc?uri=)
if (url.includes('wc?uri=')) {
const encodedUri = url.split('wc?uri=')[1];
if (!encodedUri) {
@@ -116,7 +144,7 @@ const App = () => {
return;
}
- // 3. Direct WC protocol URI (wc:)
+ // 4. Direct WC protocol URI (wc:)
// Extract from wc: onwards to remove app scheme prefix
if (url.includes('wc:')) {
const wcIndex = url.indexOf('wc:');
@@ -125,14 +153,13 @@ const App = () => {
return;
}
- // 4. Request for already paired session (wc?)
+ // 5. Request for already paired session (wc?)
if (url.includes('wc?')) {
ModalStore.open('LoadingModal', {
loadingMessage: 'Loading request...',
});
return;
}
-
},
[handleUriOrPaymentLink],
);
diff --git a/wallets/rn_cli_wallet/yarn.lock b/wallets/rn_cli_wallet/yarn.lock
index 3a91516b..cd6650b8 100644
--- a/wallets/rn_cli_wallet/yarn.lock
+++ b/wallets/rn_cli_wallet/yarn.lock
@@ -4490,6 +4490,7 @@ __metadata:
react-native-keyboard-controller: ^1.20.6
react-native-mmkv: 3.3.3
react-native-modal: 14.0.0-rc.1
+ react-native-nfc-manager: ^3.15.1
react-native-quick-base64: 2.2.2
react-native-quick-crypto: 0.7.17
react-native-reanimated: ^4.2.1
@@ -10548,6 +10549,18 @@ __metadata:
languageName: node
linkType: hard
+"react-native-nfc-manager@npm:^3.15.1":
+ version: 3.17.2
+ resolution: "react-native-nfc-manager@npm:3.17.2"
+ peerDependencies:
+ "@expo/config-plugins": "*"
+ peerDependenciesMeta:
+ "@expo/config-plugins":
+ optional: true
+ checksum: 7435840e0ddd190b442dbe4972e318da2423c953648f917ba515d99a92aba27a477d738fb0f75db89bc64fc52201b9e6880d74e5c7f800d75195c77f4c20cde3
+ languageName: node
+ linkType: hard
+
"react-native-quick-base64@npm:2.2.2, react-native-quick-base64@npm:^2.2.2":
version: 2.2.2
resolution: "react-native-quick-base64@npm:2.2.2"