From 3aa532e456a9040b7abbe067fbfee4874775fd8c Mon Sep 17 00:00:00 2001 From: Jon Tzeng Date: Mon, 19 Jan 2026 15:35:43 -0800 Subject: [PATCH 1/2] Fix EdgeSpend country selection flow --- src/actions/GiftCardActions.tsx | 46 +++++++++++++++++++ src/components/scenes/GiftCardMarketScene.tsx | 25 ++++++++++ src/components/scenes/HomeScene.tsx | 11 ++--- src/components/themed/SideMenu.tsx | 15 ++---- 4 files changed, 78 insertions(+), 19 deletions(-) create mode 100644 src/actions/GiftCardActions.tsx diff --git a/src/actions/GiftCardActions.tsx b/src/actions/GiftCardActions.tsx new file mode 100644 index 00000000000..0780e78866f --- /dev/null +++ b/src/actions/GiftCardActions.tsx @@ -0,0 +1,46 @@ +import { hasStoredPhazeIdentity } from '../plugins/gift-cards/phazeGiftCardProvider' +import type { ThunkAction } from '../types/reduxTypes' +import type { NavigationBase } from '../types/routerTypes' +import { showCountrySelectionModal } from './CountryListActions' +import { readSyncedSettings } from './SettingsActions' + +/** + * Navigates to the appropriate gift card scene (list or market) after ensuring + * a country is selected. Shows a country selection modal if needed. + * + * @returns true if navigation occurred, false if user cancelled country selection + */ +export const navigateToGiftCards = + (navigation: NavigationBase): ThunkAction> => + async (dispatch, getState) => { + const state = getState() + const { account } = state.core + let { countryCode } = state.ui.settings + + // Ensure country is set before proceeding + if (countryCode === '') { + await dispatch( + showCountrySelectionModal({ + account, + countryCode: '', + skipStateProvince: true + }) + ) + // Re-read from synced settings to determine if user actually selected + const synced = await readSyncedSettings(account) + countryCode = synced.countryCode ?? '' + } + + // User cancelled country selection + if (countryCode === '') { + return false + } + + // Navigate to list if user has purchased before, otherwise market + const hasIdentity = await hasStoredPhazeIdentity(account) + navigation.navigate('edgeAppStack', { + screen: hasIdentity ? 'giftCardList' : 'giftCardMarket' + }) + + return true + } diff --git a/src/components/scenes/GiftCardMarketScene.tsx b/src/components/scenes/GiftCardMarketScene.tsx index 55547d94d2e..cec9430097e 100644 --- a/src/components/scenes/GiftCardMarketScene.tsx +++ b/src/components/scenes/GiftCardMarketScene.tsx @@ -6,10 +6,12 @@ import LinearGradient from 'react-native-linear-gradient' import Animated from 'react-native-reanimated' import { showCountrySelectionModal } from '../../actions/CountryListActions' +import { readSyncedSettings } from '../../actions/SettingsActions' import { EDGE_CONTENT_SERVER_URI } from '../../constants/CdnConstants' import { SCROLL_INDICATOR_INSET_FIX } from '../../constants/constantSettings' import { guiPlugins } from '../../constants/plugins/GuiPlugins' import { ENV } from '../../env' +import { useAsyncEffect } from '../../hooks/useAsyncEffect' import { useGiftCardProvider } from '../../hooks/useGiftCardProvider' import { useHandler } from '../../hooks/useHandler' import { lstrings } from '../../locales/strings' @@ -159,6 +161,29 @@ export const GiftCardMarketScene: React.FC = props => { const handleScroll = useSceneScrollHandler() + // Fallback check for deep links or other direct navigation to this scene + // without going through navigateToGiftCards helper + useAsyncEffect( + async () => { + if (countryCode !== '') return + + await dispatch( + showCountrySelectionModal({ + account, + countryCode: '', + skipStateProvince: true + }) + ) + // Re-read from synced settings to determine if user actually selected + const synced = await readSyncedSettings(account) + if ((synced.countryCode ?? '') === '') { + navigation.goBack() + } + }, + [], + 'GiftCardMarketScene:countryCheck' + ) + // Helper to map brand response to MarketItem const mapBrandsToItems = React.useCallback( (brands: PhazeGiftCardBrand[]): MarketItem[] => diff --git a/src/components/scenes/HomeScene.tsx b/src/components/scenes/HomeScene.tsx index ea38501a85d..e8b141579e3 100644 --- a/src/components/scenes/HomeScene.tsx +++ b/src/components/scenes/HomeScene.tsx @@ -5,15 +5,15 @@ import FastImage from 'react-native-fast-image' import Animated from 'react-native-reanimated' import { useSafeAreaFrame } from 'react-native-safe-area-context' +import { navigateToGiftCards } from '../../actions/GiftCardActions' import { SCROLL_INDICATOR_INSET_FIX } from '../../constants/constantSettings' import { guiPlugins } from '../../constants/plugins/GuiPlugins' import { ENV } from '../../env' import { useHandler } from '../../hooks/useHandler' import { lstrings } from '../../locales/strings' -import { hasStoredPhazeIdentity } from '../../plugins/gift-cards/phazeGiftCardProvider' import { useSceneScrollHandler } from '../../state/SceneScrollState' import { config } from '../../theme/appConfig' -import { useSelector } from '../../types/reactRedux' +import { useDispatch, useSelector } from '../../types/reactRedux' import type { EdgeTabsSceneProps, NavigationBase @@ -84,9 +84,9 @@ export const HomeScene: React.FC = props => { const { navigation } = props const theme = useTheme() const styles = getStyles(theme) + const dispatch = useDispatch() const countryCode = useSelector(state => state.ui.countryCode) - const account = useSelector(state => state.core.account) const { width: screenWidth } = useSafeAreaFrame() @@ -120,10 +120,7 @@ export const HomeScene: React.FC = props => { navigation.navigate('pluginView', { plugin: guiPlugins.bitrefill }) return } - const hasIdentity = await hasStoredPhazeIdentity(account) - navigation.navigate('edgeAppStack', { - screen: hasIdentity ? 'giftCardList' : 'giftCardMarket' - }) + await dispatch(navigateToGiftCards(navigation as NavigationBase)) }) const handleViewAssetsPress = useHandler(() => { navigation.navigate('edgeTabs', { diff --git a/src/components/themed/SideMenu.tsx b/src/components/themed/SideMenu.tsx index 696141d550a..aaa6e664aa0 100644 --- a/src/components/themed/SideMenu.tsx +++ b/src/components/themed/SideMenu.tsx @@ -27,11 +27,9 @@ import FontAwesome5Icon from 'react-native-vector-icons/FontAwesome5' import Ionicons from 'react-native-vector-icons/Ionicons' import { sprintf } from 'sprintf-js' -import { - checkAndShowLightBackupModal, - showBackupModal -} from '../../actions/BackupModalActions' +import { showBackupModal } from '../../actions/BackupModalActions' import { launchDeepLink } from '../../actions/DeepLinkingActions' +import { navigateToGiftCards } from '../../actions/GiftCardActions' import { useNotifCount } from '../../actions/LocalSettingsActions' import { getRootNavigation, logoutRequest } from '../../actions/LoginActions' import { executePluginAction } from '../../actions/PluginActions' @@ -40,7 +38,6 @@ import { SCROLL_INDICATOR_INSET_FIX } from '../../constants/constantSettings' import { ENV } from '../../env' import { useWatch } from '../../hooks/useWatch' import { lstrings } from '../../locales/strings' -import { hasStoredPhazeIdentity } from '../../plugins/gift-cards/phazeGiftCardProvider' import { getDefaultFiat } from '../../selectors/SettingsSelectors' import { config } from '../../theme/appConfig' import { useDispatch, useSelector } from '../../types/reactRedux' @@ -318,13 +315,7 @@ export function SideMenuComponent(props: Props): React.ReactElement { { handlePress: async () => { navigation.dispatch(DrawerActions.closeDrawer()) - // Light accounts need to back up before using gift cards - if (checkAndShowLightBackupModal(account, navigationBase)) return - const hasIdentity = await hasStoredPhazeIdentity(account) - // Navigate to gift card list only if we have identities - navigation.navigate('edgeAppStack', { - screen: hasIdentity ? 'giftCardList' : 'giftCardMarket' - }) + await dispatch(navigateToGiftCards(navigationBase)) }, iconNameFontAwesome: 'gift', title: lstrings.gift_card_branded From 2eb6f8482878b43ca6c877572d43e7e1941365fd Mon Sep 17 00:00:00 2001 From: Jon Tzeng Date: Mon, 19 Jan 2026 15:36:06 -0800 Subject: [PATCH 2/2] Block light accounts from purchasing cards --- CHANGELOG.md | 1 + src/components/scenes/GiftCardPurchaseScene.tsx | 6 ++++++ 2 files changed, 7 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 80d992df10a..ee5c84566e8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,7 @@ - added: `chooseCaip19Asset` EdgeProvider API for precise wallet selection using CAIP-19 identifiers - added: Pass OS and app version details to core context for v2/coreRollup endpoint +- added: EdgeSpend feature for gift card purchase via Phaze - changed: Append chain names to token codes in RampCreateScene ## 4.42.0 (staging) diff --git a/src/components/scenes/GiftCardPurchaseScene.tsx b/src/components/scenes/GiftCardPurchaseScene.tsx index 22eeef3c5f3..1ab15e8b975 100644 --- a/src/components/scenes/GiftCardPurchaseScene.tsx +++ b/src/components/scenes/GiftCardPurchaseScene.tsx @@ -15,6 +15,7 @@ import Ionicons from 'react-native-vector-icons/Ionicons' import { sprintf } from 'sprintf-js' import { v4 as uuidv4 } from 'uuid' +import { checkAndShowLightBackupModal } from '../../actions/BackupModalActions' import { getFiatSymbol } from '../../constants/WalletAndCurrencyConstants' import { ENV } from '../../env' import { displayFiatAmount } from '../../hooks/useFiatText' @@ -324,6 +325,11 @@ export const GiftCardPurchaseScene: React.FC = props => { return } + // Light accounts need to back up before purchasing gift cards + if (checkAndShowLightBackupModal(account, navigation as NavigationBase)) { + return + } + // Show wallet selection modal with only supported assets const walletResult = await Airship.show(bridge => (