@phala/pay 0.8.2 → 0.8.3

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 (64) hide show
  1. package/README.md +41 -3
  2. package/THIRD_PARTY_NOTICES +25 -0
  3. package/dist/chains.d.ts +2 -0
  4. package/dist/chains.d.ts.map +1 -1
  5. package/dist/chains.js +11 -2
  6. package/dist/chains.js.map +1 -1
  7. package/dist/checkout.js +2 -2
  8. package/dist/checkout.js.map +1 -1
  9. package/dist/deposit-address.js +2 -2
  10. package/dist/deposit-address.js.map +1 -1
  11. package/dist/icon-names.d.ts +7 -0
  12. package/dist/icon-names.d.ts.map +1 -0
  13. package/dist/icon-names.js +15 -0
  14. package/dist/icon-names.js.map +1 -0
  15. package/dist/icon-svg.d.ts +6 -0
  16. package/dist/icon-svg.d.ts.map +1 -0
  17. package/dist/icon-svg.js +13 -0
  18. package/dist/icon-svg.js.map +1 -0
  19. package/dist/icons.d.ts +5 -0
  20. package/dist/icons.d.ts.map +1 -0
  21. package/dist/icons.js +27 -0
  22. package/dist/icons.js.map +1 -0
  23. package/dist/index.d.ts +1 -0
  24. package/dist/index.d.ts.map +1 -1
  25. package/dist/index.js +1 -0
  26. package/dist/index.js.map +1 -1
  27. package/dist/react/Checkout.d.ts.map +1 -1
  28. package/dist/react/Checkout.js +24 -22
  29. package/dist/react/Checkout.js.map +1 -1
  30. package/dist/react/DepositAddress.d.ts.map +1 -1
  31. package/dist/react/DepositAddress.js +6 -6
  32. package/dist/react/DepositAddress.js.map +1 -1
  33. package/dist/react/Icons.d.ts +17 -0
  34. package/dist/react/Icons.d.ts.map +1 -0
  35. package/dist/react/Icons.js +18 -0
  36. package/dist/react/Icons.js.map +1 -0
  37. package/dist/react/icon-paths.d.ts +13 -0
  38. package/dist/react/icon-paths.d.ts.map +1 -0
  39. package/dist/react/icon-paths.js +14 -0
  40. package/dist/react/icon-paths.js.map +1 -0
  41. package/dist/react/index.d.ts +1 -0
  42. package/dist/react/index.d.ts.map +1 -1
  43. package/dist/react/index.js +1 -0
  44. package/dist/react/index.js.map +1 -1
  45. package/dist/request.d.ts +2 -0
  46. package/dist/request.d.ts.map +1 -1
  47. package/dist/request.js +7 -0
  48. package/dist/request.js.map +1 -1
  49. package/package.json +9 -4
  50. package/src/chains.ts +12 -2
  51. package/src/checkout.ts +2 -2
  52. package/src/deposit-address.ts +2 -2
  53. package/src/generated/openapi.d.ts +9482 -0
  54. package/src/icon-names.ts +20 -0
  55. package/src/icon-svg.ts +13 -0
  56. package/src/icons.ts +30 -0
  57. package/src/index.ts +1 -0
  58. package/src/react/Checkout.tsx +42 -36
  59. package/src/react/DepositAddress.tsx +10 -9
  60. package/src/react/Icons.tsx +43 -0
  61. package/src/react/icon-paths.tsx +13 -0
  62. package/src/react/index.ts +1 -0
  63. package/src/request.ts +7 -0
  64. package/src/styles.css +22 -18
@@ -0,0 +1,20 @@
1
+ import type { ICON_SVG } from "./icon-svg.js";
2
+
3
+ export type IconName = keyof typeof ICON_SVG;
4
+
5
+ /** Case-insensitive token symbol; unrecognized symbols use a neutral monogram. */
6
+ export function assetIconName(asset: string): IconName | undefined {
7
+ switch (asset.trim().toLowerCase()) {
8
+ case "usdc": return "usdc";
9
+ case "usdt": return "usdt";
10
+ case "pha": return "pha";
11
+ case "eth": return "eth";
12
+ default: return undefined;
13
+ }
14
+ }
15
+
16
+ /** First character for a neutral fallback, including an empty-symbol placeholder. */
17
+ export function iconMonogram(label: string): string {
18
+ return Array.from(label.trim())[0]?.toUpperCase() ?? "?";
19
+ }
20
+
@@ -0,0 +1,13 @@
1
+ /** Generated from @web3icons/core@4.0.57 by scripts/generate-icons.mjs.
2
+ * SPDX-License-Identifier: MIT; Copyright (c) 2024 0xa3k5.
3
+ * See THIRD_PARTY_NOTICES for the full license. Do not edit.
4
+ */
5
+
6
+ export const ICON_SVG: Record<"usdc" | "usdt" | "pha" | "eth" | "ethereum" | "base", string> = {
7
+ usdc: "<svg xmlns=\"http://www.w3.org/2000/svg\" width=\"18\" height=\"18\" fill=\"none\" viewBox=\"0 0 24 24\" aria-hidden=\"true\"><path fill=\"#0B53BF\" d=\"M12 21a9 9 0 1 0 0-18 9 9 0 0 0 0 18\"/><path fill=\"#fff\" d=\"M13.62 5.45v1.159a5.64 5.64 0 0 1 4.005 5.394 5.64 5.64 0 0 1-4.005 5.394v1.16a6.74 6.74 0 0 0 5.13-6.554 6.74 6.74 0 0 0-5.13-6.553m-7.245 6.553a5.64 5.64 0 0 1 4.005-5.394V5.45a6.74 6.74 0 0 0-5.13 6.553 6.74 6.74 0 0 0 5.13 6.553v-1.159a5.63 5.63 0 0 1-4.005-5.394\"/><path fill=\"#fff\" d=\"M14.419 13.258c0-2.301-3.606-1.356-3.606-2.627 0-.456.366-.748 1.063-.748.833 0 1.12.405 1.21.95h1.147c-.102-1.024-.69-1.67-1.67-1.863v-.904h-1.125v.872c-1.075.137-1.75.762-1.75 1.693 0 2.312 3.611 1.445 3.611 2.694 0 .472-.455.787-1.226.787-1.007 0-1.339-.444-1.462-1.057H9.49c.073 1.122.764 1.823 1.947 1.999v.886h1.125v-.875c1.153-.149 1.856-.82 1.856-1.807\"/></svg>",
8
+ usdt: "<svg xmlns=\"http://www.w3.org/2000/svg\" width=\"18\" height=\"18\" fill=\"none\" viewBox=\"0 0 24 24\" aria-hidden=\"true\"><path fill=\"#009393\" d=\"m12 19.2-9-8.88L6.433 4.8h11.134L21 10.32zm.9-8.1v-1.098c1.62.08 3.132.396 3.6.805-.544.477-2.493.824-4.5.824s-3.956-.347-4.5-.824c.463-.41 1.98-.72 3.6-.81V11.1zm-5.4-.297v.661c.463.41 1.975.72 3.6.81V14.7h1.8v-2.43c1.62-.081 3.136-.396 3.6-.806v-1.318c-.464-.41-1.98-.725-3.6-.81V8.4h2.7V7.05H8.4V8.4h2.7v.936c-1.625.085-3.137.4-3.6.81z\"/></svg>",
9
+ pha: "<svg xmlns=\"http://www.w3.org/2000/svg\" width=\"18\" height=\"18\" fill=\"none\" viewBox=\"0 0 24 24\" aria-hidden=\"true\"><circle cx=\"12\" cy=\"12\" r=\"12\" fill=\"#252525\"/><path fill=\"#D1FF52\" fill-rule=\"evenodd\" d=\"M3 3h14.143v3.857H21V12h-3.857V6.857H6.857V12H3zm3.857 12.857h10.286V12H6.857zm0 0V21H3v-5.143z\" clip-rule=\"evenodd\"/></svg>",
10
+ eth: "<svg xmlns=\"http://www.w3.org/2000/svg\" width=\"18\" height=\"18\" fill=\"none\" viewBox=\"0 0 24 24\" aria-hidden=\"true\"><path fill=\"#8FFCF3\" d=\"M12 3v6.65l5.625 2.516z\"/><path fill=\"#CABCF8\" d=\"m12 3-5.625 9.166L12 9.651z\"/><path fill=\"#CBA7F5\" d=\"M12 16.477v4.522l5.625-7.784z\"/><path fill=\"#74A0F3\" d=\"M12 21v-4.523l-5.625-3.262z\"/><path fill=\"#CBA7F5\" d=\"m12 15.43 5.625-3.263L12 9.65z\"/><path fill=\"#74A0F3\" d=\"M6.375 12.167 12 15.429V9.651z\"/><path fill=\"#202699\" fill-rule=\"evenodd\" d=\"m12 15.429-5.625-3.263L12 3l5.625 9.166zM6.749 11.9l5.16-8.41v6.115zm-.077.23 5.238-2.327v5.364zm5.418-2.327v5.364l5.233-3.038zm0-.198 5.16 2.295-5.16-8.41z\" clip-rule=\"evenodd\"/><path fill=\"#202699\" fill-rule=\"evenodd\" d=\"M12 16.406 6.375 13.21 12 21l5.625-7.79zm-4.995-2.633 4.905 2.79v4.005zm5.085 2.79v4.005l4.905-6.795z\" clip-rule=\"evenodd\"/></svg>",
11
+ ethereum: "<svg xmlns=\"http://www.w3.org/2000/svg\" width=\"18\" height=\"18\" fill=\"none\" viewBox=\"0 0 24 24\" aria-hidden=\"true\"><path fill=\"#8FFCF3\" d=\"M12 3v6.651l5.625 2.516z\"/><path fill=\"#CABCF8\" d=\"m12 3-5.625 9.166L12 9.653z\"/><path fill=\"#CBA7F5\" d=\"M12 16.478V21l5.625-7.784z\"/><path fill=\"#74A0F3\" d=\"M12 21v-4.522l-5.625-3.262z\"/><path fill=\"#CBA7F5\" d=\"m12 15.43 5.625-3.263L12 9.652z\"/><path fill=\"#74A0F3\" d=\"M6.375 12.167 12 15.43V9.652z\"/><path fill=\"#202699\" fill-rule=\"evenodd\" d=\"m12 15.43-5.625-3.263L12 3l5.624 9.166zm-5.252-3.528 5.161-8.41v6.114zm-.077.229 5.238-2.327v5.364zm5.418-2.327v5.364l5.234-3.037zm0-.198 5.161 2.296-5.161-8.41z\" clip-rule=\"evenodd\"/><path fill=\"#202699\" fill-rule=\"evenodd\" d=\"m12 16.406-5.625-3.195L12 21l5.624-7.79zm-4.995-2.633 4.904 2.79v4.005zm5.084 2.79v4.005l4.905-6.795z\" clip-rule=\"evenodd\"/></svg>",
12
+ base: "<svg xmlns=\"http://www.w3.org/2000/svg\" width=\"18\" height=\"18\" fill=\"none\" viewBox=\"0 0 24 24\" aria-hidden=\"true\"><path fill=\"#00F\" d=\"M3 4.706c0-.585 0-.877.11-1.101.106-.215.28-.39.496-.495C3.83 3 4.122 3 4.706 3h14.588c.585 0 .876 0 1.101.11.215.105.389.28.494.495.111.225.111.517.111 1.101v14.588c0 .585 0 .876-.11 1.101-.106.215-.28.389-.495.494-.225.111-.517.111-1.101.111H4.706c-.585 0-.876 0-1.101-.11a1.08 1.08 0 0 1-.494-.495C3 20.17 3 19.878 3 19.294z\"/></svg>",
13
+ };
package/src/icons.ts ADDED
@@ -0,0 +1,30 @@
1
+ import { networkName, networkIconName } from "./chains.js";
2
+ import { ICON_SVG } from "./icon-svg.js";
3
+
4
+ import { assetIconName, iconMonogram } from "./icon-names.js";
5
+
6
+ function fallback(label: string): string {
7
+ const letter = iconMonogram(label).replace(/[&<>"']/g, (character) => {
8
+ switch (character) {
9
+ case "&": return "&amp;";
10
+ case "<": return "&lt;";
11
+ case ">": return "&gt;";
12
+ case '"': return "&quot;";
13
+ default: return "&#39;";
14
+ }
15
+ });
16
+ return `<svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 0 24 24" aria-hidden="true"><circle cx="12" cy="12" r="11" fill="currentColor" opacity="0.12"/><text x="12" y="16" text-anchor="middle" font-family="sans-serif" font-size="12" fill="currentColor">${letter}</text></svg>`;
17
+ }
18
+
19
+ /** Decorative inline SVG markup; testnets reuse their mainnet family artwork. */
20
+ export function networkIcon(chainId: number): string {
21
+ const name = networkIconName(chainId);
22
+ const svg = name === undefined ? fallback(networkName(chainId)) : ICON_SVG[name];
23
+ return svg;
24
+ }
25
+
26
+ /** Decorative inline SVG markup; keep the asset name visible beside it. */
27
+ export function assetIcon(asset: string): string {
28
+ const name = assetIconName(asset);
29
+ return name === undefined ? fallback(asset) : ICON_SVG[name];
30
+ }
package/src/index.ts CHANGED
@@ -44,3 +44,4 @@ export {
44
44
  type WalletErrorCode,
45
45
  type WalletInfo,
46
46
  } from "./wallet.js";
47
+ export { networkIcon, assetIcon } from "./icons.js";
@@ -22,6 +22,7 @@ import {
22
22
  type Wallet,
23
23
  } from "../wallet.js";
24
24
  import type { Appearance } from "./appearance.js";
25
+ import { AssetIcon, NetworkIcon } from "./Icons.js";
25
26
  import { Field } from "./Field.js";
26
27
  import { QrCode } from "./QrCode.js";
27
28
  import { useCheckout } from "./useCheckout.js";
@@ -65,9 +66,9 @@ export interface CheckoutProps {
65
66
  type Method = "wallet" | "qr" | "manual";
66
67
 
67
68
  const METHODS: { id: Method; label: string }[] = [
68
- { id: "wallet", label: "Browser wallet" },
69
+ { id: "wallet", label: "Wallet" },
69
70
  { id: "qr", label: "QR code" },
70
- { id: "manual", label: "Send manually" },
71
+ { id: "manual", label: "Manual" },
71
72
  ];
72
73
 
73
74
  /** A checkout for one quote: pay from a browser wallet, by QR code, or manually, with live status. */
@@ -132,10 +133,12 @@ export function Checkout({
132
133
  {quote !== null && (
133
134
  <>
134
135
  <p className="pp-amount">
136
+ <AssetIcon asset={quote.asset} size={20} />{" "}
135
137
  {formatTokenAmount(quote)} {quote.asset.toUpperCase()}
136
138
  </p>
137
139
  <p className="pp-subtitle">
138
- {formatAmount(quote)} top-up · {networkName(quote.chain_id)}
140
+ {formatAmount(quote)} top-up · <NetworkIcon chainId={quote.chain_id} size={16} />{" "}
141
+ {networkName(quote.chain_id)}
139
142
  {!quote.livemode && <span className="pp-badge"> · Test mode</span>}
140
143
  </p>
141
144
  </>
@@ -147,7 +150,6 @@ export function Checkout({
147
150
  quote={quote}
148
151
  buttonText={buttonText}
149
152
  walletClient={walletClient}
150
- now={now}
151
153
  onSent={(hash) => {
152
154
  setTxHash(hash);
153
155
  refresh();
@@ -204,17 +206,17 @@ function statusMessage(
204
206
  case "seen":
205
207
  return seenMessage(quote);
206
208
  case "confirming":
207
- return "Payment confirmed on chain, crediting…";
209
+ return "Confirmed on chain, crediting…";
208
210
  case "credited":
209
211
  return creditedMessage(quote);
210
212
  case "rejected":
211
213
  return "This payment cannot be credited. Contact support with your transaction.";
212
214
  case "reversed":
213
- return "This payment was reversed: its transaction is no longer on the blockchain, so it did not happen and its credit was taken back. Check your wallet, then start a new top-up.";
215
+ return "Payment reversed: a chain reorganization replaced it and its credit was removed. Check your wallet, then start a new top-up.";
214
216
  case "expired":
215
- return "This quote has expired. Do not send funds to this address; start a new top-up.";
217
+ return "Quote expired. Do not send funds; start a new top-up.";
216
218
  case "canceled":
217
- return "This quote was canceled. Do not send funds to this address.";
219
+ return "Quote canceled. Do not send funds.";
218
220
  case "error":
219
221
  return code === "address_mismatch"
220
222
  ? "This payment address could not be verified. Do not send funds; contact support."
@@ -224,12 +226,12 @@ function statusMessage(
224
226
 
225
227
  function seenMessage(quote: ClientQuote | null): string {
226
228
  if (quote === null) {
227
- return "Payment received, crediting once it is confirmed";
229
+ return "Received, crediting after confirmation";
228
230
  }
229
231
  const wait = formatWait(quote.typical_credit_seconds);
230
232
  return quote.confirmations === null
231
- ? `Payment received, crediting in ${wait}`
232
- : `Payment received, ${quote.confirmations} confirmation${quote.confirmations === 1 ? "" : "s"}. Crediting in ${wait}`;
233
+ ? `Received, crediting in ${wait}`
234
+ : `Received, ${quote.confirmations} confirmation${quote.confirmations === 1 ? "" : "s"}. Crediting in ${wait}`;
233
235
  }
234
236
 
235
237
  /** What was credited and, when the payment was valued at the market price, the quote it missed. */
@@ -242,7 +244,7 @@ function creditedMessage(quote: ClientQuote | null): string {
242
244
  return `Payment credited: ${credited} of ${formatAmount(quote)}`;
243
245
  }
244
246
  if (quote.amount_credited > quote.amount) {
245
- return `Payment credited: ${credited}, more than the ${formatAmount(quote)} quoted`;
247
+ return `Payment credited: ${credited} (${formatAmount(quote)} quoted)`;
246
248
  }
247
249
  return `Payment credited: ${credited}`;
248
250
  }
@@ -265,20 +267,24 @@ function Transaction({ hash, chainId }: { hash: Hash; chainId: number }) {
265
267
 
266
268
  function PaymentOptions({
267
269
  quote,
268
- now,
269
270
  buttonText,
270
271
  walletClient,
271
272
  onSent,
272
273
  onWalletError,
273
274
  }: {
274
275
  quote: ClientQuote;
275
- now: number;
276
276
  buttonText: string;
277
277
  walletClient: WalletClient | undefined;
278
278
  onSent: (hash: Hash) => void;
279
279
  onWalletError: CheckoutProps["onWalletError"];
280
280
  }) {
281
- const [method, setMethod] = useState<Method>("wallet");
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");
282
288
  const id = useId();
283
289
  const tabs = useRef<(HTMLButtonElement | null)[]>([]);
284
290
  const amount = `${formatTokenAmount(quote)} ${quote.asset.toUpperCase()}`;
@@ -307,9 +313,7 @@ function PaymentOptions({
307
313
  <>
308
314
  <p className="pp-notice">
309
315
  Send <strong>exactly {amount}</strong> on {networkName(quote.chain_id)} in one transfer
310
- before the timer ends. A different amount, or a payment after expiry, is credited at the
311
- market price instead of this quote. Exchanges may deduct a withdrawal fee, so the amount
312
- that arrives must be exact.
316
+ before the timer ends. Otherwise it’s credited at the market rate.
313
317
  </p>
314
318
  <div className="pp-tabs" role="tablist" aria-label="Payment method" onKeyDown={onKeyDown}>
315
319
  {METHODS.map((m, index) => (
@@ -322,6 +326,7 @@ function PaymentOptions({
322
326
  role="tab"
323
327
  className="pp-tab"
324
328
  id={`${id}-tab-${m.id}`}
329
+ aria-label={m.id === "wallet" ? "Browser wallet" : m.id === "manual" ? "Manual transfer" : m.label}
325
330
  aria-selected={method === m.id}
326
331
  aria-controls={`${id}-panel-${m.id}`}
327
332
  tabIndex={method === m.id ? 0 : -1}
@@ -342,6 +347,7 @@ function PaymentOptions({
342
347
  quote={quote}
343
348
  buttonText={buttonText}
344
349
  walletClient={walletClient}
350
+ discovered={discovered}
345
351
  onSent={onSent}
346
352
  onWalletError={onWalletError}
347
353
  />
@@ -350,12 +356,11 @@ function PaymentOptions({
350
356
  <div className="pp-qr-panel">
351
357
  <QrCode value={quote.payment_uri} label={`Payment request for ${amount}`} />
352
358
  <p className="pp-message">
353
- Scan with a wallet app that reads payment links, and check that it shows {amount} on{" "}
354
- {networkName(quote.chain_id)} before you confirm.
359
+ Scan with your wallet app and check it shows {amount} on {networkName(quote.chain_id)}.
355
360
  </p>
356
361
  </div>
357
362
  )}
358
- {method === "manual" && <ManualPanel quote={quote} now={now} />}
363
+ {method === "manual" && <ManualPanel quote={quote} />}
359
364
  </div>
360
365
  </>
361
366
  );
@@ -370,21 +375,18 @@ function WalletPanel({
370
375
  quote,
371
376
  buttonText,
372
377
  walletClient,
378
+ discovered,
373
379
  onSent,
374
380
  onWalletError,
375
381
  }: {
376
382
  quote: ClientQuote;
383
+ discovered: Wallet[];
377
384
  buttonText: string;
378
385
  walletClient: WalletClient | undefined;
379
386
  onSent: (hash: Hash) => void;
380
387
  onWalletError: CheckoutProps["onWalletError"];
381
388
  }) {
382
- const [discovered, setDiscovered] = useState<Wallet[]>([]);
383
389
  const [step, setStep] = useState<WalletStep>({ kind: "idle" });
384
- useEffect(
385
- () => (walletClient === undefined ? watchWallets(setDiscovered) : undefined),
386
- [walletClient],
387
- );
388
390
  const wallets: { id: string; name: string; icon: string; wallet: WalletClient | EthereumProvider }[] =
389
391
  walletClient === undefined
390
392
  ? discovered.map(({ info: { uuid, name, icon }, provider }) => ({ id: uuid, name, icon, wallet: provider }))
@@ -409,8 +411,7 @@ function WalletPanel({
409
411
  if (wallets.length === 0) {
410
412
  return (
411
413
  <p className="pp-message">
412
- No browser wallet found. Scan the QR code with a mobile wallet, or send the payment
413
- manually.
414
+ Install a browser wallet to pay here.
414
415
  </p>
415
416
  );
416
417
  }
@@ -442,16 +443,21 @@ function WalletPanel({
442
443
  );
443
444
  }
444
445
 
445
- function ManualPanel({ quote, now }: { quote: ClientQuote; now: number }) {
446
+ function ManualPanel({ quote }: { quote: ClientQuote }) {
446
447
  const token = quoteTransfer(quote).token;
447
448
  return (
448
- <dl className="pp-fields">
449
- <Field label="Network" value={`${networkName(quote.chain_id)} (chain ID ${quote.chain_id})`} />
450
- <Field label={`Token (${quote.asset.toUpperCase()}) contract`} value={token} copy />
451
- <Field label="Send to address" value={quote.address} copy />
452
- <Field label="Exact amount" value={formatTokenAmount(quote)} copy={tokenAmount(quote)} />
453
- <Field label="Time left" value={formatCountdown(quote.expires_at, now)} />
454
- </dl>
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
+ </>
455
461
  );
456
462
  }
457
463
 
@@ -12,6 +12,7 @@ import { CheckoutError, pollDelay } from "../checkout.js";
12
12
  import { formatWait } from "../format.js";
13
13
  import { depositAddressTransfer, type DepositAddressDetails } from "../payment.js";
14
14
  import type { Appearance } from "./appearance.js";
15
+ import { AssetIcon, NetworkIcon } from "./Icons.js";
15
16
  import { Field } from "./Field.js";
16
17
  import { QrCode } from "./QrCode.js";
17
18
 
@@ -72,7 +73,6 @@ export function DepositAddress({
72
73
  const { token: contract, to } = depositAddressTransfer(network, token);
73
74
  const symbol = token.asset.toUpperCase();
74
75
  const name = networkName(network.chain_id);
75
- const tokens = [...new Set(networks.flatMap((each) => each.assets.map((a) => a.asset.toUpperCase())))];
76
76
  return (
77
77
  <div
78
78
  className={className === undefined ? "pp-root" : `pp-root ${className}`}
@@ -80,8 +80,8 @@ export function DepositAddress({
80
80
  >
81
81
  <p className="pp-subtitle">
82
82
  {depositAddress.address === null
83
- ? "Your deposit address for every supported token; it differs on some networks"
84
- : "One address for all supported tokens and networks"}
83
+ ? "Reusable deposit address; varies by network"
84
+ : "One reusable address for supported tokens and networks"}
85
85
  </p>
86
86
  {networks.length > 1 && (
87
87
  <fieldset className="pp-choices">
@@ -94,6 +94,7 @@ export function DepositAddress({
94
94
  checked={each.chain_id === network.chain_id}
95
95
  onChange={() => setSelectedChain(each.chain_id)}
96
96
  />
97
+ <NetworkIcon chainId={each.chain_id} size={16} />{" "}
97
98
  {networkName(each.chain_id)}
98
99
  </label>
99
100
  ))}
@@ -110,6 +111,7 @@ export function DepositAddress({
110
111
  checked={each.asset === token.asset}
111
112
  onChange={() => setSelectedAsset(each.asset)}
112
113
  />
114
+ <AssetIcon asset={each.asset} size={16} />{" "}
113
115
  {each.asset.toUpperCase()}
114
116
  </label>
115
117
  ))}
@@ -133,9 +135,8 @@ export function DepositAddress({
133
135
  </ul>
134
136
  )}
135
137
  <p className="pp-message">
136
- Send only {tokens.join(", ")} on {networks.map((each) => networkName(each.chain_id)).join(", ")}
137
- . {creditMessage(networks.map((each) => each.chain_id), view)} Other tokens and networks are not
138
- credited. You can reuse this address.
138
+ Send {symbol} on {name}. Unsupported tokens or networks are not credited.{" "}
139
+ {creditMessage(networks.map((each) => each.chain_id), view)}
139
140
  </p>
140
141
  </div>
141
142
  );
@@ -205,7 +206,7 @@ function creditMessage(chainIds: number[], view: ClientDepositAddress | null): s
205
206
  }
206
207
  }
207
208
  if (names.size === 0) {
208
- return "Any amount is credited at the market rate once it is confirmed on its network.";
209
+ return "Any amount is credited at the market rate after confirmation.";
209
210
  }
210
211
  const list = new Intl.ListFormat("en", { type: "conjunction" });
211
212
  const [only, ...others] = names.keys();
@@ -213,7 +214,7 @@ function creditMessage(chainIds: number[], view: ClientDepositAddress | null): s
213
214
  only !== undefined && others.length === 0
214
215
  ? formatWait(only)
215
216
  : list.format([...names].map(([seconds, on]) => `${formatWait(seconds)} on ${list.format(on)}`));
216
- return `Any amount is credited at the market rate when it arrives, usually in ${usually}.`;
217
+ return `Any amount is credited at the market rate on arrival, usually in ${usually}.`;
217
218
  }
218
219
 
219
220
  function paymentMessage(payment: DepositAddressPayment): string {
@@ -234,6 +235,6 @@ function paymentMessage(payment: DepositAddressPayment): string {
234
235
  case "rejected":
235
236
  return `${amount} ${on} cannot be credited. Contact support with your transaction.`;
236
237
  case "reversed":
237
- return `${amount} ${on} was reversed: its transaction is no longer on the blockchain.`;
238
+ return `${amount} ${on} was reversed: a chain reorganization replaced it and its credit was removed.`;
238
239
  }
239
240
  }
@@ -0,0 +1,43 @@
1
+ "use client";
2
+
3
+ import { networkName, networkIconName } from "../chains.js";
4
+ import { assetIconName, iconMonogram, type IconName } from "../icon-names.js";
5
+ import { ICON_PATHS } from "./icon-paths.js";
6
+
7
+ export interface IconProps {
8
+ /** Width and height in pixels; default 18. No inline styles are injected. */
9
+ size?: number;
10
+ /** Default true next to visible text. Set false when the icon is used alone. */
11
+ decorative?: boolean;
12
+ }
13
+ export interface NetworkIconProps extends IconProps { chainId: number }
14
+ export interface AssetIconProps extends IconProps { asset: string }
15
+
16
+ function Icon({ name, label, size = 18, decorative = true }: IconProps & {
17
+ name: IconName | undefined;
18
+ label: string;
19
+ }) {
20
+ const pixels = Number.isFinite(size) && size > 0 ? size : 18;
21
+ return (
22
+ <span className="pp-icon-label" role={decorative ? undefined : "img"}
23
+ aria-label={decorative ? undefined : label || "Unknown asset"}>
24
+ <svg className="pp-icon" xmlns="http://www.w3.org/2000/svg" width={pixels} height={pixels}
25
+ viewBox="0 0 24 24" fill="none" aria-hidden="true" focusable="false">
26
+ {name === undefined ? <>
27
+ <circle cx="12" cy="12" r="11" fill="currentColor" opacity="0.12" />
28
+ <text x="12" y="16" textAnchor="middle" fontFamily="sans-serif" fontSize="12" fill="currentColor">{iconMonogram(label)}</text>
29
+ </> : ICON_PATHS[name]}
30
+ </svg>
31
+ </span>
32
+ );
33
+ }
34
+
35
+ /** Branded network icon; testnets reuse their mainnet family artwork. */
36
+ export function NetworkIcon({ chainId, ...props }: NetworkIconProps) {
37
+ return <Icon {...props} name={networkIconName(chainId)} label={networkName(chainId)} />;
38
+ }
39
+
40
+ /** Branded token icon, or a neutral first-letter monogram for unknown symbols. */
41
+ export function AssetIcon({ asset, ...props }: AssetIconProps) {
42
+ return <Icon {...props} name={assetIconName(asset)} label={asset.trim().toUpperCase()} />;
43
+ }
@@ -0,0 +1,13 @@
1
+ /** Generated from @web3icons/core@4.0.57 by scripts/generate-icons.mjs.
2
+ * SPDX-License-Identifier: MIT; Copyright (c) 2024 0xa3k5.
3
+ * See THIRD_PARTY_NOTICES for the full license. Do not edit.
4
+ */
5
+
6
+ export const ICON_PATHS = {
7
+ usdc: <><path fill="#0B53BF" d="M12 21a9 9 0 1 0 0-18 9 9 0 0 0 0 18"/><path fill="#fff" d="M13.62 5.45v1.159a5.64 5.64 0 0 1 4.005 5.394 5.64 5.64 0 0 1-4.005 5.394v1.16a6.74 6.74 0 0 0 5.13-6.554 6.74 6.74 0 0 0-5.13-6.553m-7.245 6.553a5.64 5.64 0 0 1 4.005-5.394V5.45a6.74 6.74 0 0 0-5.13 6.553 6.74 6.74 0 0 0 5.13 6.553v-1.159a5.63 5.63 0 0 1-4.005-5.394"/><path fill="#fff" d="M14.419 13.258c0-2.301-3.606-1.356-3.606-2.627 0-.456.366-.748 1.063-.748.833 0 1.12.405 1.21.95h1.147c-.102-1.024-.69-1.67-1.67-1.863v-.904h-1.125v.872c-1.075.137-1.75.762-1.75 1.693 0 2.312 3.611 1.445 3.611 2.694 0 .472-.455.787-1.226.787-1.007 0-1.339-.444-1.462-1.057H9.49c.073 1.122.764 1.823 1.947 1.999v.886h1.125v-.875c1.153-.149 1.856-.82 1.856-1.807"/></>,
8
+ usdt: <><path fill="#009393" d="m12 19.2-9-8.88L6.433 4.8h11.134L21 10.32zm.9-8.1v-1.098c1.62.08 3.132.396 3.6.805-.544.477-2.493.824-4.5.824s-3.956-.347-4.5-.824c.463-.41 1.98-.72 3.6-.81V11.1zm-5.4-.297v.661c.463.41 1.975.72 3.6.81V14.7h1.8v-2.43c1.62-.081 3.136-.396 3.6-.806v-1.318c-.464-.41-1.98-.725-3.6-.81V8.4h2.7V7.05H8.4V8.4h2.7v.936c-1.625.085-3.137.4-3.6.81z"/></>,
9
+ pha: <><circle cx="12" cy="12" r="12" fill="#252525"/><path fill="#D1FF52" fillRule="evenodd" d="M3 3h14.143v3.857H21V12h-3.857V6.857H6.857V12H3zm3.857 12.857h10.286V12H6.857zm0 0V21H3v-5.143z" clipRule="evenodd"/></>,
10
+ eth: <><path fill="#8FFCF3" d="M12 3v6.65l5.625 2.516z"/><path fill="#CABCF8" d="m12 3-5.625 9.166L12 9.651z"/><path fill="#CBA7F5" d="M12 16.477v4.522l5.625-7.784z"/><path fill="#74A0F3" d="M12 21v-4.523l-5.625-3.262z"/><path fill="#CBA7F5" d="m12 15.43 5.625-3.263L12 9.65z"/><path fill="#74A0F3" d="M6.375 12.167 12 15.429V9.651z"/><path fill="#202699" fillRule="evenodd" d="m12 15.429-5.625-3.263L12 3l5.625 9.166zM6.749 11.9l5.16-8.41v6.115zm-.077.23 5.238-2.327v5.364zm5.418-2.327v5.364l5.233-3.038zm0-.198 5.16 2.295-5.16-8.41z" clipRule="evenodd"/><path fill="#202699" fillRule="evenodd" d="M12 16.406 6.375 13.21 12 21l5.625-7.79zm-4.995-2.633 4.905 2.79v4.005zm5.085 2.79v4.005l4.905-6.795z" clipRule="evenodd"/></>,
11
+ ethereum: <><path fill="#8FFCF3" d="M12 3v6.651l5.625 2.516z"/><path fill="#CABCF8" d="m12 3-5.625 9.166L12 9.653z"/><path fill="#CBA7F5" d="M12 16.478V21l5.625-7.784z"/><path fill="#74A0F3" d="M12 21v-4.522l-5.625-3.262z"/><path fill="#CBA7F5" d="m12 15.43 5.625-3.263L12 9.652z"/><path fill="#74A0F3" d="M6.375 12.167 12 15.43V9.652z"/><path fill="#202699" fillRule="evenodd" d="m12 15.43-5.625-3.263L12 3l5.624 9.166zm-5.252-3.528 5.161-8.41v6.114zm-.077.229 5.238-2.327v5.364zm5.418-2.327v5.364l5.234-3.037zm0-.198 5.161 2.296-5.161-8.41z" clipRule="evenodd"/><path fill="#202699" fillRule="evenodd" d="m12 16.406-5.625-3.195L12 21l5.624-7.79zm-4.995-2.633 4.904 2.79v4.005zm5.084 2.79v4.005l4.905-6.795z" clipRule="evenodd"/></>,
12
+ base: <><path fill="#00F" d="M3 4.706c0-.585 0-.877.11-1.101.106-.215.28-.39.496-.495C3.83 3 4.122 3 4.706 3h14.588c.585 0 .876 0 1.101.11.215.105.389.28.494.495.111.225.111.517.111 1.101v14.588c0 .585 0 .876-.11 1.101-.106.215-.28.389-.495.494-.225.111-.517.111-1.101.111H4.706c-.585 0-.876 0-1.101-.11a1.08 1.08 0 0 1-.494-.495C3 20.17 3 19.878 3 19.294z"/></>,
13
+ };
@@ -4,3 +4,4 @@ export { Checkout, type CheckoutProps } from "./Checkout.js";
4
4
  export { DepositAddress, type DepositAddressProps } from "./DepositAddress.js";
5
5
  export { useCheckout, type UseCheckoutOptions, type UseCheckoutResult } from "./useCheckout.js";
6
6
  export type { Appearance } from "./appearance.js";
7
+ export { NetworkIcon, AssetIcon, type IconProps, type NetworkIconProps, type AssetIconProps } from "./Icons.js";
package/src/request.ts CHANGED
@@ -6,3 +6,10 @@ export function requestSignal(options: {
6
6
  const deadline = AbortSignal.timeout(options.requestTimeout ?? 10_000);
7
7
  return options.signal === undefined ? deadline : AbortSignal.any([options.signal, deadline]);
8
8
  }
9
+
10
+ /** `apiBase` without trailing slashes, in linear time (a `/\/+$/` regex is quadratic). */
11
+ export function trimTrailingSlashes(apiBase: string): string {
12
+ let end = apiBase.length;
13
+ while (end > 0 && apiBase[end - 1] === "/") end -= 1;
14
+ return apiBase.slice(0, end);
15
+ }
package/src/styles.css CHANGED
@@ -10,11 +10,11 @@
10
10
  --pp-font-family: system-ui, -apple-system, "Segoe UI", Roboto, sans-serif;
11
11
  --pp-border-radius: 8px;
12
12
  box-sizing: border-box;
13
- max-width: 440px;
14
- padding: 20px;
15
- border: 1px solid var(--pp-color-border);
13
+ max-width: var(--pp-root-max-width, 440px);
14
+ padding: var(--pp-root-padding, 20px);
15
+ border: var(--pp-root-border, 1px solid var(--pp-color-border));
16
16
  border-radius: var(--pp-border-radius);
17
- background: var(--pp-color-background);
17
+ background: var(--pp-root-background, var(--pp-color-background));
18
18
  color: var(--pp-color-text);
19
19
  font-family: var(--pp-font-family);
20
20
  font-size: 14px;
@@ -31,46 +31,50 @@
31
31
  }
32
32
  .pp-root *, .pp-root *::before, .pp-root *::after { box-sizing: inherit; }
33
33
  .pp-amount { margin: 0; overflow-wrap: anywhere; font-size: 22px; font-weight: 600; }
34
- .pp-subtitle { margin: 2px 0 16px; color: var(--pp-color-text-secondary); }
34
+ .pp-subtitle { margin: 2px 0 12px; color: var(--pp-color-text-secondary); }
35
35
  .pp-badge { font-weight: 600; }
36
36
  .pp-payments { list-style: none; margin: 12px 0 0; padding: 0; }
37
37
  .pp-payments li { margin: 4px 0; }
38
38
  .pp-status { display: flex; gap: 8px; align-items: baseline; justify-content: space-between;
39
- margin-bottom: 12px; padding: 10px 12px; border-radius: var(--pp-border-radius);
39
+ margin-bottom: 10px; padding: 8px 10px; border-radius: var(--pp-border-radius);
40
40
  border: 1px solid var(--pp-color-border); }
41
41
  .pp-status[data-tone="success"] { border-color: var(--pp-color-success); color: var(--pp-color-success); }
42
42
  .pp-status[data-tone="danger"] { border-color: var(--pp-color-danger); color: var(--pp-color-danger); }
43
43
  .pp-countdown { font-variant-numeric: tabular-nums; white-space: nowrap; }
44
- .pp-notice { margin: 0 0 16px; color: var(--pp-color-text-secondary); font-size: 13px; }
44
+ .pp-notice { margin: 0 0 12px; color: var(--pp-color-text-secondary); font-size: 13px; }
45
45
  .pp-notice strong { color: var(--pp-color-text); }
46
- .pp-tabs { display: flex; gap: 4px; border-bottom: 1px solid var(--pp-color-border); margin-bottom: 16px; }
46
+ .pp-tabs { display: flex; gap: 4px; border-bottom: 1px solid var(--pp-color-border); margin-bottom: 12px; }
47
47
  .pp-tab { appearance: none; border: 0; border-bottom: 2px solid transparent; background: none;
48
- color: var(--pp-color-text-secondary); font: inherit; padding: 8px 10px; cursor: pointer; }
48
+ color: var(--pp-color-text-secondary); font: inherit; min-height: 44px; white-space: nowrap; padding: 8px 10px; cursor: pointer; }
49
49
  .pp-tab[aria-selected="true"] { color: var(--pp-color-text); border-bottom-color: var(--pp-color-primary); font-weight: 600; }
50
50
  .pp-root button:focus-visible, .pp-root a:focus-visible { outline: 2px solid var(--pp-color-primary); outline-offset: 2px; }
51
51
  .pp-wallets { display: grid; gap: 8px; }
52
52
  .pp-button { display: flex; gap: 10px; align-items: center; justify-content: center; width: 100%;
53
53
  appearance: none; border: 1px solid var(--pp-color-primary); border-radius: var(--pp-border-radius);
54
- background: var(--pp-color-primary); color: var(--pp-accessible-color-on-color-primary); font: inherit; font-weight: 600; padding: 10px 14px; cursor: pointer; }
54
+ background: var(--pp-color-primary); color: var(--pp-accessible-color-on-color-primary); font: inherit; font-weight: 600; min-height: 44px; padding: 10px 14px; cursor: pointer; }
55
55
  .pp-button:disabled { opacity: 0.6; cursor: progress; }
56
56
  .pp-button img { width: 20px; height: 20px; }
57
57
  .pp-wallet-name { font-weight: 400; opacity: 0.85; }
58
- .pp-message { margin: 10px 0 0; color: var(--pp-color-text-secondary); }
58
+ .pp-message { margin: 8px 0 0; color: var(--pp-color-text-secondary); }
59
+ .pp-message:empty { display: none; }
59
60
  .pp-message[data-tone="danger"] { color: var(--pp-color-danger); }
60
- .pp-qr-panel { display: grid; justify-items: center; gap: 10px; text-align: center; }
61
+ .pp-qr-panel { display: grid; justify-items: center; gap: 8px; text-align: center; }
61
62
  .pp-qr { border-radius: 4px; }
62
- .pp-fields { display: grid; gap: 10px; margin: 0; }
63
+ .pp-fields { display: grid; gap: 8px; margin: 0; }
63
64
  .pp-field dt { color: var(--pp-color-text-secondary); font-size: 12px; }
64
65
  .pp-field dd { display: flex; gap: 8px; align-items: center; margin: 2px 0 0; }
65
66
  .pp-value { font-family: ui-monospace, SFMono-Regular, Menlo, monospace; word-break: break-all; }
66
- .pp-copy { flex: none; appearance: none; border: 1px solid var(--pp-color-border); border-radius: 6px;
67
- background: none; color: var(--pp-color-text); font: inherit; font-size: 12px; padding: 3px 8px; cursor: pointer; }
67
+ .pp-copy { flex: none; margin-inline-start: auto; appearance: none; border: 1px solid var(--pp-color-border); border-radius: 6px;
68
+ background: none; color: var(--pp-color-text); font: inherit; font-size: 12px; min-height: 44px; min-width: 44px; padding: 3px 8px; cursor: pointer; }
68
69
  .pp-tx { margin: 0 0 12px; font-size: 13px; }
69
70
  .pp-tx a { color: inherit; text-decoration: underline; text-underline-offset: 2px; }
70
71
  .pp-tx .pp-value { overflow-wrap: anywhere; }
71
72
 
72
- .pp-choices { display: flex; gap: 12px; border: 0; padding: 0; margin: 0 0 16px; }
73
+ .pp-choices { display: flex; flex-wrap: wrap; gap: 8px; border: 0; padding: 0; margin: 0 0 12px; }
73
74
  .pp-choices legend { padding: 0; margin-bottom: 6px; font-weight: 600; }
74
- .pp-choices label { display: flex; align-items: center; gap: 4px; cursor: pointer; }
75
- .pp-choices input { accent-color: var(--pp-color-primary); }
75
+ .pp-choices label { display: flex; align-items: center; gap: 4px; min-height: 44px; cursor: pointer; }
76
+ .pp-choices input { margin: 0; accent-color: var(--pp-color-primary); }
76
77
  .pp-choices input:focus-visible { outline: 2px solid var(--pp-color-primary); outline-offset: 2px; }
78
+
79
+ .pp-icon-label { display: inline-flex; align-items: center; gap: 4px; vertical-align: middle; white-space: nowrap; }
80
+ .pp-icon { display: inline-block; flex: none; vertical-align: middle; }