From cf6f4431d515dbb4338aa09ec92eb8599f3ade59 Mon Sep 17 00:00:00 2001 From: peachbits Date: Thu, 3 Sep 2026 13:56:56 -0700 Subject: [PATCH 1/3] Add cameraScamWarningShown account setting Tracks whether the QR scanner scam warning has been acknowledged, so it can be shown once on the first use of the camera. --- src/__tests__/actions/RequestReviewActions.test.ts | 1 + .../reducers/__snapshots__/RootReducer.test.ts.snap | 1 + src/actions/LocalSettingsActions.ts | 12 ++++++++++++ src/types/types.ts | 1 + 4 files changed, 15 insertions(+) diff --git a/src/__tests__/actions/RequestReviewActions.test.ts b/src/__tests__/actions/RequestReviewActions.test.ts index e064e05d111..15ee57474c7 100644 --- a/src/__tests__/actions/RequestReviewActions.test.ts +++ b/src/__tests__/actions/RequestReviewActions.test.ts @@ -51,6 +51,7 @@ const defaultReviewTrigger: ReviewTriggerData = { daysSinceUpgrade: [] } const defaultSettings: LocalAccountSettings = { + cameraScamWarningShown: false, contactsPermissionShown: false, developerModeOn: false, isAccountBalanceVisible: true, diff --git a/src/__tests__/reducers/__snapshots__/RootReducer.test.ts.snap b/src/__tests__/reducers/__snapshots__/RootReducer.test.ts.snap index a575e85bf19..cb8d242d971 100644 --- a/src/__tests__/reducers/__snapshots__/RootReducer.test.ts.snap +++ b/src/__tests__/reducers/__snapshots__/RootReducer.test.ts.snap @@ -110,6 +110,7 @@ exports[`initialState 1`] = ` "ip2FaNotifShown": false, }, "autoLogoutTimeInSeconds": 3600, + "cameraScamWarningShown": false, "changesLocked": true, "contactsPermissionShown": false, "countryCode": "", diff --git a/src/actions/LocalSettingsActions.ts b/src/actions/LocalSettingsActions.ts index 964b42f509b..0abec2343d5 100644 --- a/src/actions/LocalSettingsActions.ts +++ b/src/actions/LocalSettingsActions.ts @@ -264,6 +264,18 @@ export const writeNymWarningShown = async ( return await writeLocalAccountSettings(account, updatedSettings) } +/** + * Persists the user's acknowledgment of the QR scanner scam warning so it is + * only shown once per account, on the first use of the camera. + */ +export const writeCameraScamWarningShown = async ( + account: EdgeAccount +): Promise => { + const settings = await getLocalAccountSettings(account) + const updatedSettings = { ...settings, cameraScamWarningShown: true } + return await writeLocalAccountSettings(account, updatedSettings) +} + /** * Tracks whether a token gas requirement warning has been shown per a * particular currency plugin. If the plugin id exists in this array, the diff --git a/src/types/types.ts b/src/types/types.ts index 2ec12aa32be..5a482dd2eb6 100644 --- a/src/types/types.ts +++ b/src/types/types.ts @@ -205,6 +205,7 @@ export const asReviewTriggerData = asObject({ }) const asLocalAccountSettingsInner = asObject({ + cameraScamWarningShown: asMaybe(asBoolean, false), contactsPermissionShown: asMaybe(asBoolean, false), developerModeOn: asMaybe(asBoolean, false), notifState: asMaybe(asNotifState, asNotifState({})), From 8e4ee70d6a6f91c32bf9ff45df99e7999cdf84b7 Mon Sep 17 00:00:00 2001 From: peachbits Date: Thu, 3 Sep 2026 13:57:23 -0700 Subject: [PATCH 2/3] Add scan scam warning and camera permission denied modals Splits the scam warning apart from the 'enable Camera access' recovery guidance so the two have independent triggers and lifecycles. The scam warning is a non-skippable ConfirmContinueModal, so it stays up until the user ticks the checkbox and confirms. That also leaves it with no onCancel, which means EdgeModal ignores Airship's global clear event, so it handles 'clear' itself rather than outliving a logout. --- .../modals/CameraPermissionDeniedModal.tsx | 44 ++++++++++++++ .../modals/ScanScamWarningModal.tsx | 60 +++++++++++++++++++ 2 files changed, 104 insertions(+) create mode 100644 src/components/modals/CameraPermissionDeniedModal.tsx create mode 100644 src/components/modals/ScanScamWarningModal.tsx diff --git a/src/components/modals/CameraPermissionDeniedModal.tsx b/src/components/modals/CameraPermissionDeniedModal.tsx new file mode 100644 index 00000000000..1101084ca43 --- /dev/null +++ b/src/components/modals/CameraPermissionDeniedModal.tsx @@ -0,0 +1,44 @@ +import * as React from 'react' +import type { AirshipBridge } from 'react-native-airship' +import { openSettings } from 'react-native-permissions' + +import { lstrings } from '../../locales/strings' +import { ModalButtons } from '../buttons/ModalButtons' +import { showError } from '../services/AirshipInstance' +import { Paragraph } from '../themed/EdgeText' +import { EdgeModal } from './EdgeModal' + +interface Props { + bridge: AirshipBridge +} + +/** + * Recovery guidance shown when the camera permission is denied or blocked. + * + * This deliberately carries no scam warning: the warning has its own trigger + * (the first successful use of the camera) and its own modal, so that neither + * one can hide or cut short the other. + */ +export const CameraPermissionDeniedModal: React.FC = props => { + const { bridge } = props + + const handleClose = (): void => { + bridge.resolve() + } + + const handleSettings = (): void => { + openSettings().catch((error: unknown) => { + showError(error) + }) + handleClose() + } + + return ( + + {lstrings.scan_camera_permission_denied} + + + ) +} diff --git a/src/components/modals/ScanScamWarningModal.tsx b/src/components/modals/ScanScamWarningModal.tsx new file mode 100644 index 00000000000..85b66203aec --- /dev/null +++ b/src/components/modals/ScanScamWarningModal.tsx @@ -0,0 +1,60 @@ +import * as React from 'react' +import type { AirshipBridge } from 'react-native-airship' +import { sprintf } from 'sprintf-js' + +import { lstrings } from '../../locales/strings' +import { config } from '../../theme/appConfig' +import { Paragraph, WarningText } from '../themed/EdgeText' +import { ConfirmContinueModal } from './ConfirmContinueModal' + +interface Props { + bridge: AirshipBridge +} + +/** + * Scam warning shown before the first use of the QR scanner camera. + * + * This is deliberately separate from the "enable Camera access" recovery + * modal, so that the two have independent lifecycles. `ConfirmContinueModal` + * is not skippable, so there is no backdrop tap, swipe, close button or + * hardware back that can dismiss this: the user has to tick the checkbox and + * confirm. + */ +export const ScanScamWarningModal: React.FC = props => { + const { bridge } = props + + // A non-skippable `ConfirmContinueModal` passes no `onCancel`, so `EdgeModal` + // ignores Airship's global `clear` event. Without this the modal would + // outlive a logout and leave the caller awaiting a promise that never + // settles: + React.useEffect( + () => + bridge.on('clear', () => { + bridge.resolve(false) + }), + [bridge] + ) + + const warningMessage = [ + sprintf(lstrings.warning_scam_message_financial_advice_s, config.appName), + lstrings.warning_scam_message_irreversibility, + lstrings.warning_scam_message_unknown_recipients + ] + .map(bullet => `• ${bullet}`) + .join('\n\n') + + return ( + + + {warningMessage} + + + {sprintf(lstrings.warning_scam_footer_s, config.supportEmail)} + + + ) +} From 80ed05d31e67d7a416c0d7580759187ccb5bf6f8 Mon Sep 17 00:00:00 2001 From: peachbits Date: Thu, 3 Sep 2026 13:59:16 -0700 Subject: [PATCH 3/3] Sequence camera permission, scam warning, and QR scanner ScanModal chose its whole body from one ternary keyed on the redux camera permission, and requested that permission from its own mount effect. The scam warning lived only in the not-granted branch, so on a fresh install the modal opened straight into 'enable Camera access in Settings' before the user had ever been asked, the OS prompt landed on top of it, and answering it flipped the ternary. The warning's subtree was unmounted by React rather than through bridge.resolve, so it vanished without its exit animation -- the reported flash. The only way to read the warning was to deny the camera. showScanModal now runs the three steps in order, so none of them overlap: request the permission with no Edge modal on screen behind it, show the Settings recovery guidance if it was denied or blocked, then on the first camera use only, wait for an explicit acknowledgement of the scam warning before mounting the scanner. Waiting for the foreground keeps the warning from being presented while the app is still returning from the OS prompt. ScanModal is left as just the camera sheet, with no permission branch to swap. WcConnectionsScene picks up the return-type and catch-variable fixes its removal from the ESLint warnings list now requires. --- CHANGELOG.md | 2 + eslint.config.mjs | 5 +- src/actions/ScanActions.tsx | 75 ++++++++++++++++++++ src/components/modals/ScanModal.tsx | 67 +++++------------ src/components/scenes/WcConnectionsScene.tsx | 32 +++++---- src/components/themed/SideMenu.tsx | 21 +++--- src/components/tiles/AddressTile2.tsx | 23 +++--- 7 files changed, 135 insertions(+), 90 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index f2464f4f9b5..cd3180c89f7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,8 @@ ## Unreleased (develop) +- fixed: Show the QR scanner scam warning after the camera permission is granted, instead of behind the OS permission prompt where it flashed away, and show only the Settings recovery guidance when camera access is denied. + ## 4.51.0 (staging) - added: Push info-server attestation tokens into edge-core-js via `setAttestationToken` so the login server can skip CAPTCHA for attested devices, and allow `LOGIN_SERVER` / `INFO_SERVER` env overrides for local E2E stacks. diff --git a/eslint.config.mjs b/eslint.config.mjs index a818fa26926..7b00b302676 100644 --- a/eslint.config.mjs +++ b/eslint.config.mjs @@ -129,7 +129,6 @@ export default [ 'src/actions/RecoveryReminderActions.tsx', 'src/actions/ScamWarningActions.tsx', - 'src/actions/ScanActions.tsx', 'src/actions/SoundActions.ts', 'src/actions/TokenTermsActions.tsx', @@ -220,7 +219,7 @@ export default [ 'src/components/modals/RadioListModal.tsx', 'src/components/modals/RawTextModal.tsx', 'src/components/modals/ScamWarningModal.tsx', - 'src/components/modals/ScanModal.tsx', + 'src/components/modals/StateProvinceListModal.tsx', 'src/components/modals/TransferModal.tsx', @@ -310,7 +309,7 @@ export default [ 'src/components/scenes/SwapSuccessScene.tsx', 'src/components/scenes/WalletRestoreScene.tsx', - 'src/components/scenes/WcConnectionsScene.tsx', + 'src/components/scenes/WcConnectScene.tsx', 'src/components/scenes/WcDisconnectScene.tsx', 'src/components/scenes/WebViewScene.tsx', diff --git a/src/actions/ScanActions.tsx b/src/actions/ScanActions.tsx index 166856d4dfd..70b5b8955d6 100644 --- a/src/actions/ScanActions.tsx +++ b/src/actions/ScanActions.tsx @@ -8,11 +8,16 @@ import { type EdgeTokenId } from 'edge-core-js' import * as React from 'react' +import { AppState } from 'react-native' +import RNPermissions from 'react-native-permissions' import { sprintf } from 'sprintf-js' import URL from 'url-parse' import { ButtonsModal } from '../components/modals/ButtonsModal' +import { CameraPermissionDeniedModal } from '../components/modals/CameraPermissionDeniedModal' import { ConfirmContinueModal } from '../components/modals/ConfirmContinueModal' +import { ScanModal, type ScanModalProps } from '../components/modals/ScanModal' +import { ScanScamWarningModal } from '../components/modals/ScanScamWarningModal' import { WalletListModal, type WalletListResult @@ -23,6 +28,7 @@ import { showError, showWarning } from '../components/services/AirshipInstance' +import { checkAndRequestPermission } from '../components/services/PermissionsManager' import { getSpecialCurrencyInfo } from '../constants/WalletAndCurrencyConstants' import { lstrings } from '../locales/strings' import { getExchangeDenom } from '../selectors/DenominationSelectors' @@ -42,6 +48,10 @@ import { import { toListString, zeroString } from '../util/utils' import { cleanQueryFlags, openBrowserUri } from '../util/WebUtils' import { checkAndShowLightBackupModal } from './BackupModalActions' +import { + getLocalAccountSettings, + writeCameraScamWarningShown +} from './LocalSettingsActions' const RUNONCE_KEY_PREFIX = 'shownWalletGetCryptoModal:' @@ -568,3 +578,68 @@ export function checkAndShowGetCryptoModal( } } } + +/** + * Resolves once the app is in the foreground. + * + * The OS permission prompt backgrounds the app, and its callback fires while + * the app is still inactive behind the system alert. Presenting a modal in + * that window is what makes it flash past during the return transition. + */ +const waitForForeground = async (): Promise => { + if (AppState.currentState === 'active') return + await new Promise(resolve => { + const subscription = AppState.addEventListener('change', state => { + if (state === 'active') { + subscription.remove() + resolve() + } + }) + }) +} + +/** + * Opens the QR scanner, sequencing the camera permission, the first-use scam + * warning, and the scanner itself so that none of them can overlap: + * + * 1. Request the OS camera permission, with no Edge modal on screen behind it. + * 2. If it is denied or blocked, show the Settings recovery guidance and stop. + * 3. On the first camera use only, show the scam warning and wait for the user + * to explicitly acknowledge it. + * 4. Only then mount the scanner. + * + * Resolves to the scanned string, or `undefined` if the user backed out at any + * point. + */ +export const showScanModal = + (props: ScanModalProps): ThunkAction> => + async (dispatch, getState) => { + const status = await dispatch(checkAndRequestPermission('camera')) + if ( + status !== RNPermissions.RESULTS.GRANTED && + status !== RNPermissions.RESULTS.LIMITED + ) { + await Airship.show(bridge => ( + + )) + return undefined + } + + await waitForForeground() + + const account = getState().core.account + const { cameraScamWarningShown } = await getLocalAccountSettings(account) + if (!cameraScamWarningShown) { + const acknowledged = await Airship.show(bridge => ( + + )) + // The warning is only dismissable by acknowledging it, so anything else + // means we were torn down (a logout, say). Don't record it as shown: + if (!acknowledged) return undefined + await writeCameraScamWarningShown(account) + } + + return await Airship.show(bridge => ( + + )) + } diff --git a/src/components/modals/ScanModal.tsx b/src/components/modals/ScanModal.tsx index 5d5d2dc3578..3eede46cb45 100644 --- a/src/components/modals/ScanModal.tsx +++ b/src/components/modals/ScanModal.tsx @@ -1,8 +1,7 @@ import * as React from 'react' -import { Linking, View } from 'react-native' +import { View } from 'react-native' import { type AirshipBridge, AirshipModal } from 'react-native-airship' import { launchImageLibrary } from 'react-native-image-picker' -import RNPermissions from 'react-native-permissions' import { useSafeAreaFrame } from 'react-native-safe-area-context' import Ionicon from 'react-native-vector-icons/Ionicons' import { @@ -12,16 +11,11 @@ import { useCodeScanner } from 'react-native-vision-camera' import RNQRGenerator from 'rn-qr-generator' -import { sprintf } from 'sprintf-js' import { useLayout } from '../../hooks/useLayout' import { lstrings } from '../../locales/strings' -import { config } from '../../theme/appConfig' -import { useDispatch, useSelector } from '../../types/reactRedux' import { triggerHaptic } from '../../util/haptic' import { logActivity } from '../../util/logger' -import { ModalButtons } from '../buttons/ModalButtons' -import { AlertCardUi4 } from '../cards/AlertCard' import { EdgeTouchableOpacity } from '../common/EdgeTouchableOpacity' import { QrPeephole } from '../common/QrPeephole' import { TextInputModal } from '../modals/TextInputModal' @@ -31,16 +25,12 @@ import { showError, showToast } from '../services/AirshipInstance' -import { checkAndRequestPermission } from '../services/PermissionsManager' import { cacheStyles, type Theme, useTheme } from '../services/ThemeContext' -import { EdgeText, Paragraph } from '../themed/EdgeText' +import { EdgeText } from '../themed/EdgeText' import { ModalFooter } from '../themed/ModalParts' import { SceneHeaderUi4 } from '../themed/SceneHeaderUi4' -import { EdgeModal } from './EdgeModal' - -interface Props { - bridge: AirshipBridge +export interface ScanModalProps { // The initial ScanModal title scanModalTitle: string @@ -52,6 +42,19 @@ interface Props { textModalTitle?: string } +interface Props extends ScanModalProps { + bridge: AirshipBridge +} + +/** + * The QR scanner camera sheet. + * + * This assumes the camera permission has already been granted and any + * first-use scam warning acknowledged -- `showScanModal` in + * `src/actions/ScanActions.tsx` owns that sequencing. Branching on the + * permission here is what used to make the scam warning flash away the instant + * the OS prompt was answered. + */ export const ScanModal: React.FC = props => { const { bridge, @@ -62,7 +65,6 @@ export const ScanModal: React.FC = props => { scanModalTitle } = props - const dispatch = useDispatch() const theme = useTheme() const styles = getStyles(theme) @@ -76,7 +78,6 @@ export const ScanModal: React.FC = props => { handleBarCodeRead(codes) } }) - const cameraPermission = useSelector(state => state.permissions.camera) const [torchEnabled, setTorchEnabled] = React.useState(false) const [scanEnabled, setScanEnabled] = React.useState(false) @@ -88,13 +89,10 @@ export const ScanModal: React.FC = props => { // Mount effects React.useEffect(() => { setScanEnabled(true) - dispatch(checkAndRequestPermission('camera')).catch((error: unknown) => { - showError(error) - }) return () => { setScanEnabled(false) } - }, [dispatch]) + }, []) const handleBarCodeRead = (codes: Code[]): void => { setScanEnabled(false) @@ -102,11 +100,6 @@ export const ScanModal: React.FC = props => { bridge.resolve(codes[0].value) } - const handleSettings = async (): Promise => { - triggerHaptic('impactLight') - await Linking.openSettings() - } - const handleTextInput = async (): Promise => { triggerHaptic('impactLight') const uri = await Airship.show(bridge => ( @@ -276,8 +269,7 @@ export const ScanModal: React.FC = props => { ) } - return cameraPermission === RNPermissions.RESULTS.GRANTED || - cameraPermission === RNPermissions.RESULTS.LIMITED ? ( + return ( = props => { - ) : ( - - {lstrings.scan_camera_permission_denied} - - - ) } const getStyles = cacheStyles((theme: Theme) => ({ bottomSpace: { height: theme.rem(1.5) }, - cameraPermissionContainer: { - padding: theme.rem(0.5) - }, // Camera View cameraContainer: { position: 'absolute', diff --git a/src/components/scenes/WcConnectionsScene.tsx b/src/components/scenes/WcConnectionsScene.tsx index b4178d4762f..8f5060ae2c1 100644 --- a/src/components/scenes/WcConnectionsScene.tsx +++ b/src/components/scenes/WcConnectionsScene.tsx @@ -8,6 +8,7 @@ import AntDesignIcon from 'react-native-vector-icons/AntDesign' import { sprintf } from 'sprintf-js' import { checkAndShowLightBackupModal } from '../../actions/BackupModalActions' +import { showScanModal } from '../../actions/ScanActions' import { SCROLL_INDICATOR_INSET_FIX } from '../../constants/constantSettings' import { SPECIAL_CURRENCY_INFO } from '../../constants/WalletAndCurrencyConstants' import { useAsyncEffect } from '../../hooks/useAsyncEffect' @@ -18,12 +19,11 @@ import { walletConnectClient } from '../../hooks/useWalletConnect' import { lstrings } from '../../locales/strings' -import { useSelector } from '../../types/reactRedux' +import { useDispatch, useSelector } from '../../types/reactRedux' import type { EdgeAppSceneProps, NavigationBase } from '../../types/routerTypes' import type { EdgeAsset, WcConnectionInfo } from '../../types/types' import { EdgeTouchableOpacity } from '../common/EdgeTouchableOpacity' import { SceneWrapper } from '../common/SceneWrapper' -import { ScanModal } from '../modals/ScanModal' import { WalletListModal, type WalletListResult @@ -41,7 +41,7 @@ export interface WcConnectionsParams { uri?: string } -export const WcConnectionsScene = (props: Props) => { +export const WcConnectionsScene: React.FC = props => { const { navigation, route } = props const { uri } = route.params ?? {} const theme = useTheme() @@ -53,11 +53,12 @@ export const WcConnectionsScene = (props: Props) => { >(new Map()) const account = useSelector(state => state.core.account) + const dispatch = useDispatch() const walletConnect = useWalletConnect() useMount(() => { if (uri != null) - onScanSuccess(uri).catch(err => { + onScanSuccess(uri).catch((err: unknown) => { showError(err) }) }) @@ -72,7 +73,7 @@ export const WcConnectionsScene = (props: Props) => { 'WcConnectionsScene' ) - const onScanSuccess = async (qrResult: string) => { + const onScanSuccess = async (qrResult: string): Promise => { setConnecting(true) try { let proposal = sessionProposal.get(qrResult) @@ -119,22 +120,23 @@ export const WcConnectionsScene = (props: Props) => { setConnecting(false) } - const handleActiveConnectionPress = (wcConnectionInfo: WcConnectionInfo) => { + const handleActiveConnectionPress = ( + wcConnectionInfo: WcConnectionInfo + ): void => { navigation.navigate('wcDisconnect', { wcConnectionInfo }) } - const handleNewConnectionPress = async () => { + const handleNewConnectionPress = async (): Promise => { if (checkAndShowLightBackupModal(account, navigation as NavigationBase)) { await Promise.resolve() } else { - const result = await Airship.show(bridge => ( - - )) + const result = await dispatch( + showScanModal({ + scanModalTitle: lstrings.scan_qr_label, + textModalHint: lstrings.wc_scan_modal_text_modal_hint, + textModalTitle: lstrings.wc_scan_modal_text_modal_title + }) + ) if (result != null) { await onScanSuccess(result) } diff --git a/src/components/themed/SideMenu.tsx b/src/components/themed/SideMenu.tsx index 0dece84236f..4e20eead521 100644 --- a/src/components/themed/SideMenu.tsx +++ b/src/components/themed/SideMenu.tsx @@ -33,6 +33,7 @@ import { navigateToGiftCards } from '../../actions/GiftCardActions' import { useNotifCount } from '../../actions/LocalSettingsActions' import { getRootNavigation, logoutRequest } from '../../actions/LoginActions' import { executePluginAction } from '../../actions/PluginActions' +import { showScanModal } from '../../actions/ScanActions' import { Fontello } from '../../assets/vector' import { SCROLL_INDICATOR_INSET_FIX } from '../../constants/constantSettings' import { ENV } from '../../env' @@ -52,7 +53,6 @@ import { styled } from '../hoc/styled' import { IconBadge } from '../icons/IconBadge' import { ChevronDownIcon, CloseIcon } from '../icons/ThemedIcons' import { ButtonsModal } from '../modals/ButtonsModal' -import { ScanModal } from '../modals/ScanModal' import { Airship, showError } from '../services/AirshipInstance' import { Services } from '../services/Services' import { cacheStyles, type Theme, useTheme } from '../services/ThemeContext' @@ -164,16 +164,15 @@ export function SideMenuComponent(props: Props): React.ReactElement { const handleScanQr = (): void => { navigation.dispatch(DrawerActions.closeDrawer()) - Airship.show(bridge => ( - - )) + dispatch( + showScanModal({ + scanModalTitle: lstrings.scan_qr_label, + textModalAutoFocus: false, + textModalTitle: lstrings.enter_any_title, + textModalBody: lstrings.enter_any_body, + textModalHint: lstrings.enter_any_input_hint + }) + ) .then(async (result: string | undefined) => { if (result != null && result !== '') { const deepLink = parseDeepLink(result) diff --git a/src/components/tiles/AddressTile2.tsx b/src/components/tiles/AddressTile2.tsx index 4ce6c84385a..7483a5af4f8 100644 --- a/src/components/tiles/AddressTile2.tsx +++ b/src/components/tiles/AddressTile2.tsx @@ -14,12 +14,12 @@ import FontAwesome5 from 'react-native-vector-icons/FontAwesome5' import { sprintf } from 'sprintf-js' import { launchPaymentProto } from '../../actions/PaymentProtoActions' -import { addressWarnings } from '../../actions/ScanActions' +import { addressWarnings, showScanModal } from '../../actions/ScanActions' import { useHandler } from '../../hooks/useHandler' import { useMount } from '../../hooks/useMount' import { lstrings } from '../../locales/strings' import { PaymentProtoError } from '../../types/PaymentProtoError' -import { useSelector } from '../../types/reactRedux' +import { useDispatch, useSelector } from '../../types/reactRedux' import type { NavigationBase } from '../../types/routerTypes' import { getCurrencyCode } from '../../util/CurrencyInfoHelpers' import { parseDeepLink } from '../../util/DeepLinkParser' @@ -33,7 +33,6 @@ import { EdgeTouchableOpacity } from '../common/EdgeTouchableOpacity' import { AddressModal } from '../modals/AddressModal' import { showFullScreenSpinner } from '../modals/AirshipFullScreenSpinner' import { ConfirmContinueModal } from '../modals/ConfirmContinueModal' -import { ScanModal } from '../modals/ScanModal' import { WalletListModal, type WalletListResult @@ -148,6 +147,7 @@ export const AddressTile2 = React.forwardRef( // Selectors: const account = useSelector(state => state.core.account) + const dispatch = useDispatch() const fioPlugin = account.currencyConfig.fio const currencyCode = getCurrencyCode(coreWallet, tokenId) @@ -392,15 +392,14 @@ export const AddressTile2 = React.forwardRef( lstrings.send_scan_modal_text_modal_message_s, currencyCode ) - Airship.show(bridge => ( - - )) + dispatch( + showScanModal({ + scanModalTitle: lstrings.scan_qr_label, + textModalHint: lstrings.send_scan_modal_text_modal_hint, + textModalBody: message, + textModalTitle: title + }) + ) .then(async (result: string | undefined) => { if (result == null) return await changeAddress(result, 'scan')