@12-apps/payments-frontend 3.21.2 → 3.21.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.21.
|
|
3
|
+
"version": "3.21.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.",
|
|
@@ -18,6 +18,14 @@ export interface PollingOptions {
|
|
|
18
18
|
* {@link DEFAULT_ASK_TIMEOUT_MS}; consumers override it only in tests.
|
|
19
19
|
*/
|
|
20
20
|
askTimeoutMs?: number;
|
|
21
|
+
/**
|
|
22
|
+
* What the BUYER reads when an ask is abandoned for hanging.
|
|
23
|
+
*
|
|
24
|
+
* The hook passes the transport's own `copy.offline`, so a dead socket reads
|
|
25
|
+
* as the dropped connection it is. Optional only so a direct caller of
|
|
26
|
+
* `createPollLoop` need not thread copy it does not have.
|
|
27
|
+
*/
|
|
28
|
+
askTimeoutError?: string;
|
|
21
29
|
/** Poll only while `true` (e.g. after a card charge is submitted). */
|
|
22
30
|
enabled?: boolean;
|
|
23
31
|
/**
|
|
@@ -83,9 +91,10 @@ const REARM_QUIET_MS = 1_000;
|
|
|
83
91
|
const DEFAULT_ASK_TIMEOUT_MS = 15_000;
|
|
84
92
|
|
|
85
93
|
/**
|
|
86
|
-
*
|
|
87
|
-
*
|
|
88
|
-
*
|
|
94
|
+
* Last resort only, for a host that wired the loop without `askTimeoutError`.
|
|
95
|
+
* `Result.error` is what the BUYER reads — the PIX, card and wallet panels all
|
|
96
|
+
* render it verbatim — so the hook passes the transport's own `copy.offline`
|
|
97
|
+
* down and a hang reads as the dropped connection it is.
|
|
89
98
|
*/
|
|
90
99
|
const ASK_TIMED_OUT = "timeout";
|
|
91
100
|
|
|
@@ -162,6 +171,14 @@ function newRun() {
|
|
|
162
171
|
startedAt: 0,
|
|
163
172
|
askedAt: 0,
|
|
164
173
|
timer: undefined as ReturnType<typeof setTimeout> | undefined,
|
|
174
|
+
/**
|
|
175
|
+
* The wall clock, as a timer rather than a check after an ask. `outOfTime`
|
|
176
|
+
* needs an ask to RETURN, and `askTimeout` stands down when its attempt is
|
|
177
|
+
* superseded — which every `poke` does. A shopper flicking to their bank
|
|
178
|
+
* app faster than `askTimeoutMs` refreshed it forever, so `maxWaitMs` never
|
|
179
|
+
* fired and the hosted return span with no error and no check-again button.
|
|
180
|
+
*/
|
|
181
|
+
deadline: undefined as ReturnType<typeof setTimeout> | undefined,
|
|
165
182
|
};
|
|
166
183
|
}
|
|
167
184
|
|
|
@@ -183,7 +200,9 @@ function askTimeout(
|
|
|
183
200
|
): Promise<Result<OrderStatus>> {
|
|
184
201
|
return new Promise((resolve) => {
|
|
185
202
|
setTimeout(() => {
|
|
186
|
-
if (run.attempt === mine)
|
|
203
|
+
if (run.attempt === mine) {
|
|
204
|
+
resolve({ ok: false, error: options.askTimeoutError ?? ASK_TIMED_OUT });
|
|
205
|
+
}
|
|
187
206
|
}, options.askTimeoutMs ?? DEFAULT_ASK_TIMEOUT_MS);
|
|
188
207
|
});
|
|
189
208
|
}
|
|
@@ -213,6 +232,88 @@ function absorb(run: PollRun, result: Result<OrderStatus>, sink: PollSink): bool
|
|
|
213
232
|
return true;
|
|
214
233
|
}
|
|
215
234
|
|
|
235
|
+
/** Cancel whatever tick is scheduled, if any. */
|
|
236
|
+
function clearPending(run: PollRun): void {
|
|
237
|
+
if (run.timer !== undefined) clearTimeout(run.timer);
|
|
238
|
+
run.timer = undefined;
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
/** Cancel the wall clock, if it is armed. */
|
|
242
|
+
function clearDeadline(run: PollRun): void {
|
|
243
|
+
if (run.deadline !== undefined) clearTimeout(run.deadline);
|
|
244
|
+
run.deadline = undefined;
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
/**
|
|
248
|
+
* The wall clock, armed once per run by `restart`, so `maxWaitMs` holds whether
|
|
249
|
+
* or not an ask ever returns. `outOfTime` stays too: it ends the wait one delay
|
|
250
|
+
* EARLIER when asks ARE returning. This is the backstop for when they are not.
|
|
251
|
+
*/
|
|
252
|
+
function armDeadline(run: PollRun, options: PollingOptions, sink: PollSink): void {
|
|
253
|
+
clearDeadline(run);
|
|
254
|
+
if (options.maxWaitMs === undefined) return;
|
|
255
|
+
run.deadline = setTimeout(() => {
|
|
256
|
+
run.deadline = undefined;
|
|
257
|
+
if (run.cancelled || run.settled) return;
|
|
258
|
+
run.stopped = true;
|
|
259
|
+
clearPending(run);
|
|
260
|
+
sink.setTimedOut(true);
|
|
261
|
+
}, options.maxWaitMs);
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
/**
|
|
265
|
+
* One ask, bounded by {@link askTimeout} and guaranteed not to throw. `race`
|
|
266
|
+
* rejects the instant either input does, so an `ask` that throws — a host
|
|
267
|
+
* client wrapping ours — left `inFlight` true with nothing scheduled: the very
|
|
268
|
+
* wedge this file removes, through the one door the race does not close.
|
|
269
|
+
*/
|
|
270
|
+
async function askOnce(
|
|
271
|
+
ask: () => Promise<Result<OrderStatus>>,
|
|
272
|
+
run: PollRun,
|
|
273
|
+
mine: number,
|
|
274
|
+
options: PollingOptions,
|
|
275
|
+
): Promise<Result<OrderStatus>> {
|
|
276
|
+
try {
|
|
277
|
+
return await Promise.race([ask(), askTimeout(run, mine, options)]);
|
|
278
|
+
} catch (error) {
|
|
279
|
+
const fallback = options.askTimeoutError ?? ASK_TIMED_OUT;
|
|
280
|
+
return { ok: false, error: error instanceof Error ? error.message : fallback };
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
/** Whether sleeping `delay` would carry the wait past its wall-clock bound. */
|
|
285
|
+
function outOfTime(run: PollRun, options: PollingOptions, delay: number): boolean {
|
|
286
|
+
return options.maxWaitMs !== undefined && Date.now() - run.startedAt + delay >= options.maxWaitMs;
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
/**
|
|
290
|
+
* Whether this attempt's answer may still be written. A superseded one writes
|
|
291
|
+
* nothing — it may be a hung request finally answering — EXCEPT a terminal
|
|
292
|
+
* status, which is the answer the wait exists for, is idempotent, and would
|
|
293
|
+
* otherwise be dropped because a re-arm landed first.
|
|
294
|
+
*/
|
|
295
|
+
function mayWrite(run: PollRun, mine: number, result: Result<OrderStatus>): boolean {
|
|
296
|
+
return run.attempt === mine || (result.ok && TERMINAL_STATUSES.includes(result.data));
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
/** Book the next tick, or end the wait because its clock has run out. */
|
|
300
|
+
function scheduleNext(
|
|
301
|
+
run: PollRun,
|
|
302
|
+
options: PollingOptions,
|
|
303
|
+
sink: PollSink,
|
|
304
|
+
again: () => void,
|
|
305
|
+
): void {
|
|
306
|
+
const delay = pollDelay(run.healthy, run.errors, options);
|
|
307
|
+
if (outOfTime(run, options, delay)) {
|
|
308
|
+
run.stopped = true;
|
|
309
|
+
clearDeadline(run);
|
|
310
|
+
sink.setTimedOut(true);
|
|
311
|
+
return;
|
|
312
|
+
}
|
|
313
|
+
clearPending(run);
|
|
314
|
+
run.timer = setTimeout(again, delay);
|
|
315
|
+
}
|
|
316
|
+
|
|
216
317
|
/** The handle the hook holds on one running wait. */
|
|
217
318
|
export interface PollLoop {
|
|
218
319
|
/** Reset the clock and the counters, then ask immediately. */
|
|
@@ -238,13 +339,6 @@ export function createPollLoop(
|
|
|
238
339
|
): PollLoop {
|
|
239
340
|
const run = newRun();
|
|
240
341
|
|
|
241
|
-
const clearPending = (): void => {
|
|
242
|
-
if (run.timer !== undefined) clearTimeout(run.timer);
|
|
243
|
-
run.timer = undefined;
|
|
244
|
-
};
|
|
245
|
-
|
|
246
|
-
const outOfTime = (delay: number): boolean =>
|
|
247
|
-
options.maxWaitMs !== undefined && Date.now() - run.startedAt + delay >= options.maxWaitMs;
|
|
248
342
|
|
|
249
343
|
const tick = async (): Promise<void> => {
|
|
250
344
|
if (run.cancelled || run.settled || run.inFlight) return;
|
|
@@ -256,27 +350,22 @@ export function createPollLoop(
|
|
|
256
350
|
// is supposed to end after `maxWaitMs` runs forever showing "we are still
|
|
257
351
|
// trying". Racing a timer turns a hang into an ordinary failed poll, which
|
|
258
352
|
// the backoff and the re-arm already know how to handle.
|
|
259
|
-
const result = await
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
if (run.attempt !== mine) return;
|
|
263
|
-
run.inFlight = false;
|
|
353
|
+
const result = await askOnce(ask, run, mine, options);
|
|
354
|
+
if (!mayWrite(run, mine, result)) return;
|
|
355
|
+
if (run.attempt === mine) run.inFlight = false;
|
|
264
356
|
if (run.cancelled || run.settled) return;
|
|
265
|
-
if (!absorb(run, result, sink))
|
|
266
|
-
|
|
267
|
-
if (outOfTime(delay)) {
|
|
268
|
-
run.stopped = true;
|
|
269
|
-
sink.setTimedOut(true);
|
|
357
|
+
if (!absorb(run, result, sink)) {
|
|
358
|
+
clearDeadline(run);
|
|
270
359
|
return;
|
|
271
360
|
}
|
|
272
|
-
|
|
273
|
-
run
|
|
361
|
+
if (run.attempt !== mine) return;
|
|
362
|
+
scheduleNext(run, options, sink, () => void tick());
|
|
274
363
|
};
|
|
275
364
|
|
|
276
365
|
return {
|
|
277
366
|
restart: (): void => {
|
|
278
367
|
if (run.cancelled || run.settled) return;
|
|
279
|
-
clearPending();
|
|
368
|
+
clearPending(run);
|
|
280
369
|
// Same reasoning as `poke`, and this one is the buyer pressing a button:
|
|
281
370
|
// "Verificar de novo" that cleared the panel and sent nothing — because
|
|
282
371
|
// an ask was still notionally in flight — is the exact complaint.
|
|
@@ -286,6 +375,7 @@ export function createPollLoop(
|
|
|
286
375
|
run.errors = 0;
|
|
287
376
|
run.healthy = 0;
|
|
288
377
|
run.startedAt = Date.now();
|
|
378
|
+
armDeadline(run, options, sink);
|
|
289
379
|
sink.setTimedOut(false);
|
|
290
380
|
sink.setError(null);
|
|
291
381
|
void tick();
|
|
@@ -298,12 +388,13 @@ export function createPollLoop(
|
|
|
298
388
|
// socket that died while the screen was hidden. Abandon it and ask now.
|
|
299
389
|
run.attempt += 1;
|
|
300
390
|
run.inFlight = false;
|
|
301
|
-
clearPending();
|
|
391
|
+
clearPending(run);
|
|
302
392
|
void tick();
|
|
303
393
|
},
|
|
304
394
|
stop: (): void => {
|
|
305
395
|
run.cancelled = true;
|
|
306
|
-
clearPending();
|
|
396
|
+
clearPending(run);
|
|
397
|
+
clearDeadline(run);
|
|
307
398
|
},
|
|
308
399
|
};
|
|
309
400
|
}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { useCallback, useEffect, useRef, useState } from "react";
|
|
2
2
|
|
|
3
3
|
import { useCheckoutClientApi } from "./client-context";
|
|
4
|
+
import { useCheckoutCopy } from "./copy-context";
|
|
4
5
|
import { createPollLoop, type PollLoop, type PollingOptions } from "./poll-loop";
|
|
5
6
|
import type { OrderStatus } from "./types";
|
|
6
7
|
|
|
@@ -83,6 +84,11 @@ export function usePaymentPolling(
|
|
|
83
84
|
// belongs in the deps below rather than being read out of a ref: a checkout
|
|
84
85
|
// re-pointed at another mount must re-poll against THAT one.
|
|
85
86
|
const client = useCheckoutClientApi();
|
|
87
|
+
// The buyer's own sentence for "we could not reach the server", reused for an
|
|
88
|
+
// ask abandoned for hanging. `Result.error` is rendered verbatim by the PIX,
|
|
89
|
+
// card and wallet panels, so without this a hang printed the English token
|
|
90
|
+
// `timeout` into a Portuguese screen.
|
|
91
|
+
const transportCopy = useCheckoutCopy().screens.transport;
|
|
86
92
|
// The live wait, so the returned action stays stable across renders while
|
|
87
93
|
// still reaching whichever loop the effect currently owns.
|
|
88
94
|
const loop = useRef<PollLoop | null>(null);
|
|
@@ -92,7 +98,14 @@ export function usePaymentPolling(
|
|
|
92
98
|
|
|
93
99
|
const running = createPollLoop(
|
|
94
100
|
() => client.getStatus(orderId),
|
|
95
|
-
{
|
|
101
|
+
{
|
|
102
|
+
intervalMs,
|
|
103
|
+
maxWaitMs,
|
|
104
|
+
slowAfterPolls,
|
|
105
|
+
slowIntervalMs,
|
|
106
|
+
askTimeoutMs,
|
|
107
|
+
askTimeoutError: transportCopy.offline,
|
|
108
|
+
},
|
|
96
109
|
{ setStatus, setError, setTimedOut },
|
|
97
110
|
);
|
|
98
111
|
loop.current = running;
|
|
@@ -104,7 +117,17 @@ export function usePaymentPolling(
|
|
|
104
117
|
running.stop();
|
|
105
118
|
loop.current = null;
|
|
106
119
|
};
|
|
107
|
-
|
|
120
|
+
}, [
|
|
121
|
+
orderId,
|
|
122
|
+
intervalMs,
|
|
123
|
+
enabled,
|
|
124
|
+
maxWaitMs,
|
|
125
|
+
slowAfterPolls,
|
|
126
|
+
slowIntervalMs,
|
|
127
|
+
askTimeoutMs,
|
|
128
|
+
client,
|
|
129
|
+
transportCopy,
|
|
130
|
+
]);
|
|
108
131
|
|
|
109
132
|
const checkAgain = useCallback(() => {
|
|
110
133
|
loop.current?.restart();
|