@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.
- package/README.md +33 -58
- package/THIRD_PARTY_NOTICES +25 -0
- package/dist/chains.d.ts +2 -0
- package/dist/chains.d.ts.map +1 -1
- package/dist/chains.js +11 -2
- package/dist/chains.js.map +1 -1
- package/dist/checkout.d.ts +4 -2
- package/dist/checkout.d.ts.map +1 -1
- package/dist/checkout.js +18 -3
- package/dist/checkout.js.map +1 -1
- package/dist/deposit-address.js +2 -2
- package/dist/deposit-address.js.map +1 -1
- package/dist/icon-names.d.ts +7 -0
- package/dist/icon-names.d.ts.map +1 -0
- package/dist/icon-names.js +15 -0
- package/dist/icon-names.js.map +1 -0
- package/dist/icon-svg.d.ts +6 -0
- package/dist/icon-svg.d.ts.map +1 -0
- package/dist/icon-svg.js +13 -0
- package/dist/icon-svg.js.map +1 -0
- package/dist/icons.d.ts +5 -0
- package/dist/icons.d.ts.map +1 -0
- package/dist/icons.js +27 -0
- package/dist/icons.js.map +1 -0
- package/dist/index.d.ts +6 -3
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +5 -3
- package/dist/index.js.map +1 -1
- package/dist/request.d.ts +2 -0
- package/dist/request.d.ts.map +1 -1
- package/dist/request.js +7 -0
- package/dist/request.js.map +1 -1
- package/dist/shared/checkout-params.d.ts +7 -0
- package/dist/shared/checkout-params.d.ts.map +1 -0
- package/dist/shared/checkout-params.js +2 -0
- package/dist/shared/checkout-params.js.map +1 -0
- package/package.json +18 -31
- package/src/chains.ts +12 -2
- package/src/checkout.ts +21 -4
- package/src/deposit-address.ts +2 -2
- package/src/generated/openapi.d.ts +9482 -0
- package/src/icon-names.ts +20 -0
- package/src/icon-svg.ts +13 -0
- package/src/icons.ts +30 -0
- package/src/index.ts +6 -2
- package/src/request.ts +7 -0
- package/src/shared/checkout-params.ts +6 -0
- package/dist/react/Checkout.d.ts +0 -43
- package/dist/react/Checkout.d.ts.map +0 -1
- package/dist/react/Checkout.js +0 -193
- package/dist/react/Checkout.js.map +0 -1
- package/dist/react/DepositAddress.d.ts +0 -32
- package/dist/react/DepositAddress.d.ts.map +0 -1
- package/dist/react/DepositAddress.js +0 -131
- package/dist/react/DepositAddress.js.map +0 -1
- package/dist/react/Field.d.ts +0 -7
- package/dist/react/Field.d.ts.map +0 -1
- package/dist/react/Field.js +0 -21
- package/dist/react/Field.js.map +0 -1
- package/dist/react/QrCode.d.ts +0 -8
- package/dist/react/QrCode.d.ts.map +0 -1
- package/dist/react/QrCode.js +0 -20
- package/dist/react/QrCode.js.map +0 -1
- package/dist/react/appearance.d.ts +0 -5
- package/dist/react/appearance.d.ts.map +0 -1
- package/dist/react/appearance.js +0 -2
- package/dist/react/appearance.js.map +0 -1
- package/dist/react/index.d.ts +0 -5
- package/dist/react/index.d.ts.map +0 -1
- package/dist/react/index.js +0 -5
- package/dist/react/index.js.map +0 -1
- package/dist/react/useCheckout.d.ts +0 -9
- package/dist/react/useCheckout.d.ts.map +0 -1
- package/dist/react/useCheckout.js +0 -32
- package/dist/react/useCheckout.js.map +0 -1
- package/dist/server/addresses.d.ts +0 -78
- package/dist/server/addresses.d.ts.map +0 -1
- package/dist/server/addresses.js +0 -118
- package/dist/server/addresses.js.map +0 -1
- package/dist/server/index.d.ts +0 -12
- package/dist/server/index.d.ts.map +0 -1
- package/dist/server/index.js +0 -12
- package/dist/server/index.js.map +0 -1
- package/dist/server/sweeps.d.ts +0 -78
- package/dist/server/sweeps.d.ts.map +0 -1
- package/dist/server/sweeps.js +0 -117
- package/dist/server/sweeps.js.map +0 -1
- package/dist/server/webhook.d.ts +0 -61
- package/dist/server/webhook.d.ts.map +0 -1
- package/dist/server/webhook.js +0 -154
- package/dist/server/webhook.js.map +0 -1
- package/src/css.d.ts +0 -1
- package/src/react/Checkout.tsx +0 -469
- package/src/react/DepositAddress.tsx +0 -239
- package/src/react/Field.tsx +0 -46
- package/src/react/QrCode.tsx +0 -39
- package/src/react/appearance.ts +0 -4
- package/src/react/index.ts +0 -6
- package/src/react/useCheckout.ts +0 -58
- package/src/server/addresses.ts +0 -223
- package/src/server/index.ts +0 -38
- package/src/server/sweeps.ts +0 -197
- package/src/server/webhook.ts +0 -208
- 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
|
package/dist/request.js.map
CHANGED
|
@@ -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 @@
|
|
|
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 @@
|
|
|
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.
|
|
4
|
-
"description": "Phala Pay
|
|
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
|
|
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
|
-
/**
|
|
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
|
|
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,
|
|
321
|
+
const delay = pollDelay(interval, failures, lastFailure);
|
|
305
322
|
timer = setTimeout(() => {
|
|
306
323
|
void refresh().then(schedule);
|
|
307
324
|
}, delay);
|
package/src/deposit-address.ts
CHANGED
|
@@ -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
|
|
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.
|