@phala/pay 0.6.0 → 0.8.0

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 (44) hide show
  1. package/README.md +44 -22
  2. package/dist/checkout.d.ts +9 -1
  3. package/dist/checkout.d.ts.map +1 -1
  4. package/dist/checkout.js +22 -2
  5. package/dist/checkout.js.map +1 -1
  6. package/dist/client.d.ts +3 -3
  7. package/dist/client.d.ts.map +1 -1
  8. package/dist/client.js +2 -1
  9. package/dist/client.js.map +1 -1
  10. package/dist/deposit-address.d.ts +3 -0
  11. package/dist/deposit-address.d.ts.map +1 -1
  12. package/dist/deposit-address.js +6 -1
  13. package/dist/deposit-address.js.map +1 -1
  14. package/dist/react/Checkout.d.ts +1 -1
  15. package/dist/react/Checkout.d.ts.map +1 -1
  16. package/dist/react/Checkout.js +14 -9
  17. package/dist/react/Checkout.js.map +1 -1
  18. package/dist/react/DepositAddress.d.ts +1 -1
  19. package/dist/react/DepositAddress.d.ts.map +1 -1
  20. package/dist/react/DepositAddress.js +7 -9
  21. package/dist/react/DepositAddress.js.map +1 -1
  22. package/dist/react/appearance.d.ts +1 -22
  23. package/dist/react/appearance.d.ts.map +1 -1
  24. package/dist/react/appearance.js +1 -100
  25. package/dist/react/appearance.js.map +1 -1
  26. package/dist/react/useCheckout.d.ts +3 -9
  27. package/dist/react/useCheckout.d.ts.map +1 -1
  28. package/dist/react/useCheckout.js +5 -3
  29. package/dist/react/useCheckout.js.map +1 -1
  30. package/dist/request.d.ts +6 -0
  31. package/dist/request.d.ts.map +1 -0
  32. package/dist/request.js +6 -0
  33. package/dist/request.js.map +1 -0
  34. package/package.json +13 -9
  35. package/src/checkout.ts +32 -3
  36. package/src/client.ts +8 -2
  37. package/src/css.d.ts +1 -0
  38. package/src/deposit-address.ts +9 -1
  39. package/src/react/Checkout.tsx +13 -9
  40. package/src/react/DepositAddress.tsx +28 -31
  41. package/src/react/appearance.ts +1 -119
  42. package/src/react/useCheckout.ts +15 -9
  43. package/src/request.ts +8 -0
  44. package/src/styles.css +76 -0
@@ -1,26 +1,5 @@
1
- import type { CSSProperties } from "react";
2
- /** Theming in the spirit of Stripe's Appearance API: a base theme plus a few variables. */
1
+ /** Base theme; customize CSS properties on `.pp-root` in your stylesheet. */
3
2
  export interface Appearance {
4
3
  theme?: "light" | "dark";
5
- variables?: {
6
- colorPrimary?: string;
7
- /** The color of text appearing on top of any `colorPrimary` background, such as the pay button. */
8
- accessibleColorOnColorPrimary?: string;
9
- colorBackground?: string;
10
- colorText?: string;
11
- colorTextSecondary?: string;
12
- colorBorder?: string;
13
- colorDanger?: string;
14
- colorSuccess?: string;
15
- fontFamily?: string;
16
- borderRadius?: string;
17
- };
18
4
  }
19
- /** The root element's custom properties for `appearance.variables`. */
20
- export declare function appearanceStyle(appearance: Appearance | undefined): CSSProperties;
21
- /**
22
- * Every rule is scoped under `.pp-root`; the variables can also be set from the page's own CSS on
23
- * `.pp-root`.
24
- */
25
- export declare const STYLES = "\n.pp-root {\n --pp-color-primary: #0f62fe;\n --pp-accessible-color-on-color-primary: #ffffff;\n --pp-color-background: #ffffff;\n --pp-color-text: #1a1a1a;\n --pp-color-text-secondary: #5c5f66;\n --pp-color-border: #d9dce1;\n --pp-color-danger: #c62828;\n --pp-color-success: #1b7f3b;\n --pp-font-family: system-ui, -apple-system, \"Segoe UI\", Roboto, sans-serif;\n --pp-border-radius: 8px;\n box-sizing: border-box;\n max-width: 440px;\n padding: 20px;\n border: 1px solid var(--pp-color-border);\n border-radius: var(--pp-border-radius);\n background: var(--pp-color-background);\n color: var(--pp-color-text);\n font-family: var(--pp-font-family);\n font-size: 14px;\n line-height: 1.45;\n}\n.pp-root[data-theme=\"dark\"] {\n --pp-color-primary: #78a9ff;\n --pp-color-background: #161616;\n --pp-color-text: #f4f4f4;\n --pp-color-text-secondary: #a8a8a8;\n --pp-color-border: #393939;\n --pp-color-danger: #ff8389;\n --pp-color-success: #42be65;\n}\n.pp-root *, .pp-root *::before, .pp-root *::after { box-sizing: inherit; }\n.pp-amount { margin: 0; overflow-wrap: anywhere; font-size: 22px; font-weight: 600; }\n.pp-subtitle { margin: 2px 0 16px; color: var(--pp-color-text-secondary); }\n.pp-badge { font-weight: 600; }\n.pp-payments { list-style: none; margin: 12px 0 0; padding: 0; }\n.pp-payments li { margin: 4px 0; }\n.pp-status { display: flex; gap: 8px; align-items: baseline; justify-content: space-between;\n margin-bottom: 12px; padding: 10px 12px; border-radius: var(--pp-border-radius);\n border: 1px solid var(--pp-color-border); }\n.pp-status[data-tone=\"success\"] { border-color: var(--pp-color-success); color: var(--pp-color-success); }\n.pp-status[data-tone=\"danger\"] { border-color: var(--pp-color-danger); color: var(--pp-color-danger); }\n.pp-countdown { font-variant-numeric: tabular-nums; white-space: nowrap; }\n.pp-notice { margin: 0 0 16px; color: var(--pp-color-text-secondary); font-size: 13px; }\n.pp-notice strong { color: var(--pp-color-text); }\n.pp-tabs { display: flex; gap: 4px; border-bottom: 1px solid var(--pp-color-border); margin-bottom: 16px; }\n.pp-tab { appearance: none; border: 0; border-bottom: 2px solid transparent; background: none;\n color: var(--pp-color-text-secondary); font: inherit; padding: 8px 10px; cursor: pointer; }\n.pp-tab[aria-selected=\"true\"] { color: var(--pp-color-text); border-bottom-color: var(--pp-color-primary); font-weight: 600; }\n.pp-root button:focus-visible, .pp-root a:focus-visible { outline: 2px solid var(--pp-color-primary); outline-offset: 2px; }\n.pp-wallets { display: grid; gap: 8px; }\n.pp-button { display: flex; gap: 10px; align-items: center; justify-content: center; width: 100%;\n appearance: none; border: 1px solid var(--pp-color-primary); border-radius: var(--pp-border-radius);\n background: var(--pp-color-primary); color: var(--pp-accessible-color-on-color-primary); font: inherit; font-weight: 600; padding: 10px 14px; cursor: pointer; }\n.pp-button:disabled { opacity: 0.6; cursor: progress; }\n.pp-button img { width: 20px; height: 20px; }\n.pp-wallet-name { font-weight: 400; opacity: 0.85; }\n.pp-message { margin: 10px 0 0; color: var(--pp-color-text-secondary); }\n.pp-message[data-tone=\"danger\"] { color: var(--pp-color-danger); }\n.pp-qr-panel { display: grid; justify-items: center; gap: 10px; text-align: center; }\n.pp-qr { border-radius: 4px; }\n.pp-fields { display: grid; gap: 10px; margin: 0; }\n.pp-field dt { color: var(--pp-color-text-secondary); font-size: 12px; }\n.pp-field dd { display: flex; gap: 8px; align-items: center; margin: 2px 0 0; }\n.pp-value { font-family: ui-monospace, SFMono-Regular, Menlo, monospace; word-break: break-all; }\n.pp-copy { flex: none; appearance: none; border: 1px solid var(--pp-color-border); border-radius: 6px;\n background: none; color: var(--pp-color-text); font: inherit; font-size: 12px; padding: 3px 8px; cursor: pointer; }\n.pp-tx { margin: 0 0 12px; font-size: 13px; }\n.pp-tx a { color: inherit; text-decoration: underline; text-underline-offset: 2px; }\n.pp-tx .pp-value { overflow-wrap: anywhere; }\n";
26
5
  //# sourceMappingURL=appearance.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"appearance.d.ts","sourceRoot":"","sources":["../../src/react/appearance.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,OAAO,CAAC;AAE3C,2FAA2F;AAC3F,MAAM,WAAW,UAAU;IACzB,KAAK,CAAC,EAAE,OAAO,GAAG,MAAM,CAAC;IACzB,SAAS,CAAC,EAAE;QACV,YAAY,CAAC,EAAE,MAAM,CAAC;QACtB,mGAAmG;QACnG,6BAA6B,CAAC,EAAE,MAAM,CAAC;QACvC,eAAe,CAAC,EAAE,MAAM,CAAC;QACzB,SAAS,CAAC,EAAE,MAAM,CAAC;QACnB,kBAAkB,CAAC,EAAE,MAAM,CAAC;QAC5B,WAAW,CAAC,EAAE,MAAM,CAAC;QACrB,WAAW,CAAC,EAAE,MAAM,CAAC;QACrB,YAAY,CAAC,EAAE,MAAM,CAAC;QACtB,UAAU,CAAC,EAAE,MAAM,CAAC;QACpB,YAAY,CAAC,EAAE,MAAM,CAAC;KACvB,CAAC;CACH;AAeD,uEAAuE;AACvE,wBAAgB,eAAe,CAAC,UAAU,EAAE,UAAU,GAAG,SAAS,GAAG,aAAa,CAUjF;AAED;;;GAGG;AACH,eAAO,MAAM,MAAM,shIAuElB,CAAC"}
1
+ {"version":3,"file":"appearance.d.ts","sourceRoot":"","sources":["../../src/react/appearance.ts"],"names":[],"mappings":"AAAA,6EAA6E;AAC7E,MAAM,WAAW,UAAU;IACzB,KAAK,CAAC,EAAE,OAAO,GAAG,MAAM,CAAC;CAC1B"}
@@ -1,101 +1,2 @@
1
- const VARIABLES = {
2
- colorPrimary: "--pp-color-primary",
3
- accessibleColorOnColorPrimary: "--pp-accessible-color-on-color-primary",
4
- colorBackground: "--pp-color-background",
5
- colorText: "--pp-color-text",
6
- colorTextSecondary: "--pp-color-text-secondary",
7
- colorBorder: "--pp-color-border",
8
- colorDanger: "--pp-color-danger",
9
- colorSuccess: "--pp-color-success",
10
- fontFamily: "--pp-font-family",
11
- borderRadius: "--pp-border-radius",
12
- };
13
- /** The root element's custom properties for `appearance.variables`. */
14
- export function appearanceStyle(appearance) {
15
- const variables = appearance?.variables ?? {};
16
- const style = {};
17
- for (const [name, property] of Object.entries(VARIABLES)) {
18
- const value = variables[name];
19
- if (value !== undefined) {
20
- style[property] = value;
21
- }
22
- }
23
- return style;
24
- }
25
- /**
26
- * Every rule is scoped under `.pp-root`; the variables can also be set from the page's own CSS on
27
- * `.pp-root`.
28
- */
29
- export const STYLES = `
30
- .pp-root {
31
- --pp-color-primary: #0f62fe;
32
- --pp-accessible-color-on-color-primary: #ffffff;
33
- --pp-color-background: #ffffff;
34
- --pp-color-text: #1a1a1a;
35
- --pp-color-text-secondary: #5c5f66;
36
- --pp-color-border: #d9dce1;
37
- --pp-color-danger: #c62828;
38
- --pp-color-success: #1b7f3b;
39
- --pp-font-family: system-ui, -apple-system, "Segoe UI", Roboto, sans-serif;
40
- --pp-border-radius: 8px;
41
- box-sizing: border-box;
42
- max-width: 440px;
43
- padding: 20px;
44
- border: 1px solid var(--pp-color-border);
45
- border-radius: var(--pp-border-radius);
46
- background: var(--pp-color-background);
47
- color: var(--pp-color-text);
48
- font-family: var(--pp-font-family);
49
- font-size: 14px;
50
- line-height: 1.45;
51
- }
52
- .pp-root[data-theme="dark"] {
53
- --pp-color-primary: #78a9ff;
54
- --pp-color-background: #161616;
55
- --pp-color-text: #f4f4f4;
56
- --pp-color-text-secondary: #a8a8a8;
57
- --pp-color-border: #393939;
58
- --pp-color-danger: #ff8389;
59
- --pp-color-success: #42be65;
60
- }
61
- .pp-root *, .pp-root *::before, .pp-root *::after { box-sizing: inherit; }
62
- .pp-amount { margin: 0; overflow-wrap: anywhere; font-size: 22px; font-weight: 600; }
63
- .pp-subtitle { margin: 2px 0 16px; color: var(--pp-color-text-secondary); }
64
- .pp-badge { font-weight: 600; }
65
- .pp-payments { list-style: none; margin: 12px 0 0; padding: 0; }
66
- .pp-payments li { margin: 4px 0; }
67
- .pp-status { display: flex; gap: 8px; align-items: baseline; justify-content: space-between;
68
- margin-bottom: 12px; padding: 10px 12px; border-radius: var(--pp-border-radius);
69
- border: 1px solid var(--pp-color-border); }
70
- .pp-status[data-tone="success"] { border-color: var(--pp-color-success); color: var(--pp-color-success); }
71
- .pp-status[data-tone="danger"] { border-color: var(--pp-color-danger); color: var(--pp-color-danger); }
72
- .pp-countdown { font-variant-numeric: tabular-nums; white-space: nowrap; }
73
- .pp-notice { margin: 0 0 16px; color: var(--pp-color-text-secondary); font-size: 13px; }
74
- .pp-notice strong { color: var(--pp-color-text); }
75
- .pp-tabs { display: flex; gap: 4px; border-bottom: 1px solid var(--pp-color-border); margin-bottom: 16px; }
76
- .pp-tab { appearance: none; border: 0; border-bottom: 2px solid transparent; background: none;
77
- color: var(--pp-color-text-secondary); font: inherit; padding: 8px 10px; cursor: pointer; }
78
- .pp-tab[aria-selected="true"] { color: var(--pp-color-text); border-bottom-color: var(--pp-color-primary); font-weight: 600; }
79
- .pp-root button:focus-visible, .pp-root a:focus-visible { outline: 2px solid var(--pp-color-primary); outline-offset: 2px; }
80
- .pp-wallets { display: grid; gap: 8px; }
81
- .pp-button { display: flex; gap: 10px; align-items: center; justify-content: center; width: 100%;
82
- appearance: none; border: 1px solid var(--pp-color-primary); border-radius: var(--pp-border-radius);
83
- background: var(--pp-color-primary); color: var(--pp-accessible-color-on-color-primary); font: inherit; font-weight: 600; padding: 10px 14px; cursor: pointer; }
84
- .pp-button:disabled { opacity: 0.6; cursor: progress; }
85
- .pp-button img { width: 20px; height: 20px; }
86
- .pp-wallet-name { font-weight: 400; opacity: 0.85; }
87
- .pp-message { margin: 10px 0 0; color: var(--pp-color-text-secondary); }
88
- .pp-message[data-tone="danger"] { color: var(--pp-color-danger); }
89
- .pp-qr-panel { display: grid; justify-items: center; gap: 10px; text-align: center; }
90
- .pp-qr { border-radius: 4px; }
91
- .pp-fields { display: grid; gap: 10px; margin: 0; }
92
- .pp-field dt { color: var(--pp-color-text-secondary); font-size: 12px; }
93
- .pp-field dd { display: flex; gap: 8px; align-items: center; margin: 2px 0 0; }
94
- .pp-value { font-family: ui-monospace, SFMono-Regular, Menlo, monospace; word-break: break-all; }
95
- .pp-copy { flex: none; appearance: none; border: 1px solid var(--pp-color-border); border-radius: 6px;
96
- background: none; color: var(--pp-color-text); font: inherit; font-size: 12px; padding: 3px 8px; cursor: pointer; }
97
- .pp-tx { margin: 0 0 12px; font-size: 13px; }
98
- .pp-tx a { color: inherit; text-decoration: underline; text-underline-offset: 2px; }
99
- .pp-tx .pp-value { overflow-wrap: anywhere; }
100
- `;
1
+ export {};
101
2
  //# sourceMappingURL=appearance.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"appearance.js","sourceRoot":"","sources":["../../src/react/appearance.ts"],"names":[],"mappings":"AAoBA,MAAM,SAAS,GAA+D;IAC5E,YAAY,EAAE,oBAAoB;IAClC,6BAA6B,EAAE,wCAAwC;IACvE,eAAe,EAAE,uBAAuB;IACxC,SAAS,EAAE,iBAAiB;IAC5B,kBAAkB,EAAE,2BAA2B;IAC/C,WAAW,EAAE,mBAAmB;IAChC,WAAW,EAAE,mBAAmB;IAChC,YAAY,EAAE,oBAAoB;IAClC,UAAU,EAAE,kBAAkB;IAC9B,YAAY,EAAE,oBAAoB;CACnC,CAAC;AAEF,uEAAuE;AACvE,MAAM,UAAU,eAAe,CAAC,UAAkC;IAChE,MAAM,SAAS,GAAG,UAAU,EAAE,SAAS,IAAI,EAAE,CAAC;IAC9C,MAAM,KAAK,GAA2B,EAAE,CAAC;IACzC,KAAK,MAAM,CAAC,IAAI,EAAE,QAAQ,CAAC,IAAI,MAAM,CAAC,OAAO,CAAC,SAAS,CAAC,EAAE,CAAC;QACzD,MAAM,KAAK,GAAG,SAAS,CAAC,IAA8B,CAAC,CAAC;QACxD,IAAI,KAAK,KAAK,SAAS,EAAE,CAAC;YACxB,KAAK,CAAC,QAAQ,CAAC,GAAG,KAAK,CAAC;QAC1B,CAAC;IACH,CAAC;IACD,OAAO,KAAK,CAAC;AACf,CAAC;AAED;;;GAGG;AACH,MAAM,CAAC,MAAM,MAAM,GAAG;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CAuErB,CAAC"}
1
+ {"version":3,"file":"appearance.js","sourceRoot":"","sources":["../../src/react/appearance.ts"],"names":[],"mappings":""}
@@ -1,15 +1,9 @@
1
- import { type CheckoutState } from "../checkout.js";
2
- export interface UseCheckoutOptions {
3
- clientSecret: string;
4
- /** The quote's address as your backend recomputed it; another one fails closed. */
5
- expectedAddress: string;
6
- apiBase: string;
7
- pollInterval?: number;
8
- }
1
+ import { type CheckoutOptions, type CheckoutState } from "../checkout.js";
2
+ export type UseCheckoutOptions = Pick<CheckoutOptions, "clientSecret" | "expectedAddress" | "apiBase" | "pollInterval" | "signal" | "requestTimeout">;
9
3
  export interface UseCheckoutResult extends CheckoutState {
10
4
  /** Reads the quote now, for example right after the wallet broadcast the payment. */
11
5
  refresh: () => void;
12
6
  }
13
7
  /** Follows a quote's public view for as long as the component is mounted. */
14
- export declare function useCheckout({ clientSecret, expectedAddress, apiBase, pollInterval, }: UseCheckoutOptions): UseCheckoutResult;
8
+ export declare function useCheckout({ clientSecret, expectedAddress, apiBase, pollInterval, signal, requestTimeout, }: UseCheckoutOptions): UseCheckoutResult;
15
9
  //# sourceMappingURL=useCheckout.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"useCheckout.d.ts","sourceRoot":"","sources":["../../src/react/useCheckout.ts"],"names":[],"mappings":"AACA,OAAO,EAAwC,KAAK,aAAa,EAAE,MAAM,gBAAgB,CAAC;AAE1F,MAAM,WAAW,kBAAkB;IACjC,YAAY,EAAE,MAAM,CAAC;IACrB,mFAAmF;IACnF,eAAe,EAAE,MAAM,CAAC;IACxB,OAAO,EAAE,MAAM,CAAC;IAChB,YAAY,CAAC,EAAE,MAAM,CAAC;CACvB;AAED,MAAM,WAAW,iBAAkB,SAAQ,aAAa;IACtD,qFAAqF;IACrF,OAAO,EAAE,MAAM,IAAI,CAAC;CACrB;AAID,6EAA6E;AAC7E,wBAAgB,WAAW,CAAC,EAC1B,YAAY,EACZ,eAAe,EACf,OAAO,EACP,YAAY,GACb,EAAE,kBAAkB,GAAG,iBAAiB,CA2BxC"}
1
+ {"version":3,"file":"useCheckout.d.ts","sourceRoot":"","sources":["../../src/react/useCheckout.ts"],"names":[],"mappings":"AACA,OAAO,EAEL,KAAK,eAAe,EAEpB,KAAK,aAAa,EACnB,MAAM,gBAAgB,CAAC;AAExB,MAAM,MAAM,kBAAkB,GAAG,IAAI,CACnC,eAAe,EACf,cAAc,GAAG,iBAAiB,GAAG,SAAS,GAAG,cAAc,GAAG,QAAQ,GAAG,gBAAgB,CAC9F,CAAC;AAEF,MAAM,WAAW,iBAAkB,SAAQ,aAAa;IACtD,qFAAqF;IACrF,OAAO,EAAE,MAAM,IAAI,CAAC;CACrB;AAID,6EAA6E;AAC7E,wBAAgB,WAAW,CAAC,EAC1B,YAAY,EACZ,eAAe,EACf,OAAO,EACP,YAAY,EACZ,MAAM,EACN,cAAc,GACf,EAAE,kBAAkB,GAAG,iBAAiB,CA6BxC"}
@@ -1,8 +1,8 @@
1
1
  import { useCallback, useEffect, useRef, useState } from "react";
2
- import { createCheckout } from "../checkout.js";
2
+ import { createCheckout, } from "../checkout.js";
3
3
  const LOADING = { status: "loading", quote: null, error: null };
4
4
  /** Follows a quote's public view for as long as the component is mounted. */
5
- export function useCheckout({ clientSecret, expectedAddress, apiBase, pollInterval, }) {
5
+ export function useCheckout({ clientSecret, expectedAddress, apiBase, pollInterval, signal, requestTimeout, }) {
6
6
  const [current, setCurrent] = useState({ key: "", state: LOADING });
7
7
  const checkout = useRef(null);
8
8
  const key = `${apiBase} ${clientSecret} ${expectedAddress}`;
@@ -12,6 +12,8 @@ export function useCheckout({ clientSecret, expectedAddress, apiBase, pollInterv
12
12
  expectedAddress,
13
13
  apiBase,
14
14
  ...(pollInterval === undefined ? {} : { pollInterval }),
15
+ ...(signal === undefined ? {} : { signal }),
16
+ ...(requestTimeout === undefined ? {} : { requestTimeout }),
15
17
  });
16
18
  checkout.current = instance;
17
19
  const unsubscribe = instance.subscribe((state) => setCurrent({ key, state }));
@@ -20,7 +22,7 @@ export function useCheckout({ clientSecret, expectedAddress, apiBase, pollInterv
20
22
  instance.destroy();
21
23
  checkout.current = null;
22
24
  };
23
- }, [key, clientSecret, expectedAddress, apiBase, pollInterval]);
25
+ }, [key, clientSecret, expectedAddress, apiBase, pollInterval, signal, requestTimeout]);
24
26
  const refresh = useCallback(() => {
25
27
  void checkout.current?.refresh();
26
28
  }, []);
@@ -1 +1 @@
1
- {"version":3,"file":"useCheckout.js","sourceRoot":"","sources":["../../src/react/useCheckout.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,WAAW,EAAE,SAAS,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,OAAO,CAAC;AACjE,OAAO,EAAE,cAAc,EAA4C,MAAM,gBAAgB,CAAC;AAe1F,MAAM,OAAO,GAAkB,EAAE,MAAM,EAAE,SAAS,EAAE,KAAK,EAAE,IAAI,EAAE,KAAK,EAAE,IAAI,EAAE,CAAC;AAE/E,6EAA6E;AAC7E,MAAM,UAAU,WAAW,CAAC,EAC1B,YAAY,EACZ,eAAe,EACf,OAAO,EACP,YAAY,GACO;IACnB,MAAM,CAAC,OAAO,EAAE,UAAU,CAAC,GAAG,QAAQ,CAAC,EAAE,GAAG,EAAE,EAAE,EAAE,KAAK,EAAE,OAAO,EAAE,CAAC,CAAC;IACpE,MAAM,QAAQ,GAAG,MAAM,CAAyB,IAAI,CAAC,CAAC;IACtD,MAAM,GAAG,GAAG,GAAG,OAAO,IAAI,YAAY,IAAI,eAAe,EAAE,CAAC;IAE5D,SAAS,CAAC,GAAG,EAAE;QACb,MAAM,QAAQ,GAAG,cAAc,CAAC;YAC9B,YAAY;YACZ,eAAe;YACf,OAAO;YACP,GAAG,CAAC,YAAY,KAAK,SAAS,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,EAAE,YAAY,EAAE,CAAC;SACxD,CAAC,CAAC;QACH,QAAQ,CAAC,OAAO,GAAG,QAAQ,CAAC;QAC5B,MAAM,WAAW,GAAG,QAAQ,CAAC,SAAS,CAAC,CAAC,KAAK,EAAE,EAAE,CAAC,UAAU,CAAC,EAAE,GAAG,EAAE,KAAK,EAAE,CAAC,CAAC,CAAC;QAC9E,OAAO,GAAG,EAAE;YACV,WAAW,EAAE,CAAC;YACd,QAAQ,CAAC,OAAO,EAAE,CAAC;YACnB,QAAQ,CAAC,OAAO,GAAG,IAAI,CAAC;QAC1B,CAAC,CAAC;IACJ,CAAC,EAAE,CAAC,GAAG,EAAE,YAAY,EAAE,eAAe,EAAE,OAAO,EAAE,YAAY,CAAC,CAAC,CAAC;IAEhE,MAAM,OAAO,GAAG,WAAW,CAAC,GAAG,EAAE;QAC/B,KAAK,QAAQ,CAAC,OAAO,EAAE,OAAO,EAAE,CAAC;IACnC,CAAC,EAAE,EAAE,CAAC,CAAC;IAEP,kFAAkF;IAClF,OAAO,EAAE,GAAG,CAAC,OAAO,CAAC,GAAG,KAAK,GAAG,CAAC,CAAC,CAAC,OAAO,CAAC,KAAK,CAAC,CAAC,CAAC,OAAO,CAAC,EAAE,OAAO,EAAE,CAAC;AACzE,CAAC"}
1
+ {"version":3,"file":"useCheckout.js","sourceRoot":"","sources":["../../src/react/useCheckout.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,WAAW,EAAE,SAAS,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,OAAO,CAAC;AACjE,OAAO,EACL,cAAc,GAIf,MAAM,gBAAgB,CAAC;AAYxB,MAAM,OAAO,GAAkB,EAAE,MAAM,EAAE,SAAS,EAAE,KAAK,EAAE,IAAI,EAAE,KAAK,EAAE,IAAI,EAAE,CAAC;AAE/E,6EAA6E;AAC7E,MAAM,UAAU,WAAW,CAAC,EAC1B,YAAY,EACZ,eAAe,EACf,OAAO,EACP,YAAY,EACZ,MAAM,EACN,cAAc,GACK;IACnB,MAAM,CAAC,OAAO,EAAE,UAAU,CAAC,GAAG,QAAQ,CAAC,EAAE,GAAG,EAAE,EAAE,EAAE,KAAK,EAAE,OAAO,EAAE,CAAC,CAAC;IACpE,MAAM,QAAQ,GAAG,MAAM,CAAyB,IAAI,CAAC,CAAC;IACtD,MAAM,GAAG,GAAG,GAAG,OAAO,IAAI,YAAY,IAAI,eAAe,EAAE,CAAC;IAE5D,SAAS,CAAC,GAAG,EAAE;QACb,MAAM,QAAQ,GAAG,cAAc,CAAC;YAC9B,YAAY;YACZ,eAAe;YACf,OAAO;YACP,GAAG,CAAC,YAAY,KAAK,SAAS,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,EAAE,YAAY,EAAE,CAAC;YACvD,GAAG,CAAC,MAAM,KAAK,SAAS,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,EAAE,MAAM,EAAE,CAAC;YAC3C,GAAG,CAAC,cAAc,KAAK,SAAS,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,EAAE,cAAc,EAAE,CAAC;SAC5D,CAAC,CAAC;QACH,QAAQ,CAAC,OAAO,GAAG,QAAQ,CAAC;QAC5B,MAAM,WAAW,GAAG,QAAQ,CAAC,SAAS,CAAC,CAAC,KAAK,EAAE,EAAE,CAAC,UAAU,CAAC,EAAE,GAAG,EAAE,KAAK,EAAE,CAAC,CAAC,CAAC;QAC9E,OAAO,GAAG,EAAE;YACV,WAAW,EAAE,CAAC;YACd,QAAQ,CAAC,OAAO,EAAE,CAAC;YACnB,QAAQ,CAAC,OAAO,GAAG,IAAI,CAAC;QAC1B,CAAC,CAAC;IACJ,CAAC,EAAE,CAAC,GAAG,EAAE,YAAY,EAAE,eAAe,EAAE,OAAO,EAAE,YAAY,EAAE,MAAM,EAAE,cAAc,CAAC,CAAC,CAAC;IAExF,MAAM,OAAO,GAAG,WAAW,CAAC,GAAG,EAAE;QAC/B,KAAK,QAAQ,CAAC,OAAO,EAAE,OAAO,EAAE,CAAC;IACnC,CAAC,EAAE,EAAE,CAAC,CAAC;IAEP,kFAAkF;IAClF,OAAO,EAAE,GAAG,CAAC,OAAO,CAAC,GAAG,KAAK,GAAG,CAAC,CAAC,CAAC,OAAO,CAAC,KAAK,CAAC,CAAC,CAAC,OAAO,CAAC,EAAE,OAAO,EAAE,CAAC;AACzE,CAAC"}
@@ -0,0 +1,6 @@
1
+ /** Compose caller cancellation with a finite deadline, including response-body reads. */
2
+ export declare function requestSignal(options: {
3
+ signal?: AbortSignal;
4
+ requestTimeout?: number;
5
+ }): AbortSignal;
6
+ //# sourceMappingURL=request.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"request.d.ts","sourceRoot":"","sources":["../src/request.ts"],"names":[],"mappings":"AAAA,yFAAyF;AACzF,wBAAgB,aAAa,CAAC,OAAO,EAAE;IACrC,MAAM,CAAC,EAAE,WAAW,CAAC;IACrB,cAAc,CAAC,EAAE,MAAM,CAAC;CACzB,GAAG,WAAW,CAGd"}
@@ -0,0 +1,6 @@
1
+ /** Compose caller cancellation with a finite deadline, including response-body reads. */
2
+ export function requestSignal(options) {
3
+ const deadline = AbortSignal.timeout(options.requestTimeout ?? 10_000);
4
+ return options.signal === undefined ? deadline : AbortSignal.any([options.signal, deadline]);
5
+ }
6
+ //# sourceMappingURL=request.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"request.js","sourceRoot":"","sources":["../src/request.ts"],"names":[],"mappings":"AAAA,yFAAyF;AACzF,MAAM,UAAU,aAAa,CAAC,OAG7B;IACC,MAAM,QAAQ,GAAG,WAAW,CAAC,OAAO,CAAC,OAAO,CAAC,cAAc,IAAI,MAAM,CAAC,CAAC;IACvE,OAAO,OAAO,CAAC,MAAM,KAAK,SAAS,CAAC,CAAC,CAAC,QAAQ,CAAC,CAAC,CAAC,WAAW,CAAC,GAAG,CAAC,CAAC,OAAO,CAAC,MAAM,EAAE,QAAQ,CAAC,CAAC,CAAC;AAC/F,CAAC"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@phala/pay",
3
- "version": "0.6.0",
3
+ "version": "0.8.0",
4
4
  "description": "Phala Pay checkout: a framework-agnostic client and React bindings",
5
5
  "keywords": [
6
6
  "crypto",
@@ -23,7 +23,9 @@
23
23
  "directory": "sdk/js"
24
24
  },
25
25
  "type": "module",
26
- "sideEffects": false,
26
+ "sideEffects": [
27
+ "**/*.css"
28
+ ],
27
29
  "exports": {
28
30
  ".": {
29
31
  "types": "./dist/index.d.ts",
@@ -36,7 +38,8 @@
36
38
  "./server": {
37
39
  "types": "./dist/server/index.d.ts",
38
40
  "default": "./dist/server/index.js"
39
- }
41
+ },
42
+ "./styles.css": "./src/styles.css"
40
43
  },
41
44
  "files": [
42
45
  "dist",
@@ -47,7 +50,7 @@
47
50
  },
48
51
  "packageManager": "pnpm@12.6.0",
49
52
  "engines": {
50
- "node": ">=20"
53
+ "node": ">=20.3"
51
54
  },
52
55
  "scripts": {
53
56
  "build": "tsc -p tsconfig.build.json",
@@ -78,16 +81,17 @@
78
81
  "@testing-library/dom": "^10.4.2",
79
82
  "@testing-library/react": "^16.3.3",
80
83
  "@testing-library/user-event": "^14.6.7",
81
- "@types/node": "^24.19.0",
82
- "@types/react": "^18.3.31",
83
- "@types/react-dom": "^18.3.7",
84
+ "@types/node": "^26.6.3",
85
+ "@types/react": "^19.3.0",
86
+ "@types/react-dom": "^19.3.0",
84
87
  "@vitejs/plugin-react": "^6.1.1",
88
+ "axe-core": "^4.13.0",
85
89
  "eslint": "^10.11.0",
86
90
  "eslint-plugin-react-hooks": "^7.1.1",
87
91
  "jsdom": "^30.1.1",
88
92
  "jsqr": "^1.4.0",
89
- "react": "^18.3.1",
90
- "react-dom": "^18.3.1",
93
+ "react": "^19.3.0",
94
+ "react-dom": "^19.3.0",
91
95
  "typescript": "~6.0.3",
92
96
  "typescript-eslint": "^8.70.1",
93
97
  "viem": "^2.56.9",
package/src/checkout.ts CHANGED
@@ -1,3 +1,4 @@
1
+ import { requestSignal } from "./request.js";
1
2
  import { getAddress, isAddress, isAddressEqual } from "viem";
2
3
  import { parseClientQuote, quoteIdFromClientSecret, type ClientQuote } from "./quote.js";
3
4
 
@@ -109,6 +110,10 @@ export interface CheckoutOptions {
109
110
  apiBase: string;
110
111
  /** Milliseconds between status reads; default 3000. */
111
112
  pollInterval?: number;
113
+ /** Cancels this request/session together with its own deadline. */
114
+ signal?: AbortSignal;
115
+ /** Request deadline in milliseconds; default 10000. */
116
+ requestTimeout?: number;
112
117
  fetch?: typeof globalThis.fetch;
113
118
  /** Current time in milliseconds; for tests. */
114
119
  now?: () => number;
@@ -120,7 +125,7 @@ export interface CheckoutSession {
120
125
  subscribe(listener: (state: CheckoutState) => void): () => void;
121
126
  /** Reads the quote now instead of at the next poll. */
122
127
  refresh(): Promise<void>;
123
- /** Stops polling. */
128
+ /** Stops polling and aborts the active request. */
124
129
  destroy(): void;
125
130
  }
126
131
 
@@ -132,6 +137,10 @@ export interface RetrieveQuoteOptions {
132
137
  /** The address your backend recomputed; a quote naming another one is refused. */
133
138
  expectedAddress: string;
134
139
  apiBase: string;
140
+ /** Cancels this request/session together with its own deadline. */
141
+ signal?: AbortSignal;
142
+ /** Request deadline in milliseconds; default 10000. */
143
+ requestTimeout?: number;
135
144
  fetch?: typeof globalThis.fetch;
136
145
  }
137
146
 
@@ -147,7 +156,11 @@ export async function retrieveQuote(options: RetrieveQuoteOptions): Promise<Clie
147
156
  const url = `${base}/v1/quotes/${quoteId}?client_secret=${encodeURIComponent(options.clientSecret)}`;
148
157
  const fetchImpl = options.fetch ?? globalThis.fetch.bind(globalThis);
149
158
  // A simple GET with no custom headers, so the browser sends no CORS preflight.
150
- const response = await fetchImpl(url, { cache: "no-store", credentials: "omit" });
159
+ const response = await fetchImpl(url, {
160
+ cache: "no-store",
161
+ credentials: "omit",
162
+ signal: requestSignal(options),
163
+ });
151
164
  if (!response.ok) {
152
165
  throw responseError(response, "the quote or its client secret is unknown");
153
166
  }
@@ -198,6 +211,11 @@ export function checkoutStatus(quote: ClientQuote, nowSeconds: number): Checkout
198
211
  export function createCheckout(options: CheckoutOptions): CheckoutSession {
199
212
  quoteIdFromClientSecret(options.clientSecret);
200
213
  expectedAddress(options.expectedAddress);
214
+ const controller = new AbortController();
215
+ const signal =
216
+ options.signal === undefined
217
+ ? controller.signal
218
+ : AbortSignal.any([controller.signal, options.signal]);
201
219
  const now = options.now ?? Date.now;
202
220
  const interval = options.pollInterval ?? DEFAULT_POLL_INTERVAL;
203
221
  const listeners = new Set<(state: CheckoutState) => void>();
@@ -226,6 +244,7 @@ export function createCheckout(options: CheckoutOptions): CheckoutSession {
226
244
  const { status, quote } = state;
227
245
  return (
228
246
  destroyed ||
247
+ signal.aborted ||
229
248
  status === "error" ||
230
249
  status === "credited" ||
231
250
  status === "rejected" ||
@@ -237,10 +256,16 @@ export function createCheckout(options: CheckoutOptions): CheckoutSession {
237
256
 
238
257
  async function load(): Promise<void> {
239
258
  try {
240
- const quote = await retrieveQuote(options);
259
+ const quote = await retrieveQuote({ ...options, signal });
260
+ if (destroyed || signal.aborted) {
261
+ return;
262
+ }
241
263
  failures = 0;
242
264
  setState({ status: checkoutStatus(quote, now() / 1000), quote, error: null });
243
265
  } catch (cause) {
266
+ if (destroyed || signal.aborted) {
267
+ return;
268
+ }
244
269
  const error =
245
270
  cause instanceof CheckoutError
246
271
  ? cause
@@ -263,6 +288,9 @@ export function createCheckout(options: CheckoutOptions): CheckoutSession {
263
288
  }
264
289
 
265
290
  function refresh(): Promise<void> {
291
+ if (destroyed || signal.aborted) {
292
+ return Promise.resolve();
293
+ }
266
294
  inFlight ??= load().finally(() => {
267
295
  inFlight = undefined;
268
296
  });
@@ -292,6 +320,7 @@ export function createCheckout(options: CheckoutOptions): CheckoutSession {
292
320
  refresh,
293
321
  destroy() {
294
322
  destroyed = true;
323
+ controller.abort();
295
324
  clearTimeout(timer);
296
325
  listeners.clear();
297
326
  },
package/src/client.ts CHANGED
@@ -3,6 +3,7 @@ import {
3
3
  retrieveQuote,
4
4
  type CheckoutOptions,
5
5
  type CheckoutSession,
6
+ type RetrieveQuoteOptions,
6
7
  } from "./checkout.js";
7
8
  import type { ClientQuote } from "./quote.js";
8
9
 
@@ -32,10 +33,15 @@ export class PhalaPay {
32
33
  }
33
34
 
34
35
  /** Reads the quote's public view once, refusing one whose address is not `expectedAddress`. */
35
- retrieveQuote(clientSecret: string, expectedAddress: string): Promise<ClientQuote> {
36
+ retrieveQuote(
37
+ clientSecret: string,
38
+ expectedAddress: string,
39
+ options: Pick<RetrieveQuoteOptions, "signal" | "requestTimeout"> = {},
40
+ ): Promise<ClientQuote> {
36
41
  return retrieveQuote({
37
42
  clientSecret,
38
43
  expectedAddress,
44
+ ...options,
39
45
  apiBase: this.apiBase,
40
46
  ...this.#fetchOption(),
41
47
  });
@@ -44,7 +50,7 @@ export class PhalaPay {
44
50
  /** Follows the quote until it is credited, rejected, reversed, canceled, or expired. */
45
51
  checkout(
46
52
  clientSecret: string,
47
- options: Pick<CheckoutOptions, "expectedAddress" | "pollInterval">,
53
+ options: Pick<CheckoutOptions, "expectedAddress" | "pollInterval" | "signal" | "requestTimeout">,
48
54
  ): CheckoutSession {
49
55
  return createCheckout({
50
56
  clientSecret,
package/src/css.d.ts ADDED
@@ -0,0 +1 @@
1
+ declare module "*.css";
@@ -1,3 +1,4 @@
1
+ import { requestSignal } from "./request.js";
1
2
  import { CheckoutError, responseError } from "./checkout.js";
2
3
 
3
4
  /** A payment to a deposit address as the customer's page sees it. Display only. */
@@ -51,6 +52,9 @@ export interface RetrieveDepositAddressOptions {
51
52
  /** A `client_secret` from your backend's `POST /v1/deposit_addresses` or `…/rotate`. */
52
53
  clientSecret: string;
53
54
  apiBase: string;
55
+ signal?: AbortSignal;
56
+ /** Request deadline in milliseconds; default 10000. */
57
+ requestTimeout?: number;
54
58
  fetch?: typeof globalThis.fetch;
55
59
  }
56
60
 
@@ -77,7 +81,11 @@ export async function retrieveDepositAddress(
77
81
  const url = `${base}/v1/deposit_addresses/${id}?client_secret=${encodeURIComponent(options.clientSecret)}`;
78
82
  const fetchImpl = options.fetch ?? globalThis.fetch.bind(globalThis);
79
83
  // A simple GET with no custom headers, so the browser sends no CORS preflight.
80
- const response = await fetchImpl(url, { cache: "no-store", credentials: "omit" });
84
+ const response = await fetchImpl(url, {
85
+ cache: "no-store",
86
+ credentials: "omit",
87
+ signal: requestSignal(options),
88
+ });
81
89
  if (!response.ok) {
82
90
  throw responseError(response, "the address or its client secret is unknown");
83
91
  }
@@ -21,7 +21,7 @@ import {
21
21
  type EthereumProvider,
22
22
  type Wallet,
23
23
  } from "../wallet.js";
24
- import { STYLES, appearanceStyle, type Appearance } from "./appearance.js";
24
+ import type { Appearance } from "./appearance.js";
25
25
  import { Field } from "./Field.js";
26
26
  import { QrCode } from "./QrCode.js";
27
27
  import { useCheckout } from "./useCheckout.js";
@@ -104,17 +104,23 @@ export function Checkout({
104
104
  useEffect(() => {
105
105
  callbacks.current.onChange?.(state.current);
106
106
  }, [status]);
107
- const notified = useRef<string | null>(null);
107
+ const notified = useRef(new Set<string>());
108
108
  useEffect(() => {
109
- if (quote === null || notified.current === quote.id) {
109
+ if (quote === null) {
110
110
  return;
111
111
  }
112
112
  if (status === "credited") {
113
- notified.current = quote.id;
114
- callbacks.current.onSuccess?.(quote);
113
+ const key = `${quote.id}:success`;
114
+ if (!notified.current.has(key)) {
115
+ notified.current.add(key);
116
+ callbacks.current.onSuccess?.(quote);
117
+ }
115
118
  } else if (status === "expired" || status === "canceled") {
116
- notified.current = quote.id;
117
- callbacks.current.onExpire?.(quote);
119
+ const key = `${quote.id}:expire`;
120
+ if (!notified.current.has(key)) {
121
+ notified.current.add(key);
122
+ callbacks.current.onExpire?.(quote);
123
+ }
118
124
  }
119
125
  }, [status, quote]);
120
126
 
@@ -122,9 +128,7 @@ export function Checkout({
122
128
  <div
123
129
  className={className === undefined ? "pp-root" : `pp-root ${className}`}
124
130
  data-theme={appearance?.theme ?? "light"}
125
- style={appearanceStyle(appearance)}
126
131
  >
127
- <style>{STYLES}</style>
128
132
  {quote !== null && (
129
133
  <>
130
134
  <p className="pp-amount">
@@ -1,6 +1,6 @@
1
1
  "use client";
2
2
 
3
- import { useEffect, useState } from "react";
3
+ import { useEffect, useId, useState } from "react";
4
4
  import { formatUnits } from "viem";
5
5
  import { networkName } from "../chains.js";
6
6
  import {
@@ -11,7 +11,7 @@ import {
11
11
  import { CheckoutError, pollDelay } from "../checkout.js";
12
12
  import { formatWait } from "../format.js";
13
13
  import { depositAddressTransfer, type DepositAddressDetails } from "../payment.js";
14
- import { STYLES, appearanceStyle, type Appearance } from "./appearance.js";
14
+ import type { Appearance } from "./appearance.js";
15
15
  import { Field } from "./Field.js";
16
16
  import { QrCode } from "./QrCode.js";
17
17
 
@@ -55,6 +55,7 @@ export function DepositAddress({
55
55
  className,
56
56
  }: DepositAddressProps) {
57
57
  const { networks } = depositAddress;
58
+ const id = useId();
58
59
  const view = useClientView(clientSecret, apiBase, pollInterval ?? 3000);
59
60
  const payments = view?.payments ?? [];
60
61
  const [selectedChain, setSelectedChain] = useState(chainId ?? networks[0]?.chain_id);
@@ -76,49 +77,43 @@ export function DepositAddress({
76
77
  <div
77
78
  className={className === undefined ? "pp-root" : `pp-root ${className}`}
78
79
  data-theme={appearance?.theme ?? "light"}
79
- style={appearanceStyle(appearance)}
80
80
  >
81
- <style>{STYLES}</style>
82
81
  <p className="pp-subtitle">
83
82
  {depositAddress.address === null
84
83
  ? "Your deposit address for every supported token; it differs on some networks"
85
84
  : "One address for all supported tokens and networks"}
86
85
  </p>
87
86
  {networks.length > 1 && (
88
- <div className="pp-tabs" role="tablist" aria-label="Network">
87
+ <fieldset className="pp-choices">
88
+ <legend>Network</legend>
89
89
  {networks.map((each) => (
90
- <button
91
- key={each.chain_id}
92
- type="button"
93
- role="tab"
94
- className="pp-tab"
95
- aria-selected={each.chain_id === network.chain_id}
96
- onClick={() => {
97
- setSelectedChain(each.chain_id);
98
- }}
99
- >
90
+ <label key={each.chain_id}>
91
+ <input
92
+ type="radio"
93
+ name={`${id}-network`}
94
+ checked={each.chain_id === network.chain_id}
95
+ onChange={() => setSelectedChain(each.chain_id)}
96
+ />
100
97
  {networkName(each.chain_id)}
101
- </button>
98
+ </label>
102
99
  ))}
103
- </div>
100
+ </fieldset>
104
101
  )}
105
102
  {network.assets.length > 1 && (
106
- <div className="pp-tabs" role="tablist" aria-label="Token">
103
+ <fieldset className="pp-choices">
104
+ <legend>Token</legend>
107
105
  {network.assets.map((each) => (
108
- <button
109
- key={each.asset}
110
- type="button"
111
- role="tab"
112
- className="pp-tab"
113
- aria-selected={each.asset === token.asset}
114
- onClick={() => {
115
- setSelectedAsset(each.asset);
116
- }}
117
- >
106
+ <label key={each.asset}>
107
+ <input
108
+ type="radio"
109
+ name={`${id}-token`}
110
+ checked={each.asset === token.asset}
111
+ onChange={() => setSelectedAsset(each.asset)}
112
+ />
118
113
  {each.asset.toUpperCase()}
119
- </button>
114
+ </label>
120
115
  ))}
121
- </div>
116
+ </fieldset>
122
117
  )}
123
118
  <div className="pp-qr-panel">
124
119
  <QrCode value={token.payment_uri} label={`Deposit address for ${symbol} on ${name}`} />
@@ -161,13 +156,14 @@ function useClientView(
161
156
  if (clientSecret === undefined || apiBase === undefined) {
162
157
  return;
163
158
  }
159
+ const controller = new AbortController();
164
160
  let stopped = false;
165
161
  let timer: ReturnType<typeof setTimeout> | undefined;
166
162
  let failures = 0;
167
163
  const load = async () => {
168
164
  let failure: CheckoutError | null = null;
169
165
  try {
170
- const view = await retrieveDepositAddress({ clientSecret, apiBase });
166
+ const view = await retrieveDepositAddress({ clientSecret, apiBase, signal: controller.signal });
171
167
  failures = 0;
172
168
  if (!stopped) {
173
169
  setCurrent({ key, view });
@@ -187,6 +183,7 @@ function useClientView(
187
183
  void load();
188
184
  return () => {
189
185
  stopped = true;
186
+ controller.abort();
190
187
  clearTimeout(timer);
191
188
  };
192
189
  }, [key, clientSecret, apiBase, interval]);