diff --git a/CHANGELOG.md b/CHANGELOG.md index bf75af49c9..9128fcbac9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,7 @@ - Fix wrong estimated confirmation time for ERC20 tokens. - Enable unlock test wallet in testnet - Added support to show on the BitBox when a transaction's recipient is an address of a different account on the device. +- Integrate Bitrefill and add spending section # v4.47.2 - Linux: fix compatiblity with some versions of Mesa that are incompatible with the bundled wayland libraries diff --git a/backend/backend.go b/backend/backend.go index 03c23c8aec..bdf332807b 100644 --- a/backend/backend.go +++ b/backend/backend.go @@ -91,6 +91,9 @@ var fixedURLWhitelist = []string{ // BTCDirect "https://btcdirect.eu/", "https://start.btcdirect.eu/", + // Bitrefill + "https://www.bitrefill.com/", + "https://embed.bitrefill.com/", // Bitsurance "https://www.bitsurance.eu/", "https://get.bitsurance.eu/", diff --git a/backend/exchanges/bitrefill.go b/backend/exchanges/bitrefill.go new file mode 100644 index 0000000000..d8b5e3f909 --- /dev/null +++ b/backend/exchanges/bitrefill.go @@ -0,0 +1,101 @@ +// Copyright 2025 Shift Crypto AG +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package exchanges + +import ( + "slices" + + "github.com/BitBoxSwiss/bitbox-wallet-app/backend/accounts" + "github.com/BitBoxSwiss/bitbox-wallet-app/backend/coins/coin" +) + +const ( + // BitrefillName is the name of the exchange, it is unique among all the supported exchanges. + BitrefillName = "bitrefill" + + bitrefillRef = "SHU5bB6y" + + bitrefillProdUrl = "https://bitboxapp.shiftcrypto.io/widgets/bitrefill/v1/bitrefill.html" +) + +type bitrefillInfo struct { + Url string + Ref string + Address *string +} + +var bitrefillRegions = []string{ + "AE", "AN", "BL", "CH", "DE", "ES", "GD", "GB", "GI", "GS", "GR", "GL", "KN", + "KR", "LC", "LI", "LK", "LR", "LY", "LT", "LU", "MA", "MC", "MD", "ME", "MG", + "MH", "ML", "MK", "MN", "MO", "MQ", "MR", "MS", "MW", "MX", "MY", "MV", "MZ", + "NA", "NE", "NF", "NG", "NI", "NL", "NP", "NR", "NU", "NC", "MP", "NZ", "OM", + "PA", "PG", "PE", "PH", "PK", "PN", "PL", "PR", "PT", "QA", "RE", "RO", "RS", + "RW", "SA", "SB", "SC", "SH", "SI", "SK", "SL", "SM", "ST", "SG", "SS", "TG", + "TH", "TJ", "TK", "TL", "TO", "TR", "TT", "TV", "UG", "UA", "US", "UY", "UZ", + "VA", "VE", "VN", "VU", "WF", "WS", "YT", "YE", "ZM", "ZW"} + +// GetBitrefillSupportedRegions returns a string slice of the regions where Bitrefill services +// are available. +func GetBitrefillSupportedRegions() []string { + return bitrefillRegions +} + +// IsRegionSupportedBitrefill returns whether a specific region (or an empty one +// for "unspecified") is supported by Bitrefill. +func IsRegionSupportedBitrefill(region string) bool { + return len(region) == 0 || slices.Contains(bitrefillRegions, region) +} + +// IsBitrefillSupported is true if coin.Code is supported by Bitrefill. +func IsBitrefillSupported(coinCode coin.Code) bool { + supportedCoins := []coin.Code{ + coin.CodeBTC, coin.CodeLTC, coin.CodeETH, coin.CodeSEPETH, + "eth-erc20-usdc", "eth-erc20-usdt"} + + coinSupported := slices.Contains(supportedCoins, coinCode) + + return coinSupported +} + +// IsBitrefillSupportedForCoinInRegion returns whether Bitrefill is supported for the specific +// combination of coin and region. +func IsBitrefillSupportedForCoinInRegion(coinCode coin.Code, region string) bool { + return IsBitrefillSupported(coinCode) && IsRegionSupportedBitrefill(region) +} + +// BitrefillDeals returns the purchase conditions (fee and payment methods) offered by Bitrefill. +func BitrefillDeals() *ExchangeDealsList { + return &ExchangeDealsList{ + ExchangeName: BitrefillName, + Deals: []*ExchangeDeal{ + { + Fee: 0, // There is no fee on buying gift cards + Payment: SpendPayment, + }, + }, + } +} + +// BitrefillInfo returns the information needed to interact with Bitrefill, +// including the widget URL, referral code and an unused address for refunds. +func BitrefillInfo(action ExchangeAction, acct accounts.Interface) bitrefillInfo { + addr := acct.GetUnusedReceiveAddresses()[0].Addresses[0].EncodeForHumans() + res := bitrefillInfo{ + Url: bitrefillProdUrl, + Ref: bitrefillRef, + Address: &addr} + + return res +} diff --git a/backend/exchanges/exchanges.go b/backend/exchanges/exchanges.go index 270df92d7e..c9e05a11ba 100644 --- a/backend/exchanges/exchanges.go +++ b/backend/exchanges/exchanges.go @@ -65,6 +65,8 @@ const ( BuyAction ExchangeAction = "buy" // SellAction identifies a sell exchange action. SellAction ExchangeAction = "sell" + // SpendAction identifies a spend exchange action. + SpendAction ExchangeAction = "spend" ) // ParseAction parses an action string and returns an ExchangeAction. @@ -74,6 +76,8 @@ func ParseAction(action string) (ExchangeAction, error) { return BuyAction, nil case "sell": return SellAction, nil + case "spend": + return SpendAction, nil default: return "", errp.New("Invalid Exchange action") } @@ -91,6 +95,7 @@ type ExchangeRegion struct { IsMoonpayEnabled bool `json:"isMoonpayEnabled"` IsPocketEnabled bool `json:"isPocketEnabled"` IsBtcDirectEnabled bool `json:"isBtcDirectEnabled"` + IsBitrefillEnabled bool `json:"IsBitrefillEnabled"` } // PaymentMethod type is used for payment options in exchange deals. @@ -105,6 +110,8 @@ const ( SOFORTPayment PaymentMethod = "sofort" // BancontactPayment is a payment method in the SEPA region. BancontactPayment PaymentMethod = "bancontact" + // SpendPayment is a payment method using the BitBox wallet. + SpendPayment PaymentMethod = "spend" ) // ExchangeDeal represents a specific purchase option of an exchange. @@ -145,10 +152,12 @@ func ListExchangesByRegion(account accounts.Interface, httpClient *http.Client) } btcDirectRegions := GetBtcDirectSupportedRegions() + bitrefillRegions := GetBitrefillSupportedRegions() isMoonpaySupported := IsMoonpaySupported(account.Coin().Code()) isPocketSupported := IsPocketSupported(account.Coin().Code()) isBtcDirectSupported := IsBtcDirectSupported(account.Coin().Code()) + isBitrefillSupported := IsBitrefillSupported(account.Coin().Code()) exchangeRegions := ExchangeRegionList{} for _, code := range RegionCodes { @@ -161,12 +170,14 @@ func ListExchangesByRegion(account accounts.Interface, httpClient *http.Client) _, pocketEnabled = pocketRegions[code] } btcDirectEnabled := slices.Contains(btcDirectRegions, code) + bitrefillEnabled := slices.Contains(bitrefillRegions, code) exchangeRegions.Regions = append(exchangeRegions.Regions, ExchangeRegion{ Code: code, IsMoonpayEnabled: moonpayEnabled && isMoonpaySupported, IsPocketEnabled: pocketEnabled && isPocketSupported, IsBtcDirectEnabled: btcDirectEnabled && isBtcDirectSupported, + IsBitrefillEnabled: bitrefillEnabled && isBitrefillSupported, }) } @@ -176,9 +187,10 @@ func ListExchangesByRegion(account accounts.Interface, httpClient *http.Client) // GetExchangeDeals returns the exchange deals available for the specified account, region and action. func GetExchangeDeals(account accounts.Interface, regionCode string, action ExchangeAction, httpClient *http.Client) ([]*ExchangeDealsList, error) { moonpaySupportsCoin := IsMoonpaySupported(account.Coin().Code()) && action == BuyAction - pocketSupportsCoin := IsPocketSupported(account.Coin().Code()) + pocketSupportsCoin := IsPocketSupported(account.Coin().Code()) && (action == BuyAction || action == SellAction) btcDirectSupportsCoin := IsBtcDirectSupported(account.Coin().Code()) && action == BuyAction - coinSupported := moonpaySupportsCoin || pocketSupportsCoin || btcDirectSupportsCoin + bitrefillSupportsCoin := IsBitrefillSupported(account.Coin().Code()) && action == SpendAction + coinSupported := moonpaySupportsCoin || pocketSupportsCoin || btcDirectSupportsCoin || bitrefillSupportsCoin if !coinSupported { return nil, ErrCoinNotSupported } @@ -200,6 +212,7 @@ func GetExchangeDeals(account accounts.Interface, regionCode string, action Exch IsMoonpayEnabled: true, IsPocketEnabled: true, IsBtcDirectEnabled: true, + IsBitrefillEnabled: true, } } @@ -223,6 +236,12 @@ func GetExchangeDeals(account accounts.Interface, regionCode string, action Exch exchangeDealsLists = append(exchangeDealsLists, deals) } } + if bitrefillSupportsCoin && userRegion.IsBitrefillEnabled { + deals := BitrefillDeals() + if deals != nil { + exchangeDealsLists = append(exchangeDealsLists, deals) + } + } if len(exchangeDealsLists) == 0 { return nil, ErrRegionNotSupported diff --git a/backend/handlers/handlers.go b/backend/handlers/handlers.go index 1f4066baf6..d024f0ffe9 100644 --- a/backend/handlers/handlers.go +++ b/backend/handlers/handlers.go @@ -246,6 +246,7 @@ func NewHandlers( getAPIRouter(apiRouter)("/exchange/moonpay/buy-info/{code}", handlers.getExchangeMoonpayBuyInfo).Methods("GET") getAPIRouterNoError(apiRouter)("/exchange/pocket/api-url/{action}", handlers.getExchangePocketURL).Methods("GET") getAPIRouterNoError(apiRouter)("/exchange/pocket/verify-address", handlers.postPocketWidgetVerifyAddress).Methods("POST") + getAPIRouterNoError(apiRouter)("/exchange/bitrefill/info/{action}/{code}", handlers.getExchangeBitrefillInfo).Methods("GET") getAPIRouterNoError(apiRouter)("/bitsurance/lookup", handlers.postBitsuranceLookup).Methods("POST") getAPIRouterNoError(apiRouter)("/bitsurance/url", handlers.getBitsuranceURL).Methods("GET") getAPIRouterNoError(apiRouter)("/aopp", handlers.getAOPP).Methods("GET") @@ -1511,6 +1512,33 @@ func (handlers *Handlers) postPocketWidgetVerifyAddress(r *http.Request) interfa } +func (handlers *Handlers) getExchangeBitrefillInfo(r *http.Request) interface{} { + type result struct { + Success bool `json:"success"` + ErrorMessage string `json:"errorMessage"` + Url string `json:"url"` + Ref string `json:"ref"` + Address *string `json:"address"` + } + + code := accountsTypes.Code(mux.Vars(r)["code"]) + acct, err := handlers.backend.GetAccountFromCode(code) + accountValid := acct != nil && acct.Offline() == nil && !acct.FatalError() + if err != nil || !accountValid { + return result{Success: false, ErrorMessage: "Account is not valid."} + } + + action := exchanges.ExchangeAction(mux.Vars(r)["action"]) + bitrefillInfo := exchanges.BitrefillInfo(action, acct) + + return result{ + Success: true, + Url: bitrefillInfo.Url, + Ref: bitrefillInfo.Ref, + Address: bitrefillInfo.Address, + } +} + func (handlers *Handlers) getAOPP(r *http.Request) interface{} { return handlers.backend.AOPP() } diff --git a/frontends/web/src/api/exchanges.ts b/frontends/web/src/api/exchanges.ts index e23f7ddd13..188a1ed811 100644 --- a/frontends/web/src/api/exchanges.ts +++ b/frontends/web/src/api/exchanges.ts @@ -21,7 +21,7 @@ export const getExchangeRegionCodes = (): Promise => { return apiGet('exchange/region-codes'); }; -export type TPaymentMethod = 'card' | 'bank-transfer' | 'bancontact' | 'sofort'; +export type TPaymentMethod = 'card' | 'bank-transfer' | 'bancontact' | 'sofort' | 'spend'; export type ExchangeDeal = { fee: number; @@ -31,7 +31,7 @@ export type ExchangeDeal = { isHidden: boolean; } -export type TExchangeName = 'moonpay' | 'pocket' | 'btcdirect' | 'btcdirect-otc'; +export type TExchangeName = 'moonpay' | 'pocket' | 'btcdirect' | 'btcdirect-otc' | 'bitrefill'; export type ExchangeDeals = { exchangeName: TExchangeName; @@ -51,7 +51,7 @@ export type ExchangeError = { export type TExchangeDealsResponse = ExchangeDealsList | ExchangeError -export type TExchangeAction = 'buy' | 'sell'; +export type TExchangeAction = 'buy' | 'sell' | 'spend'; export const getExchangeDeals = (action: TExchangeAction, accountCode: AccountCode, region: string): Promise => { return apiGet(`exchange/deals/${action}/${accountCode}?region=${region}`); @@ -107,6 +107,23 @@ export const getBTCDirectInfo = ( return apiGet(`exchange/btcdirect/info/${action}/${code}`); }; +export type TBitrefillInfoResponse = { + success: true; + url: string; + ref: string; + address?: string; +} | { + success: false; + errorMessage: string; +}; + +export const getBitrefillInfo = ( + action: TExchangeAction, + code: string, +): Promise => { + return apiGet(`exchange/bitrefill/info/${action}/${code}`); +}; + export type SupportedExchanges= { exchanges: string[]; }; diff --git a/frontends/web/src/components/terms/bitrefill-terms.tsx b/frontends/web/src/components/terms/bitrefill-terms.tsx new file mode 100644 index 0000000000..faa0cc4422 --- /dev/null +++ b/frontends/web/src/components/terms/bitrefill-terms.tsx @@ -0,0 +1,113 @@ +/** + * Copyright 2025 Shift Crypto AG + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { useTranslation } from 'react-i18next'; +import { ChangeEvent } from 'react'; +import { Button, Checkbox } from '@/components/forms'; +import { setConfig } from '@/utils/config'; +import { i18n } from '@/i18n/i18n'; +import { A } from '../anchor/anchor'; +import style from './terms.module.css'; +import { isBitcoinOnly } from '@/routes/account/utils'; +import { IAccount } from '@/api/account'; + +type TProps = { + account: IAccount; + onAgreedTerms: () => void; +} + +// Map languages supported by Bitrefill +export const localeMapping: Readonly> = { + en: 'en', + de: 'de', + fr: 'fr', + es: 'es', + it: 'it', + pt: 'pt', + ja: 'ja', + zh: 'zh-Hans' +}; + +export const getBitrefillPrivacyLink = () => { + const hl = i18n.resolvedLanguage ? localeMapping[i18n.resolvedLanguage] : 'en'; + return 'https://www.bitrefill.com/privacy/?hl=' + hl; +}; + +const handleSkipDisclaimer = (e: ChangeEvent) => { + setConfig({ frontend: { skipBitrefillWidgetDisclaimer: e.target.checked } }); +}; + +export const BitrefillTerms = ({ account, onAgreedTerms }: TProps) => { + const { t } = useTranslation(); + + const isBitcoin = isBitcoinOnly(account.coinCode); + // TODO: update with Bitrefill text + return ( +
+
+

+ {t('buy.exchange.infoContent.btcdirectWidget.disclaimer.title', { + context: isBitcoin ? 'bitcoin' : 'crypto' + })} +

+

{t('buy.exchange.infoContent.btcdirectWidget.disclaimer.description')}

+

+ {t('buy.exchange.infoContent.btcdirectWidget.disclaimer.paymentMethods.title')} +

+
    +
  • +

    {t('buy.exchange.infoContent.btcdirectWidget.disclaimer.paymentMethods.buy')}

    +
  • +
+

{t('buy.exchange.infoContent.btcdirectWidget.disclaimer.paymentMethods.note')}

+

+ + {t('buy.exchange.infoContent.btcdirectWidget.learnmore')} + +

+

+ {t('buy.exchange.infoContent.btcdirectWidget.disclaimer.security.title')} +

+

{t('buy.exchange.infoContent.btcdirectWidget.disclaimer.security.description')}

+

+ + {t('buy.exchange.infoContent.btcdirectWidget.disclaimer.security.link')} + +

+

{t('buy.exchange.infoContent.btcdirect.disclaimer.dataProtection.title')}

+

{t('buy.exchange.infoContent.btcdirect.disclaimer.dataProtection.text')}

+

+ + {t('buy.exchange.infoContent.btcdirect.disclaimer.dataProtection.link')} + +

+
+
+ +
+
+ +
+
+ ); +}; \ No newline at end of file diff --git a/frontends/web/src/locales/en/app.json b/frontends/web/src/locales/en/app.json index 6730685ba4..0a790991fb 100644 --- a/frontends/web/src/locales/en/app.json +++ b/frontends/web/src/locales/en/app.json @@ -399,6 +399,12 @@ "upgrade": "Upgrade" }, "buy": { + "bitrefill": { + "error": { + "insufficientFunds": "Insufficient funds to buy gift card." + }, + "transactionNote": "Sent to Bitrefill" + }, "exchange": { "bankTransfer": "Bank transfer", "bestDeal": "Best deal", @@ -553,7 +559,9 @@ "noExchanges": "Sorry, there are no available exchanges in this region.", "region": "Region", "selectRegion": "Not specified", - "sell": "Sell" + "sell": "Sell", + "spend": "Spend", + "spend_bitrefill": "Shop gift cards, phone refills & more" }, "info": { "continue": "Agree and continue", @@ -806,16 +814,19 @@ }, "generic": { "buy": "Buy {{coinCode}}", - "buySell": "Buy & sell", + "buySell": "Marketplace", "buy_bitcoin": "Buy Bitcoin", "buy_crypto": "Buy crypto", "enabled_false": "Disabled", "enabled_true": "Enabled", "noOptions": "No options found", + "paymentRequestNote": "{{name}} payment {{orderId}}", "receive": "Receive {{coinCode}}", "receive_bitcoin": "Receive Bitcoin", "receive_crypto": "Receive crypto", - "search": "Search…" + "search": "Search…", + "spend_bitcoin": "Spend bitcoin", + "spend_crypto": "Spend crypto" }, "genericError": "An error occurred. If you notice any issues, please restart the application.", "goal": { diff --git a/frontends/web/src/routes/exchange/bitrefill.tsx b/frontends/web/src/routes/exchange/bitrefill.tsx new file mode 100644 index 0000000000..8365183d7b --- /dev/null +++ b/frontends/web/src/routes/exchange/bitrefill.tsx @@ -0,0 +1,235 @@ +/** + * Copyright 2025 Shift Crypto AG + * + * Licensed under the Apache License, Version 2.0 (the 'License'); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an 'AS IS' BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { useState, useEffect, useRef, useCallback } from 'react'; +import { useTranslation } from 'react-i18next'; +import { Header } from '@/components/layout'; +import { Spinner } from '@/components/spinner/Spinner'; +import { AccountCode, IAccount, proposeTx, sendTx, TTxInput } from '@/api/account'; +import { findAccount, isBitcoinOnly } from '@/routes/account/utils'; +import { useDarkmode } from '@/hooks/darkmode'; +import { getConfig } from '@/utils/config'; +import style from './iframe.module.css'; +import { i18n } from '@/i18n/i18n'; +import { alertUser } from '@/components/alert/Alert'; +import { parseExternalBtcAmount } from '@/api/coins'; +import { useLoad } from '@/hooks/api'; +import { BitrefillTerms, localeMapping } from '@/components/terms/bitrefill-terms'; +import { getBitrefillInfo } from '@/api/exchanges'; +import { getURLOrigin } from '@/utils/url'; + +// Map coins supported by Bitrefill +const coinMapping: Readonly> = { + btc: 'bitcoin', + tbtc: 'bitcoin', + ltc: 'litecoin', + eth: 'ethereum', + usdt: 'usdt_erc20', + usdc: 'usdc_erc20', +}; + +type TProps = { + accounts: IAccount[]; + code: AccountCode; +}; + +export const Bitrefill = ({ accounts, code }: TProps) => { + const { t } = useTranslation(); + const { isDarkMode } = useDarkmode(); + const account = findAccount(accounts, code); + + const containerRef = useRef(null); + const iframeRef = useRef(null); + const [iframeLoaded, setIframeLoaded] = useState(false); + const [height, setHeight] = useState(0); + const resizeTimerID = useRef | null>(null); + const bitrefillInfo = useLoad(() => getBitrefillInfo('spend', code)); + + const config = useLoad(getConfig); + const [agreedTerms, setAgreedTerms] = useState(false); + + const hasOnlyBTCAccounts = accounts.every(({ coinCode }) => isBitcoinOnly(coinCode)); + + useEffect(() => { + if (config) { + setAgreedTerms(config.frontend.skipBitrefillWidgetDisclaimer); + } + }, [config]); + + const onResize = useCallback(() => { + if (resizeTimerID.current) { + clearTimeout(resizeTimerID.current); + } + resizeTimerID.current = setTimeout(() => { + if (containerRef.current) { + setHeight(containerRef.current.offsetHeight); + } + }, 200); + }, []); + + useEffect(() => { + onResize(); + window.addEventListener('resize', onResize); + return () => { + window.removeEventListener('resize', onResize); + if (resizeTimerID.current) { + clearTimeout(resizeTimerID.current); + } + }; + }, [onResize]); + + const handleMessage = useCallback(async (event: MessageEvent) => { + if ( + !account + || !bitrefillInfo?.success + || ![getURLOrigin(bitrefillInfo.url), 'https://embed.bitrefill.com'].includes(event.origin) + ) { + return; + } + + const data = typeof event.data === 'string' ? JSON.parse(event.data) : event.data; + + switch (data.event) { + case 'request-configuration': { + event.source?.postMessage({ + event: 'configuration', + ref: bitrefillInfo.ref, + utm_source: 'BITBOX', + theme: isDarkMode ? 'dark' : 'light', + hl: i18n.resolvedLanguage ? localeMapping[i18n.resolvedLanguage] : 'en', + paymentMethods: account.coinCode ? coinMapping[account.coinCode] : 'bitcoin', + refundAddress: bitrefillInfo.address, + // Option to keep pending payment information longer in session, defaults to 'false' + paymentPending: 'true', + // Option to show payment information in the widget, defaults to 'true' + showPaymentInfo: 'true' + }, { + targetOrigin: event.origin + }); + break; + } + case 'payment_intent': { + // User clicked "Pay" in checkout + const { + invoiceId, + paymentMethod, + paymentAmount, + paymentAddress, + } = data; + + const parsedAmount = await parseExternalBtcAmount(paymentAmount.toString()); + if (!parsedAmount.success) { + alertUser(t('unknownError', { errorMessage: 'Invalid amount' })); + return; + } + // Ensure expected payment method matches account + if (coinMapping[account.coinCode] !== paymentMethod) { + alertUser(t('unknownError', { errorMessage: 'Payment method mismatch' })); + } + + const txInput: TTxInput = { + address: paymentAddress, + amount: parsedAmount.amount, + // Always use highest fee rate for Bitrefill spend + useHighestFee: true, + sendAll: 'no', + selectedUTXOs: [], + paymentRequest: null + }; + + let result = await proposeTx(code, txInput); + if (result.success) { + const txNote = t('generic.paymentRequestNote', { + name: 'Bitrefill', + orderId: invoiceId, + }); + const sendResult = await sendTx(code, txNote); + if (!sendResult.success && !('aborted' in sendResult)) { + alertUser(t('unknownError', { errorMessage: sendResult.errorMessage })); + } + } else { + if (result.errorCode === 'insufficientFunds') { + alertUser(t('buy.bitrefill.error.' + result.errorCode)); + } else if (result.errorCode) { + alertUser(t('send.error.' + result.errorCode)); + } else { + alertUser(t('genericError')); + } + } + break; + } + default: { + break; + } + } + }, [bitrefillInfo, isDarkMode, account, code, t]); + + useEffect(() => { + window.addEventListener('message', handleMessage); + return () => { + window.removeEventListener('message', handleMessage); + }; + }, [handleMessage]); + + if ( + !account + || !config + || !bitrefillInfo?.success + || !bitrefillInfo.address + ) { + return null; + } + + return ( +
+
+
+
+
{t('generic.spend', { context: hasOnlyBTCAccounts ? 'bitcoin' : 'crypto' })}} /> +
+
+ { !agreedTerms ? ( + setAgreedTerms(true)} + /> + ) : ( +
+ {!iframeLoaded && } + { bitrefillInfo?.success && ( +