@@ -15,13 +15,16 @@ import Ionicons from 'react-native-vector-icons/Ionicons'
1515import { sprintf } from 'sprintf-js'
1616import { v4 as uuidv4 } from 'uuid'
1717
18+ import { getFiatSymbol } from '../../constants/WalletAndCurrencyConstants'
1819import { ENV } from '../../env'
20+ import { displayFiatAmount } from '../../hooks/useFiatText'
1921import { useGiftCardProvider } from '../../hooks/useGiftCardProvider'
2022import { useHandler } from '../../hooks/useHandler'
2123import { usePhazeBrand } from '../../hooks/usePhazeBrand'
2224import { lstrings } from '../../locales/strings'
2325import type {
2426 PhazeCreateOrderResponse ,
27+ PhazeFxRate ,
2528 PhazeGiftCardBrand ,
2629 PhazeToken
2730} from '../../plugins/gift-cards/phazeGiftCardTypes'
@@ -36,6 +39,7 @@ import { DropdownInputButton } from '../buttons/DropdownInputButton'
3639import { KavButtons } from '../buttons/KavButtons'
3740import { AlertCardUi4 } from '../cards/AlertCard'
3841import { EdgeCard } from '../cards/EdgeCard'
42+ import { ErrorCard } from '../cards/ErrorCard'
3943import { EdgeAnim } from '../common/EdgeAnim'
4044import { EdgeTouchableOpacity } from '../common/EdgeTouchableOpacity'
4145import { SceneWrapper } from '../common/SceneWrapper'
@@ -118,6 +122,13 @@ export const GiftCardPurchaseScene: React.FC<Props> = props => {
118122 footer : string
119123 } | null > ( null )
120124
125+ // Warning state for product unavailable
126+ const [ productUnavailable , setProductUnavailable ] =
127+ React . useState < boolean > ( false )
128+
129+ // Error state for unexpected errors
130+ const [ error , setError ] = React . useState < unknown > ( null )
131+
121132 // Fetch allowed tokens from Phaze API
122133 const { data : tokenQueryResult } = useQuery ( {
123134 queryKey : [ 'phazeTokens' , account ?. id , isReady ] ,
@@ -155,6 +166,42 @@ export const GiftCardPurchaseScene: React.FC<Props> = props => {
155166 gcTime : 10 * 60 * 1000
156167 } )
157168
169+ // Get cached FX rates (already loaded during provider initialization)
170+ const fxRates = provider ?. getCachedFxRates ( ) ?? null
171+
172+ /**
173+ * Convert USD amount to brand's currency using FX rates.
174+ * Returns formatted string like "€5" or "$5.00" for USD brands.
175+ */
176+ const formatMinimumInBrandCurrency = React . useCallback (
177+ ( minimumUsd : number ) : string => {
178+ const symbol = getFiatSymbol ( brand . currency )
179+
180+ if ( brand . currency === 'USD' ) {
181+ return `${ symbol } ${ displayFiatAmount ( minimumUsd , 2 ) } `
182+ }
183+
184+ if ( fxRates == null ) {
185+ // Fallback to USD if rates not loaded
186+ return `$${ displayFiatAmount ( minimumUsd , 2 ) } `
187+ }
188+
189+ const rate = fxRates . find (
190+ ( r : PhazeFxRate ) =>
191+ r . fromCurrency === 'USD' && r . toCurrency === brand . currency
192+ )
193+ if ( rate == null ) {
194+ // Fallback to USD if rate not found
195+ return `$${ displayFiatAmount ( minimumUsd , 2 ) } `
196+ }
197+
198+ const amountInBrandCurrency = Math . ceil ( minimumUsd * rate . rate )
199+ // Use 0 decimals for non-USD since we ceil to whole number
200+ return `${ symbol } ${ displayFiatAmount ( amountInBrandCurrency , 0 ) } `
201+ } ,
202+ [ fxRates , brand . currency ]
203+ )
204+
158205 // Extract assets for wallet list modal and sync token map to ref
159206 // This ensures the ref is populated even when query returns cached data
160207 const allowedAssets = tokenQueryResult ?. assets
@@ -195,8 +242,10 @@ export const GiftCardPurchaseScene: React.FC<Props> = props => {
195242
196243 // Handle amount text change for variable range
197244 const handleAmountChange = useHandler ( ( text : string ) => {
198- // Clear minimum warning when user modifies amount
245+ // Clear warnings/errors when user modifies amount
199246 setMinimumWarning ( null )
247+ setProductUnavailable ( false )
248+ setError ( null )
200249
201250 // Only allow numbers and decimal point
202251 const cleaned = text . replace ( / [ ^ 0 - 9 . ] / g, '' )
@@ -216,6 +265,8 @@ export const GiftCardPurchaseScene: React.FC<Props> = props => {
216265 const handleMaxPress = useHandler ( ( ) => {
217266 if ( hasVariableRange ) {
218267 setMinimumWarning ( null )
268+ setProductUnavailable ( false )
269+ setError ( null )
219270 setAmountText ( String ( maxVal ) )
220271 setSelectedAmount ( maxVal )
221272 }
@@ -259,8 +310,10 @@ export const GiftCardPurchaseScene: React.FC<Props> = props => {
259310 )
260311
261312 if ( result != null ) {
262- // Clear minimum warning when user modifies amount
313+ // Clear warnings/errors when user modifies amount
263314 setMinimumWarning ( null )
315+ setProductUnavailable ( false )
316+ setError ( null )
264317 setSelectedAmount ( result . amount )
265318 setAmountText ( String ( result . amount ) )
266319 }
@@ -278,7 +331,6 @@ export const GiftCardPurchaseScene: React.FC<Props> = props => {
278331 headerTitle = { lstrings . gift_card_pay_from_wallet }
279332 navigation = { navigation as NavigationBase }
280333 allowedAssets = { allowedAssets }
281- showCreateWallet
282334 />
283335 ) )
284336
@@ -324,7 +376,7 @@ export const GiftCardPurchaseScene: React.FC<Props> = props => {
324376 ) ,
325377 footer : sprintf (
326378 lstrings . gift_card_minimum_warning_footer ,
327- `$ ${ tokenInfo . minimumAmountInUSD . toFixed ( 2 ) } USD`
379+ formatMinimumInBrandCurrency ( tokenInfo . minimumAmountInUSD )
328380 )
329381 } )
330382 return
@@ -384,8 +436,8 @@ export const GiftCardPurchaseScene: React.FC<Props> = props => {
384436 const quantity = orderResponse . quantity . toFixed ( DECIMAL_PRECISION )
385437 const nativeAmount = String ( ceil ( mul ( quantity , multiplier ) , 0 ) )
386438
387- // Calculate expiry time
388- const expiryDate = new Date ( orderResponse . quoteExpiry * 1000 )
439+ // Calculate expiry time (quoteExpiry is Unix timestamp in milliseconds)
440+ const expiryDate = new Date ( orderResponse . quoteExpiry )
389441 const isoExpireDate = expiryDate . toISOString ( )
390442
391443 // Navigate to SendScene2
@@ -436,6 +488,11 @@ export const GiftCardPurchaseScene: React.FC<Props> = props => {
436488 </ Paragraph >
437489 ) ,
438490 isoExpireDate,
491+ onExpired : ( ) => {
492+ // Quote expired - navigate back to purchase scene and show toast
493+ navigation . goBack ( )
494+ showToast ( lstrings . gift_card_quote_expired_toast )
495+ } ,
439496 onDone : async ( error : Error | null , tx ?: EdgeTransaction ) => {
440497 if ( error != null ) {
441498 debugLog ( 'phaze' , 'Transaction error:' , error )
@@ -492,8 +549,20 @@ export const GiftCardPurchaseScene: React.FC<Props> = props => {
492549 } catch ( err : unknown ) {
493550 debugLog ( 'phaze' , 'Order creation error:' , err )
494551
495- // Check for minimum amount error from API
552+ // Clear previous warnings/errors
553+ setMinimumWarning ( null )
554+ setProductUnavailable ( false )
555+ setError ( null )
556+
496557 const errorMessage = err instanceof Error ? err . message : ''
558+
559+ // Check for product unavailable error
560+ if ( errorMessage . includes ( 'Product is unavailable' ) ) {
561+ setProductUnavailable ( true )
562+ return
563+ }
564+
565+ // Check for minimum amount error from API
497566 const minimumMatch = / M i n i m u m c a r t c o s t s h o u l d b e a b o v e : ( [ \d . ] + ) / . exec (
498567 errorMessage
499568 )
@@ -507,11 +576,12 @@ export const GiftCardPurchaseScene: React.FC<Props> = props => {
507576 ) ,
508577 footer : sprintf (
509578 lstrings . gift_card_minimum_warning_footer ,
510- `$ ${ minimumUSD . toFixed ( 2 ) } USD`
579+ formatMinimumInBrandCurrency ( minimumUSD )
511580 )
512581 } )
513582 } else {
514- showError ( err )
583+ // Show ErrorCard for other errors
584+ setError ( err )
515585 }
516586 } finally {
517587 setIsCreatingOrder ( false )
@@ -628,6 +698,8 @@ export const GiftCardPurchaseScene: React.FC<Props> = props => {
628698 style = { styles . maxButton }
629699 onPress = { ( ) => {
630700 setMinimumWarning ( null )
701+ setProductUnavailable ( false )
702+ setError ( null )
631703 const maxDenom =
632704 sortedDenominations [ sortedDenominations . length - 1 ]
633705 setSelectedAmount ( maxDenom )
@@ -664,14 +736,22 @@ export const GiftCardPurchaseScene: React.FC<Props> = props => {
664736 ) }
665737 </ EdgeAnim >
666738
667- { /* Minimum Amount Warning */ }
668- { minimumWarning != null ? (
739+ { /* Warnings/Errors - product unavailable takes precedence */ }
740+ { productUnavailable ? (
741+ < AlertCardUi4
742+ type = "warning"
743+ title = { lstrings . gift_card_product_unavailable_title }
744+ body = { lstrings . gift_card_product_unavailable_warning }
745+ />
746+ ) : minimumWarning != null ? (
669747 < AlertCardUi4
670748 type = "warning"
671749 title = { lstrings . gift_card_minimum_warning_title }
672750 header = { minimumWarning . header }
673751 footer = { minimumWarning . footer }
674752 />
753+ ) : error != null ? (
754+ < ErrorCard error = { error } />
675755 ) : null }
676756
677757 { /* Product Description Card */ }
0 commit comments