Merge pull request #3821 from myxmaster/fix/nfc-availability-checks

fix: check NFC support and enabled state before NFC operations
This commit is contained in:
Evan Kaloudis
2026-04-03 22:38:58 -04:00
committed by GitHub
10 changed files with 146 additions and 96 deletions
+35 -5
View File
@@ -1,6 +1,7 @@
import React from 'react';
import { View, StyleSheet, Text } from 'react-native';
import { inject, observer } from 'mobx-react';
import NfcManager from 'react-native-nfc-manager';
import Button from '../Button';
import ModalBox from '../ModalBox';
@@ -24,7 +25,11 @@ export default class AndroidNfcModal extends React.Component<
> {
render() {
const { ModalStore } = this.props;
const { showAndroidNfcModal, toggleAndroidNfcModal } = ModalStore;
const {
showAndroidNfcModal,
toggleAndroidNfcModal,
androidNfcModalIsNfcEnabled
} = ModalStore;
return (
<ModalBox
@@ -67,9 +72,15 @@ export default class AndroidNfcModal extends React.Component<
marginBottom: 30
}}
>
{localeString('components.AndroidNfcModal.ready')}
{androidNfcModalIsNfcEnabled
? localeString(
'components.AndroidNfcModal.ready'
)
: localeString(
'components.AndroidNfcModal.nfcDisabled'
)}
</Text>
<NFC />
{androidNfcModalIsNfcEnabled && <NFC />}
<Text
style={{
fontSize: 18,
@@ -79,14 +90,33 @@ export default class AndroidNfcModal extends React.Component<
textAlign: 'center'
}}
>
{localeString('components.AndroidNfcModal.hold')}
{androidNfcModalIsNfcEnabled
? localeString(
'components.AndroidNfcModal.hold'
)
: localeString(
'components.AndroidNfcModal.enableInstructions'
)}
</Text>
<View style={styles.buttons}>
{!androidNfcModalIsNfcEnabled && (
<View style={styles.button}>
<Button
title={localeString(
'general.openNfcSettings'
)}
onPress={() => {
toggleAndroidNfcModal(false);
NfcManager.goToNfcSetting();
}}
quaternary
></Button>
</View>
)}
<View style={styles.button}>
<Button
title={localeString('general.cancel')}
onPress={() => toggleAndroidNfcModal(false)}
quaternary
></Button>
</View>
</View>
+14 -1
View File
@@ -1,5 +1,7 @@
import * as React from 'react';
import { Platform, TouchableOpacity } from 'react-native';
import { inject, observer } from 'mobx-react';
import { checkNfcEnabled } from '../utils/NFCUtils';
import HCESession, { NFCContentType, NFCTagType4 } from 'react-native-hce';
@@ -7,6 +9,8 @@ import NfcIcon from '../assets/images/SVG/NFC-alt.svg';
import Button from './../components/Button';
import ModalStore from '../stores/ModalStore';
import { localeString } from '../utils/LocaleUtils';
import { themeColor } from '../utils/ThemeUtils';
@@ -15,12 +19,15 @@ interface NFCButtonProps {
icon?: any;
noUppercase?: boolean;
iconOnly?: boolean;
ModalStore?: ModalStore;
}
interface NFCButtonState {
nfcBroadcast: boolean;
}
@inject('ModalStore')
@observer
export default class NFCButton extends React.Component<
NFCButtonProps,
NFCButtonState
@@ -43,7 +50,13 @@ export default class NFCButton extends React.Component<
this.stopSimulation();
}
}
toggleNfc = () => {
toggleNfc = async () => {
const { ModalStore } = this.props;
if (!this.state.nfcBroadcast) {
if (!(await checkNfcEnabled(ModalStore!))) return;
}
if (this.state.nfcBroadcast) {
this.stopSimulation();
} else {
+3
View File
@@ -6,6 +6,7 @@
"general.request": "Request",
"general.scan": "Scan",
"general.enableNfc": "Enable NFC",
"general.openNfcSettings": "Open NFC Settings",
"general.receiveNfc": "Receive via NFC",
"general.payNfc": "Pay via NFC",
"general.confirm": "Confirm",
@@ -288,6 +289,8 @@
"components.ExternalLinkModal.copied": "Copied!",
"components.AndroidNfcModal.ready": "Ready to scan",
"components.AndroidNfcModal.hold": "Hold your Android phone near an NFC tag to read it",
"components.AndroidNfcModal.nfcDisabled": "NFC is disabled",
"components.AndroidNfcModal.enableInstructions": "Please enable NFC in your device settings to use this feature",
"components.QRCodeScanner.chooseFromGallery": "Choose from gallery",
"components.QRCodeScanner.flashOn": "Flash on",
"components.QRCodeScanner.flashOff": "Flash off",
+8 -1
View File
@@ -10,6 +10,7 @@ import {
export default class ModalStore {
@observable public showExternalLinkModal: boolean = false;
@observable public showAndroidNfcModal: boolean = false;
@observable public androidNfcModalIsNfcEnabled: boolean = false;
@observable public showInfoModal: boolean = false;
@observable public showAlertModal: boolean = false;
@observable public showShareModal: boolean = false;
@@ -187,8 +188,14 @@ export default class ModalStore {
/* Android NFC Modal */
@action
public toggleAndroidNfcModal = (status: boolean) => {
public toggleAndroidNfcModal = (
status: boolean,
nfcEnabled: boolean = true
) => {
this.showAndroidNfcModal = status;
if (status) {
this.androidNfcModalIsNfcEnabled = nfcEnabled;
}
};
/* Channel Backup Modal */
+20
View File
@@ -1,3 +1,23 @@
import NfcManager from 'react-native-nfc-manager';
import ModalStore from '../stores/ModalStore';
/**
* Checks whether NFC is enabled on the device.
* If not, shows the AndroidNfcModal with the disabled state and returns false.
* On iOS, NfcManager.isEnabled() always returns true, so this function
* only ever returns false on Android.
*/
export async function checkNfcEnabled(
modalStore: ModalStore
): Promise<boolean> {
const nfcEnabled = await NfcManager.isEnabled();
if (!nfcEnabled) {
modalStore.toggleAndroidNfcModal(true, false);
return false;
}
return true;
}
class NFCUtils {
nfcUtf8ArrayToStr = (data: any) => {
const extraByteMap = [1, 1, 1, 1, 2, 2, 3, 0];
+4 -1
View File
@@ -56,7 +56,7 @@ import UnitsStore from '../../stores/UnitsStore';
import CashuInvoice from '../../models/CashuInvoice';
import { localeString } from '../../utils/LocaleUtils';
import NFCUtils from '../../utils/NFCUtils';
import NFCUtils, { checkNfcEnabled } from '../../utils/NFCUtils';
import { themeColor } from '../../utils/ThemeUtils';
import { getAmountFromSats } from '../../utils/AmountUtils';
@@ -269,6 +269,9 @@ export default class ReceiveEcash extends React.Component<
enableNfc = async () => {
const { ModalStore } = this.props;
if (!(await checkNfcEnabled(ModalStore))) return;
this.disableNfc();
await NfcManager.start().catch((e) => console.warn(e.message));
+1 -55
View File
@@ -6,13 +6,11 @@ import {
Text,
TouchableHighlight,
TouchableOpacity,
ScrollView,
Platform
ScrollView
} from 'react-native';
import { inject, observer } from 'mobx-react';
import { duration } from 'moment';
import { NativeStackNavigationProp } from '@react-navigation/native-stack';
import NfcManager, { NfcEvents, TagEvent } from 'react-native-nfc-manager';
import BigNumber from 'bignumber.js';
import { ChannelsHeader } from '../../components/Channels/ChannelsHeader';
@@ -49,8 +47,6 @@ import Channel from '../../models/Channel';
import { Status, ExpirationStatus } from '../../models/Status';
import ClosedChannel from '../../models/ClosedChannel';
import { ErrorMessage } from '../../components/SuccessErrorMessage';
import ModalStore from '../../stores/ModalStore';
import nfcUtils from '../../utils/NFCUtils';
import handleAnything from '../../utils/handleAnything';
import { SafeAreaView } from 'react-native-safe-area-context';
import Peer from '../../models/Peer';
@@ -69,7 +65,6 @@ interface ChannelsProps {
LSPStore?: LSPStore;
NodeInfoStore?: NodeInfoStore;
SettingsStore?: SettingsStore;
ModalStore?: ModalStore;
}
interface ChannelsState {
@@ -143,55 +138,6 @@ export default class ChannelsPane extends React.PureComponent<
}
}
disableNfc = () => {
NfcManager.setEventListener(NfcEvents.DiscoverTag, null);
NfcManager.setEventListener(NfcEvents.SessionClosed, null);
};
enableNfc = async () => {
const { ModalStore } = this.props;
this.disableNfc();
await NfcManager.start();
return new Promise((resolve: any) => {
let tagFound: TagEvent | null = null;
// enable NFC
if (Platform.OS === 'android')
ModalStore?.toggleAndroidNfcModal(true);
NfcManager.setEventListener(
NfcEvents.DiscoverTag,
(tag: TagEvent) => {
tagFound = tag;
const bytes = new Uint8Array(
tagFound.ndefMessage[0].payload
);
const str = nfcUtils.nfcUtf8ArrayToStr(bytes) || '';
// close NFC
if (Platform.OS === 'android')
ModalStore?.toggleAndroidNfcModal(false);
resolve(this.validateNodeUri(str));
NfcManager.unregisterTagEvent().catch(() => 0);
}
);
NfcManager.setEventListener(NfcEvents.SessionClosed, () => {
// close NFC
if (Platform.OS === 'android')
ModalStore?.toggleAndroidNfcModal(false);
if (!tagFound) {
resolve();
}
});
NfcManager.registerTagEvent();
});
};
initFromProps(props: ChannelsProps) {
const { NodeInfoStore, SettingsStore } = props;
+31 -18
View File
@@ -30,7 +30,7 @@ import TextInput from '../components/TextInput';
import UTXOPicker from '../components/UTXOPicker';
import handleAnything from '../utils/handleAnything';
import NFCUtils from '../utils/NFCUtils';
import NFCUtils, { checkNfcEnabled } from '../utils/NFCUtils';
import NodeUriUtils from '../utils/NodeUriUtils';
import BackendUtils from '../utils/BackendUtils';
import ValidationUtils from '../utils/ValidationUtils';
@@ -91,6 +91,7 @@ interface OpenChannelState {
additionalChannels: Array<AdditionalChannel>;
isNodePubkeyValid: boolean;
isNodeHostValid: boolean;
nfcSupported: boolean;
}
@inject(
@@ -132,7 +133,8 @@ export default class OpenChannel extends React.Component<
account: 'default',
additionalChannels: [],
isNodePubkeyValid: true,
isNodeHostValid: true
isNodeHostValid: true,
nfcSupported: false
};
}
@@ -173,6 +175,9 @@ export default class OpenChannel extends React.Component<
if (this.props.ChannelsStore.channelsView === ChannelsView.Peers) {
this.setState({ connectPeerOnly: true });
}
const nfcSupported = await NfcManager.isSupported();
this.setState({ nfcSupported });
}
disableNfc = () => {
@@ -182,6 +187,9 @@ export default class OpenChannel extends React.Component<
enableNfc = async () => {
const { ModalStore } = this.props;
if (!(await checkNfcEnabled(ModalStore))) return;
this.disableNfc();
await NfcManager.start();
@@ -336,7 +344,8 @@ export default class OpenChannel extends React.Component<
advancedSettingsToggle,
additionalChannels,
isNodePubkeyValid,
isNodeHostValid
isNodeHostValid,
nfcSupported
} = this.state;
const { implementation } = SettingsStore;
@@ -1361,21 +1370,25 @@ export default class OpenChannel extends React.Component<
/>
</View>
<View style={styles.button}>
<Button
title={localeString('general.enableNfc')}
icon={
<NfcIcon
stroke={themeColor('highlight')}
width={25}
height={25}
style={{ marginRight: 10 }}
/>
}
onPress={() => this.enableNfc()}
secondary
/>
</View>
{nfcSupported && (
<View style={styles.button}>
<Button
title={localeString(
'general.enableNfc'
)}
icon={
<NfcIcon
stroke={themeColor('highlight')}
width={25}
height={25}
style={{ marginRight: 10 }}
/>
}
onPress={() => this.enableNfc()}
secondary
/>
</View>
)}
</View>
</ScrollView>
)}
+5 -1
View File
@@ -79,7 +79,7 @@ import BalanceStore from '../stores/BalanceStore';
import { localeString } from '../utils/LocaleUtils';
import BackendUtils from '../utils/BackendUtils';
import Base64Utils from '../utils/Base64Utils';
import NFCUtils from '../utils/NFCUtils';
import NFCUtils, { checkNfcEnabled } from '../utils/NFCUtils';
import { themeColor } from '../utils/ThemeUtils';
import { SATS_PER_BTC } from '../utils/UnitsUtils';
import { getAmountFromSats } from '../utils/AmountUtils';
@@ -193,6 +193,7 @@ const LOCKED_EXPIRY_SECONDS = '3600';
@inject(
'ChannelsStore',
'InvoicesStore',
'ModalStore',
'SettingsStore',
'UnitsStore',
'PosStore',
@@ -680,6 +681,9 @@ export default class Receive extends React.Component<
enableNfc = async () => {
const { ModalStore } = this.props;
if (!(await checkNfcEnabled(ModalStore))) return;
this.disableNfc();
await NfcManager.start().catch((e) => console.warn(e.message));
+25 -14
View File
@@ -53,7 +53,7 @@ import UTXOPicker from '../components/UTXOPicker';
import BackendUtils from '../utils/BackendUtils';
import { errorToUserFriendly } from '../utils/ErrorUtils';
import NFCUtils from '../utils/NFCUtils';
import NFCUtils, { checkNfcEnabled } from '../utils/NFCUtils';
import { localeString } from '../utils/LocaleUtils';
import { themeColor } from '../utils/ThemeUtils';
import { getUnformattedAmount, getAmountFromSats } from '../utils/AmountUtils';
@@ -121,6 +121,7 @@ interface SendState {
additionalOutputs: Array<AdditionalOutput>;
fundMax: boolean;
validAmountToSwap: boolean;
nfcSupported: boolean;
}
@inject(
@@ -191,7 +192,8 @@ export default class Send extends React.Component<SendProps, SendState> {
account: 'default',
additionalOutputs: [],
fundMax: false,
validAmountToSwap: false
validAmountToSwap: false,
nfcSupported: false
};
}
@@ -333,6 +335,9 @@ export default class Send extends React.Component<SendProps, SendState> {
'hardwareBackPress',
this.backPressed.bind(this)
);
const nfcSupported = await NfcManager.isSupported();
this.setState({ nfcSupported });
}
componentWillUnmount(): void {
@@ -371,6 +376,9 @@ export default class Send extends React.Component<SendProps, SendState> {
enableNfc = async () => {
const { ModalStore } = this.props;
if (!(await checkNfcEnabled(ModalStore))) return;
this.disableNfc();
await NfcManager.start().catch((e) => console.warn(e.message));
@@ -746,7 +754,8 @@ export default class Send extends React.Component<SendProps, SendState> {
fundMax,
account,
validAmountToSwap,
utxos
utxos,
nfcSupported
} = this.state;
const {
confirmedBlockchainBalance,
@@ -846,17 +855,19 @@ export default class Send extends React.Component<SendProps, SendState> {
</TouchableOpacity>
</View>
)}
<View style={{ marginRight: 15 }}>
<TouchableOpacity
onPress={() => this.enableNfc()}
>
<NFC
stroke={themeColor('text')}
width={30}
height={30}
/>
</TouchableOpacity>
</View>
{nfcSupported && (
<View style={{ marginRight: 15 }}>
<TouchableOpacity
onPress={() => this.enableNfc()}
>
<NFC
stroke={themeColor('text')}
width={30}
height={30}
/>
</TouchableOpacity>
</View>
)}
<View>
<TouchableOpacity
onPress={() =>