@@ -18,18 +18,20 @@ import { useState, useEffect, useRef, useCallback } from 'react';
18
18
import { useTranslation } from 'react-i18next' ;
19
19
import { Header } from '@/components/layout' ;
20
20
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' ;
22
22
import { findAccount , isBitcoinOnly } from '@/routes/account/utils' ;
23
23
import { useDarkmode } from '@/hooks/darkmode' ;
24
24
import { getConfig } from '@/utils/config' ;
25
- import style from './iframe.module.css' ;
26
25
import { i18n } from '@/i18n/i18n' ;
27
26
import { alertUser } from '@/components/alert/Alert' ;
28
27
import { parseExternalBtcAmount } from '@/api/coins' ;
29
28
import { useLoad } from '@/hooks/api' ;
30
29
import { BitrefillTerms , localeMapping } from '@/components/terms/bitrefill-terms' ;
31
30
import { getBitrefillInfo } from '@/api/market' ;
32
31
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' ;
33
35
34
36
// Map coins supported by Bitrefill
35
37
const coinMapping : Readonly < Record < string , string > > = {
@@ -61,6 +63,9 @@ export const Bitrefill = ({ accounts, code }: TProps) => {
61
63
const config = useLoad ( getConfig ) ;
62
64
const [ agreedTerms , setAgreedTerms ] = useState ( false ) ;
63
65
66
+ const [ pendingPayment , setPendingPayment ] = useState < boolean > ( false ) ;
67
+ const [ verifyPaymentRequest , setVerifyPaymentRequest ] = useState < TTxProposalResult & { address : string } | false > ( false ) ;
68
+
64
69
const hasOnlyBTCAccounts = accounts . every ( ( { coinCode } ) => isBitcoinOnly ( coinCode ) ) ;
65
70
66
71
useEffect ( ( ) => {
@@ -91,10 +96,100 @@ export const Bitrefill = ({ accounts, code }: TProps) => {
91
96
} ;
92
97
} , [ onResize ] ) ;
93
98
94
- const handleMessage = useCallback ( async ( event : MessageEvent ) => {
99
+ const handleConfiguration = useCallback ( async ( event : MessageEvent ) => {
95
100
if (
96
101
! account
97
102
|| ! 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
98
193
|| ! [ getURLOrigin ( bitrefillInfo . url ) , 'https://embed.bitrefill.com' ] . includes ( event . origin )
99
194
) {
100
195
return ;
@@ -104,78 +199,18 @@ export const Bitrefill = ({ accounts, code }: TProps) => {
104
199
105
200
switch ( data . event ) {
106
201
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 ) ;
122
203
break ;
123
204
}
124
205
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 ) ;
172
207
break ;
173
208
}
174
209
default : {
175
210
break ;
176
211
}
177
212
}
178
- } , [ bitrefillInfo , isDarkMode , account , code , t ] ) ;
213
+ } , [ bitrefillInfo , handleConfiguration , handlePaymentRequest ] ) ;
179
214
180
215
useEffect ( ( ) => {
181
216
window . addEventListener ( 'message' , handleMessage ) ;
@@ -225,6 +260,26 @@ export const Bitrefill = ({ accounts, code }: TProps) => {
225
260
} }
226
261
/>
227
262
) }
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
+ ) }
228
283
</ div >
229
284
) }
230
285
</ div >
0 commit comments