@phala/pay 0.8.3 → 0.9.1

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.
Files changed (84) hide show
  1. package/README.md +20 -83
  2. package/dist/checkout.d.ts +4 -2
  3. package/dist/checkout.d.ts.map +1 -1
  4. package/dist/checkout.js +16 -1
  5. package/dist/checkout.js.map +1 -1
  6. package/dist/index.d.ts +5 -3
  7. package/dist/index.d.ts.map +1 -1
  8. package/dist/index.js +4 -3
  9. package/dist/index.js.map +1 -1
  10. package/dist/shared/checkout-params.d.ts +7 -0
  11. package/dist/shared/checkout-params.d.ts.map +1 -0
  12. package/dist/shared/checkout-params.js +2 -0
  13. package/dist/shared/checkout-params.js.map +1 -0
  14. package/package.json +16 -34
  15. package/src/checkout.ts +19 -2
  16. package/src/index.ts +5 -2
  17. package/src/shared/checkout-params.ts +6 -0
  18. package/dist/react/Checkout.d.ts +0 -43
  19. package/dist/react/Checkout.d.ts.map +0 -1
  20. package/dist/react/Checkout.js +0 -195
  21. package/dist/react/Checkout.js.map +0 -1
  22. package/dist/react/DepositAddress.d.ts +0 -32
  23. package/dist/react/DepositAddress.d.ts.map +0 -1
  24. package/dist/react/DepositAddress.js +0 -131
  25. package/dist/react/DepositAddress.js.map +0 -1
  26. package/dist/react/Field.d.ts +0 -7
  27. package/dist/react/Field.d.ts.map +0 -1
  28. package/dist/react/Field.js +0 -21
  29. package/dist/react/Field.js.map +0 -1
  30. package/dist/react/Icons.d.ts +0 -17
  31. package/dist/react/Icons.d.ts.map +0 -1
  32. package/dist/react/Icons.js +0 -18
  33. package/dist/react/Icons.js.map +0 -1
  34. package/dist/react/QrCode.d.ts +0 -8
  35. package/dist/react/QrCode.d.ts.map +0 -1
  36. package/dist/react/QrCode.js +0 -20
  37. package/dist/react/QrCode.js.map +0 -1
  38. package/dist/react/appearance.d.ts +0 -5
  39. package/dist/react/appearance.d.ts.map +0 -1
  40. package/dist/react/appearance.js +0 -2
  41. package/dist/react/appearance.js.map +0 -1
  42. package/dist/react/icon-paths.d.ts +0 -13
  43. package/dist/react/icon-paths.d.ts.map +0 -1
  44. package/dist/react/icon-paths.js +0 -14
  45. package/dist/react/icon-paths.js.map +0 -1
  46. package/dist/react/index.d.ts +0 -6
  47. package/dist/react/index.d.ts.map +0 -1
  48. package/dist/react/index.js +0 -6
  49. package/dist/react/index.js.map +0 -1
  50. package/dist/react/useCheckout.d.ts +0 -9
  51. package/dist/react/useCheckout.d.ts.map +0 -1
  52. package/dist/react/useCheckout.js +0 -32
  53. package/dist/react/useCheckout.js.map +0 -1
  54. package/dist/server/addresses.d.ts +0 -78
  55. package/dist/server/addresses.d.ts.map +0 -1
  56. package/dist/server/addresses.js +0 -118
  57. package/dist/server/addresses.js.map +0 -1
  58. package/dist/server/index.d.ts +0 -12
  59. package/dist/server/index.d.ts.map +0 -1
  60. package/dist/server/index.js +0 -12
  61. package/dist/server/index.js.map +0 -1
  62. package/dist/server/sweeps.d.ts +0 -78
  63. package/dist/server/sweeps.d.ts.map +0 -1
  64. package/dist/server/sweeps.js +0 -117
  65. package/dist/server/sweeps.js.map +0 -1
  66. package/dist/server/webhook.d.ts +0 -61
  67. package/dist/server/webhook.d.ts.map +0 -1
  68. package/dist/server/webhook.js +0 -154
  69. package/dist/server/webhook.js.map +0 -1
  70. package/src/css.d.ts +0 -1
  71. package/src/react/Checkout.tsx +0 -475
  72. package/src/react/DepositAddress.tsx +0 -240
  73. package/src/react/Field.tsx +0 -46
  74. package/src/react/Icons.tsx +0 -43
  75. package/src/react/QrCode.tsx +0 -39
  76. package/src/react/appearance.ts +0 -4
  77. package/src/react/icon-paths.tsx +0 -13
  78. package/src/react/index.ts +0 -7
  79. package/src/react/useCheckout.ts +0 -58
  80. package/src/server/addresses.ts +0 -223
  81. package/src/server/index.ts +0 -38
  82. package/src/server/sweeps.ts +0 -197
  83. package/src/server/webhook.ts +0 -208
  84. package/src/styles.css +0 -80
@@ -1,154 +0,0 @@
1
- /**
2
- * Standard Webhooks `v1a` verification of Phala Pay deliveries (design D11): an ed25519 signature
3
- * over `{webhook-id}.{webhook-timestamp}.{body}` by your account's webhook key in the endpoint's
4
- * mode, pinned from `GET /v1/attestation`. Uses WebCrypto, so it runs on Node 20+, Deno, Bun, and
5
- * edge runtimes.
6
- */
7
- /** A delivery that did not verify: answer `400` and do nothing. */
8
- export class WebhookSignatureError extends Error {
9
- name = "WebhookSignatureError";
10
- }
11
- /**
12
- * Verifies a delivery and returns its event, as Stripe's `constructEvent` does, failing closed
13
- * unless a `v1a` signature verifies with one of `publicKeys` (ed25519 keys in Standard Webhooks'
14
- * `whpk_` form, as `GET /v1/attestation` lists them; pass both while a rotation overlaps), the
15
- * timestamp is within tolerance, the body's id is the `webhook-id`, and the event is
16
- * `expectedAccount`'s in `expectedLivemode`.
17
- *
18
- * `payload` is the raw request body, before any JSON parsing.
19
- */
20
- export async function constructEvent(payload, headers, publicKeys, options) {
21
- if (options.expectedAccount === "") {
22
- throw new TypeError("expectedAccount is required");
23
- }
24
- const now = options.now ?? Math.floor(Date.now() / 1000);
25
- const tolerance = options.tolerance ?? 300;
26
- if (!Number.isFinite(now) || !Number.isFinite(tolerance) || tolerance < 0) {
27
- throw new TypeError("webhook now must be finite and tolerance finite and non-negative");
28
- }
29
- const body = typeof payload === "string" ? new TextEncoder().encode(payload) : payload;
30
- const id = header(headers, "webhook-id");
31
- const timestamp = header(headers, "webhook-timestamp");
32
- const signatures = header(headers, "webhook-signature");
33
- if (id === undefined || timestamp === undefined || signatures === undefined) {
34
- throw new WebhookSignatureError("webhook headers missing");
35
- }
36
- if (!/^\d+$/.test(timestamp) || !Number.isSafeInteger(Number(timestamp))) {
37
- throw new WebhookSignatureError("webhook timestamp malformed");
38
- }
39
- if (Math.abs(now - Number(timestamp)) > tolerance) {
40
- throw new WebhookSignatureError("webhook timestamp outside tolerance");
41
- }
42
- const signed = concat(new TextEncoder().encode(`${id}.${timestamp}.`), body);
43
- const keys = await Promise.all((typeof publicKeys === "string" ? [publicKeys] : publicKeys).map(importKey));
44
- if (keys.length === 0) {
45
- throw new TypeError("no webhook public key pinned");
46
- }
47
- let verified = false;
48
- for (const entry of signatures.split(" ")) {
49
- const [version, encoded] = entry.split(",", 2);
50
- const signature = version === "v1a" && encoded !== undefined ? base64(encoded) : undefined;
51
- if (signature === undefined) {
52
- continue;
53
- }
54
- for (const key of keys) {
55
- if (await crypto.subtle.verify("Ed25519", key, signature, signed)) {
56
- verified = true;
57
- }
58
- }
59
- }
60
- if (!verified) {
61
- throw new WebhookSignatureError("no valid webhook signature");
62
- }
63
- let event;
64
- try {
65
- event = JSON.parse(new TextDecoder().decode(body));
66
- }
67
- catch {
68
- throw new TypeError("webhook body is not an event");
69
- }
70
- if (!isRecord(event)) {
71
- throw new TypeError("webhook body is not an event");
72
- }
73
- const e = event;
74
- const data = e["data"];
75
- const object = isRecord(data) ? data["object"] : null;
76
- const { id: eventId, type, account, livemode, created, actor, request } = e;
77
- if (e["object"] !== "event" ||
78
- typeof eventId !== "string" ||
79
- typeof type !== "string" ||
80
- typeof account !== "string" ||
81
- typeof livemode !== "boolean" ||
82
- typeof created !== "number" ||
83
- !Number.isSafeInteger(created) ||
84
- !isRecord(object) ||
85
- typeof actor !== "string" ||
86
- !(request === null || isRequest(request))) {
87
- throw new TypeError("webhook body is not an event");
88
- }
89
- if (eventId !== id) {
90
- throw new WebhookSignatureError("webhook id does not match the event");
91
- }
92
- if (account !== options.expectedAccount) {
93
- throw new WebhookSignatureError("webhook event is for another account");
94
- }
95
- if (livemode !== options.expectedLivemode) {
96
- throw new WebhookSignatureError("webhook event is for the other mode");
97
- }
98
- const previous = isRecord(data) ? data["previous_attributes"] : undefined;
99
- return {
100
- id: eventId,
101
- object: "event",
102
- account,
103
- livemode,
104
- type,
105
- created,
106
- actor,
107
- request,
108
- data: {
109
- object,
110
- ...(isRecord(previous) ? { previous_attributes: previous } : {}),
111
- },
112
- };
113
- }
114
- function header(headers, name) {
115
- if (headers instanceof Headers) {
116
- return headers.get(name)?.trim() ?? undefined;
117
- }
118
- for (const [key, value] of Object.entries(headers)) {
119
- if (key.toLowerCase() === name) {
120
- return (Array.isArray(value) ? value.join(" ") : value)?.trim();
121
- }
122
- }
123
- return undefined;
124
- }
125
- function isRecord(value) {
126
- return typeof value === "object" && value !== null && !Array.isArray(value);
127
- }
128
- function isRequest(value) {
129
- if (!isRecord(value)) {
130
- return false;
131
- }
132
- const { id, idempotency_key: key } = value;
133
- return typeof id === "string" && (key === null || typeof key === "string");
134
- }
135
- async function importKey(encoded) {
136
- const raw = encoded.startsWith("whpk_") ? base64(encoded.slice("whpk_".length)) : undefined;
137
- if (raw?.length !== 32) {
138
- throw new TypeError("a webhook public key is whpk_ and the base64 of 32 bytes");
139
- }
140
- return crypto.subtle.importKey("raw", raw, "Ed25519", false, ["verify"]);
141
- }
142
- function base64(value) {
143
- if (!/^[A-Za-z0-9+/]*={0,2}$/.test(value) || value.length % 4 !== 0) {
144
- return undefined;
145
- }
146
- return Uint8Array.from(atob(value), (char) => char.charCodeAt(0));
147
- }
148
- function concat(left, right) {
149
- const joined = new Uint8Array(left.length + right.length);
150
- joined.set(left);
151
- joined.set(right, left.length);
152
- return joined;
153
- }
154
- //# sourceMappingURL=webhook.js.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"webhook.js","sourceRoot":"","sources":["../../src/server/webhook.ts"],"names":[],"mappings":"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"}
package/src/css.d.ts DELETED
@@ -1 +0,0 @@
1
- declare module "*.css";
@@ -1,475 +0,0 @@
1
- "use client";
2
-
3
- import { useEffect, useId, useRef, useState, type KeyboardEvent } from "react";
4
- import type { Hash, WalletClient } from "viem";
5
- import type { CheckoutErrorCode, CheckoutState, CheckoutStatus } from "../checkout.js";
6
- import { networkName, transactionUrl } from "../chains.js";
7
- import {
8
- formatAmount,
9
- formatCountdown,
10
- formatMinorAmount,
11
- formatTokenAmount,
12
- formatWait,
13
- tokenAmount,
14
- } from "../format.js";
15
- import { quoteTransfer } from "../payment.js";
16
- import type { ClientQuote } from "../quote.js";
17
- import {
18
- WalletError,
19
- payWithWallet,
20
- watchWallets,
21
- type EthereumProvider,
22
- type Wallet,
23
- } from "../wallet.js";
24
- import type { Appearance } from "./appearance.js";
25
- import { AssetIcon, NetworkIcon } from "./Icons.js";
26
- import { Field } from "./Field.js";
27
- import { QrCode } from "./QrCode.js";
28
- import { useCheckout } from "./useCheckout.js";
29
-
30
- export interface CheckoutProps {
31
- /** The quote's `client_secret`, from your backend's `POST /v1/quotes`. */
32
- clientSecret: string;
33
- /**
34
- * The quote's `address` as your backend's SDK recomputed it from the pinned forwarder
35
- * (`PhalaPay(forwarder=…)` in Python). Required: when the service's quote names another
36
- * address, the checkout fails closed and shows nothing to pay.
37
- */
38
- expectedAddress: string;
39
- /** The service origin, for example `https://topup.example.com`. */
40
- apiBase: string;
41
- /** Called once when the payment is credited, with the quote: its `amount_credited` is what was
42
- * credited, which differs from `amount` for a payment of another amount or a late one. Fulfil
43
- * from the `deposit.credited` webhook, not here. */
44
- onSuccess?: (quote: ClientQuote) => void;
45
- /** Called once when the quote expires or is canceled without a payment. */
46
- onExpire?: (quote: ClientQuote) => void;
47
- /** Called whenever the checkout's status changes, like Stripe Elements' `onChange`: for example,
48
- * to hide your own "new payment" control while a payment is `seen` or `confirming`. */
49
- onChange?: (state: CheckoutState) => void;
50
- /** Called when the wallet tab could not send the payment, with the `WalletError` and the wallet
51
- * it tried (the chosen browser wallet's provider, or `walletClient`): for example on
52
- * `insufficient_balance` (nothing was sent), to offer your own way to fund that wallet. The
53
- * checkout shows the error's message itself. */
54
- onWalletError?: (error: WalletError, wallet: WalletClient | EthereumProvider) => void;
55
- appearance?: Appearance;
56
- /** Milliseconds between status reads; default 3000. */
57
- pollInterval?: number;
58
- className?: string;
59
- /** The wallet button's label; default "Pay with crypto". */
60
- buttonText?: string;
61
- /** Your page's connected wallet, for example wagmi's `useWalletClient().data`. When set, the
62
- * wallet tab pays with it and its account instead of listing the browser's wallets. */
63
- walletClient?: WalletClient | undefined;
64
- }
65
-
66
- type Method = "wallet" | "qr" | "manual";
67
-
68
- const METHODS: { id: Method; label: string }[] = [
69
- { id: "wallet", label: "Wallet" },
70
- { id: "qr", label: "QR code" },
71
- { id: "manual", label: "Manual" },
72
- ];
73
-
74
- /** A checkout for one quote: pay from a browser wallet, by QR code, or manually, with live status. */
75
- export function Checkout({
76
- clientSecret,
77
- expectedAddress,
78
- apiBase,
79
- onSuccess,
80
- onExpire,
81
- onChange,
82
- onWalletError,
83
- appearance,
84
- pollInterval,
85
- className,
86
- buttonText = "Pay with crypto",
87
- walletClient,
88
- }: CheckoutProps) {
89
- const { status, quote, error, refresh } = useCheckout({
90
- clientSecret,
91
- expectedAddress,
92
- apiBase,
93
- ...(pollInterval === undefined ? {} : { pollInterval }),
94
- });
95
- const [txHash, setTxHash] = useState<Hash | null>(null);
96
- const now = useNow(status === "waiting");
97
-
98
- const callbacks = useRef({ onSuccess, onExpire, onChange });
99
- const state = useRef<CheckoutState>({ status, quote, error });
100
- useEffect(() => {
101
- callbacks.current = { onSuccess, onExpire, onChange };
102
- state.current = { status, quote, error };
103
- });
104
- // Once per status change, not on every poll.
105
- useEffect(() => {
106
- callbacks.current.onChange?.(state.current);
107
- }, [status]);
108
- const notified = useRef(new Set<string>());
109
- useEffect(() => {
110
- if (quote === null) {
111
- return;
112
- }
113
- if (status === "credited") {
114
- const key = `${quote.id}:success`;
115
- if (!notified.current.has(key)) {
116
- notified.current.add(key);
117
- callbacks.current.onSuccess?.(quote);
118
- }
119
- } else if (status === "expired" || status === "canceled") {
120
- const key = `${quote.id}:expire`;
121
- if (!notified.current.has(key)) {
122
- notified.current.add(key);
123
- callbacks.current.onExpire?.(quote);
124
- }
125
- }
126
- }, [status, quote]);
127
-
128
- return (
129
- <div
130
- className={className === undefined ? "pp-root" : `pp-root ${className}`}
131
- data-theme={appearance?.theme ?? "light"}
132
- >
133
- {quote !== null && (
134
- <>
135
- <p className="pp-amount">
136
- <AssetIcon asset={quote.asset} size={20} />{" "}
137
- {formatTokenAmount(quote)} {quote.asset.toUpperCase()}
138
- </p>
139
- <p className="pp-subtitle">
140
- {formatAmount(quote)} top-up · <NetworkIcon chainId={quote.chain_id} size={16} />{" "}
141
- {networkName(quote.chain_id)}
142
- {!quote.livemode && <span className="pp-badge"> · Test mode</span>}
143
- </p>
144
- </>
145
- )}
146
- <StatusLine status={status} quote={quote} now={now} error={error} />
147
- {txHash !== null && quote !== null && <Transaction hash={txHash} chainId={quote.chain_id} />}
148
- {status === "waiting" && quote !== null && (
149
- <PaymentOptions
150
- quote={quote}
151
- buttonText={buttonText}
152
- walletClient={walletClient}
153
- onSent={(hash) => {
154
- setTxHash(hash);
155
- refresh();
156
- }}
157
- onWalletError={onWalletError}
158
- />
159
- )}
160
- </div>
161
- );
162
- }
163
-
164
- function StatusLine({
165
- status,
166
- quote,
167
- now,
168
- error,
169
- }: {
170
- status: CheckoutStatus;
171
- quote: ClientQuote | null;
172
- now: number;
173
- error: CheckoutState["error"];
174
- }) {
175
- const tone =
176
- status === "credited"
177
- ? "success"
178
- : ["rejected", "reversed", "expired", "canceled", "error"].includes(status)
179
- ? "danger"
180
- : "neutral";
181
- return (
182
- <div className="pp-status" data-tone={tone}>
183
- <span role="status" aria-live="polite">
184
- {statusMessage(status, quote, error?.code)}
185
- {error !== null && status !== "error" ? " (reconnecting…)" : ""}
186
- </span>
187
- {status === "waiting" && quote !== null && (
188
- <span className="pp-countdown" aria-label="Time left to pay">
189
- {formatCountdown(quote.expires_at, now)}
190
- </span>
191
- )}
192
- </div>
193
- );
194
- }
195
-
196
- function statusMessage(
197
- status: CheckoutStatus,
198
- quote: ClientQuote | null,
199
- code: CheckoutErrorCode | undefined,
200
- ): string {
201
- switch (status) {
202
- case "loading":
203
- return "Loading payment details…";
204
- case "waiting":
205
- return "Waiting for your payment";
206
- case "seen":
207
- return seenMessage(quote);
208
- case "confirming":
209
- return "Confirmed on chain, crediting…";
210
- case "credited":
211
- return creditedMessage(quote);
212
- case "rejected":
213
- return "This payment cannot be credited. Contact support with your transaction.";
214
- case "reversed":
215
- return "Payment reversed: a chain reorganization replaced it and its credit was removed. Check your wallet, then start a new top-up.";
216
- case "expired":
217
- return "Quote expired. Do not send funds; start a new top-up.";
218
- case "canceled":
219
- return "Quote canceled. Do not send funds.";
220
- case "error":
221
- return code === "address_mismatch"
222
- ? "This payment address could not be verified. Do not send funds; contact support."
223
- : "This payment link is not valid. Start a new top-up.";
224
- }
225
- }
226
-
227
- function seenMessage(quote: ClientQuote | null): string {
228
- if (quote === null) {
229
- return "Received, crediting after confirmation";
230
- }
231
- const wait = formatWait(quote.typical_credit_seconds);
232
- return quote.confirmations === null
233
- ? `Received, crediting in ${wait}`
234
- : `Received, ${quote.confirmations} confirmation${quote.confirmations === 1 ? "" : "s"}. Crediting in ${wait}`;
235
- }
236
-
237
- /** What was credited and, when the payment was valued at the market price, the quote it missed. */
238
- function creditedMessage(quote: ClientQuote | null): string {
239
- if (quote?.amount_credited == null) {
240
- return "Payment credited";
241
- }
242
- const credited = formatMinorAmount(quote, quote.amount_credited);
243
- if (quote.amount_credited < quote.amount) {
244
- return `Payment credited: ${credited} of ${formatAmount(quote)}`;
245
- }
246
- if (quote.amount_credited > quote.amount) {
247
- return `Payment credited: ${credited} (${formatAmount(quote)} quoted)`;
248
- }
249
- return `Payment credited: ${credited}`;
250
- }
251
-
252
- function Transaction({ hash, chainId }: { hash: Hash; chainId: number }) {
253
- const url = transactionUrl(chainId, hash);
254
- return (
255
- <p className="pp-tx">
256
- Transaction sent:{" "}
257
- {url === undefined ? (
258
- <span className="pp-value">{hash}</span>
259
- ) : (
260
- <a className="pp-value" href={url} target="_blank" rel="noreferrer">
261
- {hash}
262
- </a>
263
- )}
264
- </p>
265
- );
266
- }
267
-
268
- function PaymentOptions({
269
- quote,
270
- buttonText,
271
- walletClient,
272
- onSent,
273
- onWalletError,
274
- }: {
275
- quote: ClientQuote;
276
- buttonText: string;
277
- walletClient: WalletClient | undefined;
278
- onSent: (hash: Hash) => void;
279
- onWalletError: CheckoutProps["onWalletError"];
280
- }) {
281
- const [selectedMethod, setMethod] = useState<Method | null>(null);
282
- const [discovered, setDiscovered] = useState<Wallet[]>([]);
283
- useEffect(
284
- () => (walletClient === undefined ? watchWallets(setDiscovered) : undefined),
285
- [walletClient],
286
- );
287
- const method = selectedMethod ?? (walletClient !== undefined || discovered.length > 0 ? "wallet" : "qr");
288
- const id = useId();
289
- const tabs = useRef<(HTMLButtonElement | null)[]>([]);
290
- const amount = `${formatTokenAmount(quote)} ${quote.asset.toUpperCase()}`;
291
-
292
- const onKeyDown = (event: KeyboardEvent<HTMLDivElement>) => {
293
- const index = METHODS.findIndex((m) => m.id === method);
294
- const next =
295
- event.key === "ArrowRight"
296
- ? (index + 1) % METHODS.length
297
- : event.key === "ArrowLeft"
298
- ? (index - 1 + METHODS.length) % METHODS.length
299
- : event.key === "Home"
300
- ? 0
301
- : event.key === "End"
302
- ? METHODS.length - 1
303
- : null;
304
- const target = next === null ? undefined : METHODS[next];
305
- if (next !== null && target !== undefined) {
306
- event.preventDefault();
307
- setMethod(target.id);
308
- tabs.current[next]?.focus();
309
- }
310
- };
311
-
312
- return (
313
- <>
314
- <p className="pp-notice">
315
- Send <strong>exactly {amount}</strong> on {networkName(quote.chain_id)} in one transfer
316
- before the timer ends. Otherwise it’s credited at the market rate.
317
- </p>
318
- <div className="pp-tabs" role="tablist" aria-label="Payment method" onKeyDown={onKeyDown}>
319
- {METHODS.map((m, index) => (
320
- <button
321
- key={m.id}
322
- ref={(element) => {
323
- tabs.current[index] = element;
324
- }}
325
- type="button"
326
- role="tab"
327
- className="pp-tab"
328
- id={`${id}-tab-${m.id}`}
329
- aria-label={m.id === "wallet" ? "Browser wallet" : m.id === "manual" ? "Manual transfer" : m.label}
330
- aria-selected={method === m.id}
331
- aria-controls={`${id}-panel-${m.id}`}
332
- tabIndex={method === m.id ? 0 : -1}
333
- onClick={() => setMethod(m.id)}
334
- >
335
- {m.label}
336
- </button>
337
- ))}
338
- </div>
339
- <div
340
- role="tabpanel"
341
- id={`${id}-panel-${method}`}
342
- aria-labelledby={`${id}-tab-${method}`}
343
- tabIndex={0}
344
- >
345
- {method === "wallet" && (
346
- <WalletPanel
347
- quote={quote}
348
- buttonText={buttonText}
349
- walletClient={walletClient}
350
- discovered={discovered}
351
- onSent={onSent}
352
- onWalletError={onWalletError}
353
- />
354
- )}
355
- {method === "qr" && (
356
- <div className="pp-qr-panel">
357
- <QrCode value={quote.payment_uri} label={`Payment request for ${amount}`} />
358
- <p className="pp-message">
359
- Scan with your wallet app and check it shows {amount} on {networkName(quote.chain_id)}.
360
- </p>
361
- </div>
362
- )}
363
- {method === "manual" && <ManualPanel quote={quote} />}
364
- </div>
365
- </>
366
- );
367
- }
368
-
369
- type WalletStep =
370
- | { kind: "idle" }
371
- | { kind: "pending"; wallet: string }
372
- | { kind: "failed"; message: string };
373
-
374
- function WalletPanel({
375
- quote,
376
- buttonText,
377
- walletClient,
378
- discovered,
379
- onSent,
380
- onWalletError,
381
- }: {
382
- quote: ClientQuote;
383
- discovered: Wallet[];
384
- buttonText: string;
385
- walletClient: WalletClient | undefined;
386
- onSent: (hash: Hash) => void;
387
- onWalletError: CheckoutProps["onWalletError"];
388
- }) {
389
- const [step, setStep] = useState<WalletStep>({ kind: "idle" });
390
- const wallets: { id: string; name: string; icon: string; wallet: WalletClient | EthereumProvider }[] =
391
- walletClient === undefined
392
- ? discovered.map(({ info: { uuid, name, icon }, provider }) => ({ id: uuid, name, icon, wallet: provider }))
393
- : [{ id: "client", name: "your wallet", icon: "", wallet: walletClient }];
394
-
395
- const pay = async ({ name, wallet }: (typeof wallets)[number]) => {
396
- setStep({ kind: "pending", wallet: name });
397
- try {
398
- onSent(await payWithWallet(wallet, quote));
399
- setStep({ kind: "idle" });
400
- } catch (error) {
401
- setStep({
402
- kind: "failed",
403
- message: error instanceof WalletError ? error.message : "The payment could not be sent",
404
- });
405
- if (error instanceof WalletError) {
406
- onWalletError?.(error, wallet);
407
- }
408
- }
409
- };
410
-
411
- if (wallets.length === 0) {
412
- return (
413
- <p className="pp-message">
414
- Install a browser wallet to pay here.
415
- </p>
416
- );
417
- }
418
- return (
419
- <div className="pp-wallets">
420
- {wallets.map((choice) => (
421
- <button
422
- key={choice.id}
423
- type="button"
424
- className="pp-button"
425
- disabled={step.kind === "pending"}
426
- onClick={() => void pay(choice)}
427
- aria-label={walletClient === undefined ? `${buttonText} (${choice.name})` : buttonText}
428
- >
429
- {choice.icon !== "" && <img src={choice.icon} alt="" />}
430
- <span>{buttonText}</span>
431
- {walletClient === undefined && <span className="pp-wallet-name">{choice.name}</span>}
432
- </button>
433
- ))}
434
- <p
435
- className="pp-message"
436
- data-tone={step.kind === "failed" ? "danger" : undefined}
437
- aria-live="polite"
438
- >
439
- {step.kind === "pending" && `Confirm the payment in ${step.wallet}…`}
440
- {step.kind === "failed" && step.message}
441
- </p>
442
- </div>
443
- );
444
- }
445
-
446
- function ManualPanel({ quote }: { quote: ClientQuote }) {
447
- const token = quoteTransfer(quote).token;
448
- return (
449
- <>
450
- <dl className="pp-fields">
451
- <Field label="Network" value={`${networkName(quote.chain_id)} (chain ID ${quote.chain_id})`} />
452
- <Field label={`Token (${quote.asset.toUpperCase()}) contract`} value={token} copy />
453
- <Field label="Send to address" value={quote.address} copy />
454
- <Field label="Exact amount" value={formatTokenAmount(quote)} copy={tokenAmount(quote)} />
455
- </dl>
456
- <p className="pp-message">
457
- Only {quote.asset.toUpperCase()} on {networkName(quote.chain_id)} is credited. Exchange
458
- withdrawal fees must not reduce the amount received.
459
- </p>
460
- </>
461
- );
462
- }
463
-
464
- /** The current time in milliseconds, updated every second while `active`. */
465
- function useNow(active: boolean): number {
466
- const [now, setNow] = useState(() => Date.now());
467
- useEffect(() => {
468
- if (!active) {
469
- return;
470
- }
471
- const timer = setInterval(() => setNow(Date.now()), 1000);
472
- return () => clearInterval(timer);
473
- }, [active]);
474
- return now;
475
- }