Skip to content

Commit 6cb644b

Browse files
committed
Merge branch 'frontend-block-bitrefill-when-singing' into staging-bitrefill
2 parents 1e61b05 + b092422 commit 6cb644b

File tree

1 file changed

+121
-66
lines changed

1 file changed

+121
-66
lines changed

frontends/web/src/routes/market/bitrefill.tsx

Lines changed: 121 additions & 66 deletions
Original file line numberDiff line numberDiff line change
@@ -18,18 +18,20 @@ import { useState, useEffect, useRef, useCallback } from 'react';
1818
import { useTranslation } from 'react-i18next';
1919
import { Header } from '@/components/layout';
2020
import { Spinner } from '@/components/spinner/Spinner';
21-
import { AccountCode, IAccount, proposeTx, sendTx, TTxInput } from '@/api/account';
21+
import { AccountCode, IAccount, proposeTx, sendTx, TTxInput, TTxProposalResult } from '@/api/account';
2222
import { findAccount, isBitcoinOnly } from '@/routes/account/utils';
2323
import { useDarkmode } from '@/hooks/darkmode';
2424
import { getConfig } from '@/utils/config';
25-
import style from './iframe.module.css';
2625
import { i18n } from '@/i18n/i18n';
2726
import { alertUser } from '@/components/alert/Alert';
2827
import { parseExternalBtcAmount } from '@/api/coins';
2928
import { useLoad } from '@/hooks/api';
3029
import { BitrefillTerms, localeMapping } from '@/components/terms/bitrefill-terms';
3130
import { getBitrefillInfo } from '@/api/market';
3231
import { getURLOrigin } from '@/utils/url';
32+
import { WaitDialog } from '@/components/wait-dialog/wait-dialog';
33+
import { AmountWithUnit } from '@/components/amount/amount-with-unit';
34+
import style from './iframe.module.css';
3335

3436
// Map coins supported by Bitrefill
3537
const coinMapping: Readonly<Record<string, string>> = {
@@ -61,6 +63,9 @@ export const Bitrefill = ({ accounts, code }: TProps) => {
6163
const config = useLoad(getConfig);
6264
const [agreedTerms, setAgreedTerms] = useState(false);
6365

66+
const [pendingPayment, setPendingPayment] = useState<boolean>(false);
67+
const [verifyPaymentRequest, setVerifyPaymentRequest] = useState<TTxProposalResult & { address: string } | false>(false);
68+
6469
const hasOnlyBTCAccounts = accounts.every(({ coinCode }) => isBitcoinOnly(coinCode));
6570

6671
useEffect(() => {
@@ -91,10 +96,100 @@ export const Bitrefill = ({ accounts, code }: TProps) => {
9196
};
9297
}, [onResize]);
9398

94-
const handleMessage = useCallback(async (event: MessageEvent) => {
99+
const handleConfiguration = useCallback(async (event: MessageEvent) => {
95100
if (
96101
!account
97102
|| !bitrefillInfo?.success
103+
) {
104+
return;
105+
}
106+
event.source?.postMessage({
107+
event: 'configuration',
108+
ref: bitrefillInfo.ref,
109+
utm_source: 'BITBOX',
110+
theme: isDarkMode ? 'dark' : 'light',
111+
hl: i18n.resolvedLanguage ? localeMapping[i18n.resolvedLanguage] : 'en',
112+
paymentMethods: account.coinCode ? coinMapping[account.coinCode] : 'bitcoin',
113+
refundAddress: bitrefillInfo.address,
114+
// Option to keep pending payment information longer in session, defaults to 'false'
115+
paymentPending: 'true',
116+
// Option to show payment information in the widget, defaults to 'true'
117+
showPaymentInfo: 'true'
118+
}, {
119+
targetOrigin: event.origin
120+
});
121+
}, [account, bitrefillInfo, isDarkMode]);
122+
123+
const handlePaymentRequest = useCallback(async (event: MessageEvent) => {
124+
if (!account || pendingPayment) {
125+
return;
126+
}
127+
setPendingPayment(true);
128+
129+
const data = typeof event.data === 'string' ? JSON.parse(event.data) : event.data;
130+
131+
// User clicked "Pay" in checkout
132+
const {
133+
invoiceId,
134+
paymentMethod,
135+
paymentAmount,
136+
paymentAddress,
137+
} = data;
138+
139+
const parsedAmount = await parseExternalBtcAmount(paymentAmount.toString());
140+
if (!parsedAmount.success) {
141+
alertUser(t('unknownError', { errorMessage: 'Invalid amount' }));
142+
setPendingPayment(false);
143+
return;
144+
}
145+
// Ensure expected payment method matches account
146+
if (coinMapping[account.coinCode] !== paymentMethod) {
147+
alertUser(t('unknownError', { errorMessage: 'Payment method mismatch' }));
148+
setPendingPayment(false);
149+
return;
150+
}
151+
152+
const txInput: TTxInput = {
153+
address: paymentAddress,
154+
amount: parsedAmount.amount,
155+
// Always use highest fee rate for Bitrefill spend
156+
useHighestFee: true,
157+
sendAll: 'no',
158+
selectedUTXOs: [],
159+
paymentRequest: null
160+
};
161+
162+
let result = await proposeTx(code, txInput);
163+
if (result.success) {
164+
const txNote = t('generic.paymentRequestNote', {
165+
name: 'Bitrefill',
166+
orderId: invoiceId,
167+
});
168+
169+
setVerifyPaymentRequest({
170+
address: paymentAddress,
171+
...result
172+
});
173+
const sendResult = await sendTx(code, txNote);
174+
setVerifyPaymentRequest(false);
175+
if (!sendResult.success && !('aborted' in sendResult)) {
176+
alertUser(t('unknownError', { errorMessage: sendResult.errorMessage }));
177+
}
178+
} else {
179+
if (result.errorCode === 'insufficientFunds') {
180+
alertUser(t('buy.bitrefill.error.' + result.errorCode));
181+
} else if (result.errorCode) {
182+
alertUser(t('send.error.' + result.errorCode));
183+
} else {
184+
alertUser(t('genericError'));
185+
}
186+
}
187+
setPendingPayment(false);
188+
}, [account, code, pendingPayment, t]);
189+
190+
const handleMessage = useCallback(async (event: MessageEvent) => {
191+
if (
192+
!bitrefillInfo?.success
98193
|| ![getURLOrigin(bitrefillInfo.url), 'https://embed.bitrefill.com'].includes(event.origin)
99194
) {
100195
return;
@@ -104,78 +199,18 @@ export const Bitrefill = ({ accounts, code }: TProps) => {
104199

105200
switch (data.event) {
106201
case 'request-configuration': {
107-
event.source?.postMessage({
108-
event: 'configuration',
109-
ref: bitrefillInfo.ref,
110-
utm_source: 'BITBOX',
111-
theme: isDarkMode ? 'dark' : 'light',
112-
hl: i18n.resolvedLanguage ? localeMapping[i18n.resolvedLanguage] : 'en',
113-
paymentMethods: account.coinCode ? coinMapping[account.coinCode] : 'bitcoin',
114-
refundAddress: bitrefillInfo.address,
115-
// Option to keep pending payment information longer in session, defaults to 'false'
116-
paymentPending: 'true',
117-
// Option to show payment information in the widget, defaults to 'true'
118-
showPaymentInfo: 'true'
119-
}, {
120-
targetOrigin: event.origin
121-
});
202+
handleConfiguration(event);
122203
break;
123204
}
124205
case 'payment_intent': {
125-
// User clicked "Pay" in checkout
126-
const {
127-
invoiceId,
128-
paymentMethod,
129-
paymentAmount,
130-
paymentAddress,
131-
} = data;
132-
133-
const parsedAmount = await parseExternalBtcAmount(paymentAmount.toString());
134-
if (!parsedAmount.success) {
135-
alertUser(t('unknownError', { errorMessage: 'Invalid amount' }));
136-
return;
137-
}
138-
// Ensure expected payment method matches account
139-
if (coinMapping[account.coinCode] !== paymentMethod) {
140-
alertUser(t('unknownError', { errorMessage: 'Payment method mismatch' }));
141-
}
142-
143-
const txInput: TTxInput = {
144-
address: paymentAddress,
145-
amount: parsedAmount.amount,
146-
// Always use highest fee rate for Bitrefill spend
147-
useHighestFee: true,
148-
sendAll: 'no',
149-
selectedUTXOs: [],
150-
paymentRequest: null
151-
};
152-
153-
let result = await proposeTx(code, txInput);
154-
if (result.success) {
155-
const txNote = t('generic.paymentRequestNote', {
156-
name: 'Bitrefill',
157-
orderId: invoiceId,
158-
});
159-
const sendResult = await sendTx(code, txNote);
160-
if (!sendResult.success && !('aborted' in sendResult)) {
161-
alertUser(t('unknownError', { errorMessage: sendResult.errorMessage }));
162-
}
163-
} else {
164-
if (result.errorCode === 'insufficientFunds') {
165-
alertUser(t('buy.bitrefill.error.' + result.errorCode));
166-
} else if (result.errorCode) {
167-
alertUser(t('send.error.' + result.errorCode));
168-
} else {
169-
alertUser(t('genericError'));
170-
}
171-
}
206+
handlePaymentRequest(event);
172207
break;
173208
}
174209
default: {
175210
break;
176211
}
177212
}
178-
}, [bitrefillInfo, isDarkMode, account, code, t]);
213+
}, [bitrefillInfo, handleConfiguration, handlePaymentRequest]);
179214

180215
useEffect(() => {
181216
window.addEventListener('message', handleMessage);
@@ -225,6 +260,26 @@ export const Bitrefill = ({ accounts, code }: TProps) => {
225260
}}
226261
/>
227262
)}
263+
264+
{ verifyPaymentRequest && verifyPaymentRequest.success && (
265+
<WaitDialog title={t('receive.verifyBitBox02')}>
266+
<p>
267+
{t('transaction.details.address')}
268+
<br />
269+
{verifyPaymentRequest.address}
270+
</p>
271+
<p>
272+
{t('transaction.details.amount')}
273+
<br />
274+
<AmountWithUnit amount={verifyPaymentRequest.amount} />
275+
</p>
276+
<p>
277+
{t('transaction.fee')}
278+
<br />
279+
<AmountWithUnit amount={verifyPaymentRequest.fee} />
280+
</p>
281+
</WaitDialog>
282+
)}
228283
</div>
229284
)}
230285
</div>

0 commit comments

Comments
 (0)