@12-apps/payments-frontend 3.28.1 → 3.29.1
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 +2 -2
- package/src/components/ProviderStatusBar.tsx +54 -1
- package/src/components/checkout/live-context.tsx +9 -6
- package/src/components/checkout/poll-delay.ts +54 -0
- package/src/components/checkout/poll-hint.ts +58 -0
- package/src/components/checkout/poll-loop.ts +31 -52
- package/src/components/connection-state.ts +23 -3
- package/src/components/settings-copy.ts +8 -0
- package/src/components/settings-en-US.ts +6 -0
- package/src/components/settings-pt-BR.ts +6 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@12-apps/payments-frontend",
|
|
3
|
-
"version": "3.
|
|
3
|
+
"version": "3.29.1",
|
|
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.",
|
|
@@ -23,7 +23,7 @@
|
|
|
23
23
|
"storybook:build": "storybook build"
|
|
24
24
|
},
|
|
25
25
|
"dependencies": {
|
|
26
|
-
"@12-apps/payments-backend": "^4.
|
|
26
|
+
"@12-apps/payments-backend": "^4.29.0",
|
|
27
27
|
"react-qr-code": "^2.2.0"
|
|
28
28
|
},
|
|
29
29
|
"peerDependencies": {
|
|
@@ -4,7 +4,7 @@ import { Box, Stack, Switch, Tooltip, Typography } from '@mui/material';
|
|
|
4
4
|
|
|
5
5
|
import type { MaskedProviderConfig, ProviderDescriptor } from '@12-apps/payments-backend';
|
|
6
6
|
|
|
7
|
-
import { isConnected } from './connection-state';
|
|
7
|
+
import { certificateExpiryProximity, isConnected } from './connection-state';
|
|
8
8
|
import { T } from './panel-tokens';
|
|
9
9
|
import type { ConnectionStatusCopy } from './settings-copy';
|
|
10
10
|
import { usePaymentsSettingsCopy } from './settings-copy-context';
|
|
@@ -218,6 +218,58 @@ function ProviderName({
|
|
|
218
218
|
);
|
|
219
219
|
}
|
|
220
220
|
|
|
221
|
+
/**
|
|
222
|
+
* The saved production certificate's renewal warning, under the headline.
|
|
223
|
+
*
|
|
224
|
+
* Only for a store on PRODUCTION: that is the one whose PIX stops when the
|
|
225
|
+
* certificate lapses, and a sandbox store testing the form has nothing to
|
|
226
|
+
* renew yet. Silent outside the 30-day window (`certificateExpiryProximity`) —
|
|
227
|
+
* a date a year out is not news, and it is on the provider's portal anyway.
|
|
228
|
+
* A `status`, lapsed or not: it is state on load, and the probe's own error is
|
|
229
|
+
* already the alert — a second would be read out twice.
|
|
230
|
+
*/
|
|
231
|
+
function CertificateExpiryNote({ copy, config }: { copy: ConnectionStatusCopy; config: MaskedProviderConfig | null }) {
|
|
232
|
+
const note = certificateNote(copy, config);
|
|
233
|
+
if (!note) return null;
|
|
234
|
+
const tone = note.past
|
|
235
|
+
? { border: T.badLine, background: T.badSoft, color: T.badInk }
|
|
236
|
+
: { border: T.warnLine, background: T.warnSoft, color: T.warnInk };
|
|
237
|
+
return (
|
|
238
|
+
<Typography
|
|
239
|
+
role="status"
|
|
240
|
+
data-testid="payments-certificate-expiry"
|
|
241
|
+
sx={{
|
|
242
|
+
fontSize: '12.5px',
|
|
243
|
+
fontWeight: 600,
|
|
244
|
+
lineHeight: 1.5,
|
|
245
|
+
maxWidth: '52ch',
|
|
246
|
+
mt: '8px',
|
|
247
|
+
px: '10px',
|
|
248
|
+
py: '7px',
|
|
249
|
+
borderRadius: '8px',
|
|
250
|
+
border: `1px solid ${tone.border}`,
|
|
251
|
+
background: tone.background,
|
|
252
|
+
color: tone.color,
|
|
253
|
+
}}
|
|
254
|
+
>
|
|
255
|
+
{note.text}
|
|
256
|
+
</Typography>
|
|
257
|
+
);
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
/** What the certificate note says, or null when it has nothing to say. */
|
|
261
|
+
function certificateNote(
|
|
262
|
+
copy: ConnectionStatusCopy,
|
|
263
|
+
config: MaskedProviderConfig | null,
|
|
264
|
+
): { text: string; past: boolean } | null {
|
|
265
|
+
const expiresAt = config?.environment === 'PRODUCTION' ? config.credentialExpiresAt : null;
|
|
266
|
+
if (!expiresAt) return null;
|
|
267
|
+
const proximity = certificateExpiryProximity(expiresAt);
|
|
268
|
+
if (proximity === 'PAST') return { text: copy.certificateExpired(expiresAt), past: true };
|
|
269
|
+
if (proximity === 'NEAR') return { text: copy.certificateExpires(expiresAt), past: false };
|
|
270
|
+
return null;
|
|
271
|
+
}
|
|
272
|
+
|
|
221
273
|
export function ProviderStatusBar({
|
|
222
274
|
descriptor,
|
|
223
275
|
config,
|
|
@@ -248,6 +300,7 @@ export function ProviderStatusBar({
|
|
|
248
300
|
>
|
|
249
301
|
{sub}
|
|
250
302
|
</Typography>
|
|
303
|
+
<CertificateExpiryNote copy={copy} config={config} />
|
|
251
304
|
</Box>
|
|
252
305
|
{/* The switch belongs at the far edge: it is the one control here that
|
|
253
306
|
changes what buyers experience, and crowding it against the status
|
|
@@ -59,11 +59,14 @@ interface LiveWait {
|
|
|
59
59
|
type LoopRef = { readonly current: PollLoop | null };
|
|
60
60
|
|
|
61
61
|
/**
|
|
62
|
-
*
|
|
63
|
-
* once that quiet window is over. The
|
|
64
|
-
*
|
|
65
|
-
*
|
|
66
|
-
*
|
|
62
|
+
* A channel that DROPS: ask now — and if the loop declines because it asked a
|
|
63
|
+
* 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 its next tick sleeps out
|
|
65
|
+
* the live floor, so declining for good would leave the buyer waiting with
|
|
66
|
+
* nobody left to wake them.
|
|
67
|
+
*
|
|
68
|
+
* A HINT does not come here. It is `PollLoop.hint` (FUT-3205): deferring a
|
|
69
|
+
* hint by the quiet window cost a paid buyer a full second on the pay step.
|
|
67
70
|
*/
|
|
68
71
|
function wake(loop: LoopRef, pending: { timer?: ReturnType<typeof setTimeout> }): void {
|
|
69
72
|
if (loop.current?.poke() !== false) return;
|
|
@@ -94,7 +97,7 @@ export function useLiveWait(loop: LoopRef): LiveWait {
|
|
|
94
97
|
useEffect(() => {
|
|
95
98
|
if (!subscribe) return undefined;
|
|
96
99
|
const retry = pending.current;
|
|
97
|
-
const unsubscribe = subscribe(() =>
|
|
100
|
+
const unsubscribe = subscribe(() => loop.current?.hint());
|
|
98
101
|
return () => {
|
|
99
102
|
unsubscribe();
|
|
100
103
|
clearTimeout(retry.timer);
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import { withLive } from "./poll-live";
|
|
2
|
+
import type { PollingOptions } from "./poll-loop";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* How long a payment wait sleeps before its next ask — pure functions of the
|
|
6
|
+
* options and the run's counters, split out of `poll-loop.ts` so the loop
|
|
7
|
+
* stays one question: WHEN to ask, not how long.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* The slowest a failing poll may go (FUT-1144).
|
|
12
|
+
*
|
|
13
|
+
* Consecutive failures double the delay — 2.5 s, 5 s, 10 s — and stop there.
|
|
14
|
+
* The cap is what keeps the recovery cheap: a shopper whose signal comes back
|
|
15
|
+
* during a Wi-Fi→4G handoff waits at most this long to be told they paid, and
|
|
16
|
+
* the re-arm events (`poll-rearm.ts`) usually beat it outright.
|
|
17
|
+
*/
|
|
18
|
+
const MAX_ERROR_BACKOFF_MS = 10_000;
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* How long before the next ask, given how many healthy polls have happened.
|
|
22
|
+
*
|
|
23
|
+
* A pure function of the options, so it lives out here rather than inside the
|
|
24
|
+
* effect — the hook is at its size gate, and a scheduling RULE is easier to
|
|
25
|
+
* read (and to test) stated once than threaded through a closure.
|
|
26
|
+
*
|
|
27
|
+
* Reads the count AFTER the poll just made, so the slow phase begins on the
|
|
28
|
+
* poll FOLLOWING the threshold rather than one early: a wait described as "N
|
|
29
|
+
* fast polls" has to actually make N of them.
|
|
30
|
+
*/
|
|
31
|
+
function healthyDelay(healthy: number, options: PollingOptions): number {
|
|
32
|
+
const { intervalMs = 2500, slowAfterPolls, slowIntervalMs } = options;
|
|
33
|
+
const backingOff =
|
|
34
|
+
slowAfterPolls !== undefined && slowIntervalMs !== undefined && healthy >= slowAfterPolls;
|
|
35
|
+
return withLive(backingOff ? slowIntervalMs : intervalMs, options);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* The same rule with consecutive FAILURES folded in (FUT-1144).
|
|
40
|
+
*
|
|
41
|
+
* Errors never stop the wait, they only slow it. Doubling from the healthy
|
|
42
|
+
* cadence is what makes a ten-second blip cost one extra beat instead of the
|
|
43
|
+
* whole confirmation: four failures used to be terminal, and four failures is
|
|
44
|
+
* what a Wi-Fi→4G handoff produces at the default 2.5 s.
|
|
45
|
+
*
|
|
46
|
+
* The cap is `MAX_ERROR_BACKOFF_MS` or the healthy cadence, whichever is
|
|
47
|
+
* larger — a failing poll must never ask FASTER than a succeeding one, which
|
|
48
|
+
* is what a bare cap would do to a consumer whose slow interval is longer.
|
|
49
|
+
*/
|
|
50
|
+
export function pollDelay(healthy: number, errors: number, options: PollingOptions): number {
|
|
51
|
+
const base = healthyDelay(healthy, options);
|
|
52
|
+
if (errors === 0) return base;
|
|
53
|
+
return Math.min(base * 2 ** (errors - 1), Math.max(base, MAX_ERROR_BACKOFF_MS));
|
|
54
|
+
}
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import { claimRearm, REARM_QUIET_MS } from "./poll-rearm";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* What a realtime hint does to a running payment wait (FUT-3205).
|
|
5
|
+
*
|
|
6
|
+
* A hint is NOT a re-arm. `poke`'s quiet window exists to collapse
|
|
7
|
+
* `visibilitychange` and `online` bursts, and a hint is a server fact.
|
|
8
|
+
* Deferring it by that window cost a paid buyer a full second on the pay step
|
|
9
|
+
* (P-040: the hint at 12,145 ms, the second read at 13,148 ms). So, by the
|
|
10
|
+
* loop's state:
|
|
11
|
+
*
|
|
12
|
+
* - The wait ended (settled, stopped, cancelled): nothing (FUT-1144).
|
|
13
|
+
* - No ask in flight: ask now.
|
|
14
|
+
* - An ask in flight for `REARM_QUIET_MS` or longer: supersede it the way
|
|
15
|
+
* `poke` does, within `claimRearm`'s budget — the hung-socket case (FUT-1259).
|
|
16
|
+
* - Otherwise, or with that budget spent: keep the ask and MARK the run. One
|
|
17
|
+
* more ask is owed the moment it answers non-terminal, so a hint is never
|
|
18
|
+
* dropped (FUT-649), and a burst of hints during one ask costs one extra ask.
|
|
19
|
+
*
|
|
20
|
+
* The mark is cleared by `restart`, by a `poke` that supersedes the marked
|
|
21
|
+
* ask, by sending its follow-up and by a terminal answer. The deadline does
|
|
22
|
+
* NOT clear it: a marked ask answering after the deadline is caught by
|
|
23
|
+
* {@link owedAfterAnswer} and neither asks nor schedules.
|
|
24
|
+
*
|
|
25
|
+
* Structural state, as in `poll-rearm.ts`: the loop's run satisfies it.
|
|
26
|
+
*/
|
|
27
|
+
interface HintState {
|
|
28
|
+
cancelled: boolean;
|
|
29
|
+
settled: boolean;
|
|
30
|
+
stopped: boolean;
|
|
31
|
+
inFlight: boolean;
|
|
32
|
+
askedAt: number;
|
|
33
|
+
supersededAsks: number;
|
|
34
|
+
hinted: boolean;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/** Take a hint: whether the loop must ask now. Marks the run when it may not. */
|
|
38
|
+
export function claimHint(run: HintState): boolean {
|
|
39
|
+
if (run.cancelled || run.settled || run.stopped) return false;
|
|
40
|
+
const young = run.inFlight && Date.now() - run.askedAt < REARM_QUIET_MS;
|
|
41
|
+
if (!run.inFlight || (!young && claimRearm(run))) {
|
|
42
|
+
run.hinted = false;
|
|
43
|
+
return true;
|
|
44
|
+
}
|
|
45
|
+
run.hinted = true;
|
|
46
|
+
return false;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* After a non-terminal answer: `"ask"` when a mark owes one more ask now,
|
|
51
|
+
* `"ended"` when the wait stopped while that ask was out, `"none"` when nothing
|
|
52
|
+
* is owed and the loop schedules as usual. Clears the mark.
|
|
53
|
+
*/
|
|
54
|
+
export function owedAfterAnswer(run: Pick<HintState, "hinted" | "stopped">): "ask" | "ended" | "none" {
|
|
55
|
+
if (!run.hinted) return "none";
|
|
56
|
+
run.hinted = false;
|
|
57
|
+
return run.stopped ? "ended" : "ask";
|
|
58
|
+
}
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import type { Result } from "../../result";
|
|
2
|
-
import {
|
|
2
|
+
import { pollDelay } from "./poll-delay";
|
|
3
|
+
import { waitsForDeadline, type LiveCadence } from "./poll-live";
|
|
4
|
+
import { claimHint, owedAfterAnswer } from "./poll-hint";
|
|
3
5
|
import { claimRearm } from "./poll-rearm";
|
|
4
6
|
import { TERMINAL_STATUSES, type OrderStatus } from "./types";
|
|
5
7
|
|
|
@@ -64,16 +66,6 @@ export interface PollingOptions extends LiveCadence {
|
|
|
64
66
|
slowIntervalMs?: number;
|
|
65
67
|
}
|
|
66
68
|
|
|
67
|
-
/**
|
|
68
|
-
* The slowest a failing poll may go (FUT-1144).
|
|
69
|
-
*
|
|
70
|
-
* Consecutive failures double the delay — 2.5 s, 5 s, 10 s — and stop there.
|
|
71
|
-
* The cap is what keeps the recovery cheap: a shopper whose signal comes back
|
|
72
|
-
* during a Wi-Fi→4G handoff waits at most this long to be told they paid, and
|
|
73
|
-
* the two re-arm events below usually beat it outright.
|
|
74
|
-
*/
|
|
75
|
-
const MAX_ERROR_BACKOFF_MS = 10_000;
|
|
76
|
-
|
|
77
69
|
/**
|
|
78
70
|
* How long ONE status ask may take before the loop stops waiting on it.
|
|
79
71
|
*
|
|
@@ -92,42 +84,6 @@ const DEFAULT_ASK_TIMEOUT_MS = 15_000;
|
|
|
92
84
|
*/
|
|
93
85
|
const ASK_TIMED_OUT = "timeout";
|
|
94
86
|
|
|
95
|
-
/**
|
|
96
|
-
* How long before the next ask, given how many healthy polls have happened.
|
|
97
|
-
*
|
|
98
|
-
* A pure function of the options, so it lives out here rather than inside the
|
|
99
|
-
* effect — the hook is at its size gate, and a scheduling RULE is easier to
|
|
100
|
-
* read (and to test) stated once than threaded through a closure.
|
|
101
|
-
*
|
|
102
|
-
* Reads the count AFTER the poll just made, so the slow phase begins on the
|
|
103
|
-
* poll FOLLOWING the threshold rather than one early: a wait described as "N
|
|
104
|
-
* fast polls" has to actually make N of them.
|
|
105
|
-
*/
|
|
106
|
-
function healthyDelay(healthy: number, options: PollingOptions): number {
|
|
107
|
-
const { intervalMs = 2500, slowAfterPolls, slowIntervalMs } = options;
|
|
108
|
-
const backingOff =
|
|
109
|
-
slowAfterPolls !== undefined && slowIntervalMs !== undefined && healthy >= slowAfterPolls;
|
|
110
|
-
return withLive(backingOff ? slowIntervalMs : intervalMs, options);
|
|
111
|
-
}
|
|
112
|
-
|
|
113
|
-
/**
|
|
114
|
-
* The same rule with consecutive FAILURES folded in (FUT-1144).
|
|
115
|
-
*
|
|
116
|
-
* Errors never stop the wait, they only slow it. Doubling from the healthy
|
|
117
|
-
* cadence is what makes a ten-second blip cost one extra beat instead of the
|
|
118
|
-
* whole confirmation: four failures used to be terminal, and four failures is
|
|
119
|
-
* what a Wi-Fi→4G handoff produces at the default 2.5 s.
|
|
120
|
-
*
|
|
121
|
-
* The cap is `MAX_ERROR_BACKOFF_MS` or the healthy cadence, whichever is
|
|
122
|
-
* larger — a failing poll must never ask FASTER than a succeeding one, which
|
|
123
|
-
* is what a bare cap would do to a consumer whose slow interval is longer.
|
|
124
|
-
*/
|
|
125
|
-
function pollDelay(healthy: number, errors: number, options: PollingOptions): number {
|
|
126
|
-
const base = healthyDelay(healthy, options);
|
|
127
|
-
if (errors === 0) return base;
|
|
128
|
-
return Math.min(base * 2 ** (errors - 1), Math.max(base, MAX_ERROR_BACKOFF_MS));
|
|
129
|
-
}
|
|
130
|
-
|
|
131
87
|
/** Where a running wait writes what it has learned. */
|
|
132
88
|
export interface PollSink {
|
|
133
89
|
setStatus: (status: OrderStatus) => void;
|
|
@@ -166,6 +122,8 @@ function newRun() {
|
|
|
166
122
|
askedAt: 0,
|
|
167
123
|
/** Live asks abandoned by a re-arm since the last answer. See `claimRearm`. */
|
|
168
124
|
supersededAsks: 0,
|
|
125
|
+
/** A hint owes one more ask once the one in flight answers. See `poll-hint.ts`. */
|
|
126
|
+
hinted: false,
|
|
169
127
|
timer: undefined as ReturnType<typeof setTimeout> | undefined,
|
|
170
128
|
/**
|
|
171
129
|
* The wall clock, as a timer rather than a check after an ask. `outOfTime`
|
|
@@ -314,12 +272,29 @@ function scheduleNext(
|
|
|
314
272
|
run.timer = setTimeout(again, delay);
|
|
315
273
|
}
|
|
316
274
|
|
|
275
|
+
/** Abandon whatever ask is current and ask now, keeping the clock. */
|
|
276
|
+
function askNow(run: PollRun, tick: () => Promise<void>): void {
|
|
277
|
+
run.attempt += 1;
|
|
278
|
+
run.inFlight = false;
|
|
279
|
+
clearPending(run);
|
|
280
|
+
void tick();
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
/** After a non-terminal answer: the ask a hint owes, or the usual schedule. */
|
|
284
|
+
function continueAfter(run: PollRun, tick: () => Promise<void>, schedule: () => void): void {
|
|
285
|
+
const owed = owedAfterAnswer(run);
|
|
286
|
+
if (owed === "ask") askNow(run, tick);
|
|
287
|
+
else if (owed === "none") schedule();
|
|
288
|
+
}
|
|
289
|
+
|
|
317
290
|
/** The handle the hook holds on one running wait. */
|
|
318
291
|
export interface PollLoop {
|
|
319
292
|
/** Reset the clock and the counters, then ask immediately. */
|
|
320
293
|
restart: () => void;
|
|
321
294
|
/** Ask immediately, keeping the clock — the re-arm events' entry point. Whether it asked. */
|
|
322
295
|
poke: () => boolean;
|
|
296
|
+
/** A realtime hint: ask now, or owe one ask after the current one (`poll-hint.ts`, FUT-3205). */
|
|
297
|
+
hint: () => void;
|
|
323
298
|
/** Tear down: nothing further is scheduled and nothing further is written. */
|
|
324
299
|
stop: () => void;
|
|
325
300
|
}
|
|
@@ -353,11 +328,12 @@ export function createPollLoop(
|
|
|
353
328
|
if (run.attempt === mine) run.inFlight = false;
|
|
354
329
|
if (run.cancelled || run.settled) return;
|
|
355
330
|
if (!absorb(run, result, sink)) {
|
|
331
|
+
run.hinted = false;
|
|
356
332
|
clearDeadline(run);
|
|
357
333
|
return;
|
|
358
334
|
}
|
|
359
335
|
if (run.attempt !== mine) return;
|
|
360
|
-
scheduleNext(run, options, sink, () => void tick());
|
|
336
|
+
continueAfter(run, tick, () => scheduleNext(run, options, sink, () => void tick()));
|
|
361
337
|
};
|
|
362
338
|
|
|
363
339
|
return {
|
|
@@ -372,6 +348,7 @@ export function createPollLoop(
|
|
|
372
348
|
run.errors = 0;
|
|
373
349
|
run.healthy = 0;
|
|
374
350
|
run.supersededAsks = 0;
|
|
351
|
+
run.hinted = false;
|
|
375
352
|
run.startedAt = Date.now();
|
|
376
353
|
armDeadline(run, options, sink);
|
|
377
354
|
sink.setTimedOut(false);
|
|
@@ -385,12 +362,14 @@ export function createPollLoop(
|
|
|
385
362
|
// socket that died while the screen was hidden. Abandon it and ask now —
|
|
386
363
|
// but `claimRearm` bounds how many live asks that may abandon in a row.
|
|
387
364
|
if (!claimRearm(run)) return false;
|
|
388
|
-
|
|
389
|
-
run.
|
|
390
|
-
|
|
391
|
-
void tick();
|
|
365
|
+
// The new ask is sent after any hint that marked the old one.
|
|
366
|
+
run.hinted = false;
|
|
367
|
+
askNow(run, tick);
|
|
392
368
|
return true;
|
|
393
369
|
},
|
|
370
|
+
hint: (): void => {
|
|
371
|
+
if (claimHint(run)) askNow(run, tick);
|
|
372
|
+
},
|
|
394
373
|
stop: (): void => {
|
|
395
374
|
run.cancelled = true;
|
|
396
375
|
clearPending(run);
|
|
@@ -71,11 +71,31 @@ export function expiryProximity(
|
|
|
71
71
|
expiresAt: string | null | undefined,
|
|
72
72
|
now: Date = new Date(),
|
|
73
73
|
): ExpiryProximity | null {
|
|
74
|
-
|
|
75
|
-
|
|
74
|
+
return proximityWithin(EXPIRY_WARNING_WINDOW_MS, expiresAt, now);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* A saved CERTIFICATE's proximity (`credentialExpiresAt`) — a different
|
|
79
|
+
* window from the OAuth grant's on purpose. No sweep renews a certificate:
|
|
80
|
+
* the owner generates a new one by hand, and Itaú opens that renewal 30 days
|
|
81
|
+
* before expiry, so the warning starts the day there is something to do about
|
|
82
|
+
* it rather than a week before the store's PIX stops.
|
|
83
|
+
*/
|
|
84
|
+
const CERTIFICATE_WARNING_WINDOW_MS = 30 * 24 * 60 * 60 * 1000;
|
|
85
|
+
|
|
86
|
+
export function certificateExpiryProximity(
|
|
87
|
+
credentialExpiresAt: string | null | undefined,
|
|
88
|
+
now: Date = new Date(),
|
|
89
|
+
): ExpiryProximity | null {
|
|
90
|
+
return proximityWithin(CERTIFICATE_WARNING_WINDOW_MS, credentialExpiresAt, now);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
function proximityWithin(windowMs: number, iso: string | null | undefined, now: Date): ExpiryProximity | null {
|
|
94
|
+
if (!iso) return null;
|
|
95
|
+
const at = new Date(iso).getTime();
|
|
76
96
|
if (Number.isNaN(at)) return null;
|
|
77
97
|
if (at <= now.getTime()) return 'PAST';
|
|
78
|
-
return at - now.getTime() <=
|
|
98
|
+
return at - now.getTime() <= windowMs ? 'NEAR' : 'SAFE';
|
|
79
99
|
}
|
|
80
100
|
|
|
81
101
|
/**
|
|
@@ -49,6 +49,14 @@ export interface ConnectionStatusCopy {
|
|
|
49
49
|
pausedChip: string;
|
|
50
50
|
/** Proven, not switched on yet. */
|
|
51
51
|
readyNotReceiving: { state: string; sub: string };
|
|
52
|
+
/**
|
|
53
|
+
* The saved production certificate lapses inside the renewal window, or has
|
|
54
|
+
* lapsed. Given the ISO `validTo`; the pack formats the date in its own
|
|
55
|
+
* locale. Provider-neutral wording — the certificate is the store's, the
|
|
56
|
+
* portal is the provider's.
|
|
57
|
+
*/
|
|
58
|
+
certificateExpires: (iso: string) => string;
|
|
59
|
+
certificateExpired: (iso: string) => string;
|
|
52
60
|
}
|
|
53
61
|
|
|
54
62
|
/**
|
|
@@ -39,6 +39,12 @@ export const EN_US_PAYMENTS_SETTINGS_COPY: PaymentsSettingsCopy = {
|
|
|
39
39
|
state: 'Not taking sales yet',
|
|
40
40
|
sub: 'Everything is ready — flip the switch to start taking payments.',
|
|
41
41
|
},
|
|
42
|
+
certificateExpires: (iso) =>
|
|
43
|
+
`The production certificate expires on ${new Date(iso).toLocaleDateString('en-US')}. ` +
|
|
44
|
+
'Before then, generate a new certificate and private key in the provider’s portal and paste both on the Production tab — with an expired certificate, Pix through this provider does not work.',
|
|
45
|
+
certificateExpired: (iso) =>
|
|
46
|
+
`The production certificate expired on ${new Date(iso).toLocaleDateString('en-US')}. ` +
|
|
47
|
+
'Until it is replaced, Pix through this provider does not work. Generate a new certificate and private key in the provider’s portal and paste both on the Production tab.',
|
|
42
48
|
},
|
|
43
49
|
listBadge: {
|
|
44
50
|
active: 'Active',
|
|
@@ -31,6 +31,12 @@ export const PT_BR_PAYMENTS_SETTINGS_COPY: PaymentsSettingsCopy = {
|
|
|
31
31
|
state: 'Ainda não está recebendo',
|
|
32
32
|
sub: 'Tudo pronto — ligue a chave para começar a receber.',
|
|
33
33
|
},
|
|
34
|
+
certificateExpires: (iso) =>
|
|
35
|
+
`O certificado de produção vence em ${new Date(iso).toLocaleDateString('pt-BR')}. ` +
|
|
36
|
+
'Antes disso, gere um novo certificado e uma nova chave privada no portal do provedor e cole os dois na aba Produção — com o certificado vencido, o Pix por este provedor não funciona.',
|
|
37
|
+
certificateExpired: (iso) =>
|
|
38
|
+
`O certificado de produção venceu em ${new Date(iso).toLocaleDateString('pt-BR')}. ` +
|
|
39
|
+
'Enquanto ele não for trocado, o Pix por este provedor não funciona. Gere um novo certificado e uma nova chave privada no portal do provedor e cole os dois na aba Produção.',
|
|
34
40
|
},
|
|
35
41
|
listBadge: {
|
|
36
42
|
active: 'Ativo',
|