@rhinestone/deposit-modal 0.0.0-dev-20260921145008 → 0.0.0-dev-20260922130636
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +58 -3
- package/dist/BankOnrampStep-TGCFBOK6.mjs +581 -0
- package/dist/BankOnrampStep-YRLTVIUO.cjs +581 -0
- package/dist/{DepositModalReown-SPDVO4JA.mjs → DepositModalReown-EUTQNZJH.mjs} +4 -3
- package/dist/{DepositModalReown-FZ2EKFGG.cjs → DepositModalReown-U722QHH6.cjs} +17 -16
- package/dist/{WithdrawModal-GP5YD7GH.mjs → WithdrawModal-AKHOLJWO.mjs} +4 -3
- package/dist/WithdrawModal-FPNYN6PW.cjs +12 -0
- package/dist/{chunk-Y5ZEE4LP.mjs → chunk-AGVLO2I4.mjs} +101 -46
- package/dist/{chunk-MSMSLMVK.mjs → chunk-DFYHOBOV.mjs} +1 -1
- package/dist/{chunk-67EYNPTZ.cjs → chunk-EJVUZ3TW.cjs} +2 -2
- package/dist/chunk-EY2YNGFX.mjs +19 -0
- package/dist/chunk-G264OM23.cjs +224 -0
- package/dist/chunk-NU2EY65R.cjs +19 -0
- package/dist/{chunk-LE6CPB5Z.mjs → chunk-PXL3MSJT.mjs} +10 -8
- package/dist/{chunk-4T4LX4Z6.cjs → chunk-Q3YAJLOP.cjs} +96 -295
- package/dist/{chunk-45ADKWY3.cjs → chunk-SH5DPTYM.cjs} +396 -341
- package/dist/{chunk-UNO5CQL2.cjs → chunk-TCLVBWA7.cjs} +74 -72
- package/dist/{chunk-6WCIP52T.mjs → chunk-TQKXJDDO.mjs} +1 -1
- package/dist/chunk-WRDH7PR7.cjs +38 -0
- package/dist/{chunk-X4EH2ZV6.mjs → chunk-X3IBAIQB.mjs} +444 -643
- package/dist/chunk-XCLR5YGQ.mjs +38 -0
- package/dist/{chunk-G6IR4SMZ.cjs → chunk-YNERADQK.cjs} +1 -1
- package/dist/chunk-YY4ZOAH5.mjs +224 -0
- package/dist/compliance.cjs +4 -7
- package/dist/compliance.d.cts +24 -1
- package/dist/compliance.d.ts +24 -1
- package/dist/compliance.mjs +6 -9
- package/dist/constants.cjs +2 -2
- package/dist/constants.d.cts +1 -1
- package/dist/constants.d.ts +1 -1
- package/dist/constants.mjs +1 -1
- package/dist/deposit.cjs +5 -4
- package/dist/deposit.d.cts +4 -2
- package/dist/deposit.d.ts +4 -2
- package/dist/deposit.mjs +4 -3
- package/dist/embed.cjs +15 -14
- package/dist/embed.d.cts +3 -1
- package/dist/embed.d.ts +3 -1
- package/dist/embed.mjs +5 -4
- package/dist/index.cjs +6 -5
- package/dist/index.d.cts +3 -1
- package/dist/index.d.ts +3 -1
- package/dist/index.mjs +5 -4
- package/dist/offramp.cjs +4 -4
- package/dist/offramp.d.cts +2 -2
- package/dist/offramp.d.ts +2 -2
- package/dist/offramp.mjs +2 -2
- package/dist/onramp.cjs +5 -33
- package/dist/onramp.d.cts +3 -2
- package/dist/onramp.d.ts +3 -2
- package/dist/onramp.mjs +6 -34
- package/dist/{ramp-types-C6aUz4SZ.d.cts → ramp-types-CGnr3nRp.d.cts} +1 -1
- package/dist/{ramp-types-C6aUz4SZ.d.ts → ramp-types-CGnr3nRp.d.ts} +1 -1
- package/dist/styles.css +105 -0
- package/dist/{types-BShYNpvO.d.cts → types-D3EoU6Ux.d.cts} +22 -1
- package/dist/{types-BOxa1DE-.d.ts → types-DBbMympO.d.ts} +22 -1
- package/dist/withdraw.cjs +5 -4
- package/dist/withdraw.d.cts +4 -2
- package/dist/withdraw.d.ts +4 -2
- package/dist/withdraw.mjs +4 -3
- package/package.json +4 -4
- package/dist/WithdrawModal-3W7IS2MB.cjs +0 -11
package/README.md
CHANGED
|
@@ -308,6 +308,61 @@ frame-src https://widget.swapped.com https://sandbox.swapped.com https://connect
|
|
|
308
308
|
| `enableQrTransfer` | `boolean` | `true` | Show the Transfer Crypto / QR row. Set false for fiat-only. |
|
|
309
309
|
| `fiatMethods` | `FiatMethodsConfig` | regional | Optional exact subset of standard `payment_group`s. Providing it opts out of regional personalization; empty means none. |
|
|
310
310
|
|
|
311
|
+
### Bank transfer accounts (Noah)
|
|
312
|
+
|
|
313
|
+
> **Preview.** One rough edge remains, and it is easy to misread as a bug in
|
|
314
|
+
> your integration: when hosted onboarding finishes, the provider redirects the
|
|
315
|
+
> frame to a confirmation page that does not exist yet, so the customer briefly
|
|
316
|
+
> sees a 404 inside the modal. Nothing is lost — the modal reads the outcome
|
|
317
|
+
> from the compliance status, not from the frame, so verification still
|
|
318
|
+
> completes and the flow continues.
|
|
319
|
+
>
|
|
320
|
+
> Your proxy must forward `POST /compliance/verification` on the customer
|
|
321
|
+
> bearer; deploy it before shipping a modal that calls it, or first-time
|
|
322
|
+
> verification 404s at the proxy.
|
|
323
|
+
>
|
|
324
|
+
> The prop is opt-in and inert unless you pass it. Treat it as unstable until
|
|
325
|
+
> this note is removed.
|
|
326
|
+
|
|
327
|
+
Pass `bankOnramp` to replace only the Bank transfer row with reusable USD/EUR
|
|
328
|
+
account details. Card and Apple Pay continue through Swapped. A customer who has
|
|
329
|
+
not been verified is taken through hosted verification first; an existing active
|
|
330
|
+
account is resumed; otherwise the selected currency is configured once.
|
|
331
|
+
|
|
332
|
+
Payment status is shown under the bank details and polls while the account is on
|
|
333
|
+
screen, so a customer who transferred earlier sees where their money is on
|
|
334
|
+
reopening. `delivered` means the crypto has been confirmed on chain — not merely
|
|
335
|
+
that the provider reported sending it.
|
|
336
|
+
|
|
337
|
+
```tsx
|
|
338
|
+
<DepositModal
|
|
339
|
+
bankOnramp={{
|
|
340
|
+
country: "US",
|
|
341
|
+
currency: "USD", // Optional; omit to show every available currency.
|
|
342
|
+
getSession: (smartAccount) =>
|
|
343
|
+
authenticatedSessionClient.getCustomerSession({ smartAccount }),
|
|
344
|
+
}}
|
|
345
|
+
// ...
|
|
346
|
+
/>
|
|
347
|
+
```
|
|
348
|
+
|
|
349
|
+
`getSession` must call your authenticated server endpoint, which mints a
|
|
350
|
+
short-lived customer session with `createCustomerSessionHandler`. It is called
|
|
351
|
+
for every request; do not put the Rhinestone API key in the browser. Hosted bank
|
|
352
|
+
details are minted fresh for every view and retry, and the modal never receives
|
|
353
|
+
or stores the underlying bank coordinates.
|
|
354
|
+
|
|
355
|
+
`bankOnramp` enables Cash when `fiatMethods` is omitted. When an exact
|
|
356
|
+
`fiatMethods` subset is provided, it must include
|
|
357
|
+
`{ "bank-transfer": true }`. The proxy must forward the `/onramp/noah/*`
|
|
358
|
+
customer routes with the bearer token and without attaching its API key.
|
|
359
|
+
|
|
360
|
+
Hosts with a strict CSP must allow Noah Checkout:
|
|
361
|
+
|
|
362
|
+
```
|
|
363
|
+
frame-src https://checkout.noah.com https://checkout.sandbox.noah.com;
|
|
364
|
+
```
|
|
365
|
+
|
|
311
366
|
## Fund from Exchange (Swapped Connect)
|
|
312
367
|
|
|
313
368
|
Pass `enableExchangeConnect` to expose a "Fund from Exchange" row alongside the existing options. Clicking it opens an exchange picker backed by `/onramp/swapped/connect-exchanges`; after the user picks an exchange, the modal embeds Swapped's Connect iframe for that selected `connection`, signs in, and Swapped pulls the crypto on-chain to the smart account.
|
|
@@ -453,6 +508,6 @@ No crypto-input quoting is exposed. Accounts return safe references/config/statu
|
|
|
453
508
|
not bank instructions. Hosted interaction completion never means payment
|
|
454
509
|
settlement; funding, payout and late returns have independent status fields.
|
|
455
510
|
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
511
|
+
`listPayments`, `getPayment` and the whole offramp client answer 503 until their
|
|
512
|
+
processor implementations are deployed, so payment lifecycle is not readable yet.
|
|
513
|
+
Do not log or store hosted URLs.
|
|
@@ -0,0 +1,581 @@
|
|
|
1
|
+
import {
|
|
2
|
+
BankIcon,
|
|
3
|
+
BodyHeader,
|
|
4
|
+
Spinner,
|
|
5
|
+
useLatestRef
|
|
6
|
+
} from "./chunk-YY4ZOAH5.mjs";
|
|
7
|
+
import {
|
|
8
|
+
createOnrampService
|
|
9
|
+
} from "./chunk-XCLR5YGQ.mjs";
|
|
10
|
+
import "./chunk-PP4QE32S.mjs";
|
|
11
|
+
import {
|
|
12
|
+
getComplianceStatus,
|
|
13
|
+
startComplianceVerification
|
|
14
|
+
} from "./chunk-EY2YNGFX.mjs";
|
|
15
|
+
import "./chunk-TQKXJDDO.mjs";
|
|
16
|
+
import "./chunk-DFYHOBOV.mjs";
|
|
17
|
+
|
|
18
|
+
// src/components/steps/BankOnrampStep.tsx
|
|
19
|
+
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
|
20
|
+
|
|
21
|
+
// src/core/bank-onramp-progress.ts
|
|
22
|
+
var HELD = "on_hold";
|
|
23
|
+
var FAILED = "failed";
|
|
24
|
+
var DONE = "completed";
|
|
25
|
+
function started(status) {
|
|
26
|
+
return status !== "not_started";
|
|
27
|
+
}
|
|
28
|
+
function toPaymentProgress(payment) {
|
|
29
|
+
if (payment.bankReceipt === HELD || payment.conversion === HELD || payment.cryptoDelivery === HELD) {
|
|
30
|
+
return "on_hold";
|
|
31
|
+
}
|
|
32
|
+
if (payment.refund !== "none" || payment.adjustment !== "none") {
|
|
33
|
+
return "returned";
|
|
34
|
+
}
|
|
35
|
+
if (payment.bankReceipt === FAILED || payment.conversion === FAILED || payment.cryptoDelivery === FAILED) {
|
|
36
|
+
return "failed";
|
|
37
|
+
}
|
|
38
|
+
if (payment.cryptoDelivery === DONE) return "delivered";
|
|
39
|
+
if (started(payment.cryptoDelivery)) return "sending";
|
|
40
|
+
if (payment.conversion === DONE) return "sending";
|
|
41
|
+
if (started(payment.conversion)) return "converting";
|
|
42
|
+
if (payment.bankReceipt === DONE) return "received";
|
|
43
|
+
return "awaiting_transfer";
|
|
44
|
+
}
|
|
45
|
+
var TERMINAL = /* @__PURE__ */ new Set([
|
|
46
|
+
"delivered",
|
|
47
|
+
"returned",
|
|
48
|
+
"failed"
|
|
49
|
+
]);
|
|
50
|
+
function toPaymentView(payment) {
|
|
51
|
+
const progress = toPaymentProgress(payment);
|
|
52
|
+
return {
|
|
53
|
+
id: payment.id,
|
|
54
|
+
amount: payment.amount,
|
|
55
|
+
currency: payment.currency,
|
|
56
|
+
progress,
|
|
57
|
+
terminal: TERMINAL.has(progress)
|
|
58
|
+
};
|
|
59
|
+
}
|
|
60
|
+
function selectActivePayment(payments, currency) {
|
|
61
|
+
const scoped = currency ? payments.filter((p) => p.currency === currency) : payments;
|
|
62
|
+
if (scoped.length === 0) return null;
|
|
63
|
+
const views = scoped.map(toPaymentView);
|
|
64
|
+
const byRecency = [...views].sort((a, b) => {
|
|
65
|
+
const at = scoped.find((p) => p.id === a.id)?.updatedAt ?? "";
|
|
66
|
+
const bt = scoped.find((p) => p.id === b.id)?.updatedAt ?? "";
|
|
67
|
+
return bt.localeCompare(at);
|
|
68
|
+
});
|
|
69
|
+
return byRecency.find((view) => !view.terminal) ?? byRecency[0] ?? null;
|
|
70
|
+
}
|
|
71
|
+
function shouldKeepPolling(view) {
|
|
72
|
+
return view === null || !view.terminal;
|
|
73
|
+
}
|
|
74
|
+
var BANK_ONRAMP_COPY = {
|
|
75
|
+
awaiting_transfer: {
|
|
76
|
+
title: "Waiting for your transfer",
|
|
77
|
+
subtitle: "Send the transfer from your bank using the details above."
|
|
78
|
+
},
|
|
79
|
+
received: {
|
|
80
|
+
title: "Transfer received",
|
|
81
|
+
subtitle: "Your bank transfer has arrived and is being processed."
|
|
82
|
+
},
|
|
83
|
+
converting: {
|
|
84
|
+
title: "Converting",
|
|
85
|
+
subtitle: "Your money is being converted to crypto."
|
|
86
|
+
},
|
|
87
|
+
sending: {
|
|
88
|
+
title: "Sending to your account",
|
|
89
|
+
subtitle: "The crypto is on its way. This usually takes a few minutes."
|
|
90
|
+
},
|
|
91
|
+
delivered: {
|
|
92
|
+
title: "Funds delivered",
|
|
93
|
+
subtitle: "The crypto has arrived in your account."
|
|
94
|
+
},
|
|
95
|
+
on_hold: {
|
|
96
|
+
title: "Under review",
|
|
97
|
+
subtitle: "Your provider is reviewing this transfer and may contact you for more information."
|
|
98
|
+
},
|
|
99
|
+
returned: {
|
|
100
|
+
title: "Transfer returned",
|
|
101
|
+
subtitle: "This transfer was returned. Contact support if unexpected."
|
|
102
|
+
},
|
|
103
|
+
failed: {
|
|
104
|
+
title: "Transfer failed",
|
|
105
|
+
subtitle: "This transfer could not be completed. Contact support."
|
|
106
|
+
}
|
|
107
|
+
};
|
|
108
|
+
|
|
109
|
+
// src/core/noah-hosted.ts
|
|
110
|
+
var NOAH_HOSTED_ORIGINS = [
|
|
111
|
+
"https://checkout.noah.com",
|
|
112
|
+
"https://checkout.sandbox.noah.com"
|
|
113
|
+
];
|
|
114
|
+
function isTrustedNoahHostedUrl(url) {
|
|
115
|
+
let parsed;
|
|
116
|
+
try {
|
|
117
|
+
parsed = new URL(url);
|
|
118
|
+
} catch {
|
|
119
|
+
return false;
|
|
120
|
+
}
|
|
121
|
+
return NOAH_HOSTED_ORIGINS.includes(parsed.origin);
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
// src/components/steps/BankOnrampStep.tsx
|
|
125
|
+
import { jsx, jsxs } from "react/jsx-runtime";
|
|
126
|
+
var SAFE_ERROR = "Bank details are unavailable. Try again.";
|
|
127
|
+
var OPTIONS_ERROR = "Bank transfer options are unavailable. Try again.";
|
|
128
|
+
var PAYMENT_POLL_MS = 15e3;
|
|
129
|
+
var VERIFICATION_POLL_MS = 5e3;
|
|
130
|
+
function BankOnrampStep({
|
|
131
|
+
backendUrl,
|
|
132
|
+
smartAccount,
|
|
133
|
+
bankOnramp,
|
|
134
|
+
onGoBack,
|
|
135
|
+
onError,
|
|
136
|
+
openExternally
|
|
137
|
+
}) {
|
|
138
|
+
const getSessionRef = useLatestRef(bankOnramp.getSession);
|
|
139
|
+
const sessionOptions = useMemo(
|
|
140
|
+
() => ({
|
|
141
|
+
backendUrl,
|
|
142
|
+
getSession: () => getSessionRef.current(smartAccount)
|
|
143
|
+
}),
|
|
144
|
+
[backendUrl, getSessionRef, smartAccount]
|
|
145
|
+
);
|
|
146
|
+
const service = useMemo(
|
|
147
|
+
() => createOnrampService(sessionOptions),
|
|
148
|
+
[sessionOptions]
|
|
149
|
+
);
|
|
150
|
+
const [options, setOptions] = useState([]);
|
|
151
|
+
const [accounts, setAccounts] = useState([]);
|
|
152
|
+
const [loading, setLoading] = useState(true);
|
|
153
|
+
const [selectedAccount, setSelectedAccount] = useState(null);
|
|
154
|
+
const [hostedUrl, setHostedUrl] = useState(null);
|
|
155
|
+
const [iframeLoaded, setIframeLoaded] = useState(false);
|
|
156
|
+
const [externalOpened, setExternalOpened] = useState(false);
|
|
157
|
+
const [error, setError] = useState(null);
|
|
158
|
+
const [payment, setPayment] = useState(null);
|
|
159
|
+
const [verifying, setVerifying] = useState(null);
|
|
160
|
+
const requestGenerationRef = useRef(0);
|
|
161
|
+
const isCurrentRequest = useCallback(
|
|
162
|
+
(generation) => requestGenerationRef.current === generation,
|
|
163
|
+
[]
|
|
164
|
+
);
|
|
165
|
+
const ensureVerified = useCallback(async () => {
|
|
166
|
+
const current = await getComplianceStatus(sessionOptions);
|
|
167
|
+
if (current.status === "approved") return "approved";
|
|
168
|
+
if (current.status === "declined") return "declined";
|
|
169
|
+
const session = await startComplianceVerification(sessionOptions, {
|
|
170
|
+
// Verification is per fiat currency. Without an explicit choice the US
|
|
171
|
+
// dollar journey is the one every configured country can complete.
|
|
172
|
+
currency: bankOnramp.currency ?? "USD"
|
|
173
|
+
});
|
|
174
|
+
if (session.status === "failed") return "declined";
|
|
175
|
+
if (!session.nextAction) return "reviewing";
|
|
176
|
+
if (!isTrustedNoahHostedUrl(session.nextAction.url)) return "reviewing";
|
|
177
|
+
if (openExternally) {
|
|
178
|
+
await openExternally(session.nextAction.url);
|
|
179
|
+
setExternalOpened(true);
|
|
180
|
+
return "action_required";
|
|
181
|
+
}
|
|
182
|
+
setHostedUrl(session.nextAction.url);
|
|
183
|
+
setIframeLoaded(false);
|
|
184
|
+
return "action_required";
|
|
185
|
+
}, [bankOnramp.currency, openExternally, sessionOptions]);
|
|
186
|
+
const loadAccounts = useCallback(async () => {
|
|
187
|
+
const generation = ++requestGenerationRef.current;
|
|
188
|
+
setLoading(true);
|
|
189
|
+
setError(null);
|
|
190
|
+
try {
|
|
191
|
+
const verification = await ensureVerified();
|
|
192
|
+
if (!isCurrentRequest(generation)) return;
|
|
193
|
+
if (verification !== "approved") {
|
|
194
|
+
setVerifying(verification);
|
|
195
|
+
return;
|
|
196
|
+
}
|
|
197
|
+
setVerifying(null);
|
|
198
|
+
setExternalOpened(false);
|
|
199
|
+
const intent = {
|
|
200
|
+
country: bankOnramp.country,
|
|
201
|
+
paymentMethod: "bank_transfer",
|
|
202
|
+
...bankOnramp.currency ? { currency: bankOnramp.currency } : {}
|
|
203
|
+
};
|
|
204
|
+
const [available, existing] = await Promise.all([
|
|
205
|
+
service.getOptions(intent),
|
|
206
|
+
service.listAccounts()
|
|
207
|
+
]);
|
|
208
|
+
if (!isCurrentRequest(generation)) return;
|
|
209
|
+
setOptions(available.items);
|
|
210
|
+
setAccounts(existing.items);
|
|
211
|
+
} catch {
|
|
212
|
+
if (!isCurrentRequest(generation)) return;
|
|
213
|
+
setError(OPTIONS_ERROR);
|
|
214
|
+
onError?.(OPTIONS_ERROR, "BANK_ONRAMP_OPTIONS_FAILED");
|
|
215
|
+
} finally {
|
|
216
|
+
if (isCurrentRequest(generation)) setLoading(false);
|
|
217
|
+
}
|
|
218
|
+
}, [
|
|
219
|
+
bankOnramp.country,
|
|
220
|
+
bankOnramp.currency,
|
|
221
|
+
ensureVerified,
|
|
222
|
+
isCurrentRequest,
|
|
223
|
+
onError,
|
|
224
|
+
service
|
|
225
|
+
]);
|
|
226
|
+
useEffect(() => {
|
|
227
|
+
setSelectedAccount(null);
|
|
228
|
+
setHostedUrl(null);
|
|
229
|
+
setExternalOpened(false);
|
|
230
|
+
void loadAccounts();
|
|
231
|
+
return () => {
|
|
232
|
+
requestGenerationRef.current += 1;
|
|
233
|
+
};
|
|
234
|
+
}, [loadAccounts]);
|
|
235
|
+
const openDetails = useCallback(
|
|
236
|
+
async (account) => {
|
|
237
|
+
const generation = ++requestGenerationRef.current;
|
|
238
|
+
setLoading(true);
|
|
239
|
+
setError(null);
|
|
240
|
+
setHostedUrl(null);
|
|
241
|
+
setIframeLoaded(false);
|
|
242
|
+
setExternalOpened(false);
|
|
243
|
+
try {
|
|
244
|
+
const details = await service.getAccountDetails(account.id);
|
|
245
|
+
if (!isCurrentRequest(generation)) return;
|
|
246
|
+
const url = details.nextAction.url;
|
|
247
|
+
if (!isTrustedNoahHostedUrl(url)) {
|
|
248
|
+
if (!isCurrentRequest(generation)) return;
|
|
249
|
+
setError(SAFE_ERROR);
|
|
250
|
+
onError?.(SAFE_ERROR, "BANK_ONRAMP_DETAILS_FAILED");
|
|
251
|
+
return;
|
|
252
|
+
}
|
|
253
|
+
if (openExternally) {
|
|
254
|
+
try {
|
|
255
|
+
await openExternally(url);
|
|
256
|
+
} catch {
|
|
257
|
+
if (!isCurrentRequest(generation)) return;
|
|
258
|
+
setError(SAFE_ERROR);
|
|
259
|
+
onError?.(SAFE_ERROR, "BANK_ONRAMP_EXTERNAL_OPEN_FAILED");
|
|
260
|
+
return;
|
|
261
|
+
}
|
|
262
|
+
if (!isCurrentRequest(generation)) return;
|
|
263
|
+
setExternalOpened(true);
|
|
264
|
+
} else {
|
|
265
|
+
setHostedUrl(url);
|
|
266
|
+
}
|
|
267
|
+
} catch {
|
|
268
|
+
if (!isCurrentRequest(generation)) return;
|
|
269
|
+
setError(SAFE_ERROR);
|
|
270
|
+
onError?.(SAFE_ERROR, "BANK_ONRAMP_DETAILS_FAILED");
|
|
271
|
+
} finally {
|
|
272
|
+
if (isCurrentRequest(generation)) setLoading(false);
|
|
273
|
+
}
|
|
274
|
+
},
|
|
275
|
+
[isCurrentRequest, onError, openExternally, service]
|
|
276
|
+
);
|
|
277
|
+
const selectOption = useCallback(
|
|
278
|
+
async (option, account) => {
|
|
279
|
+
if (account) {
|
|
280
|
+
setSelectedAccount(account);
|
|
281
|
+
await openDetails(account);
|
|
282
|
+
return;
|
|
283
|
+
}
|
|
284
|
+
const generation = ++requestGenerationRef.current;
|
|
285
|
+
setLoading(true);
|
|
286
|
+
setError(null);
|
|
287
|
+
try {
|
|
288
|
+
const request = {
|
|
289
|
+
purpose: "account_setup",
|
|
290
|
+
country: bankOnramp.country,
|
|
291
|
+
currency: option.currency,
|
|
292
|
+
paymentMethod: option.paymentMethod,
|
|
293
|
+
optionId: option.optionId
|
|
294
|
+
};
|
|
295
|
+
const nextAccount = (await service.createSetup(request)).account;
|
|
296
|
+
if (!isCurrentRequest(generation)) return;
|
|
297
|
+
setSelectedAccount(nextAccount);
|
|
298
|
+
await openDetails(nextAccount);
|
|
299
|
+
} catch {
|
|
300
|
+
if (!isCurrentRequest(generation)) return;
|
|
301
|
+
setError(SAFE_ERROR);
|
|
302
|
+
onError?.(SAFE_ERROR, "BANK_ONRAMP_SETUP_FAILED");
|
|
303
|
+
setLoading(false);
|
|
304
|
+
}
|
|
305
|
+
},
|
|
306
|
+
[bankOnramp.country, isCurrentRequest, onError, openDetails, service]
|
|
307
|
+
);
|
|
308
|
+
const backToCurrencies = useCallback(() => {
|
|
309
|
+
requestGenerationRef.current += 1;
|
|
310
|
+
setSelectedAccount(null);
|
|
311
|
+
setHostedUrl(null);
|
|
312
|
+
setExternalOpened(false);
|
|
313
|
+
setError(null);
|
|
314
|
+
}, []);
|
|
315
|
+
useEffect(() => {
|
|
316
|
+
if (verifying !== "action_required" && verifying !== "reviewing") return;
|
|
317
|
+
let active = true;
|
|
318
|
+
const timer = setInterval(() => {
|
|
319
|
+
void (async () => {
|
|
320
|
+
try {
|
|
321
|
+
const status = await getComplianceStatus(sessionOptions);
|
|
322
|
+
if (!active) return;
|
|
323
|
+
if (status.status === "approved") {
|
|
324
|
+
setHostedUrl(null);
|
|
325
|
+
void loadAccounts();
|
|
326
|
+
} else if (status.status === "declined") {
|
|
327
|
+
setVerifying("declined");
|
|
328
|
+
}
|
|
329
|
+
} catch {
|
|
330
|
+
}
|
|
331
|
+
})();
|
|
332
|
+
}, VERIFICATION_POLL_MS);
|
|
333
|
+
return () => {
|
|
334
|
+
active = false;
|
|
335
|
+
clearInterval(timer);
|
|
336
|
+
};
|
|
337
|
+
}, [verifying, sessionOptions, loadAccounts]);
|
|
338
|
+
useEffect(() => {
|
|
339
|
+
if (!selectedAccount) {
|
|
340
|
+
setPayment(null);
|
|
341
|
+
return;
|
|
342
|
+
}
|
|
343
|
+
let active = true;
|
|
344
|
+
let timer;
|
|
345
|
+
const load = async () => {
|
|
346
|
+
try {
|
|
347
|
+
const page = await service.listPayments({ limit: 10 });
|
|
348
|
+
if (!active) return;
|
|
349
|
+
const view = selectActivePayment(
|
|
350
|
+
page.items ?? [],
|
|
351
|
+
selectedAccount.currency
|
|
352
|
+
);
|
|
353
|
+
setPayment(view);
|
|
354
|
+
if (!shouldKeepPolling(view) && timer) clearInterval(timer);
|
|
355
|
+
} catch {
|
|
356
|
+
}
|
|
357
|
+
};
|
|
358
|
+
void load();
|
|
359
|
+
timer = setInterval(() => void load(), PAYMENT_POLL_MS);
|
|
360
|
+
return () => {
|
|
361
|
+
active = false;
|
|
362
|
+
if (timer) clearInterval(timer);
|
|
363
|
+
};
|
|
364
|
+
}, [selectedAccount, service]);
|
|
365
|
+
const leaveStep = useCallback(() => {
|
|
366
|
+
requestGenerationRef.current += 1;
|
|
367
|
+
onGoBack();
|
|
368
|
+
}, [onGoBack]);
|
|
369
|
+
const retry = selectedAccount ? () => void openDetails(selectedAccount) : () => void loadAccounts();
|
|
370
|
+
const paymentStatus = payment ? /* @__PURE__ */ jsxs(
|
|
371
|
+
"div",
|
|
372
|
+
{
|
|
373
|
+
className: `rs-bank-onramp-status rs-bank-onramp-status--${payment.progress}`,
|
|
374
|
+
"aria-live": "polite",
|
|
375
|
+
"data-testid": "bank-onramp-payment-status",
|
|
376
|
+
children: [
|
|
377
|
+
/* @__PURE__ */ jsx("div", { className: "rs-bank-onramp-status-title", children: BANK_ONRAMP_COPY[payment.progress].title }),
|
|
378
|
+
/* @__PURE__ */ jsx("div", { className: "rs-bank-onramp-status-subtitle", children: BANK_ONRAMP_COPY[payment.progress].subtitle })
|
|
379
|
+
]
|
|
380
|
+
}
|
|
381
|
+
) : null;
|
|
382
|
+
return /* @__PURE__ */ jsx("div", { className: "rs-screen rs-bank-onramp", children: /* @__PURE__ */ jsxs("div", { className: "rs-screen-body rs-screen-body--gap-32", children: [
|
|
383
|
+
/* @__PURE__ */ jsx(
|
|
384
|
+
BodyHeader,
|
|
385
|
+
{
|
|
386
|
+
icon: /* @__PURE__ */ jsx(BankIcon, {}),
|
|
387
|
+
title: "Bank transfer",
|
|
388
|
+
subtitle: "Choose an account to view transfer details"
|
|
389
|
+
}
|
|
390
|
+
),
|
|
391
|
+
loading && /* @__PURE__ */ jsxs("div", { className: "rs-loading-state", "aria-live": "polite", children: [
|
|
392
|
+
/* @__PURE__ */ jsx(Spinner, {}),
|
|
393
|
+
/* @__PURE__ */ jsx("div", { className: "rs-loading-title", children: "Preparing bank details\u2026" })
|
|
394
|
+
] }),
|
|
395
|
+
!loading && error && /* @__PURE__ */ jsxs("div", { className: "rs-loading-state", "aria-live": "polite", children: [
|
|
396
|
+
/* @__PURE__ */ jsx("div", { className: "rs-loading-title", children: error }),
|
|
397
|
+
/* @__PURE__ */ jsx(
|
|
398
|
+
"button",
|
|
399
|
+
{
|
|
400
|
+
type: "button",
|
|
401
|
+
className: "rs-button rs-button--default",
|
|
402
|
+
onClick: retry,
|
|
403
|
+
children: "Retry"
|
|
404
|
+
}
|
|
405
|
+
),
|
|
406
|
+
/* @__PURE__ */ jsx(
|
|
407
|
+
"button",
|
|
408
|
+
{
|
|
409
|
+
type: "button",
|
|
410
|
+
className: "rs-button rs-button--default",
|
|
411
|
+
onClick: selectedAccount ? backToCurrencies : leaveStep,
|
|
412
|
+
children: "Back"
|
|
413
|
+
}
|
|
414
|
+
)
|
|
415
|
+
] }),
|
|
416
|
+
!loading && !error && verifying === "declined" && /* @__PURE__ */ jsxs("div", { className: "rs-loading-state", "aria-live": "polite", children: [
|
|
417
|
+
/* @__PURE__ */ jsx("div", { className: "rs-loading-title", children: "Verification unsuccessful" }),
|
|
418
|
+
/* @__PURE__ */ jsx("div", { className: "rs-loading-subtitle", children: "Your provider could not verify these details. Contact support to continue." }),
|
|
419
|
+
/* @__PURE__ */ jsx(
|
|
420
|
+
"button",
|
|
421
|
+
{
|
|
422
|
+
type: "button",
|
|
423
|
+
className: "rs-button rs-button--default",
|
|
424
|
+
onClick: leaveStep,
|
|
425
|
+
children: "Back"
|
|
426
|
+
}
|
|
427
|
+
)
|
|
428
|
+
] }),
|
|
429
|
+
!loading && !error && verifying === "reviewing" && /* @__PURE__ */ jsxs("div", { className: "rs-loading-state", "aria-live": "polite", children: [
|
|
430
|
+
/* @__PURE__ */ jsx(Spinner, {}),
|
|
431
|
+
/* @__PURE__ */ jsx("div", { className: "rs-loading-title", children: "Checking your details" }),
|
|
432
|
+
/* @__PURE__ */ jsx("div", { className: "rs-loading-subtitle", children: "This usually takes a moment. You can close this and come back." }),
|
|
433
|
+
/* @__PURE__ */ jsx(
|
|
434
|
+
"button",
|
|
435
|
+
{
|
|
436
|
+
type: "button",
|
|
437
|
+
className: "rs-button rs-button--default",
|
|
438
|
+
onClick: leaveStep,
|
|
439
|
+
children: "Back"
|
|
440
|
+
}
|
|
441
|
+
)
|
|
442
|
+
] }),
|
|
443
|
+
!loading && !error && verifying === "action_required" && externalOpened && /* @__PURE__ */ jsxs("div", { className: "rs-loading-state", "aria-live": "polite", children: [
|
|
444
|
+
/* @__PURE__ */ jsx("div", { className: "rs-loading-title", children: "Verification opened" }),
|
|
445
|
+
/* @__PURE__ */ jsx("div", { className: "rs-loading-subtitle", children: "Finish verifying in your browser, then return here." }),
|
|
446
|
+
/* @__PURE__ */ jsx(
|
|
447
|
+
"button",
|
|
448
|
+
{
|
|
449
|
+
type: "button",
|
|
450
|
+
className: "rs-button rs-button--default",
|
|
451
|
+
onClick: leaveStep,
|
|
452
|
+
children: "Back"
|
|
453
|
+
}
|
|
454
|
+
)
|
|
455
|
+
] }),
|
|
456
|
+
!loading && !error && verifying === "action_required" && hostedUrl && /* @__PURE__ */ jsxs("div", { className: "rs-bank-onramp-hosted", children: [
|
|
457
|
+
!iframeLoaded && /* @__PURE__ */ jsx("div", { className: "rs-bank-onramp-frame-loading", "aria-live": "polite", children: /* @__PURE__ */ jsx(Spinner, {}) }),
|
|
458
|
+
/* @__PURE__ */ jsx(
|
|
459
|
+
"iframe",
|
|
460
|
+
{
|
|
461
|
+
title: "Verify your details",
|
|
462
|
+
src: hostedUrl,
|
|
463
|
+
sandbox: "allow-forms allow-scripts allow-same-origin",
|
|
464
|
+
referrerPolicy: "no-referrer",
|
|
465
|
+
allow: "camera; microphone; clipboard-write",
|
|
466
|
+
onLoad: () => setIframeLoaded(true)
|
|
467
|
+
}
|
|
468
|
+
),
|
|
469
|
+
/* @__PURE__ */ jsx("div", { className: "rs-screen-button-row", children: /* @__PURE__ */ jsx(
|
|
470
|
+
"button",
|
|
471
|
+
{
|
|
472
|
+
type: "button",
|
|
473
|
+
className: "rs-button rs-button--default",
|
|
474
|
+
onClick: leaveStep,
|
|
475
|
+
children: "Back"
|
|
476
|
+
}
|
|
477
|
+
) })
|
|
478
|
+
] }),
|
|
479
|
+
!loading && !error && !verifying && !selectedAccount && /* @__PURE__ */ jsxs("div", { className: "rs-list", children: [
|
|
480
|
+
options.map((option) => {
|
|
481
|
+
const account = accounts.find(
|
|
482
|
+
(candidate) => candidate.status === "active" && candidate.currency === option.currency
|
|
483
|
+
);
|
|
484
|
+
return /* @__PURE__ */ jsxs(
|
|
485
|
+
"button",
|
|
486
|
+
{
|
|
487
|
+
type: "button",
|
|
488
|
+
className: "rs-list-row",
|
|
489
|
+
onClick: () => void selectOption(option, account),
|
|
490
|
+
"aria-label": `${account ? "View" : "Set up"} ${option.currency} bank account`,
|
|
491
|
+
children: [
|
|
492
|
+
/* @__PURE__ */ jsx("span", { className: "rs-list-row-leading", children: /* @__PURE__ */ jsx(BankIcon, {}) }),
|
|
493
|
+
/* @__PURE__ */ jsx("span", { className: "rs-list-row-body", children: /* @__PURE__ */ jsxs("span", { className: "rs-list-row-text", children: [
|
|
494
|
+
/* @__PURE__ */ jsxs("span", { className: "rs-list-row-title", children: [
|
|
495
|
+
option.currency,
|
|
496
|
+
" account"
|
|
497
|
+
] }),
|
|
498
|
+
/* @__PURE__ */ jsx("span", { className: "rs-list-row-subtitle", children: account ? "View bank details" : "Set up bank details" })
|
|
499
|
+
] }) })
|
|
500
|
+
]
|
|
501
|
+
},
|
|
502
|
+
option.currency
|
|
503
|
+
);
|
|
504
|
+
}),
|
|
505
|
+
options.length === 0 && /* @__PURE__ */ jsxs("div", { className: "rs-loading-state", children: [
|
|
506
|
+
/* @__PURE__ */ jsx("div", { className: "rs-loading-title", children: "No bank transfer options are available." }),
|
|
507
|
+
/* @__PURE__ */ jsx(
|
|
508
|
+
"button",
|
|
509
|
+
{
|
|
510
|
+
type: "button",
|
|
511
|
+
className: "rs-button rs-button--default",
|
|
512
|
+
onClick: leaveStep,
|
|
513
|
+
children: "Back"
|
|
514
|
+
}
|
|
515
|
+
)
|
|
516
|
+
] })
|
|
517
|
+
] }),
|
|
518
|
+
!loading && !error && !verifying && hostedUrl && /* @__PURE__ */ jsxs("div", { className: "rs-bank-onramp-hosted", children: [
|
|
519
|
+
!iframeLoaded && /* @__PURE__ */ jsx("div", { className: "rs-bank-onramp-frame-loading", "aria-live": "polite", children: /* @__PURE__ */ jsx(Spinner, {}) }),
|
|
520
|
+
/* @__PURE__ */ jsx(
|
|
521
|
+
"iframe",
|
|
522
|
+
{
|
|
523
|
+
title: "Bank account details",
|
|
524
|
+
src: hostedUrl,
|
|
525
|
+
sandbox: "allow-forms allow-scripts allow-same-origin",
|
|
526
|
+
referrerPolicy: "no-referrer",
|
|
527
|
+
allow: "clipboard-write",
|
|
528
|
+
onLoad: () => setIframeLoaded(true)
|
|
529
|
+
}
|
|
530
|
+
),
|
|
531
|
+
paymentStatus,
|
|
532
|
+
/* @__PURE__ */ jsxs("div", { className: "rs-screen-button-row", children: [
|
|
533
|
+
/* @__PURE__ */ jsx(
|
|
534
|
+
"button",
|
|
535
|
+
{
|
|
536
|
+
type: "button",
|
|
537
|
+
className: "rs-button rs-button--default",
|
|
538
|
+
onClick: retry,
|
|
539
|
+
children: "Retry"
|
|
540
|
+
}
|
|
541
|
+
),
|
|
542
|
+
/* @__PURE__ */ jsx(
|
|
543
|
+
"button",
|
|
544
|
+
{
|
|
545
|
+
type: "button",
|
|
546
|
+
className: "rs-button rs-button--default",
|
|
547
|
+
onClick: backToCurrencies,
|
|
548
|
+
children: "Back"
|
|
549
|
+
}
|
|
550
|
+
)
|
|
551
|
+
] })
|
|
552
|
+
] }),
|
|
553
|
+
!loading && !error && !verifying && externalOpened && /* @__PURE__ */ jsxs("div", { className: "rs-loading-state", "aria-live": "polite", children: [
|
|
554
|
+
/* @__PURE__ */ jsx("div", { className: "rs-loading-title", children: "Bank details opened" }),
|
|
555
|
+
/* @__PURE__ */ jsx("div", { className: "rs-loading-subtitle", children: "Return here when you have finished." }),
|
|
556
|
+
paymentStatus,
|
|
557
|
+
/* @__PURE__ */ jsx(
|
|
558
|
+
"button",
|
|
559
|
+
{
|
|
560
|
+
type: "button",
|
|
561
|
+
className: "rs-button rs-button--default",
|
|
562
|
+
onClick: retry,
|
|
563
|
+
children: "Retry"
|
|
564
|
+
}
|
|
565
|
+
),
|
|
566
|
+
/* @__PURE__ */ jsx(
|
|
567
|
+
"button",
|
|
568
|
+
{
|
|
569
|
+
type: "button",
|
|
570
|
+
className: "rs-button rs-button--default",
|
|
571
|
+
onClick: backToCurrencies,
|
|
572
|
+
children: "Back"
|
|
573
|
+
}
|
|
574
|
+
)
|
|
575
|
+
] })
|
|
576
|
+
] }) });
|
|
577
|
+
}
|
|
578
|
+
BankOnrampStep.displayName = "BankOnrampStep";
|
|
579
|
+
export {
|
|
580
|
+
BankOnrampStep
|
|
581
|
+
};
|