Skip to content

Commit 72d375f

Browse files
committed
frontend: integrate bitrefill widget
Integrates Bitrefill in the frontend. On the previous "Buy & sell" menu, there is now a third selectable pill "Spend", opening the available exchange options (Bitrefill in this case). The widget is requested from bitboxapp.shiftcrypto.io using config parameters from the user/app, handled in the "request-configuration" event. Note that the iframe will still have its source set to "https://embed.bitrefill.com". When the user clicks "Pay" inside the Bitrefill widget to purchase a gift card, the returned address and amount is used to propose a transaction on the connected BitBox. The PaymentMethod component is adjusted to allow custom messages to be shown in the exchange overview, as these will differ in the "Spend" section between providers. Disclaimer and info texts are added technically, but the actual text is still missing in this commit. Bitrefill developer documentation: https://www.bitrefill.com/playground/documentation/url-params
1 parent bbd6b52 commit 72d375f

File tree

12 files changed

+473
-13
lines changed

12 files changed

+473
-13
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
- Fix wrong estimated confirmation time for ERC20 tokens.
1010
- Enable unlock test wallet in testnet
1111
- Added support to show on the BitBox when a transaction's recipient is an address of a different account on the device.
12+
- Integrate Bitrefill and add spending section
1213

1314
# v4.47.2
1415
- Linux: fix compatiblity with some versions of Mesa that are incompatible with the bundled wayland libraries

frontends/web/src/api/exchanges.ts

Lines changed: 20 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ export const getExchangeRegionCodes = (): Promise<string[]> => {
2121
return apiGet('exchange/region-codes');
2222
};
2323

24-
export type TPaymentMethod = 'card' | 'bank-transfer' | 'bancontact' | 'sofort';
24+
export type TPaymentMethod = 'card' | 'bank-transfer' | 'bancontact' | 'sofort' | 'spend';
2525

2626
export type ExchangeDeal = {
2727
fee: number;
@@ -31,7 +31,7 @@ export type ExchangeDeal = {
3131
isHidden: boolean;
3232
}
3333

34-
export type TExchangeName = 'moonpay' | 'pocket' | 'btcdirect' | 'btcdirect-otc';
34+
export type TExchangeName = 'moonpay' | 'pocket' | 'btcdirect' | 'btcdirect-otc' | 'bitrefill';
3535

3636
export type ExchangeDeals = {
3737
exchangeName: TExchangeName;
@@ -51,7 +51,7 @@ export type ExchangeError = {
5151

5252
export type TExchangeDealsResponse = ExchangeDealsList | ExchangeError
5353

54-
export type TExchangeAction = 'buy' | 'sell';
54+
export type TExchangeAction = 'buy' | 'sell' | 'spend';
5555

5656
export const getExchangeDeals = (action: TExchangeAction, accountCode: AccountCode, region: string): Promise<TExchangeDealsResponse> => {
5757
return apiGet(`exchange/deals/${action}/${accountCode}?region=${region}`);
@@ -107,6 +107,23 @@ export const getBTCDirectInfo = (
107107
return apiGet(`exchange/btcdirect/info/${action}/${code}`);
108108
};
109109

110+
export type TBitrefillInfoResponse = {
111+
success: true;
112+
url: string;
113+
ref: string;
114+
address?: string;
115+
} | {
116+
success: false;
117+
errorMessage: string;
118+
};
119+
120+
export const getBitrefillInfo = (
121+
action: TExchangeAction,
122+
code: string,
123+
): Promise<TBitrefillInfoResponse> => {
124+
return apiGet(`exchange/bitrefill/info/${action}/${code}`);
125+
};
126+
110127
export type SupportedExchanges= {
111128
exchanges: string[];
112129
};
Lines changed: 113 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,113 @@
1+
/**
2+
* Copyright 2025 Shift Crypto AG
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
import { useTranslation } from 'react-i18next';
18+
import { ChangeEvent } from 'react';
19+
import { Button, Checkbox } from '@/components/forms';
20+
import { setConfig } from '@/utils/config';
21+
import { i18n } from '@/i18n/i18n';
22+
import { A } from '../anchor/anchor';
23+
import style from './terms.module.css';
24+
import { isBitcoinOnly } from '@/routes/account/utils';
25+
import { IAccount } from '@/api/account';
26+
27+
type TProps = {
28+
account: IAccount;
29+
onAgreedTerms: () => void;
30+
}
31+
32+
// Map languages supported by Bitrefill
33+
export const localeMapping: Readonly<Record<string, string>> = {
34+
en: 'en',
35+
de: 'de',
36+
fr: 'fr',
37+
es: 'es',
38+
it: 'it',
39+
pt: 'pt',
40+
ja: 'ja',
41+
zh: 'zh-Hans'
42+
};
43+
44+
export const getBitrefillPrivacyLink = () => {
45+
const hl = i18n.resolvedLanguage ? localeMapping[i18n.resolvedLanguage] : 'en';
46+
return 'https://www.bitrefill.com/privacy/?hl=' + hl;
47+
};
48+
49+
const handleSkipDisclaimer = (e: ChangeEvent<HTMLInputElement>) => {
50+
setConfig({ frontend: { skipBitrefillWidgetDisclaimer: e.target.checked } });
51+
};
52+
53+
export const BitrefillTerms = ({ account, onAgreedTerms }: TProps) => {
54+
const { t } = useTranslation();
55+
56+
const isBitcoin = isBitcoinOnly(account.coinCode);
57+
// TODO: update with Bitrefill text
58+
return (
59+
<div className={style.disclaimerContainer}>
60+
<div className={style.disclaimer}>
61+
<h2 className={style.title}>
62+
{t('buy.exchange.infoContent.btcdirectWidget.disclaimer.title', {
63+
context: isBitcoin ? 'bitcoin' : 'crypto'
64+
})}
65+
</h2>
66+
<p>{t('buy.exchange.infoContent.btcdirectWidget.disclaimer.description')}</p>
67+
<h2 className={style.title}>
68+
{t('buy.exchange.infoContent.btcdirectWidget.disclaimer.paymentMethods.title')}
69+
</h2>
70+
<ul>
71+
<li>
72+
<p>{t('buy.exchange.infoContent.btcdirectWidget.disclaimer.paymentMethods.buy')}</p>
73+
</li>
74+
</ul>
75+
<p>{t('buy.exchange.infoContent.btcdirectWidget.disclaimer.paymentMethods.note')}</p>
76+
<p>
77+
<A href={getBitrefillPrivacyLink()}>
78+
{t('buy.exchange.infoContent.btcdirectWidget.learnmore')}
79+
</A>
80+
</p>
81+
<h2 className={style.title}>
82+
{t('buy.exchange.infoContent.btcdirectWidget.disclaimer.security.title')}
83+
</h2>
84+
<p>{t('buy.exchange.infoContent.btcdirectWidget.disclaimer.security.description')}</p>
85+
<p>
86+
<A href="https://bitbox.swiss/bitbox02/threat-model/">
87+
{t('buy.exchange.infoContent.btcdirectWidget.disclaimer.security.link')}
88+
</A>
89+
</p>
90+
<h2 className={style.title}>{t('buy.exchange.infoContent.btcdirect.disclaimer.dataProtection.title')}</h2>
91+
<p>{t('buy.exchange.infoContent.btcdirect.disclaimer.dataProtection.text')}</p>
92+
<p>
93+
<A href={getBitrefillPrivacyLink()}>
94+
{t('buy.exchange.infoContent.btcdirect.disclaimer.dataProtection.link')}
95+
</A>
96+
</p>
97+
</div>
98+
<div className="text-center m-bottom-quarter">
99+
<Checkbox
100+
id="skip_disclaimer"
101+
label={t('buy.info.skip')}
102+
onChange={handleSkipDisclaimer} />
103+
</div>
104+
<div className="buttons text-center m-bottom-xlarge">
105+
<Button
106+
primary
107+
onClick={onAgreedTerms}>
108+
{t('buy.info.continue')}
109+
</Button>
110+
</div>
111+
</div>
112+
);
113+
};

frontends/web/src/locales/en/app.json

Lines changed: 13 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -397,6 +397,12 @@
397397
"upgrade": "Upgrade"
398398
},
399399
"buy": {
400+
"bitrefill": {
401+
"error": {
402+
"insufficientFunds": "Insufficient funds to buy gift card."
403+
},
404+
"transactionNote": "Sent to Bitrefill"
405+
},
400406
"exchange": {
401407
"bankTransfer": "Bank transfer",
402408
"bestDeal": "Best deal",
@@ -551,7 +557,9 @@
551557
"noExchanges": "Sorry, there are no available exchanges in this region.",
552558
"region": "Region",
553559
"selectRegion": "Not specified",
554-
"sell": "Sell"
560+
"sell": "Sell",
561+
"spend": "Spend",
562+
"spend_bitrefill": "Shop gift cards, phone refills & more"
555563
},
556564
"info": {
557565
"continue": "Agree and continue",
@@ -804,7 +812,7 @@
804812
},
805813
"generic": {
806814
"buy": "Buy {{coinCode}}",
807-
"buySell": "Buy & sell",
815+
"buySell": "Marketplace",
808816
"buy_bitcoin": "Buy Bitcoin",
809817
"buy_crypto": "Buy crypto",
810818
"enabled_false": "Disabled",
@@ -813,7 +821,9 @@
813821
"receive": "Receive {{coinCode}}",
814822
"receive_bitcoin": "Receive Bitcoin",
815823
"receive_crypto": "Receive crypto",
816-
"search": "Search…"
824+
"search": "Search…",
825+
"spend_bitcoin": "Spend bitcoin",
826+
"spend_crypto": "Spend crypto"
817827
},
818828
"genericError": "An error occurred. If you notice any issues, please restart the application.",
819829
"goal": {

0 commit comments

Comments
 (0)