@molecule/app-react 1.0.3 → 1.1.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 +106 -1
- package/dist/hooks/index.d.ts +1 -0
- package/dist/hooks/index.d.ts.map +1 -1
- package/dist/hooks/index.js +1 -0
- package/dist/hooks/index.js.map +1 -1
- package/dist/hooks/useVerifyPaymentReturn.d.ts +81 -0
- package/dist/hooks/useVerifyPaymentReturn.d.ts.map +1 -0
- package/dist/hooks/useVerifyPaymentReturn.js +119 -0
- package/dist/hooks/useVerifyPaymentReturn.js.map +1 -0
- package/dist/index.d.ts +8 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +8 -0
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -3,7 +3,7 @@ AUTO-GENERATED — DO NOT EDIT THIS FILE.
|
|
|
3
3
|
Generated by `mlcl sync-docs` from the package's src/index.ts JSDoc + mlcl/registry.json.
|
|
4
4
|
Edits here are overwritten on the next commit (molecule's pre-commit hook regenerates).
|
|
5
5
|
To change this document, edit the module-level JSDoc in src/index.ts.
|
|
6
|
-
Generated: 2026-08-
|
|
6
|
+
Generated: 2026-08-15T19:34:35.768Z
|
|
7
7
|
-->
|
|
8
8
|
|
|
9
9
|
# @molecule/app-react
|
|
@@ -1364,6 +1364,66 @@ interface UseTranslationResult {
|
|
|
1364
1364
|
}
|
|
1365
1365
|
```
|
|
1366
1366
|
|
|
1367
|
+
#### `UseVerifyPaymentReturnOptions`
|
|
1368
|
+
|
|
1369
|
+
Options for {@link useVerifyPaymentReturn}.
|
|
1370
|
+
|
|
1371
|
+
```typescript
|
|
1372
|
+
interface UseVerifyPaymentReturnOptions {
|
|
1373
|
+
/**
|
|
1374
|
+
* Provider name to verify with, when the return URL carries no `provider`
|
|
1375
|
+
* query parameter. Leave unset to verify only what the URL names.
|
|
1376
|
+
*/
|
|
1377
|
+
provider?: string
|
|
1378
|
+
|
|
1379
|
+
/** Set `false` to skip verification entirely (e.g. behind a feature flag). */
|
|
1380
|
+
enabled?: boolean
|
|
1381
|
+
|
|
1382
|
+
/**
|
|
1383
|
+
* Remove the provider's query parameters from the address bar once the
|
|
1384
|
+
* purchase is verified, so a reload (or a shared link) cannot replay the
|
|
1385
|
+
* verification. Defaults to `true`.
|
|
1386
|
+
*/
|
|
1387
|
+
cleanUrl?: boolean
|
|
1388
|
+
|
|
1389
|
+
/** Called once, after the purchase verifies. */
|
|
1390
|
+
onVerified?: () => void
|
|
1391
|
+
|
|
1392
|
+
/** Called when verification fails. */
|
|
1393
|
+
onError?: (error: Error) => void
|
|
1394
|
+
}
|
|
1395
|
+
```
|
|
1396
|
+
|
|
1397
|
+
#### `UseVerifyPaymentReturnResult`
|
|
1398
|
+
|
|
1399
|
+
State of the post-checkout verification.
|
|
1400
|
+
|
|
1401
|
+
```typescript
|
|
1402
|
+
interface UseVerifyPaymentReturnResult {
|
|
1403
|
+
/**
|
|
1404
|
+
* `idle` when this page load is not a checkout return (no transaction id in
|
|
1405
|
+
* the URL) or auth is still hydrating; `verifying` while the API call is in
|
|
1406
|
+
* flight; then `verified` or `failed`.
|
|
1407
|
+
*/
|
|
1408
|
+
status: VerifyPaymentReturnStatus
|
|
1409
|
+
|
|
1410
|
+
/** `true` when the URL identifies a purchase to verify. */
|
|
1411
|
+
isReturn: boolean
|
|
1412
|
+
|
|
1413
|
+
/** The provider named by the URL (or the `provider` option). */
|
|
1414
|
+
provider: string | null
|
|
1415
|
+
|
|
1416
|
+
/** The provider transaction/session id read from the URL. */
|
|
1417
|
+
transactionId: string | null
|
|
1418
|
+
|
|
1419
|
+
/** Why verification failed, when it did. */
|
|
1420
|
+
error: Error | null
|
|
1421
|
+
|
|
1422
|
+
/** Re-run the verification — wire this to a retry button. */
|
|
1423
|
+
retry: () => void
|
|
1424
|
+
}
|
|
1425
|
+
```
|
|
1426
|
+
|
|
1367
1427
|
#### `UseVersionResult`
|
|
1368
1428
|
|
|
1369
1429
|
Hook return type.
|
|
@@ -1439,6 +1499,14 @@ type UseCapacitorAppResult = CapacitorAppState & {
|
|
|
1439
1499
|
}
|
|
1440
1500
|
```
|
|
1441
1501
|
|
|
1502
|
+
#### `VerifyPaymentReturnStatus`
|
|
1503
|
+
|
|
1504
|
+
How far the return-page verification has got.
|
|
1505
|
+
|
|
1506
|
+
```typescript
|
|
1507
|
+
type VerifyPaymentReturnStatus = 'idle' | 'verifying' | 'verified' | 'failed'
|
|
1508
|
+
```
|
|
1509
|
+
|
|
1442
1510
|
### Functions
|
|
1443
1511
|
|
|
1444
1512
|
#### `AuthProvider(props)`
|
|
@@ -2382,6 +2450,35 @@ function useUser(): T | null
|
|
|
2382
2450
|
|
|
2383
2451
|
**Returns:** The authenticated user or null
|
|
2384
2452
|
|
|
2453
|
+
#### `useVerifyPaymentReturn(options)`
|
|
2454
|
+
|
|
2455
|
+
Reads the payment id a provider put in the return URL and confirms the
|
|
2456
|
+
purchase server-side with `POST /users/:id/verify-payment/:provider`.
|
|
2457
|
+
|
|
2458
|
+
**This is why a hosted checkout returns the buyer to the APP and not the
|
|
2459
|
+
API.** Session cookies are host-only on the app's origin, so a top-level
|
|
2460
|
+
redirect from the provider straight to an authenticated API callback on a
|
|
2461
|
+
different host arrives with NO credentials — it answers 401 and the paid plan
|
|
2462
|
+
is never granted. The request this hook makes is a same-origin call from a
|
|
2463
|
+
loaded app page, so the credentials apply. It waits for auth to hydrate
|
|
2464
|
+
first: after the redirect the page is a cold load, and the session is
|
|
2465
|
+
restored from the httpOnly cookie via `GET /users/me`.
|
|
2466
|
+
|
|
2467
|
+
Verification is idempotent server-side (first-claim-wins on the transaction),
|
|
2468
|
+
and this hook additionally runs at most once per transaction id per page.
|
|
2469
|
+
Safe on pages that are also reached normally: with no id in the URL it stays
|
|
2470
|
+
`idle` and issues no request.
|
|
2471
|
+
|
|
2472
|
+
```typescript
|
|
2473
|
+
function useVerifyPaymentReturn(
|
|
2474
|
+
options?: UseVerifyPaymentReturnOptions,
|
|
2475
|
+
): UseVerifyPaymentReturnResult
|
|
2476
|
+
```
|
|
2477
|
+
|
|
2478
|
+
- `options` — Provider fallback + lifecycle callbacks (see {@link UseVerifyPaymentReturnOptions}).
|
|
2479
|
+
|
|
2480
|
+
**Returns:** The verification state (see {@link UseVerifyPaymentReturnResult}).
|
|
2481
|
+
|
|
2385
2482
|
#### `useVersion()`
|
|
2386
2483
|
|
|
2387
2484
|
Hook for version state and update actions.
|
|
@@ -2635,6 +2732,14 @@ Peer dependencies:
|
|
|
2635
2732
|
mounted). Scaffolded Vite configs ship
|
|
2636
2733
|
`resolve.dedupe: ['react', 'react-dom', 'react-router', 'react-router']` — keep it, and
|
|
2637
2734
|
add any new hook-bearing peer library there too.
|
|
2735
|
+
- **A payment provider's post-checkout redirect must land on the APP, and the page it
|
|
2736
|
+
lands on has to finish the purchase.** `useVerifyPaymentReturn()` reads the id the
|
|
2737
|
+
provider left in the query and confirms it with
|
|
2738
|
+
`POST /users/:id/verify-payment/:provider` — a same-origin call, so the session cookie
|
|
2739
|
+
applies. Redirecting straight to that API route from the provider's domain sends a
|
|
2740
|
+
top-level navigation with NO credentials: it answers 401 and the paid plan is never
|
|
2741
|
+
granted. The shipped confirmation pages (`@molecule/app-plan-updated-page-react`,
|
|
2742
|
+
`@molecule/app-legal-pages-react`) already call it.
|
|
2638
2743
|
- `RouterProvider` carries a molecule `Router` (e.g. `createReactRouter()` from
|
|
2639
2744
|
`@molecule/app-routing-react-router`). react-router's own `<BrowserRouter>` context is
|
|
2640
2745
|
separate — components that render react-router `<Link>` (several in
|
package/dist/hooks/index.d.ts
CHANGED
|
@@ -27,6 +27,7 @@ export * from './useStorage.js';
|
|
|
27
27
|
export * from './useStore.js';
|
|
28
28
|
export * from './useTheme.js';
|
|
29
29
|
export * from './useTranslation.js';
|
|
30
|
+
export * from './useVerifyPaymentReturn.js';
|
|
30
31
|
export * from './useVersion.js';
|
|
31
32
|
export * from './useWorkspace.js';
|
|
32
33
|
//# sourceMappingURL=index.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/hooks/index.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAEH,cAAc,kBAAkB,CAAA;AAChC,cAAc,oBAAoB,CAAA;AAClC,cAAc,cAAc,CAAA;AAC5B,cAAc,sBAAsB,CAAA;AACpC,cAAc,wBAAwB,CAAA;AACtC,cAAc,cAAc,CAAA;AAC5B,cAAc,gBAAgB,CAAA;AAC9B,cAAc,gBAAgB,CAAA;AAC9B,cAAc,cAAc,CAAA;AAC5B,cAAc,cAAc,CAAA;AAC5B,cAAc,gBAAgB,CAAA;AAC9B,cAAc,eAAe,CAAA;AAC7B,cAAc,eAAe,CAAA;AAC7B,cAAc,uBAAuB,CAAA;AACrC,cAAc,kBAAkB,CAAA;AAChC,cAAc,iBAAiB,CAAA;AAC/B,cAAc,iBAAiB,CAAA;AAC/B,cAAc,cAAc,CAAA;AAC5B,cAAc,gBAAgB,CAAA;AAC9B,cAAc,gBAAgB,CAAA;AAC9B,cAAc,iBAAiB,CAAA;AAC/B,cAAc,eAAe,CAAA;AAC7B,cAAc,eAAe,CAAA;AAC7B,cAAc,qBAAqB,CAAA;AACnC,cAAc,iBAAiB,CAAA;AAC/B,cAAc,mBAAmB,CAAA"}
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/hooks/index.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAEH,cAAc,kBAAkB,CAAA;AAChC,cAAc,oBAAoB,CAAA;AAClC,cAAc,cAAc,CAAA;AAC5B,cAAc,sBAAsB,CAAA;AACpC,cAAc,wBAAwB,CAAA;AACtC,cAAc,cAAc,CAAA;AAC5B,cAAc,gBAAgB,CAAA;AAC9B,cAAc,gBAAgB,CAAA;AAC9B,cAAc,cAAc,CAAA;AAC5B,cAAc,cAAc,CAAA;AAC5B,cAAc,gBAAgB,CAAA;AAC9B,cAAc,eAAe,CAAA;AAC7B,cAAc,eAAe,CAAA;AAC7B,cAAc,uBAAuB,CAAA;AACrC,cAAc,kBAAkB,CAAA;AAChC,cAAc,iBAAiB,CAAA;AAC/B,cAAc,iBAAiB,CAAA;AAC/B,cAAc,cAAc,CAAA;AAC5B,cAAc,gBAAgB,CAAA;AAC9B,cAAc,gBAAgB,CAAA;AAC9B,cAAc,iBAAiB,CAAA;AAC/B,cAAc,eAAe,CAAA;AAC7B,cAAc,eAAe,CAAA;AAC7B,cAAc,qBAAqB,CAAA;AACnC,cAAc,6BAA6B,CAAA;AAC3C,cAAc,iBAAiB,CAAA;AAC/B,cAAc,mBAAmB,CAAA"}
|
package/dist/hooks/index.js
CHANGED
|
@@ -27,6 +27,7 @@ export * from './useStorage.js';
|
|
|
27
27
|
export * from './useStore.js';
|
|
28
28
|
export * from './useTheme.js';
|
|
29
29
|
export * from './useTranslation.js';
|
|
30
|
+
export * from './useVerifyPaymentReturn.js';
|
|
30
31
|
export * from './useVersion.js';
|
|
31
32
|
export * from './useWorkspace.js';
|
|
32
33
|
//# sourceMappingURL=index.js.map
|
package/dist/hooks/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/hooks/index.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAEH,cAAc,kBAAkB,CAAA;AAChC,cAAc,oBAAoB,CAAA;AAClC,cAAc,cAAc,CAAA;AAC5B,cAAc,sBAAsB,CAAA;AACpC,cAAc,wBAAwB,CAAA;AACtC,cAAc,cAAc,CAAA;AAC5B,cAAc,gBAAgB,CAAA;AAC9B,cAAc,gBAAgB,CAAA;AAC9B,cAAc,cAAc,CAAA;AAC5B,cAAc,cAAc,CAAA;AAC5B,cAAc,gBAAgB,CAAA;AAC9B,cAAc,eAAe,CAAA;AAC7B,cAAc,eAAe,CAAA;AAC7B,cAAc,uBAAuB,CAAA;AACrC,cAAc,kBAAkB,CAAA;AAChC,cAAc,iBAAiB,CAAA;AAC/B,cAAc,iBAAiB,CAAA;AAC/B,cAAc,cAAc,CAAA;AAC5B,cAAc,gBAAgB,CAAA;AAC9B,cAAc,gBAAgB,CAAA;AAC9B,cAAc,iBAAiB,CAAA;AAC/B,cAAc,eAAe,CAAA;AAC7B,cAAc,eAAe,CAAA;AAC7B,cAAc,qBAAqB,CAAA;AACnC,cAAc,iBAAiB,CAAA;AAC/B,cAAc,mBAAmB,CAAA"}
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/hooks/index.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAEH,cAAc,kBAAkB,CAAA;AAChC,cAAc,oBAAoB,CAAA;AAClC,cAAc,cAAc,CAAA;AAC5B,cAAc,sBAAsB,CAAA;AACpC,cAAc,wBAAwB,CAAA;AACtC,cAAc,cAAc,CAAA;AAC5B,cAAc,gBAAgB,CAAA;AAC9B,cAAc,gBAAgB,CAAA;AAC9B,cAAc,cAAc,CAAA;AAC5B,cAAc,cAAc,CAAA;AAC5B,cAAc,gBAAgB,CAAA;AAC9B,cAAc,eAAe,CAAA;AAC7B,cAAc,eAAe,CAAA;AAC7B,cAAc,uBAAuB,CAAA;AACrC,cAAc,kBAAkB,CAAA;AAChC,cAAc,iBAAiB,CAAA;AAC/B,cAAc,iBAAiB,CAAA;AAC/B,cAAc,cAAc,CAAA;AAC5B,cAAc,gBAAgB,CAAA;AAC9B,cAAc,gBAAgB,CAAA;AAC9B,cAAc,iBAAiB,CAAA;AAC/B,cAAc,eAAe,CAAA;AAC7B,cAAc,eAAe,CAAA;AAC7B,cAAc,qBAAqB,CAAA;AACnC,cAAc,6BAA6B,CAAA;AAC3C,cAAc,iBAAiB,CAAA;AAC/B,cAAc,mBAAmB,CAAA"}
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* React hook for the post-checkout return: verify the purchase with the API
|
|
3
|
+
* from the APP origin, where the session cookie applies.
|
|
4
|
+
*
|
|
5
|
+
* @module
|
|
6
|
+
*/
|
|
7
|
+
/** How far the return-page verification has got. */
|
|
8
|
+
export type VerifyPaymentReturnStatus = 'idle' | 'verifying' | 'verified' | 'failed';
|
|
9
|
+
/** Options for {@link useVerifyPaymentReturn}. */
|
|
10
|
+
export interface UseVerifyPaymentReturnOptions {
|
|
11
|
+
/**
|
|
12
|
+
* Provider name to verify with, when the return URL carries no `provider`
|
|
13
|
+
* query parameter. Leave unset to verify only what the URL names.
|
|
14
|
+
*/
|
|
15
|
+
provider?: string;
|
|
16
|
+
/** Set `false` to skip verification entirely (e.g. behind a feature flag). */
|
|
17
|
+
enabled?: boolean;
|
|
18
|
+
/**
|
|
19
|
+
* Remove the provider's query parameters from the address bar once the
|
|
20
|
+
* purchase is verified, so a reload (or a shared link) cannot replay the
|
|
21
|
+
* verification. Defaults to `true`.
|
|
22
|
+
*/
|
|
23
|
+
cleanUrl?: boolean;
|
|
24
|
+
/** Called once, after the purchase verifies. */
|
|
25
|
+
onVerified?: () => void;
|
|
26
|
+
/** Called when verification fails. */
|
|
27
|
+
onError?: (error: Error) => void;
|
|
28
|
+
}
|
|
29
|
+
/** State of the post-checkout verification. */
|
|
30
|
+
export interface UseVerifyPaymentReturnResult {
|
|
31
|
+
/**
|
|
32
|
+
* `idle` when this page load is not a checkout return (no transaction id in
|
|
33
|
+
* the URL) or auth is still hydrating; `verifying` while the API call is in
|
|
34
|
+
* flight; then `verified` or `failed`.
|
|
35
|
+
*/
|
|
36
|
+
status: VerifyPaymentReturnStatus;
|
|
37
|
+
/** `true` when the URL identifies a purchase to verify. */
|
|
38
|
+
isReturn: boolean;
|
|
39
|
+
/** The provider named by the URL (or the `provider` option). */
|
|
40
|
+
provider: string | null;
|
|
41
|
+
/** The provider transaction/session id read from the URL. */
|
|
42
|
+
transactionId: string | null;
|
|
43
|
+
/** Why verification failed, when it did. */
|
|
44
|
+
error: Error | null;
|
|
45
|
+
/** Re-run the verification — wire this to a retry button. */
|
|
46
|
+
retry: () => void;
|
|
47
|
+
}
|
|
48
|
+
/**
|
|
49
|
+
* Reads the payment id a provider put in the return URL and confirms the
|
|
50
|
+
* purchase server-side with `POST /users/:id/verify-payment/:provider`.
|
|
51
|
+
*
|
|
52
|
+
* **This is why a hosted checkout returns the buyer to the APP and not the
|
|
53
|
+
* API.** Session cookies are host-only on the app's origin, so a top-level
|
|
54
|
+
* redirect from the provider straight to an authenticated API callback on a
|
|
55
|
+
* different host arrives with NO credentials — it answers 401 and the paid plan
|
|
56
|
+
* is never granted. The request this hook makes is a same-origin call from a
|
|
57
|
+
* loaded app page, so the credentials apply. It waits for auth to hydrate
|
|
58
|
+
* first: after the redirect the page is a cold load, and the session is
|
|
59
|
+
* restored from the httpOnly cookie via `GET /users/me`.
|
|
60
|
+
*
|
|
61
|
+
* Verification is idempotent server-side (first-claim-wins on the transaction),
|
|
62
|
+
* and this hook additionally runs at most once per transaction id per page.
|
|
63
|
+
* Safe on pages that are also reached normally: with no id in the URL it stays
|
|
64
|
+
* `idle` and issues no request.
|
|
65
|
+
*
|
|
66
|
+
* @param options - Provider fallback + lifecycle callbacks (see
|
|
67
|
+
* {@link UseVerifyPaymentReturnOptions}).
|
|
68
|
+
* @returns The verification state (see {@link UseVerifyPaymentReturnResult}).
|
|
69
|
+
*
|
|
70
|
+
* @example
|
|
71
|
+
* ```tsx
|
|
72
|
+
* // /plan-updated — the page a provider returns the buyer to.
|
|
73
|
+
* const { status, retry } = useVerifyPaymentReturn()
|
|
74
|
+
*
|
|
75
|
+
* if (status === 'verifying') return <Spinner />
|
|
76
|
+
* if (status === 'failed') return <button onClick={retry}>Try again</button>
|
|
77
|
+
* return <h1>Thank you!</h1>
|
|
78
|
+
* ```
|
|
79
|
+
*/
|
|
80
|
+
export declare function useVerifyPaymentReturn(options?: UseVerifyPaymentReturnOptions): UseVerifyPaymentReturnResult;
|
|
81
|
+
//# sourceMappingURL=useVerifyPaymentReturn.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"useVerifyPaymentReturn.d.ts","sourceRoot":"","sources":["../../src/hooks/useVerifyPaymentReturn.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAqBH,oDAAoD;AACpD,MAAM,MAAM,yBAAyB,GAAG,MAAM,GAAG,WAAW,GAAG,UAAU,GAAG,QAAQ,CAAA;AAEpF,kDAAkD;AAClD,MAAM,WAAW,6BAA6B;IAC5C;;;OAGG;IACH,QAAQ,CAAC,EAAE,MAAM,CAAA;IAEjB,8EAA8E;IAC9E,OAAO,CAAC,EAAE,OAAO,CAAA;IAEjB;;;;OAIG;IACH,QAAQ,CAAC,EAAE,OAAO,CAAA;IAElB,gDAAgD;IAChD,UAAU,CAAC,EAAE,MAAM,IAAI,CAAA;IAEvB,sCAAsC;IACtC,OAAO,CAAC,EAAE,CAAC,KAAK,EAAE,KAAK,KAAK,IAAI,CAAA;CACjC;AAED,+CAA+C;AAC/C,MAAM,WAAW,4BAA4B;IAC3C;;;;OAIG;IACH,MAAM,EAAE,yBAAyB,CAAA;IAEjC,2DAA2D;IAC3D,QAAQ,EAAE,OAAO,CAAA;IAEjB,gEAAgE;IAChE,QAAQ,EAAE,MAAM,GAAG,IAAI,CAAA;IAEvB,6DAA6D;IAC7D,aAAa,EAAE,MAAM,GAAG,IAAI,CAAA;IAE5B,4CAA4C;IAC5C,KAAK,EAAE,KAAK,GAAG,IAAI,CAAA;IAEnB,6DAA6D;IAC7D,KAAK,EAAE,MAAM,IAAI,CAAA;CAClB;AAED;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA+BG;AACH,wBAAgB,sBAAsB,CACpC,OAAO,GAAE,6BAAkC,GAC1C,4BAA4B,CA0E9B"}
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* React hook for the post-checkout return: verify the purchase with the API
|
|
3
|
+
* from the APP origin, where the session cookie applies.
|
|
4
|
+
*
|
|
5
|
+
* @module
|
|
6
|
+
*/
|
|
7
|
+
import { useCallback, useEffect, useRef, useState } from 'react';
|
|
8
|
+
import { useAuth } from './useAuth.js';
|
|
9
|
+
import { useHttpClient } from './useHttp.js';
|
|
10
|
+
/** Query parameters a payment provider may use for its transaction id. */
|
|
11
|
+
const TRANSACTION_ID_PARAMS = [
|
|
12
|
+
// What `resolveCheckoutRedirectUrls` (@molecule/api-payments) asks providers
|
|
13
|
+
// to substitute — Stripe's `{CHECKOUT_SESSION_ID}`.
|
|
14
|
+
'sessionId',
|
|
15
|
+
'session_id',
|
|
16
|
+
'subscriptionId',
|
|
17
|
+
// PayPal appends its OWN names to the return URL after buyer approval.
|
|
18
|
+
'subscription_id',
|
|
19
|
+
'token',
|
|
20
|
+
];
|
|
21
|
+
/**
|
|
22
|
+
* Reads the payment id a provider put in the return URL and confirms the
|
|
23
|
+
* purchase server-side with `POST /users/:id/verify-payment/:provider`.
|
|
24
|
+
*
|
|
25
|
+
* **This is why a hosted checkout returns the buyer to the APP and not the
|
|
26
|
+
* API.** Session cookies are host-only on the app's origin, so a top-level
|
|
27
|
+
* redirect from the provider straight to an authenticated API callback on a
|
|
28
|
+
* different host arrives with NO credentials — it answers 401 and the paid plan
|
|
29
|
+
* is never granted. The request this hook makes is a same-origin call from a
|
|
30
|
+
* loaded app page, so the credentials apply. It waits for auth to hydrate
|
|
31
|
+
* first: after the redirect the page is a cold load, and the session is
|
|
32
|
+
* restored from the httpOnly cookie via `GET /users/me`.
|
|
33
|
+
*
|
|
34
|
+
* Verification is idempotent server-side (first-claim-wins on the transaction),
|
|
35
|
+
* and this hook additionally runs at most once per transaction id per page.
|
|
36
|
+
* Safe on pages that are also reached normally: with no id in the URL it stays
|
|
37
|
+
* `idle` and issues no request.
|
|
38
|
+
*
|
|
39
|
+
* @param options - Provider fallback + lifecycle callbacks (see
|
|
40
|
+
* {@link UseVerifyPaymentReturnOptions}).
|
|
41
|
+
* @returns The verification state (see {@link UseVerifyPaymentReturnResult}).
|
|
42
|
+
*
|
|
43
|
+
* @example
|
|
44
|
+
* ```tsx
|
|
45
|
+
* // /plan-updated — the page a provider returns the buyer to.
|
|
46
|
+
* const { status, retry } = useVerifyPaymentReturn()
|
|
47
|
+
*
|
|
48
|
+
* if (status === 'verifying') return <Spinner />
|
|
49
|
+
* if (status === 'failed') return <button onClick={retry}>Try again</button>
|
|
50
|
+
* return <h1>Thank you!</h1>
|
|
51
|
+
* ```
|
|
52
|
+
*/
|
|
53
|
+
export function useVerifyPaymentReturn(options = {}) {
|
|
54
|
+
const { provider: providerOption, enabled = true, cleanUrl = true, onVerified, onError } = options;
|
|
55
|
+
const http = useHttpClient();
|
|
56
|
+
const { state, refresh } = useAuth();
|
|
57
|
+
const search = typeof window === 'undefined' ? '' : window.location.search;
|
|
58
|
+
const params = new URLSearchParams(search);
|
|
59
|
+
const transactionId = TRANSACTION_ID_PARAMS.map((name) => params.get(name)).find((value) => !!value) ?? null;
|
|
60
|
+
const provider = params.get('provider') || providerOption || null;
|
|
61
|
+
const isReturn = Boolean(enabled && transactionId && provider);
|
|
62
|
+
const [status, setStatus] = useState('idle');
|
|
63
|
+
const [error, setError] = useState(null);
|
|
64
|
+
// One attempt per transaction id per page — an effect that re-fires (auth
|
|
65
|
+
// refresh, strict-mode double-invoke) must not re-POST a purchase.
|
|
66
|
+
const attemptedRef = useRef(null);
|
|
67
|
+
// Callbacks are read through a ref so a caller passing inline functions does
|
|
68
|
+
// not re-trigger the effect on every render.
|
|
69
|
+
const callbacksRef = useRef({ onVerified, onError });
|
|
70
|
+
callbacksRef.current = { onVerified, onError };
|
|
71
|
+
const userId = state.user?.id ?? null;
|
|
72
|
+
const verify = useCallback(async () => {
|
|
73
|
+
if (!transactionId || !provider || !userId)
|
|
74
|
+
return;
|
|
75
|
+
setStatus('verifying');
|
|
76
|
+
setError(null);
|
|
77
|
+
try {
|
|
78
|
+
await http.post(`/users/${userId}/verify-payment/${provider}`, {
|
|
79
|
+
subscriptionId: transactionId,
|
|
80
|
+
});
|
|
81
|
+
// The plan on the cached profile is now stale — re-read it so the UI
|
|
82
|
+
// (plan badges, gated features) reflects what was just paid for.
|
|
83
|
+
await refresh().catch(() => { });
|
|
84
|
+
setStatus('verified');
|
|
85
|
+
callbacksRef.current.onVerified?.();
|
|
86
|
+
if (cleanUrl && typeof window !== 'undefined' && window.history?.replaceState) {
|
|
87
|
+
const url = new URL(window.location.href);
|
|
88
|
+
for (const name of TRANSACTION_ID_PARAMS)
|
|
89
|
+
url.searchParams.delete(name);
|
|
90
|
+
url.searchParams.delete('provider');
|
|
91
|
+
window.history.replaceState({}, '', `${url.pathname}${url.search}${url.hash}`);
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
catch (caught) {
|
|
95
|
+
const failure = caught instanceof Error ? caught : new Error(String(caught));
|
|
96
|
+
setStatus('failed');
|
|
97
|
+
setError(failure);
|
|
98
|
+
callbacksRef.current.onError?.(failure);
|
|
99
|
+
}
|
|
100
|
+
}, [cleanUrl, http, provider, refresh, transactionId, userId]);
|
|
101
|
+
useEffect(() => {
|
|
102
|
+
if (!isReturn || !transactionId)
|
|
103
|
+
return;
|
|
104
|
+
// Auth is still hydrating from the httpOnly cookie — verifying now would
|
|
105
|
+
// send the request unauthenticated.
|
|
106
|
+
if (!state.initialized || !userId)
|
|
107
|
+
return;
|
|
108
|
+
if (attemptedRef.current === transactionId)
|
|
109
|
+
return;
|
|
110
|
+
attemptedRef.current = transactionId;
|
|
111
|
+
void verify();
|
|
112
|
+
}, [isReturn, state.initialized, transactionId, userId, verify]);
|
|
113
|
+
const retry = useCallback(() => {
|
|
114
|
+
attemptedRef.current = transactionId;
|
|
115
|
+
void verify();
|
|
116
|
+
}, [transactionId, verify]);
|
|
117
|
+
return { status, isReturn, provider, transactionId, error, retry };
|
|
118
|
+
}
|
|
119
|
+
//# sourceMappingURL=useVerifyPaymentReturn.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"useVerifyPaymentReturn.js","sourceRoot":"","sources":["../../src/hooks/useVerifyPaymentReturn.ts"],"names":[],"mappings":"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"}
|
package/dist/index.d.ts
CHANGED
|
@@ -65,6 +65,14 @@
|
|
|
65
65
|
* mounted). Scaffolded Vite configs ship
|
|
66
66
|
* `resolve.dedupe: ['react', 'react-dom', 'react-router', 'react-router']` — keep it, and
|
|
67
67
|
* add any new hook-bearing peer library there too.
|
|
68
|
+
* - **A payment provider's post-checkout redirect must land on the APP, and the page it
|
|
69
|
+
* lands on has to finish the purchase.** `useVerifyPaymentReturn()` reads the id the
|
|
70
|
+
* provider left in the query and confirms it with
|
|
71
|
+
* `POST /users/:id/verify-payment/:provider` — a same-origin call, so the session cookie
|
|
72
|
+
* applies. Redirecting straight to that API route from the provider's domain sends a
|
|
73
|
+
* top-level navigation with NO credentials: it answers 401 and the paid plan is never
|
|
74
|
+
* granted. The shipped confirmation pages (`@molecule/app-plan-updated-page-react`,
|
|
75
|
+
* `@molecule/app-legal-pages-react`) already call it.
|
|
68
76
|
* - `RouterProvider` carries a molecule `Router` (e.g. `createReactRouter()` from
|
|
69
77
|
* `@molecule/app-routing-react-router`). react-router's own `<BrowserRouter>` context is
|
|
70
78
|
* separate — components that render react-router `<Link>` (several in
|
package/dist/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAiFG;AAEH,cAAc,qBAAqB,CAAA;AACnC,cAAc,eAAe,CAAA;AAC7B,cAAc,kBAAkB,CAAA;AAChC,cAAc,gBAAgB,CAAA;AAC9B,cAAc,YAAY,CAAA"}
|
package/dist/index.js
CHANGED
|
@@ -65,6 +65,14 @@
|
|
|
65
65
|
* mounted). Scaffolded Vite configs ship
|
|
66
66
|
* `resolve.dedupe: ['react', 'react-dom', 'react-router', 'react-router']` — keep it, and
|
|
67
67
|
* add any new hook-bearing peer library there too.
|
|
68
|
+
* - **A payment provider's post-checkout redirect must land on the APP, and the page it
|
|
69
|
+
* lands on has to finish the purchase.** `useVerifyPaymentReturn()` reads the id the
|
|
70
|
+
* provider left in the query and confirms it with
|
|
71
|
+
* `POST /users/:id/verify-payment/:provider` — a same-origin call, so the session cookie
|
|
72
|
+
* applies. Redirecting straight to that API route from the provider's domain sends a
|
|
73
|
+
* top-level navigation with NO credentials: it answers 401 and the paid plan is never
|
|
74
|
+
* granted. The shipped confirmation pages (`@molecule/app-plan-updated-page-react`,
|
|
75
|
+
* `@molecule/app-legal-pages-react`) already call it.
|
|
68
76
|
* - `RouterProvider` carries a molecule `Router` (e.g. `createReactRouter()` from
|
|
69
77
|
* `@molecule/app-routing-react-router`). react-router's own `<BrowserRouter>` context is
|
|
70
78
|
* separate — components that render react-router `<Link>` (several in
|
package/dist/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAiFG;AAEH,cAAc,qBAAqB,CAAA;AACnC,cAAc,eAAe,CAAA;AAC7B,cAAc,kBAAkB,CAAA;AAChC,cAAc,gBAAgB,CAAA;AAC9B,cAAc,YAAY,CAAA"}
|