From 3cd31a8f82c9c1a1650250b8b989cd53e3dde906 Mon Sep 17 00:00:00 2001 From: Jon Tzeng Date: Wed, 11 Feb 2026 15:47:42 -0800 Subject: [PATCH 01/11] Make Phaze order cleaners fault-tolerant Add asTolerantArray helper that skips malformed items instead of throwing on the entire array. Apply it to brand lists and voucher arrays so a single bad entry doesn't break the whole UI. Also add 'failed' to recognized order statuses and make deliveryAddress optional with an empty-string default. --- src/plugins/gift-cards/phazeGiftCardTypes.ts | 30 ++++++++++++++++---- 1 file changed, 24 insertions(+), 6 deletions(-) diff --git a/src/plugins/gift-cards/phazeGiftCardTypes.ts b/src/plugins/gift-cards/phazeGiftCardTypes.ts index f0d227ace73..912ad0df1d8 100644 --- a/src/plugins/gift-cards/phazeGiftCardTypes.ts +++ b/src/plugins/gift-cards/phazeGiftCardTypes.ts @@ -3,10 +3,12 @@ import { asBoolean, asDate, asEither, + asMaybe, asNumber, asObject, asOptional, asString, + asUnknown, asValue, type Cleaner } from 'cleaners' @@ -50,6 +52,21 @@ const asNumberOrNumericString: Cleaner = asEither( } ) +/** + * Tolerant array cleaner: skips items that fail cleaning instead of throwing. + */ +const asTolerantArray = + (cleaner: Cleaner): Cleaner => + (raw: unknown): T[] => { + const arr = asArray(asUnknown)(raw) + const out: T[] = [] + for (const item of arr) { + const cleaned = asMaybe(cleaner)(item) + if (cleaned != null) out.push(cleaned) + } + return out + } + /** * Cleaner for denominations array that deduplicates values. * The Phaze API sometimes returns duplicate denominations. @@ -192,7 +209,7 @@ export type PhazeGiftCardBrand = ReturnType export const asPhazeGiftCardsResponse = asObject({ country: asString, countryCode: asString, - brands: asArray(asPhazeGiftCardBrand), + brands: asTolerantArray(asPhazeGiftCardBrand), currentPage: asNumber, totalCount: asNumber }) @@ -239,7 +256,8 @@ export const asPhazeOrderStatusValue = asEither( asValue('complete'), asValue('pending'), asValue('processing'), - asValue('expired') + asValue('expired'), + asValue('failed') ) export type PhazeOrderStatusValue = ReturnType @@ -247,7 +265,7 @@ export const asPhazeCreateOrderResponse = asObject({ externalUserId: asString, quoteId: asString, status: asPhazeOrderStatusValue, - deliveryAddress: asString, + deliveryAddress: asOptional(asString, ''), tokenIdentifier: asString, quantity: asNumber, amountInUSD: asNumber, @@ -297,7 +315,7 @@ export const asPhazeCompletedCartItem = asObject({ status: asOptional(asPhazeCartItemStatus), faceValue: asOptional(asNumber), voucherCurrency: asOptional(asString), - vouchers: asOptional(asArray(asPhazeVoucher)), + vouchers: asOptional(asTolerantArray(asPhazeVoucher)), // Additional fields we may use externalUserId: asOptional(asString), voucherDiscountPercent: asOptional(asNumber), @@ -315,7 +333,7 @@ export const asPhazeOrderStatusItem = asObject({ externalUserId: asString, quoteId: asString, status: asPhazeOrderStatusValue, - deliveryAddress: asString, + deliveryAddress: asOptional(asString, ''), tokenIdentifier: asString, quantity: asNumber, amountInUSD: asNumber, @@ -326,7 +344,7 @@ export const asPhazeOrderStatusItem = asObject({ export type PhazeOrderStatusItem = ReturnType export const asPhazeOrderStatusResponse = asObject({ - data: asArray(asPhazeOrderStatusItem), + data: asTolerantArray(asPhazeOrderStatusItem), totalCount: asNumber }) export type PhazeOrderStatusResponse = ReturnType< From 0d20feb13c88286bc621c0d6f2c7685f482f6394 Mon Sep 17 00:00:00 2001 From: Jon Tzeng Date: Wed, 11 Feb 2026 15:48:42 -0800 Subject: [PATCH 02/11] Improve error resilience across gift card scenes Expose isError/error from useGiftCardProvider so scenes can detect provider initialization failures. On GiftCardListScene, stop clearing orders on API errors (keep last-known-good data visible), pause polling when offline and auto-resume on reconnect, and show an informational warning banner that auto-clears on success. On GiftCardMarketScene, gate the brand query on network connectivity so it auto-retries when online, and show a warning instead of an infinite loader when the initial fetch fails. On GiftCardPurchaseScene, guard against empty delivery addresses and show a warning when the provider fails to initialize. --- src/components/scenes/GiftCardListScene.tsx | 66 ++++++++++++++++--- src/components/scenes/GiftCardMarketScene.tsx | 21 ++++-- .../scenes/GiftCardPurchaseScene.tsx | 52 +++++++++++++-- src/hooks/useGiftCardProvider.ts | 11 +++- src/locales/en_US.ts | 10 +++ src/locales/strings/enUS.json | 5 ++ yarn.lock | 4 +- 7 files changed, 147 insertions(+), 22 deletions(-) diff --git a/src/components/scenes/GiftCardListScene.tsx b/src/components/scenes/GiftCardListScene.tsx index 6e1b331e11f..a16486156f2 100644 --- a/src/components/scenes/GiftCardListScene.tsx +++ b/src/components/scenes/GiftCardListScene.tsx @@ -27,6 +27,7 @@ import type { EdgeAppSceneProps } from '../../types/routerTypes' import { debugLog } from '../../util/logger' import { makePeriodicTask } from '../../util/PeriodicTask' import { SceneButtons } from '../buttons/SceneButtons' +import { AlertCardUi4 } from '../cards/AlertCard' import { EdgeCard } from '../cards/EdgeCard' import { GiftCardDisplayCard, @@ -79,11 +80,13 @@ export const GiftCardListScene: React.FC = (props: Props) => { const dispatch = useDispatch() const account = useSelector(state => state.core.account) + const isConnected = useSelector(state => state.network.isConnected) const { countryCode, stateProvinceCode } = useSelector( state => state.ui.settings ) - // Clear cache if account changed (prevents data leaking between users) + // Clear module-level cache if account changed (prevents data leaking + // between users). Runs before useState so initializers see empty caches. if (cachedAccountId !== account.id) { clearOrderCache() cachedAccountId = account.id @@ -109,6 +112,20 @@ export const GiftCardListScene: React.FC = (props: Props) => { React.useState(cachedRedeemedOrders) // Only show loading on very first load; subsequent mounts refresh silently const [isLoading, setIsLoading] = React.useState(!hasLoadedOnce) + // Error flag for API load failures (informational only, auto-clears on success) + const [loadError, setLoadError] = React.useState(false) + + // Reset React state on account switch. Module-level caches are already + // cleared above (before useState), but useState initializers only run on + // mount, so re-renders with a different account keep stale values. + const prevAccountIdRef = React.useRef(account.id) + if (prevAccountIdRef.current !== account.id) { + prevAccountIdRef.current = account.id + setActiveOrders([]) + setRedeemedOrders([]) + setIsLoading(true) + setLoadError(false) + } // Footer height for floating button const [footerHeight, setFooterHeight] = React.useState() @@ -169,11 +186,12 @@ export const GiftCardListScene: React.FC = (props: Props) => { } applyAugments(allOrders) + setLoadError(false) return didFetchBrands } catch (err: unknown) { debugLog('phaze', 'Error loading orders:', err) - setActiveOrders([]) - setRedeemedOrders([]) + // Keep existing cached orders visible — don't clear state on error + setLoadError(true) return false } finally { setIsLoading(false) @@ -200,9 +218,17 @@ export const GiftCardListScene: React.FC = (props: Props) => { ) // Reload orders when scene comes into focus, then poll periodically - // to detect when pending orders receive their vouchers + // to detect when pending orders receive their vouchers. + // Polling pauses when offline and auto-resumes when connectivity returns. useFocusEffect( React.useCallback(() => { + // Don't attempt API calls while offline. Clear loading state so the + // scene can show the offline error or empty state instead of spinning. + if (!isConnected) { + setIsLoading(false) + return + } + // First load: fetch both brands and orders // Subsequent loads: only fetch orders (brands change infrequently) const includeBrands = !hasFetchedBrands @@ -225,7 +251,7 @@ export const GiftCardListScene: React.FC = (props: Props) => { return () => { task.stop() } - }, [loadOrdersFromApi]) + }, [isConnected, loadOrdersFromApi]) ) const handlePurchaseNew = useHandler(async () => { @@ -390,19 +416,25 @@ export const GiftCardListScene: React.FC = (props: Props) => { ) }, - [handleFooterLayoutHeight, handlePurchaseNew] + [handleFooterLayoutHeight, handlePurchaseNew, isConnected, loadError] ) const hasNoCards = activeOrders.length === 0 && redeemedOrders.length === 0 return ( - + {({ insetStyle, undoInsetStyle }) => ( = (props: Props) => { > {isLoading ? ( + ) : hasNoCards && loadError ? ( + + {isConnected + ? lstrings.gift_card_service_error + : lstrings.gift_card_network_error} + ) : hasNoCards ? ( {lstrings.gift_card_list_no_cards} ) : ( <> + {/* Error banner when data exists but refresh failed */} + {loadError ? ( + + ) : null} + {/* Active Cards Section */} {activeOrders.map(order => renderCard(order))} diff --git a/src/components/scenes/GiftCardMarketScene.tsx b/src/components/scenes/GiftCardMarketScene.tsx index cec9430097e..b34437fe8eb 100644 --- a/src/components/scenes/GiftCardMarketScene.tsx +++ b/src/components/scenes/GiftCardMarketScene.tsx @@ -23,6 +23,7 @@ import { useDispatch, useSelector } from '../../types/reactRedux' import type { EdgeAppSceneProps } from '../../types/routerTypes' import { debugLog } from '../../util/logger' import { CountryButton } from '../buttons/RegionButton' +import { AlertCardUi4 } from '../cards/AlertCard' import { EdgeCard } from '../cards/EdgeCard' import { GiftCardTile } from '../cards/GiftCardTile' import { CircularBrandIcon } from '../common/CircularBrandIcon' @@ -107,6 +108,7 @@ export const GiftCardMarketScene: React.FC = props => { // Get user's current country code (specific selector to avoid re-renders on other setting changes) const countryCode = useSelector(state => state.ui.settings.countryCode) const account = useSelector(state => state.core.account) + const isConnected = useSelector(state => state.network.isConnected) // Provider (requires API key configured) const phazeConfig = ENV.PLUGIN_API_KEYS?.phaze @@ -238,8 +240,10 @@ export const GiftCardMarketScene: React.FC = props => { prevCountryCodeRef.current = countryCode }, [countryCode]) - // Fetch brands. Initial data comes from synchronous cache read in useState - const { data: apiBrands } = useQuery({ + // Fetch brands. Initial data comes from synchronous cache read in useState. + // Adding isConnected to enabled so the query auto-retries when connectivity + // returns after being offline. + const { data: apiBrands, isError: isBrandsError } = useQuery({ queryKey: ['phazeBrands', countryCode, isReady], queryFn: async () => { if (provider == null || cache == null) { @@ -279,7 +283,7 @@ export const GiftCardMarketScene: React.FC = props => { return allBrands }, - enabled: isReady && provider != null && countryCode !== '', + enabled: isConnected && isReady && provider != null && countryCode !== '', staleTime: 5 * 60 * 1000, // 5 minutes gcTime: 10 * 60 * 1000, retry: 1 @@ -489,7 +493,16 @@ export const GiftCardMarketScene: React.FC = props => { headerTitle={lstrings.title_gift_card_market} headerTitleChildren={} > - {items == null ? ( + {items == null && isBrandsError ? ( + + ) : items == null ? ( ) : ( <> diff --git a/src/components/scenes/GiftCardPurchaseScene.tsx b/src/components/scenes/GiftCardPurchaseScene.tsx index 640bf3b9967..0814ec4185e 100644 --- a/src/components/scenes/GiftCardPurchaseScene.tsx +++ b/src/components/scenes/GiftCardPurchaseScene.tsx @@ -91,11 +91,16 @@ export const GiftCardPurchaseScene: React.FC = props => { const { width: screenWidth } = useWindowDimensions() const account = useSelector(state => state.core.account) + const isConnected = useSelector(state => state.network.isConnected) // Provider (requires API key configured) const phazeConfig = (ENV.PLUGIN_API_KEYS as Record) ?.phaze as { apiKey?: string; baseUrl?: string } | undefined - const { provider, isReady } = useGiftCardProvider({ + const { + provider, + isReady, + isError: isProviderError + } = useGiftCardProvider({ account, apiKey: phazeConfig?.apiKey ?? '', baseUrl: phazeConfig?.baseUrl ?? '' @@ -131,7 +136,7 @@ export const GiftCardPurchaseScene: React.FC = props => { const [error, setError] = React.useState(null) // Fetch allowed tokens from Phaze API - const { data: tokenQueryResult } = useQuery({ + const { data: tokenQueryResult, refetch: refetchTokens } = useQuery({ queryKey: ['phazeTokens', account?.id, isReady], queryFn: async () => { if (provider == null) { @@ -360,13 +365,34 @@ export const GiftCardPurchaseScene: React.FC = props => { return } + // Ensure token data is available before proceeding. If a previous fetch + // failed, attempt a fresh fetch so the user isn't stuck on a dead-end. + let resolvedAssets = allowedAssets + if (resolvedAssets == null) { + setIsCreatingOrder(true) + try { + const { data } = await refetchTokens() + resolvedAssets = data?.assets + } catch (err: unknown) { + showError(err) + return + } finally { + setIsCreatingOrder(false) + } + } + + if (resolvedAssets == null || resolvedAssets.length === 0) { + showError(new Error(lstrings.gift_card_no_supported_assets)) + return + } + // Show wallet selection modal with only supported assets const walletResult = await Airship.show(bridge => ( )) @@ -450,6 +476,11 @@ export const GiftCardPurchaseScene: React.FC = props => { quoteExpiry: orderResponse.quoteExpiry }) + // Ensure we have a payment address before navigating to send + if (orderResponse.deliveryAddress === '') { + throw new Error(lstrings.gift_card_no_payment_address) + } + // Store the order for the onDone callback pendingOrderRef.current = orderResponse @@ -688,7 +719,7 @@ export const GiftCardPurchaseScene: React.FC = props => { primary={{ label: lstrings.string_next_capitalized, onPress: handleNextPress, - disabled: !isAmountValid || isCreatingOrder, + disabled: !isAmountValid || isCreatingOrder || isProviderError, spinner: isCreatingOrder }} /> @@ -782,8 +813,17 @@ export const GiftCardPurchaseScene: React.FC = props => { )} - {/* Warnings/Errors - product unavailable takes precedence */} - {productUnavailable ? ( + {/* Warnings/Errors - provider error, product unavailable, minimum, or general */} + {isProviderError ? ( + + ) : productUnavailable ? ( { const instance = makePhazeGiftCardProvider({ @@ -36,5 +43,5 @@ export function useGiftCardProvider(options: UseGiftCardProviderOptions): { gcTime: 300000 }) - return { provider, isReady: isSuccess } + return { provider, isReady: isSuccess, isError, error } } diff --git a/src/locales/en_US.ts b/src/locales/en_US.ts index e274ddf1b8f..6cc0ef4726d 100644 --- a/src/locales/en_US.ts +++ b/src/locales/en_US.ts @@ -1933,8 +1933,16 @@ const strings = { gift_card_slider_terms: 'By sliding to confirm, you are agreeing to the {{link}}gift card terms and conditions{{/link}}.', gift_card_more_options: 'Browse more gift cards', + gift_card_no_payment_address: + 'Order created but no payment address was returned. Please try again.', gift_card_network_error: 'Unable to load gift cards. Please check your network connection.', + gift_card_service_error: + 'Gift card service is temporarily unavailable. Please try again later.', + gift_card_refresh_error: + 'Unable to refresh. Card information may not be up to date.', + gift_card_refresh_service_error: + 'Gift card service unavailable. Card information may not be up to date.', gift_card_minimum_warning_title: 'Below Minimum', gift_card_minimum_warning_header_1s: 'The selected amount is below the minimum for %1$s.', @@ -1952,6 +1960,8 @@ const strings = { gift_card_product_unavailable_title: 'Temporarily Unavailable', gift_card_product_unavailable_warning: 'Card is temporarily unavailable. Please select another card from this brand or try again later.', + gift_card_no_supported_assets: + 'No supported payment methods available. Please try again later.', // #endregion diff --git a/src/locales/strings/enUS.json b/src/locales/strings/enUS.json index d3ce2839f27..89f2f7858e6 100644 --- a/src/locales/strings/enUS.json +++ b/src/locales/strings/enUS.json @@ -1506,7 +1506,11 @@ "gift_card_terms_and_conditions_body": "By purchasing a gift card, you are agreeing to the terms and conditions that apply. {{link}}Read the terms and conditions here.{{/link}}", "gift_card_slider_terms": "By sliding to confirm, you are agreeing to the {{link}}gift card terms and conditions{{/link}}.", "gift_card_more_options": "Browse more gift cards", + "gift_card_no_payment_address": "Order created but no payment address was returned. Please try again.", "gift_card_network_error": "Unable to load gift cards. Please check your network connection.", + "gift_card_service_error": "Gift card service is temporarily unavailable. Please try again later.", + "gift_card_refresh_error": "Unable to refresh. Card information may not be up to date.", + "gift_card_refresh_service_error": "Gift card service unavailable. Card information may not be up to date.", "gift_card_minimum_warning_title": "Below Minimum", "gift_card_minimum_warning_header_1s": "The selected amount is below the minimum for %1$s.", "gift_card_minimum_warning_footer_1s": "Please select a different payment method or increase your purchase amount to at least %1$s.", @@ -1519,6 +1523,7 @@ "gift_card_quote_expired_toast": "Your quote has expired. Please try again.", "gift_card_product_unavailable_title": "Temporarily Unavailable", "gift_card_product_unavailable_warning": "Card is temporarily unavailable. Please select another card from this brand or try again later.", + "gift_card_no_supported_assets": "No supported payment methods available. Please try again later.", "countdown_hours": ">%sh", "countdown_minutes_seconds": "%sm %ss", "countdown_seconds": "%ss", diff --git a/yarn.lock b/yarn.lock index 8e221c0d71f..274b28906bf 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2120,9 +2120,9 @@ randombytes "^2.1.0" text-encoding "0.7.0" -"@fioprotocol/fiosdk@git+https://github.com/EdgeApp/fiosdk_typescript.git#47df5818442edec69b735d6a723747aad33b8d71": +"@fioprotocol/fiosdk@https://github.com/EdgeApp/fiosdk_typescript.git#47df5818442edec69b735d6a723747aad33b8d71": version "1.9.0" - resolved "git+https://github.com/EdgeApp/fiosdk_typescript.git#47df5818442edec69b735d6a723747aad33b8d71" + resolved "https://github.com/EdgeApp/fiosdk_typescript.git#47df5818442edec69b735d6a723747aad33b8d71" dependencies: "@fioprotocol/fiojs" "1.0.1" "@types/text-encoding" "0.0.35" From 59c0f67ed92318676d527bd182103670a4745f64 Mon Sep 17 00:00:00 2001 From: Jon Tzeng Date: Wed, 11 Feb 2026 16:22:23 -0800 Subject: [PATCH 03/11] Prevent double-tap on Next in gift card purchase scene Add isCreatingOrder to the early-return guard in handleNextPress so a second tap during the brief window before React disables the button cannot trigger duplicate order creation. --- src/components/scenes/GiftCardPurchaseScene.tsx | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/src/components/scenes/GiftCardPurchaseScene.tsx b/src/components/scenes/GiftCardPurchaseScene.tsx index 0814ec4185e..e7855224f9c 100644 --- a/src/components/scenes/GiftCardPurchaseScene.tsx +++ b/src/components/scenes/GiftCardPurchaseScene.tsx @@ -356,7 +356,12 @@ export const GiftCardPurchaseScene: React.FC = props => { }) const handleNextPress = useHandler(async () => { - if (selectedAmount == null || provider == null || !isReady) { + if ( + selectedAmount == null || + provider == null || + !isReady || + isCreatingOrder + ) { return } From 978fbdfb97ce0e11f0c8be3f6e1e56c7164a05e1 Mon Sep 17 00:00:00 2001 From: Jon Tzeng Date: Wed, 11 Feb 2026 17:09:25 -0800 Subject: [PATCH 04/11] Add confirming and failed states to gift card display card Distinguish between awaiting blockchain confirmations (txid exists but no voucher yet), pending voucher delivery, and failed/expired orders. Failed cards are dimmed like redeemed cards. --- src/components/cards/GiftCardDisplayCard.tsx | 29 +++++++++++++++----- src/components/scenes/GiftCardListScene.tsx | 15 ++++++++-- src/locales/en_US.ts | 2 ++ src/locales/strings/enUS.json | 2 ++ 4 files changed, 38 insertions(+), 10 deletions(-) diff --git a/src/components/cards/GiftCardDisplayCard.tsx b/src/components/cards/GiftCardDisplayCard.tsx index 9ce751064ba..c1c1c5c0871 100644 --- a/src/components/cards/GiftCardDisplayCard.tsx +++ b/src/components/cards/GiftCardDisplayCard.tsx @@ -25,11 +25,18 @@ const ZOOM_FACTOR = 1.025 /** * Card display states: - * - pending: Order broadcasted but voucher not yet received + * - confirming: Payment tx broadcasted, awaiting blockchain confirmations + * - pending: Confirmations received, waiting for voucher from Phaze * - available: Voucher received, ready to redeem + * - failed: Order failed or expired * - redeemed: User has marked as redeemed */ -export type GiftCardStatus = 'pending' | 'available' | 'redeemed' +export type GiftCardStatus = + | 'confirming' + | 'pending' + | 'available' + | 'failed' + | 'redeemed' interface Props { order: PhazeDisplayOrder @@ -53,9 +60,9 @@ export const GiftCardDisplayCard: React.FC = props => { const code = order.vouchers?.[0]?.code const redemptionUrl = order.vouchers?.[0]?.url - // Redeemed cards are dimmed; pending cards use shimmer overlay instead + // Redeemed and failed cards are dimmed; pending/confirming use shimmer const cardContainerStyle = - status === 'redeemed' + status === 'redeemed' || status === 'failed' ? [styles.cardContainer, styles.dimmedCard] : styles.cardContainer @@ -136,10 +143,18 @@ export const GiftCardDisplayCard: React.FC = props => { )} - {status === 'pending' ? ( + {status === 'confirming' ? ( + + {lstrings.gift_card_confirming} + + ) : status === 'pending' ? ( {lstrings.gift_card_pending} + ) : status === 'failed' ? ( + + {lstrings.gift_card_failed} + ) : status === 'available' && redemptionUrl != null ? ( = props => { - {/* Shimmer overlay for pending state */} - + {/* Shimmer overlay for confirming/pending states */} + ) } diff --git a/src/components/scenes/GiftCardListScene.tsx b/src/components/scenes/GiftCardListScene.tsx index a16486156f2..7201f743d71 100644 --- a/src/components/scenes/GiftCardListScene.tsx +++ b/src/components/scenes/GiftCardListScene.tsx @@ -340,15 +340,24 @@ export const GiftCardListScene: React.FC = (props: Props) => { /** * Derive card status from order data: - * - pending: Broadcasted but no voucher yet + * - confirming: Payment tx sent, awaiting blockchain confirmations + * - pending: Confirmations received, waiting for voucher * - available: Has voucher, not yet redeemed + * - failed: Order failed or expired * - redeemed: User marked as redeemed */ const getCardStatus = React.useCallback( (order: PhazeDisplayOrder): GiftCardStatus => { if (order.redeemedDate != null) return 'redeemed' - if (order.vouchers.length === 0) return 'pending' - return 'available' + if (order.status === 'failed' || order.status === 'expired') + return 'failed' + if (order.vouchers.length > 0) return 'available' + // Phaze API status 'processing' means payment confirmed, generating + // vouchers. Otherwise txid present means tx broadcast, awaiting + // Phaze confirmation. + if (order.status === 'processing') return 'pending' + if (order.txid != null) return 'confirming' + return 'pending' }, [] ) diff --git a/src/locales/en_US.ts b/src/locales/en_US.ts index 6cc0ef4726d..bf83580c5e0 100644 --- a/src/locales/en_US.ts +++ b/src/locales/en_US.ts @@ -1953,6 +1953,8 @@ const strings = { gift_card_redeemed_cards: 'Redeemed Cards', gift_card_unmark_as_redeemed: 'Unmark as Redeemed', gift_card_active_cards: 'Active Cards', + gift_card_confirming: 'Awaiting Payment Confirmations...', + gift_card_failed: 'Failed', gift_card_pending: 'Pending Delivery, Please Wait...', gift_card_pending_toast: 'Your gift card is being delivered. Please wait for a few minutes for it to arrive.', diff --git a/src/locales/strings/enUS.json b/src/locales/strings/enUS.json index 89f2f7858e6..60edbd34814 100644 --- a/src/locales/strings/enUS.json +++ b/src/locales/strings/enUS.json @@ -1518,6 +1518,8 @@ "gift_card_redeemed_cards": "Redeemed Cards", "gift_card_unmark_as_redeemed": "Unmark as Redeemed", "gift_card_active_cards": "Active Cards", + "gift_card_confirming": "Awaiting Payment Confirmations...", + "gift_card_failed": "Failed", "gift_card_pending": "Pending Delivery, Please Wait...", "gift_card_pending_toast": "Your gift card is being delivered. Please wait for a few minutes for it to arrive.", "gift_card_quote_expired_toast": "Your quote has expired. Please try again.", From 5c90121849e501c344d7a44f99e38d611b599909 Mon Sep 17 00:00:00 2001 From: Jon Tzeng Date: Wed, 11 Feb 2026 17:10:47 -0800 Subject: [PATCH 05/11] Show QuoteID in gift card menu modal Display the Phaze quoteId at the top of the kebab menu modal for easier debugging and support reference. --- src/components/modals/GiftCardMenuModal.tsx | 11 +++++++++++ src/locales/en_US.ts | 1 + src/locales/strings/enUS.json | 1 + 3 files changed, 13 insertions(+) diff --git a/src/components/modals/GiftCardMenuModal.tsx b/src/components/modals/GiftCardMenuModal.tsx index a2b2374c230..dfe37e6982e 100644 --- a/src/components/modals/GiftCardMenuModal.tsx +++ b/src/components/modals/GiftCardMenuModal.tsx @@ -2,6 +2,7 @@ import type { EdgeCurrencyWallet, EdgeTransaction } from 'edge-core-js' import * as React from 'react' import { ActivityIndicator, View } from 'react-native' import type { AirshipBridge } from 'react-native-airship' +import { sprintf } from 'sprintf-js' import { useHandler } from '../../hooks/useHandler' import { useWatch } from '../../hooks/useWatch' @@ -10,6 +11,7 @@ import type { PhazeDisplayOrder } from '../../plugins/gift-cards/phazeGiftCardTy import { useSelector } from '../../types/reactRedux' import { ArrowRightIcon, CheckIcon } from '../icons/ThemedIcons' import { cacheStyles, type Theme, useTheme } from '../services/ThemeContext' +import { EdgeText } from '../themed/EdgeText' import { SelectableRow } from '../themed/SelectableRow' import { EdgeModal } from './EdgeModal' @@ -105,6 +107,9 @@ export const GiftCardMenuModal: React.FC = props => { return ( + + {sprintf(lstrings.gift_card_quote_id_label_1s, order.quoteId)} + = props => { } const getStyles = cacheStyles((theme: Theme) => ({ + quoteIdText: { + fontSize: theme.rem(0.75), + color: theme.secondaryText, + marginHorizontal: theme.rem(0.5), + marginBottom: theme.rem(0.5) + }, iconContainer: { width: theme.rem(2.5), height: theme.rem(2.5), diff --git a/src/locales/en_US.ts b/src/locales/en_US.ts index bf83580c5e0..dc7f4161da6 100644 --- a/src/locales/en_US.ts +++ b/src/locales/en_US.ts @@ -1958,6 +1958,7 @@ const strings = { gift_card_pending: 'Pending Delivery, Please Wait...', gift_card_pending_toast: 'Your gift card is being delivered. Please wait for a few minutes for it to arrive.', + gift_card_quote_id_label_1s: 'QuoteID: %1$s', gift_card_quote_expired_toast: 'Your quote has expired. Please try again.', gift_card_product_unavailable_title: 'Temporarily Unavailable', gift_card_product_unavailable_warning: diff --git a/src/locales/strings/enUS.json b/src/locales/strings/enUS.json index 60edbd34814..cc47b0d9522 100644 --- a/src/locales/strings/enUS.json +++ b/src/locales/strings/enUS.json @@ -1522,6 +1522,7 @@ "gift_card_failed": "Failed", "gift_card_pending": "Pending Delivery, Please Wait...", "gift_card_pending_toast": "Your gift card is being delivered. Please wait for a few minutes for it to arrive.", + "gift_card_quote_id_label_1s": "QuoteID: %1$s", "gift_card_quote_expired_toast": "Your quote has expired. Please try again.", "gift_card_product_unavailable_title": "Temporarily Unavailable", "gift_card_product_unavailable_warning": "Card is temporarily unavailable. Please select another card from this brand or try again later.", From fa88125afcebf1406dceed930a7269d44e151a58 Mon Sep 17 00:00:00 2001 From: Jon Tzeng Date: Fri, 13 Feb 2026 15:19:29 -0800 Subject: [PATCH 06/11] Add QuoteId row and card-level copy to GiftCardDetailsCard Extend the gift card tx details card with a Quote ID row and a single copy button on the right side that copies all data at once. Dividers stop short of the copy button. The redeem row remains separate with its own chevron. Document backward-compat: orderId stores quoteId in prior versions. --- src/components/cards/GiftCardDetailsCard.tsx | 119 +++++++++++++++---- src/locales/en_US.ts | 1 + src/locales/strings/enUS.json | 1 + 3 files changed, 98 insertions(+), 23 deletions(-) diff --git a/src/components/cards/GiftCardDetailsCard.tsx b/src/components/cards/GiftCardDetailsCard.tsx index 8f8a082f2be..535f800d875 100644 --- a/src/components/cards/GiftCardDetailsCard.tsx +++ b/src/components/cards/GiftCardDetailsCard.tsx @@ -1,12 +1,19 @@ +import Clipboard from '@react-native-clipboard/clipboard' import type { EdgeTxActionGiftCard } from 'edge-core-js' import * as React from 'react' -import { Linking } from 'react-native' +import { Linking, View } from 'react-native' import { useHandler } from '../../hooks/useHandler' import { lstrings } from '../../locales/strings' +import { triggerHaptic } from '../../util/haptic' import { removeIsoPrefix } from '../../util/utils' import { CircularBrandIcon } from '../common/CircularBrandIcon' +import { DividerLineUi4 } from '../common/DividerLineUi4' +import { EdgeTouchableOpacity } from '../common/EdgeTouchableOpacity' +import { CopyIcon } from '../icons/ThemedIcons' import { EdgeRow } from '../rows/EdgeRow' +import { showToast } from '../services/AirshipInstance' +import { cacheStyles, type Theme, useTheme } from '../services/ThemeContext' import { EdgeText } from '../themed/EdgeText' import { EdgeCard } from './EdgeCard' @@ -15,11 +22,21 @@ interface Props { } /** - * Displays gift card details including brand, amount, and redemption code - * in TransactionDetailsScene for gift card purchases. + * Displays gift card details including brand, amount, quote ID, and redemption + * code in TransactionDetailsScene for gift card purchases. + * + * Layout: A left column of data rows with dividers and a single card-level copy + * button on the right. Dividers stop short of the copy button area. */ export const GiftCardDetailsCard: React.FC = ({ action }) => { const { card, redemption } = action + const theme = useTheme() + const styles = getStyles(theme) + + // Backward compat: Prior versions stored the quoteId in the `orderId` field + // of EdgeTxActionGiftCard. Once edge-core-js adds an explicit `quoteId` + // field, prefer that and fall back to `orderId` for older transactions. + const quoteId = action.orderId const handleRedeemPress = useHandler(() => { if (redemption?.url != null) { @@ -43,30 +60,86 @@ export const GiftCardDetailsCard: React.FC = ({ action }) => { const fiatCurrency = removeIsoPrefix(card.fiatCurrencyCode) const amountDisplay = `${card.fiatAmount} ${fiatCurrency}` + // Build formatted string for card-level copy + const copyText = React.useMemo(() => { + const lines = [ + `${lstrings.gift_card_label}: ${card.name}`, + `${lstrings.string_amount}: ${amountDisplay}`, + `${lstrings.gift_card_quote_id_label}: ${quoteId}` + ] + if (redemption?.code != null) { + lines.push(`${lstrings.gift_card_security_code}: ${redemption.code}`) + } + return lines.join('\n') + }, [card.name, amountDisplay, quoteId, redemption?.code]) + + const handleCopyAll = useHandler(() => { + triggerHaptic('impactLight') + Clipboard.setString(copyText) + showToast(lstrings.fragment_copied) + }) + return ( - - - {card.name} - - - - - {redemption?.code != null ? ( - - ) : null} + + + {/* Left column: data rows with dividers */} + + + {card.name} + + + + + + + + + + {redemption?.code != null ? ( + <> + + + + ) : null} + + + {/* Right column: card-level copy button */} + + + + + + {/* Redeem row outside the copy layout - has its own chevron */} {redemption?.url != null ? ( - + <> + + + ) : null} ) } + +const getStyles = cacheStyles((theme: Theme) => ({ + cardLayout: { + flexDirection: 'row' as const + }, + dataColumn: { + flex: 1, + flexDirection: 'column' as const + }, + copyColumn: { + justifyContent: 'center' as const, + alignItems: 'center' as const, + paddingHorizontal: theme.rem(0.75) + } +})) diff --git a/src/locales/en_US.ts b/src/locales/en_US.ts index dc7f4161da6..f3092e3b597 100644 --- a/src/locales/en_US.ts +++ b/src/locales/en_US.ts @@ -1958,6 +1958,7 @@ const strings = { gift_card_pending: 'Pending Delivery, Please Wait...', gift_card_pending_toast: 'Your gift card is being delivered. Please wait for a few minutes for it to arrive.', + gift_card_quote_id_label: 'Quote ID', gift_card_quote_id_label_1s: 'QuoteID: %1$s', gift_card_quote_expired_toast: 'Your quote has expired. Please try again.', gift_card_product_unavailable_title: 'Temporarily Unavailable', diff --git a/src/locales/strings/enUS.json b/src/locales/strings/enUS.json index cc47b0d9522..97e5411d651 100644 --- a/src/locales/strings/enUS.json +++ b/src/locales/strings/enUS.json @@ -1522,6 +1522,7 @@ "gift_card_failed": "Failed", "gift_card_pending": "Pending Delivery, Please Wait...", "gift_card_pending_toast": "Your gift card is being delivered. Please wait for a few minutes for it to arrive.", + "gift_card_quote_id_label": "Quote ID", "gift_card_quote_id_label_1s": "QuoteID: %1$s", "gift_card_quote_expired_toast": "Your quote has expired. Please try again.", "gift_card_product_unavailable_title": "Temporarily Unavailable", From 219a2602028bb48e4364d0a560379d385603f45b Mon Sep 17 00:00:00 2001 From: Jon Tzeng Date: Mon, 16 Feb 2026 13:13:24 -0800 Subject: [PATCH 07/11] Add Gift Card Account Information scene New scene to view Phaze account credentials behind a confirmation wall. Shows quoteId context when accessed from a specific card. After the user confirms a ConfirmContinueModal warning about redemption risk, identity data (email, user ID) is revealed with copy buttons. --- src/components/Main.tsx | 12 ++ .../scenes/GiftCardAccountInfoScene.tsx | 134 ++++++++++++++++++ src/locales/en_US.ts | 9 ++ src/locales/strings/enUS.json | 7 + src/types/routerTypes.tsx | 2 + 5 files changed, 164 insertions(+) create mode 100644 src/components/scenes/GiftCardAccountInfoScene.tsx diff --git a/src/components/Main.tsx b/src/components/Main.tsx index 82e1977a731..ac7c71d8729 100644 --- a/src/components/Main.tsx +++ b/src/components/Main.tsx @@ -49,6 +49,7 @@ import { EdgeHeader } from './navigation/EdgeHeader' import { PluginBackButton } from './navigation/GuiPluginBackButton' import { HeaderBackground } from './navigation/HeaderBackground' import { HeaderTextButton } from './navigation/HeaderTextButton' +import { HeaderTitle } from './navigation/HeaderTitle' import { ParamHeaderTitle } from './navigation/ParamHeaderTitle' import { SideMenuButton } from './navigation/SideMenuButton' import { TransactionDetailsTitle } from './navigation/TransactionDetailsTitle' @@ -96,6 +97,7 @@ import { FioSentRequestDetailsScene as FioSentRequestDetailsSceneComponent } fro import { FioStakingChangeScene as FioStakingChangeSceneComponent } from './scenes/Fio/FioStakingChangeScene' import { FioStakingOverviewScene as FioStakingOverviewSceneComponent } from './scenes/Fio/FioStakingOverviewScene' import { GettingStartedScene } from './scenes/GettingStartedScene' +import { GiftCardAccountInfoScene as GiftCardAccountInfoSceneComponent } from './scenes/GiftCardAccountInfoScene' import { GiftCardListScene as GiftCardListSceneComponent } from './scenes/GiftCardListScene' import { GiftCardMarketScene as GiftCardMarketSceneComponent } from './scenes/GiftCardMarketScene' import { GiftCardPurchaseScene as GiftCardPurchaseSceneComponent } from './scenes/GiftCardPurchaseScene' @@ -244,6 +246,7 @@ const FioStakingChangeScene = ifLoggedIn(FioStakingChangeSceneComponent) const FioStakingOverviewScene = ifLoggedIn(FioStakingOverviewSceneComponent) const GuiPluginViewScene = ifLoggedIn(GuiPluginViewSceneComponent) const HomeScene = ifLoggedIn(HomeSceneComponent) +const GiftCardAccountInfoScene = ifLoggedIn(GiftCardAccountInfoSceneComponent) const GiftCardListScene = ifLoggedIn(GiftCardListSceneComponent) const GiftCardMarketScene = ifLoggedIn(GiftCardMarketSceneComponent) const GiftCardPurchaseScene = ifLoggedIn(GiftCardPurchaseSceneComponent) @@ -951,6 +954,15 @@ const EdgeAppStack: React.FC = () => { name="fioStakingOverview" component={FioStakingOverviewScene} /> + ( + + ) + }} + /> +> = props => { + const { route } = props + const { quoteId } = route.params + const theme = useTheme() + const styles = getStyles(theme) + + const account = useSelector(state => state.core.account) + + // Provider for identity lookup + const phazeConfig = (ENV.PLUGIN_API_KEYS as Record) + ?.phaze as { apiKey?: string; baseUrl?: string } | undefined + const { provider } = useGiftCardProvider({ + account, + apiKey: phazeConfig?.apiKey ?? '', + baseUrl: phazeConfig?.baseUrl ?? '' + }) + + const [isRevealed, setIsRevealed] = React.useState(false) + const [identities, setIdentities] = React.useState([]) + + // Load identities only after user confirms the reveal modal + React.useEffect(() => { + if (!isRevealed || provider == null) return + provider + .listIdentities(account) + .then(setIdentities) + .catch((err: unknown) => { + showError(err) + }) + }, [isRevealed, account, provider]) + + const handleReveal = useHandler(async () => { + const confirmed = await Airship.show(bridge => ( + true} + /> + )) + if (confirmed) { + setIsRevealed(true) + } + }) + + return ( + + + {lstrings.gift_card_account_info_body} + + {quoteId != null ? ( + + + + ) : null} + + {!isRevealed ? ( + + + + ) : ( + + {identities.map(identity => ( + + + + + ))} + + )} + + + ) +} + +const getStyles = cacheStyles((theme: Theme) => ({ + container: { + padding: theme.rem(0.5) + }, + containerFill: { + flex: 1 + }, + buttonContainer: { + flex: 1, + alignItems: 'center' as const, + justifyContent: 'center' as const + } +})) diff --git a/src/locales/en_US.ts b/src/locales/en_US.ts index f3092e3b597..4361824a3e3 100644 --- a/src/locales/en_US.ts +++ b/src/locales/en_US.ts @@ -1955,6 +1955,15 @@ const strings = { gift_card_active_cards: 'Active Cards', gift_card_confirming: 'Awaiting Payment Confirmations...', gift_card_failed: 'Failed', + gift_card_get_help: 'Get Help', + gift_card_account_info_title: 'Gift Card Account Information', + gift_card_account_info_body: + 'The information below is your customer credentials used with our Phaze gift card provider. Share this with our support team to troubleshoot gift card purchases.', + gift_card_account_info_reveal_button: 'View Phaze Account Info', + gift_card_account_info_warning: + 'Anyone with access to this information may be able to redeem your unredeemed gift cards. Do not share this publicly.', + gift_card_account_info_email: 'Account Email', + gift_card_account_info_user_id: 'Phaze User ID', gift_card_pending: 'Pending Delivery, Please Wait...', gift_card_pending_toast: 'Your gift card is being delivered. Please wait for a few minutes for it to arrive.', diff --git a/src/locales/strings/enUS.json b/src/locales/strings/enUS.json index 97e5411d651..1646e61308a 100644 --- a/src/locales/strings/enUS.json +++ b/src/locales/strings/enUS.json @@ -1520,6 +1520,13 @@ "gift_card_active_cards": "Active Cards", "gift_card_confirming": "Awaiting Payment Confirmations...", "gift_card_failed": "Failed", + "gift_card_get_help": "Get Help", + "gift_card_account_info_title": "Gift Card Account Information", + "gift_card_account_info_body": "The information below is your customer credentials used with our Phaze gift card provider. Share this with our support team to troubleshoot gift card purchases.", + "gift_card_account_info_reveal_button": "View Phaze Account Info", + "gift_card_account_info_warning": "Anyone with access to this information may be able to redeem your unredeemed gift cards. Do not share this publicly.", + "gift_card_account_info_email": "Account Email", + "gift_card_account_info_user_id": "Phaze User ID", "gift_card_pending": "Pending Delivery, Please Wait...", "gift_card_pending_toast": "Your gift card is being delivered. Please wait for a few minutes for it to arrive.", "gift_card_quote_id_label": "Quote ID", diff --git a/src/types/routerTypes.tsx b/src/types/routerTypes.tsx index e3ec4b65425..9c53eddc19f 100644 --- a/src/types/routerTypes.tsx +++ b/src/types/routerTypes.tsx @@ -35,6 +35,7 @@ import type { FioSentRequestDetailsParams } from '../components/scenes/Fio/FioSe import type { FioStakingChangeParams } from '../components/scenes/Fio/FioStakingChangeScene' import type { FioStakingOverviewParams } from '../components/scenes/Fio/FioStakingOverviewScene' import type { GettingStartedParams } from '../components/scenes/GettingStartedScene' +import type { GiftCardAccountInfoParams } from '../components/scenes/GiftCardAccountInfoScene' import type { GiftCardPurchaseParams } from '../components/scenes/GiftCardPurchaseScene' import type { GuiPluginListParams } from '../components/scenes/GuiPluginListScene' import type { PluginViewParams } from '../components/scenes/GuiPluginViewScene' @@ -206,6 +207,7 @@ export type EdgeAppStackParamList = {} & { fioSentRequestDetails: FioSentRequestDetailsParams fioStakingChange: FioStakingChangeParams fioStakingOverview: FioStakingOverviewParams + giftCardAccountInfo: GiftCardAccountInfoParams giftCardList: undefined giftCardMarket: undefined giftCardPurchase: GiftCardPurchaseParams From 3f0ee3ecf05dfefe7cab40c24e60a737d8168208 Mon Sep 17 00:00:00 2001 From: Jon Tzeng Date: Fri, 13 Feb 2026 15:27:09 -0800 Subject: [PATCH 08/11] Add Get Help to kebab menu and failed card button Add a 'Get Help' row to the gift card kebab menu that navigates to the new Gift Card Account Information scene with the quoteId. Also surface a 'Get Help' button on failed cards, reusing the existing redeem button pattern. --- src/components/cards/GiftCardDisplayCard.tsx | 20 ++++++++++++++++---- src/components/modals/GiftCardMenuModal.tsx | 16 +++++++++++++++- src/components/scenes/GiftCardListScene.tsx | 13 +++++++++++++ 3 files changed, 44 insertions(+), 5 deletions(-) diff --git a/src/components/cards/GiftCardDisplayCard.tsx b/src/components/cards/GiftCardDisplayCard.tsx index c1c1c5c0871..85b3933ed76 100644 --- a/src/components/cards/GiftCardDisplayCard.tsx +++ b/src/components/cards/GiftCardDisplayCard.tsx @@ -43,6 +43,8 @@ interface Props { /** Display state of the card */ status: GiftCardStatus onMenuPress: () => void + /** Called when user taps the "Get Help" button on failed cards */ + onGetHelpPress?: () => void /** Called when user taps redeem and completes viewing (webview closes) */ onRedeemComplete?: () => void } @@ -53,7 +55,7 @@ interface Props { * and redemption link overlaid. */ export const GiftCardDisplayCard: React.FC = props => { - const { order, status, onMenuPress, onRedeemComplete } = props + const { order, status, onMenuPress, onGetHelpPress, onRedeemComplete } = props const theme = useTheme() const styles = getStyles(theme) @@ -152,9 +154,19 @@ export const GiftCardDisplayCard: React.FC = props => { {lstrings.gift_card_pending} ) : status === 'failed' ? ( - - {lstrings.gift_card_failed} - + + + {lstrings.gift_card_get_help} + + + ) : status === 'available' && redemptionUrl != null ? ( = props => { }) }) + const handleGetHelp = useHandler(() => { + bridge.resolve({ type: 'getHelp' }) + }) + // Determine "Go to Transaction" state const hasTx = transaction != null const canNavigate = hasTx && order.walletId != null @@ -140,6 +145,15 @@ export const GiftCardMenuModal: React.FC = props => { } /> + + + + } + /> ) } diff --git a/src/components/scenes/GiftCardListScene.tsx b/src/components/scenes/GiftCardListScene.tsx index 7201f743d71..a5431de221a 100644 --- a/src/components/scenes/GiftCardListScene.tsx +++ b/src/components/scenes/GiftCardListScene.tsx @@ -302,6 +302,10 @@ export const GiftCardListScene: React.FC = (props: Props) => { await saveOrderAugment(account, order.quoteId, { redeemedDate: undefined }) + } else if (result.type === 'getHelp') { + navigation.navigate('giftCardAccountInfo', { + quoteId: order.quoteId + }) } } ) @@ -401,6 +405,15 @@ export const GiftCardListScene: React.FC = (props: Props) => { onMenuPress={() => { handleMenuPress(order, false).catch(() => {}) }} + onGetHelpPress={ + status !== 'failed' + ? undefined + : () => { + navigation.navigate('giftCardAccountInfo', { + quoteId: order.quoteId + }) + } + } onRedeemComplete={ status !== 'available' ? undefined From d1444312b61a98b4a7c2dfc5f055a8acac668ba6 Mon Sep 17 00:00:00 2001 From: Jon Tzeng Date: Fri, 13 Feb 2026 15:30:29 -0800 Subject: [PATCH 09/11] Add developer settings row for Gift Card Account Info Add a Gift Card Account Info row in the developer mode section of SettingsScene that navigates to the new account information scene. --- src/components/scenes/SettingsScene.tsx | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/src/components/scenes/SettingsScene.tsx b/src/components/scenes/SettingsScene.tsx index b707dbffbe2..6f87e29bd85 100644 --- a/src/components/scenes/SettingsScene.tsx +++ b/src/components/scenes/SettingsScene.tsx @@ -724,6 +724,14 @@ export const SettingsScene: React.FC = props => { onPress={handleSelectTheme} /> )} + {developerModeOn && ( + { + navigation.navigate('giftCardAccountInfo', {}) + }} + /> + )} )} From 6230ecbc95f64f94af82f27b2f8b7aab217d991e Mon Sep 17 00:00:00 2001 From: Jon Tzeng Date: Fri, 13 Feb 2026 15:33:53 -0800 Subject: [PATCH 10/11] Lock network fee to high for gift card purchases Set networkFeeOption to high and lock the fee tile so users cannot lower the fee priority. Gift card orders are time-sensitive with expiring quotes, so high priority reduces the risk of missed deadlines. --- src/components/scenes/GiftCardPurchaseScene.tsx | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/components/scenes/GiftCardPurchaseScene.tsx b/src/components/scenes/GiftCardPurchaseScene.tsx index e7855224f9c..cb4cd6ed69c 100644 --- a/src/components/scenes/GiftCardPurchaseScene.tsx +++ b/src/components/scenes/GiftCardPurchaseScene.tsx @@ -512,6 +512,7 @@ export const GiftCardPurchaseScene: React.FC = props => { tokenId, spendInfo: { tokenId, + networkFeeOption: 'high', spendTargets: [ { publicAddress: orderResponse.deliveryAddress, @@ -527,6 +528,7 @@ export const GiftCardPurchaseScene: React.FC = props => { lockTilesMap: { address: true, amount: true, + fee: true, wallet: true }, hiddenFeaturesMap: { From d6f22c28179d05c3a20b8c6be02698848a96392a Mon Sep 17 00:00:00 2001 From: Jon Tzeng Date: Fri, 13 Feb 2026 15:34:25 -0800 Subject: [PATCH 11/11] Wire up new EdgeTxActionGiftCard fields from edge-core-js Populate quoteId, productId, and orderId correctly in the saved action now that the core type has explicit fields. Update GiftCardDetailsCard with backward-compat detection: if quoteId is absent, treat orderId as the quoteId per legacy behavior. --- CHANGELOG.md | 4 ++ src/components/cards/GiftCardDetailsCard.tsx | 39 ++++++++++++++++--- .../scenes/GiftCardPurchaseScene.tsx | 5 ++- src/locales/en_US.ts | 2 + src/locales/strings/enUS.json | 2 + 5 files changed, 46 insertions(+), 6 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 9b6bda9efd8..62c51934e8d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,10 @@ - added: Nym (NYM) wallet support - added: opBNB (BNB) support - added: Register SwapKit V3 as a separate exchange plugin +- added: Gift card account information scene with Get Help access from kebab menu and failed cards +- added: Quote ID display in gift card transaction details with card-level copy +- changed: Distinguish network vs service errors in gift card scenes +- changed: Lock network fee to high priority for gift card purchases - changed: Manage tokens scene saves changes on explicit save instead of live toggling - changed: Unify split wallet scene titles and add chain-specific descriptions for EVM and UTXO splits - fixed: Missing 2-factor approve / deny scene on login diff --git a/src/components/cards/GiftCardDetailsCard.tsx b/src/components/cards/GiftCardDetailsCard.tsx index 535f800d875..9f85b1dfd11 100644 --- a/src/components/cards/GiftCardDetailsCard.tsx +++ b/src/components/cards/GiftCardDetailsCard.tsx @@ -33,10 +33,13 @@ export const GiftCardDetailsCard: React.FC = ({ action }) => { const theme = useTheme() const styles = getStyles(theme) - // Backward compat: Prior versions stored the quoteId in the `orderId` field - // of EdgeTxActionGiftCard. Once edge-core-js adds an explicit `quoteId` - // field, prefer that and fall back to `orderId` for older transactions. - const quoteId = action.orderId + // Backward compat: Prior versions stored the quoteId in the orderId field. + // Detect legacy transactions by checking whether the explicit quoteId field + // is populated. If missing, fall back to orderId which held the quoteId. + const quoteId = action.quoteId ?? action.orderId + const productId = action.productId + // orderId is only meaningful when quoteId is separately populated (new format) + const orderId = action.quoteId != null ? action.orderId : undefined const handleRedeemPress = useHandler(() => { if (redemption?.url != null) { @@ -67,11 +70,17 @@ export const GiftCardDetailsCard: React.FC = ({ action }) => { `${lstrings.string_amount}: ${amountDisplay}`, `${lstrings.gift_card_quote_id_label}: ${quoteId}` ] + if (productId != null) { + lines.push(`${lstrings.gift_card_product_id_label}: ${productId}`) + } + if (orderId != null) { + lines.push(`${lstrings.gift_card_order_id_label}: ${orderId}`) + } if (redemption?.code != null) { lines.push(`${lstrings.gift_card_security_code}: ${redemption.code}`) } return lines.join('\n') - }, [card.name, amountDisplay, quoteId, redemption?.code]) + }, [card.name, amountDisplay, quoteId, productId, orderId, redemption?.code]) const handleCopyAll = useHandler(() => { triggerHaptic('impactLight') @@ -96,6 +105,26 @@ export const GiftCardDetailsCard: React.FC = ({ action }) => { + {productId != null ? ( + <> + + + + ) : null} + + {orderId != null ? ( + <> + + + + ) : null} + {redemption?.code != null ? ( <> diff --git a/src/components/scenes/GiftCardPurchaseScene.tsx b/src/components/scenes/GiftCardPurchaseScene.tsx index cb4cd6ed69c..edb6b9d22ed 100644 --- a/src/components/scenes/GiftCardPurchaseScene.tsx +++ b/src/components/scenes/GiftCardPurchaseScene.tsx @@ -572,9 +572,12 @@ export const GiftCardPurchaseScene: React.FC = props => { const order = pendingOrderRef.current // Save the gift card action to the transaction (synced via edge-core) + const cartItem = order.cart[0] const savedAction: EdgeTxActionGiftCard = { actionType: 'giftCard', - orderId: order.quoteId, + orderId: cartItem.orderId, + quoteId: order.quoteId, + productId: cartItem.productId, provider: { providerId: 'phaze', displayName: 'Phaze' diff --git a/src/locales/en_US.ts b/src/locales/en_US.ts index 4361824a3e3..c2a99dc8454 100644 --- a/src/locales/en_US.ts +++ b/src/locales/en_US.ts @@ -1967,6 +1967,8 @@ const strings = { gift_card_pending: 'Pending Delivery, Please Wait...', gift_card_pending_toast: 'Your gift card is being delivered. Please wait for a few minutes for it to arrive.', + gift_card_order_id_label: 'Order ID', + gift_card_product_id_label: 'Product ID', gift_card_quote_id_label: 'Quote ID', gift_card_quote_id_label_1s: 'QuoteID: %1$s', gift_card_quote_expired_toast: 'Your quote has expired. Please try again.', diff --git a/src/locales/strings/enUS.json b/src/locales/strings/enUS.json index 1646e61308a..4a2146ffd82 100644 --- a/src/locales/strings/enUS.json +++ b/src/locales/strings/enUS.json @@ -1529,6 +1529,8 @@ "gift_card_account_info_user_id": "Phaze User ID", "gift_card_pending": "Pending Delivery, Please Wait...", "gift_card_pending_toast": "Your gift card is being delivered. Please wait for a few minutes for it to arrive.", + "gift_card_order_id_label": "Order ID", + "gift_card_product_id_label": "Product ID", "gift_card_quote_id_label": "Quote ID", "gift_card_quote_id_label_1s": "QuoteID: %1$s", "gift_card_quote_expired_toast": "Your quote has expired. Please try again.",