@phala/pay 0.8.2 → 0.9.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 (104) hide show
  1. package/README.md +33 -58
  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.d.ts +4 -2
  8. package/dist/checkout.d.ts.map +1 -1
  9. package/dist/checkout.js +18 -3
  10. package/dist/checkout.js.map +1 -1
  11. package/dist/deposit-address.js +2 -2
  12. package/dist/deposit-address.js.map +1 -1
  13. package/dist/icon-names.d.ts +7 -0
  14. package/dist/icon-names.d.ts.map +1 -0
  15. package/dist/icon-names.js +15 -0
  16. package/dist/icon-names.js.map +1 -0
  17. package/dist/icon-svg.d.ts +6 -0
  18. package/dist/icon-svg.d.ts.map +1 -0
  19. package/dist/icon-svg.js +13 -0
  20. package/dist/icon-svg.js.map +1 -0
  21. package/dist/icons.d.ts +5 -0
  22. package/dist/icons.d.ts.map +1 -0
  23. package/dist/icons.js +27 -0
  24. package/dist/icons.js.map +1 -0
  25. package/dist/index.d.ts +6 -3
  26. package/dist/index.d.ts.map +1 -1
  27. package/dist/index.js +5 -3
  28. package/dist/index.js.map +1 -1
  29. package/dist/request.d.ts +2 -0
  30. package/dist/request.d.ts.map +1 -1
  31. package/dist/request.js +7 -0
  32. package/dist/request.js.map +1 -1
  33. package/dist/shared/checkout-params.d.ts +7 -0
  34. package/dist/shared/checkout-params.d.ts.map +1 -0
  35. package/dist/shared/checkout-params.js +2 -0
  36. package/dist/shared/checkout-params.js.map +1 -0
  37. package/package.json +18 -31
  38. package/src/chains.ts +12 -2
  39. package/src/checkout.ts +21 -4
  40. package/src/deposit-address.ts +2 -2
  41. package/src/generated/openapi.d.ts +9482 -0
  42. package/src/icon-names.ts +20 -0
  43. package/src/icon-svg.ts +13 -0
  44. package/src/icons.ts +30 -0
  45. package/src/index.ts +6 -2
  46. package/src/request.ts +7 -0
  47. package/src/shared/checkout-params.ts +6 -0
  48. package/dist/react/Checkout.d.ts +0 -43
  49. package/dist/react/Checkout.d.ts.map +0 -1
  50. package/dist/react/Checkout.js +0 -193
  51. package/dist/react/Checkout.js.map +0 -1
  52. package/dist/react/DepositAddress.d.ts +0 -32
  53. package/dist/react/DepositAddress.d.ts.map +0 -1
  54. package/dist/react/DepositAddress.js +0 -131
  55. package/dist/react/DepositAddress.js.map +0 -1
  56. package/dist/react/Field.d.ts +0 -7
  57. package/dist/react/Field.d.ts.map +0 -1
  58. package/dist/react/Field.js +0 -21
  59. package/dist/react/Field.js.map +0 -1
  60. package/dist/react/QrCode.d.ts +0 -8
  61. package/dist/react/QrCode.d.ts.map +0 -1
  62. package/dist/react/QrCode.js +0 -20
  63. package/dist/react/QrCode.js.map +0 -1
  64. package/dist/react/appearance.d.ts +0 -5
  65. package/dist/react/appearance.d.ts.map +0 -1
  66. package/dist/react/appearance.js +0 -2
  67. package/dist/react/appearance.js.map +0 -1
  68. package/dist/react/index.d.ts +0 -5
  69. package/dist/react/index.d.ts.map +0 -1
  70. package/dist/react/index.js +0 -5
  71. package/dist/react/index.js.map +0 -1
  72. package/dist/react/useCheckout.d.ts +0 -9
  73. package/dist/react/useCheckout.d.ts.map +0 -1
  74. package/dist/react/useCheckout.js +0 -32
  75. package/dist/react/useCheckout.js.map +0 -1
  76. package/dist/server/addresses.d.ts +0 -78
  77. package/dist/server/addresses.d.ts.map +0 -1
  78. package/dist/server/addresses.js +0 -118
  79. package/dist/server/addresses.js.map +0 -1
  80. package/dist/server/index.d.ts +0 -12
  81. package/dist/server/index.d.ts.map +0 -1
  82. package/dist/server/index.js +0 -12
  83. package/dist/server/index.js.map +0 -1
  84. package/dist/server/sweeps.d.ts +0 -78
  85. package/dist/server/sweeps.d.ts.map +0 -1
  86. package/dist/server/sweeps.js +0 -117
  87. package/dist/server/sweeps.js.map +0 -1
  88. package/dist/server/webhook.d.ts +0 -61
  89. package/dist/server/webhook.d.ts.map +0 -1
  90. package/dist/server/webhook.js +0 -154
  91. package/dist/server/webhook.js.map +0 -1
  92. package/src/css.d.ts +0 -1
  93. package/src/react/Checkout.tsx +0 -469
  94. package/src/react/DepositAddress.tsx +0 -239
  95. package/src/react/Field.tsx +0 -46
  96. package/src/react/QrCode.tsx +0 -39
  97. package/src/react/appearance.ts +0 -4
  98. package/src/react/index.ts +0 -6
  99. package/src/react/useCheckout.ts +0 -58
  100. package/src/server/addresses.ts +0 -223
  101. package/src/server/index.ts +0 -38
  102. package/src/server/sweeps.ts +0 -197
  103. package/src/server/webhook.ts +0 -208
  104. package/src/styles.css +0 -76
package/dist/request.js CHANGED
@@ -3,4 +3,11 @@ export function requestSignal(options) {
3
3
  const deadline = AbortSignal.timeout(options.requestTimeout ?? 10_000);
4
4
  return options.signal === undefined ? deadline : AbortSignal.any([options.signal, deadline]);
5
5
  }
6
+ /** `apiBase` without trailing slashes, in linear time (a `/\/+$/` regex is quadratic). */
7
+ export function trimTrailingSlashes(apiBase) {
8
+ let end = apiBase.length;
9
+ while (end > 0 && apiBase[end - 1] === "/")
10
+ end -= 1;
11
+ return apiBase.slice(0, end);
12
+ }
6
13
  //# sourceMappingURL=request.js.map
@@ -1 +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"}
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;AAED,0FAA0F;AAC1F,MAAM,UAAU,mBAAmB,CAAC,OAAe;IACjD,IAAI,GAAG,GAAG,OAAO,CAAC,MAAM,CAAC;IACzB,OAAO,GAAG,GAAG,CAAC,IAAI,OAAO,CAAC,GAAG,GAAG,CAAC,CAAC,KAAK,GAAG;QAAE,GAAG,IAAI,CAAC,CAAC;IACrD,OAAO,OAAO,CAAC,KAAK,CAAC,CAAC,EAAE,GAAG,CAAC,CAAC;AAC/B,CAAC"}
@@ -0,0 +1,7 @@
1
+ /** The only server-to-browser checkout handoff; contains no merchant credentials or pins. */
2
+ export interface CheckoutParams {
3
+ clientSecret: string;
4
+ expectedAddress: string;
5
+ apiBase: string;
6
+ }
7
+ //# sourceMappingURL=checkout-params.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"checkout-params.d.ts","sourceRoot":"","sources":["../../src/shared/checkout-params.ts"],"names":[],"mappings":"AAAA,6FAA6F;AAC7F,MAAM,WAAW,cAAc;IAC7B,YAAY,EAAE,MAAM,CAAC;IACrB,eAAe,EAAE,MAAM,CAAC;IACxB,OAAO,EAAE,MAAM,CAAC;CACjB"}
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=checkout-params.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"checkout-params.js","sourceRoot":"","sources":["../../src/shared/checkout-params.ts"],"names":[],"mappings":""}
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@phala/pay",
3
- "version": "0.8.2",
4
- "description": "Phala Pay checkout: a framework-agnostic client and React bindings",
3
+ "version": "0.9.0",
4
+ "description": "Phala Pay framework-free browser client",
5
5
  "keywords": [
6
6
  "crypto",
7
7
  "checkout",
@@ -9,7 +9,6 @@
9
9
  "erc20",
10
10
  "eip-681",
11
11
  "eip-6963",
12
- "react",
13
12
  "viem"
14
13
  ],
15
14
  "license": "Apache-2.0",
@@ -30,20 +29,12 @@
30
29
  ".": {
31
30
  "types": "./dist/index.d.ts",
32
31
  "default": "./dist/index.js"
33
- },
34
- "./react": {
35
- "types": "./dist/react/index.d.ts",
36
- "default": "./dist/react/index.js"
37
- },
38
- "./server": {
39
- "types": "./dist/server/index.d.ts",
40
- "default": "./dist/server/index.js"
41
- },
42
- "./styles.css": "./src/styles.css"
32
+ }
43
33
  },
44
34
  "files": [
45
35
  "dist",
46
- "src"
36
+ "src",
37
+ "THIRD_PARTY_NOTICES"
47
38
  ],
48
39
  "publishConfig": {
49
40
  "access": "public"
@@ -54,48 +45,44 @@
54
45
  },
55
46
  "scripts": {
56
47
  "build": "tsc -p tsconfig.build.json",
48
+ "icons:generate": "node scripts/generate-icons.mjs",
57
49
  "prepare": "tsc -p tsconfig.build.json",
58
50
  "typecheck": "tsc --noEmit",
59
51
  "lint": "eslint .",
60
52
  "test": "vitest run",
61
53
  "e2e": "playwright test",
62
54
  "e2e:docker": "e2e/docker.sh",
63
- "check": "pnpm run typecheck && pnpm run lint && pnpm run test && pnpm run build"
55
+ "check": "pnpm run typecheck && pnpm run lint && pnpm run test && pnpm run build && pnpm run check-generated",
56
+ "generate:types": "openapi-typescript ../../crates/topup/openapi.json -o src/generated/openapi.d.ts --immutable",
57
+ "check-generated": "bash ../scripts/check-generated-js.sh",
58
+ "test:fixtures": "pnpm --filter @phala/pay-server run test -- --run test/shared-fixtures.test.ts test/merchant-pins-ledger.test.ts test/merchant-transport.test.ts"
64
59
  },
65
60
  "dependencies": {
66
- "mipd": "^0.0.7",
67
- "uqr": "^0.1.3"
61
+ "mipd": "^0.0.7"
68
62
  },
69
63
  "peerDependencies": {
70
- "react": "^18.2.0 || ^19.0.0",
71
64
  "viem": "^2.21.0"
72
65
  },
73
- "peerDependenciesMeta": {
74
- "react": {
75
- "optional": true
76
- }
77
- },
78
66
  "devDependencies": {
79
67
  "@eslint/js": "^10.0.1",
80
68
  "@playwright/test": "1.63.0",
81
69
  "@testing-library/dom": "^10.4.2",
82
- "@testing-library/react": "^16.3.3",
83
- "@testing-library/user-event": "^14.6.7",
84
70
  "@types/node": "^26.6.3",
85
- "@types/react": "^19.3.0",
86
- "@types/react-dom": "^19.3.0",
87
- "@vitejs/plugin-react": "^6.1.1",
88
71
  "axe-core": "^4.13.0",
89
72
  "eslint": "^10.11.0",
90
73
  "eslint-plugin-react-hooks": "^7.1.1",
91
74
  "jsdom": "^30.1.1",
92
75
  "jsqr": "^1.4.0",
93
- "react": "^19.3.0",
94
- "react-dom": "^19.3.0",
95
76
  "typescript": "~6.0.3",
96
77
  "typescript-eslint": "^8.70.1",
97
78
  "viem": "^2.56.9",
98
79
  "vite": "^8.3.1",
99
- "vitest": "^5.0.2"
80
+ "vitest": "^5.0.2",
81
+ "openapi-typescript": "7.13.0",
82
+ "@types/react": "^19.3.0",
83
+ "@types/react-dom": "^19.3.0",
84
+ "@vitejs/plugin-react": "^6.1.1",
85
+ "react": "^19.3.0",
86
+ "react-dom": "^19.3.0"
100
87
  }
101
88
  }
package/src/chains.ts CHANGED
@@ -2,10 +2,20 @@ import type { Chain } from "viem";
2
2
  import { base, baseSepolia, mainnet, sepolia } from "viem/chains";
3
3
 
4
4
  /** Chains a wallet can be asked to add; any other chain must already be in the wallet. */
5
- const CHAINS: readonly Chain[] = [mainnet, sepolia, base, baseSepolia];
5
+ const CHAINS: readonly { chain: Chain; icon: "ethereum" | "base" }[] = [
6
+ { chain: mainnet, icon: "ethereum" },
7
+ { chain: sepolia, icon: "ethereum" },
8
+ { chain: base, icon: "base" },
9
+ { chain: baseSepolia, icon: "base" },
10
+ ];
11
+
12
+ /** Icon family stored with the chain registry, including testnets. */
13
+ export function networkIconName(chainId: number): "ethereum" | "base" | undefined {
14
+ return CHAINS.find(({ chain }) => chain.id === chainId)?.icon;
15
+ }
6
16
 
7
17
  export function knownChain(chainId: number): Chain | undefined {
8
- return CHAINS.find((chain) => chain.id === chainId);
18
+ return CHAINS.find(({ chain }) => chain.id === chainId)?.chain;
9
19
  }
10
20
 
11
21
  /** A human name for the network, for example `Sepolia`. */
package/src/checkout.ts CHANGED
@@ -1,4 +1,4 @@
1
- import { requestSignal } from "./request.js";
1
+ import { requestSignal, trimTrailingSlashes } from "./request.js";
2
2
  import { getAddress, isAddress, isAddressEqual } from "viem";
3
3
  import { parseClientQuote, quoteIdFromClientSecret, type ClientQuote } from "./quote.js";
4
4
 
@@ -32,6 +32,7 @@ export type CheckoutErrorCode =
32
32
  | "address_mismatch"
33
33
  | "rate_limited"
34
34
  | "network_error"
35
+ | "service_unavailable"
35
36
  | "invalid_response"
36
37
  | "api_error";
37
38
 
@@ -70,6 +71,13 @@ export function responseError(response: Response, notFound: string): CheckoutErr
70
71
  if (response.status === 404) {
71
72
  return new CheckoutError("invalid_client_secret", notFound, requestId);
72
73
  }
74
+ if (response.status >= 500) {
75
+ const seconds = Number(response.headers.get("retry-after"));
76
+ return new CheckoutError("service_unavailable", "the payment service is temporarily unavailable", {
77
+ ...requestId,
78
+ ...(Number.isFinite(seconds) && seconds > 0 ? { retryAfter: seconds } : {}),
79
+ });
80
+ }
73
81
  if (response.status === 429) {
74
82
  const seconds = Number(response.headers.get("retry-after"));
75
83
  return new CheckoutError("rate_limited", "too many status requests", {
@@ -92,8 +100,10 @@ export function pollDelay(interval: number, failures: number, error: CheckoutErr
92
100
 
93
101
  export interface CheckoutState {
94
102
  status: CheckoutStatus;
103
+ /** The last known view is retained while polling reconnects after a temporary outage. */
104
+ reconnecting?: boolean;
95
105
  quote: ClientQuote | null;
96
- /** The last failed refresh; polling continues after every error but `invalid_client_secret`. */
106
+ /** Permanent/validation failure details; temporary outages use `reconnecting` instead. */
97
107
  error: CheckoutError | null;
98
108
  }
99
109
 
@@ -152,7 +162,7 @@ export interface RetrieveQuoteOptions {
152
162
  export async function retrieveQuote(options: RetrieveQuoteOptions): Promise<ClientQuote> {
153
163
  const quoteId = quoteIdFromClientSecret(options.clientSecret);
154
164
  const expected = expectedAddress(options.expectedAddress);
155
- const base = options.apiBase.replace(/\/+$/, "");
165
+ const base = trimTrailingSlashes(options.apiBase);
156
166
  const url = `${base}/v1/quotes/${quoteId}?client_secret=${encodeURIComponent(options.clientSecret)}`;
157
167
  const fetchImpl = options.fetch ?? globalThis.fetch.bind(globalThis);
158
168
  // A simple GET with no custom headers, so the browser sends no CORS preflight.
@@ -223,12 +233,14 @@ export function createCheckout(options: CheckoutOptions): CheckoutSession {
223
233
  let state: CheckoutState = { status: "loading", quote: null, error: null };
224
234
  let timer: ReturnType<typeof setTimeout> | undefined;
225
235
  let failures = 0;
236
+ let lastFailure: CheckoutError | null = null;
226
237
  let destroyed = false;
227
238
  let inFlight: Promise<void> | undefined;
228
239
 
229
240
  function setState(next: CheckoutState): void {
230
241
  if (
231
242
  next.status === state.status &&
243
+ next.reconnecting === state.reconnecting &&
232
244
  next.error === state.error &&
233
245
  JSON.stringify(next.quote) === JSON.stringify(state.quote)
234
246
  ) {
@@ -261,6 +273,7 @@ export function createCheckout(options: CheckoutOptions): CheckoutSession {
261
273
  return;
262
274
  }
263
275
  failures = 0;
276
+ lastFailure = null;
264
277
  setState({ status: checkoutStatus(quote, now() / 1000), quote, error: null });
265
278
  } catch (cause) {
266
279
  if (destroyed || signal.aborted) {
@@ -271,12 +284,16 @@ export function createCheckout(options: CheckoutOptions): CheckoutSession {
271
284
  ? cause
272
285
  : new CheckoutError("network_error", "could not reach the payment service", { cause });
273
286
  failures += 1;
287
+ lastFailure = error;
274
288
  const quote = state.quote;
275
289
  if (error.code === "address_mismatch") {
276
290
  // Fail closed: forget the quote, so nothing about its address is shown.
277
291
  setState({ status: "error", quote: null, error });
278
292
  } else if (error.code === "invalid_client_secret") {
279
293
  setState({ status: "error", quote, error });
294
+ } else if (error.code === "network_error" || error.code === "service_unavailable") {
295
+ // Do not infer expiry or discard the payer's view while chain progress is unknown.
296
+ setState({ status: state.status, quote, error: null, reconnecting: true });
280
297
  } else {
281
298
  setState({
282
299
  status: quote === null ? state.status : checkoutStatus(quote, now() / 1000),
@@ -301,7 +318,7 @@ export function createCheckout(options: CheckoutOptions): CheckoutSession {
301
318
  if (finished()) {
302
319
  return;
303
320
  }
304
- const delay = pollDelay(interval, failures, state.error);
321
+ const delay = pollDelay(interval, failures, lastFailure);
305
322
  timer = setTimeout(() => {
306
323
  void refresh().then(schedule);
307
324
  }, delay);
@@ -1,4 +1,4 @@
1
- import { requestSignal } from "./request.js";
1
+ import { requestSignal, trimTrailingSlashes } from "./request.js";
2
2
  import { CheckoutError, responseError } from "./checkout.js";
3
3
 
4
4
  /** A payment to a deposit address as the customer's page sees it. Display only. */
@@ -77,7 +77,7 @@ export async function retrieveDepositAddress(
77
77
  options: RetrieveDepositAddressOptions,
78
78
  ): Promise<ClientDepositAddress> {
79
79
  const id = depositAddressIdFromClientSecret(options.clientSecret);
80
- const base = options.apiBase.replace(/\/+$/, "");
80
+ const base = trimTrailingSlashes(options.apiBase);
81
81
  const url = `${base}/v1/deposit_addresses/${id}?client_secret=${encodeURIComponent(options.clientSecret)}`;
82
82
  const fetchImpl = options.fetch ?? globalThis.fetch.bind(globalThis);
83
83
  // A simple GET with no custom headers, so the browser sends no CORS preflight.