@12-apps/payments-frontend 3.34.2 → 3.34.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@12-apps/payments-frontend",
|
|
3
|
-
"version": "3.34.
|
|
3
|
+
"version": "3.34.3",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"sideEffects": false,
|
|
6
6
|
"description": "Browser half of the vendor-agnostic payments platform: plug-and-play MUI components for the per-provider settings page (credential form from each provider's schema, masked hints, verify/enable) and the checkout page (PIX QR + polling, card tokenization, hosted-checkout redirect), plus the headless hooks and fetch clients they build on. Talks only to the host's payments HTTP surface — never to a provider directly. Microfrontend-ready: no app coupling, host injects theme and auth.",
|
|
@@ -5,9 +5,10 @@
|
|
|
5
5
|
* Nothing here knows what the channel is. A host that can hear an order move —
|
|
6
6
|
* a signed-in buyer's own topic, say — passes whether that channel is live and
|
|
7
7
|
* a way to be told when it fires; every wait under the provider then asks the
|
|
8
|
-
* moment it fires and
|
|
9
|
-
* no provider, or a host that passes
|
|
10
|
-
* of their own), polls exactly as
|
|
8
|
+
* moment it fires, and keeps no timer of its own while the channel is live
|
|
9
|
+
* (FUT-3223, `poll-live.ts`). A tree with no provider, or a host that passes
|
|
10
|
+
* `live: false` (a guest, who has no channel of their own), polls exactly as
|
|
11
|
+
* before.
|
|
11
12
|
*
|
|
12
13
|
* The hint carries nothing and is never trusted as an answer: it only makes the
|
|
13
14
|
* wait ask `/status` now. The endpoint stays the truth.
|
|
@@ -26,14 +27,19 @@ export interface CheckoutLiveSignal {
|
|
|
26
27
|
* unsubscribe. Should be stable across renders: a new function re-subscribes.
|
|
27
28
|
*/
|
|
28
29
|
subscribe: (onHint: () => void) => () => void;
|
|
29
|
-
/**
|
|
30
|
+
/**
|
|
31
|
+
* The healthy delay while live (ms) for the one wait that keeps reading while
|
|
32
|
+
* live: a hosted-checkout return carrying the settlement pair (`poll-live.ts`).
|
|
33
|
+
* Every other wait keeps no timer while live. Defaults to
|
|
34
|
+
* {@link DEFAULT_LIVE_INTERVAL_MS}.
|
|
35
|
+
*/
|
|
30
36
|
liveIntervalMs?: number;
|
|
31
37
|
}
|
|
32
38
|
|
|
33
39
|
/**
|
|
34
|
-
* The floor
|
|
35
|
-
* wait already backs off to, so a wait whose channel is up never
|
|
36
|
-
* than the slowest wait it would have become anyway.
|
|
40
|
+
* The floor a hosted-checkout return keeps while the channel is live: the same
|
|
41
|
+
* 15 s the PIX wait already backs off to, so a wait whose channel is up never
|
|
42
|
+
* asks faster than the slowest wait it would have become anyway.
|
|
37
43
|
*/
|
|
38
44
|
export const DEFAULT_LIVE_INTERVAL_MS = 15_000;
|
|
39
45
|
|
|
@@ -61,9 +67,16 @@ type LoopRef = { readonly current: PollLoop | null };
|
|
|
61
67
|
/**
|
|
62
68
|
* A channel that DROPS: ask now — and if the loop declines because it asked a
|
|
63
69
|
* moment ago, ask again once that quiet window is over. The ask it lands just
|
|
64
|
-
* after may have been answered before the drop, and
|
|
65
|
-
*
|
|
66
|
-
*
|
|
70
|
+
* after may have been answered before the drop, and a live wait books no next
|
|
71
|
+
* tick, so declining for good would leave the buyer waiting with nobody left
|
|
72
|
+
* to wake them.
|
|
73
|
+
*
|
|
74
|
+
* A channel that (RE-)OPENS does not come here either: it is taken as a HINT
|
|
75
|
+
* (FUT-3223). A hint sent while the channel was down was never heard, and a
|
|
76
|
+
* live wait has no timer to catch it, so the open is the re-read that bounds a
|
|
77
|
+
* lost hint — and `PollLoop.hint` never drops it: with an ask in flight it
|
|
78
|
+
* owes one more the moment that ask answers, where a declined poke could leave
|
|
79
|
+
* a pre-open answer as the last word.
|
|
67
80
|
*
|
|
68
81
|
* A HINT does not come here. It is `PollLoop.hint` (FUT-3205): deferring a
|
|
69
82
|
* hint by the quiet window cost a paid buyer a full second on the pay step.
|
|
@@ -75,9 +88,10 @@ function wake(loop: LoopRef, pending: { timer?: ReturnType<typeof setTimeout> })
|
|
|
75
88
|
}
|
|
76
89
|
|
|
77
90
|
/**
|
|
78
|
-
* Tie one wait to the host's channel: a hint asks now,
|
|
79
|
-
*
|
|
80
|
-
*
|
|
91
|
+
* Tie one wait to the host's channel: a hint asks now, a channel that DROPS
|
|
92
|
+
* asks now too (see {@link wake}), and a channel that (RE-)OPENS is taken as a
|
|
93
|
+
* hint. The channel's state at mount needs no ask of its own: the wait's first
|
|
94
|
+
* ask is already that read.
|
|
81
95
|
*
|
|
82
96
|
* `loop` is the hook's own ref to its running loop, read when the event lands.
|
|
83
97
|
*/
|
|
@@ -92,6 +106,7 @@ export function useLiveWait(loop: LoopRef): LiveWait {
|
|
|
92
106
|
const was = liveRef.current;
|
|
93
107
|
liveRef.current = live;
|
|
94
108
|
if (was && !live) wake(loop, pending.current);
|
|
109
|
+
else if (!was && live) loop.current?.hint();
|
|
95
110
|
}, [live, loop]);
|
|
96
111
|
|
|
97
112
|
useEffect(() => {
|
|
@@ -1,14 +1,29 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* The cadence a payment wait keeps while the host HEARS the order move
|
|
3
|
-
* (FUT-649).
|
|
3
|
+
* (FUT-649, FUT-3223).
|
|
4
4
|
*
|
|
5
5
|
* The wait asks `/status` every 2.5 s because, on its own, asking is the only
|
|
6
6
|
* way it can learn that a webhook settled the order in another process. A host
|
|
7
7
|
* that holds a realtime channel for this buyer already learns it the moment it
|
|
8
8
|
* happens — `CheckoutLiveProvider` hands that hint in and the wait asks at once.
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
9
|
+
* So while that channel is live a healthy answer books NO timer: the wait asks
|
|
10
|
+
* on a hint, on `visibilitychange`/`online`, and when the channel opens or
|
|
11
|
+
* re-opens — a screen hears live data over the socket and polls only while the
|
|
12
|
+
* socket is down. The host's server owns what a timer used to catch: a lapsed
|
|
13
|
+
* code and a webhook that never came are found server-side and published to
|
|
14
|
+
* the same channel. The moment the channel drops, the ordinary cadence is back.
|
|
15
|
+
*
|
|
16
|
+
* Two exceptions keep a timer while live:
|
|
17
|
+
*
|
|
18
|
+
* - A FAILED ask is a read that did not happen, not a check for news, so it
|
|
19
|
+
* retries on the ordinary error backoff (`poll-delay.ts`: 2.5 s, 5 s, then
|
|
20
|
+
* every 10 s), as a wait with no channel does — no longer stretched to the
|
|
21
|
+
* 15 s live floor. Otherwise a hint whose ask hit a 500 would leave a paid
|
|
22
|
+
* buyer waiting for a second hint that is never sent.
|
|
23
|
+
* - A wait given {@link LiveCadence.liveIntervalMs} keeps that floor. The hook
|
|
24
|
+
* gives it one only on a hosted-checkout return carrying the settlement pair
|
|
25
|
+
* (`carriesSettlementPair`), where the buyer's own read is what confirms the
|
|
26
|
+
* payment and no server sweep can stand in for it.
|
|
12
27
|
*
|
|
13
28
|
* Split out of `poll-loop.ts` so the loop stays one question: WHEN to ask. This
|
|
14
29
|
* file answers only how being live changes that answer.
|
|
@@ -21,12 +36,21 @@ export interface LiveCadence {
|
|
|
21
36
|
*/
|
|
22
37
|
isLive?: () => boolean;
|
|
23
38
|
/**
|
|
24
|
-
* The healthy delay while live (ms)
|
|
25
|
-
*
|
|
39
|
+
* The healthy delay while live (ms), for a wait that must keep reading while
|
|
40
|
+
* live (see the module docblock). A floor, never a speed-up: a slower cadence
|
|
41
|
+
* already in force (the PIX backoff) is kept. Undefined: no timer while live.
|
|
26
42
|
*/
|
|
27
43
|
liveIntervalMs?: number;
|
|
28
44
|
}
|
|
29
45
|
|
|
46
|
+
/**
|
|
47
|
+
* Whether the wait books nothing after this answer and waits to be told: a
|
|
48
|
+
* healthy answer, while live, for a wait with no live floor.
|
|
49
|
+
*/
|
|
50
|
+
export function idlesWhileLive(errors: number, options: LiveCadence): boolean {
|
|
51
|
+
return errors === 0 && options.liveIntervalMs === undefined && options.isLive?.() === true;
|
|
52
|
+
}
|
|
53
|
+
|
|
30
54
|
/** `delay`, stretched to the live floor while the host's channel is live. */
|
|
31
55
|
export function withLive(delay: number, options: LiveCadence): number {
|
|
32
56
|
if (options.liveIntervalMs === undefined || options.isLive?.() !== true) return delay;
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import type { Result } from "../../result";
|
|
2
2
|
import { pollDelay } from "./poll-delay";
|
|
3
|
-
import { waitsForDeadline, type LiveCadence } from "./poll-live";
|
|
3
|
+
import { idlesWhileLive, waitsForDeadline, type LiveCadence } from "./poll-live";
|
|
4
4
|
import { claimHint, owedAfterAnswer } from "./poll-hint";
|
|
5
5
|
import { claimRearm } from "./poll-rearm";
|
|
6
6
|
import { TERMINAL_STATUSES, type OrderStatus } from "./types";
|
|
@@ -254,13 +254,19 @@ function mayWrite(run: PollRun, mine: number, result: Result<OrderStatus>): bool
|
|
|
254
254
|
return run.attempt === mine || (result.ok && TERMINAL_STATUSES.includes(result.data));
|
|
255
255
|
}
|
|
256
256
|
|
|
257
|
-
/**
|
|
257
|
+
/**
|
|
258
|
+
* Book the next tick, or end the wait because its clock has run out — or, live
|
|
259
|
+
* with a healthy answer, book nothing: the hint, the re-arm events and the
|
|
260
|
+
* channel re-opening are what ask next (`poll-live.ts`, FUT-3223).
|
|
261
|
+
*/
|
|
258
262
|
function scheduleNext(
|
|
259
263
|
run: PollRun,
|
|
260
264
|
options: PollingOptions,
|
|
261
265
|
sink: PollSink,
|
|
262
266
|
again: () => void,
|
|
263
267
|
): void {
|
|
268
|
+
clearPending(run);
|
|
269
|
+
if (idlesWhileLive(run.errors, options)) return;
|
|
264
270
|
const delay = pollDelay(run.healthy, run.errors, options);
|
|
265
271
|
if (outOfTime(run, options, delay)) {
|
|
266
272
|
run.stopped = true;
|
|
@@ -268,7 +274,6 @@ function scheduleNext(
|
|
|
268
274
|
sink.setTimedOut(true);
|
|
269
275
|
return;
|
|
270
276
|
}
|
|
271
|
-
clearPending(run);
|
|
272
277
|
run.timer = setTimeout(again, delay);
|
|
273
278
|
}
|
|
274
279
|
|
|
@@ -152,6 +152,16 @@ function refused<T>(json: ApiEnvelope<T> | null, copy: CheckoutTransportCopy): R
|
|
|
152
152
|
return err(json?.error ?? copy.failed, json?.code);
|
|
153
153
|
}
|
|
154
154
|
|
|
155
|
+
/**
|
|
156
|
+
* Whether this page is a hosted-checkout return carrying the WHOLE settlement
|
|
157
|
+
* pair — the one case where a `/status` read is itself what confirms the
|
|
158
|
+
* payment, so a live wait keeps reading on its floor (FUT-3223, `poll-live.ts`).
|
|
159
|
+
*/
|
|
160
|
+
export function carriesSettlementPair(): boolean {
|
|
161
|
+
const { transactionNsu, slug } = returnedSettlement();
|
|
162
|
+
return Boolean(transactionNsu && slug);
|
|
163
|
+
}
|
|
164
|
+
|
|
155
165
|
/**
|
|
156
166
|
* What a hosted checkout appended to the return URL when it sent the buyer
|
|
157
167
|
* back, or undefined.
|
|
@@ -4,6 +4,7 @@ import { useCheckoutClientApi } from "./client-context";
|
|
|
4
4
|
import { useCheckoutCopy } from "./copy-context";
|
|
5
5
|
import { createPollLoop, type PollLoop, type PollingOptions, type PollSink } from "./poll-loop";
|
|
6
6
|
import { useLiveWait } from "./live-context";
|
|
7
|
+
import { carriesSettlementPair } from "./transport";
|
|
7
8
|
import type { OrderStatus } from "./types";
|
|
8
9
|
|
|
9
10
|
/** What a consumer reads off the wait, and the one action it can take. */
|
|
@@ -110,6 +111,10 @@ function listenForRearm(poke: () => void): () => void {
|
|
|
110
111
|
* QR under a red alert with no retry, a card spinner replaced by one, and the
|
|
111
112
|
* hosted return spinning forever. None of the three could recover on its own,
|
|
112
113
|
* which is what made a blip cost a payment nobody was ever told about.
|
|
114
|
+
*
|
|
115
|
+
* WHILE LIVE it keeps no timer (FUT-3223, `poll-live.ts`), except on a hosted
|
|
116
|
+
* return carrying the settlement pair, where the read itself confirms the
|
|
117
|
+
* payment and the live floor is kept.
|
|
113
118
|
*/
|
|
114
119
|
export function usePaymentPolling(
|
|
115
120
|
orderId: string | null,
|
|
@@ -149,7 +154,8 @@ export function usePaymentPolling(
|
|
|
149
154
|
slowIntervalMs,
|
|
150
155
|
askTimeoutMs,
|
|
151
156
|
askTimeoutError: transportCopy.offline,
|
|
152
|
-
|
|
157
|
+
isLive,
|
|
158
|
+
liveIntervalMs: carriesSettlementPair() ? liveIntervalMs : undefined,
|
|
153
159
|
},
|
|
154
160
|
sinkFor(orderId, setAnswer),
|
|
155
161
|
);
|