@12-apps/payments-frontend 3.2.3 → 3.2.4
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.2.
|
|
3
|
+
"version": "3.2.4",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"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.",
|
|
6
6
|
"exports": {
|
|
@@ -177,21 +177,40 @@ function handOverToProvider(
|
|
|
177
177
|
/**
|
|
178
178
|
* How long the resumed screen keeps asking, and how often.
|
|
179
179
|
*
|
|
180
|
-
*
|
|
181
|
-
*
|
|
182
|
-
*
|
|
183
|
-
*
|
|
184
|
-
*
|
|
185
|
-
*
|
|
186
|
-
*
|
|
180
|
+
* TWO RATES, because one rate cannot serve this wait. The interval decides two
|
|
181
|
+
* things that pull opposite ways: how fast a buyer WHO PAID is told so, and
|
|
182
|
+
* what an abandoned checkout costs for the rest of the window. Every poll is a
|
|
183
|
+
* provider round trip, so a slow rate is cheap and leaves a paying buyer
|
|
184
|
+
* watching a spinner seconds longer than they need to — and the person on this
|
|
185
|
+
* screen has almost always paid. A single number picks one of them to lose;
|
|
186
|
+
* this shipped at a flat 5 s and picked the wrong one.
|
|
187
|
+
*
|
|
188
|
+
* So: 2.5 s for the first two minutes, which is where essentially every real
|
|
189
|
+
* webhook lands (it fires within seconds of the payment, and this rate matches
|
|
190
|
+
* what card and PIX already use), then 10 s for the remaining thirteen. A
|
|
191
|
+
* confirmation is at most 2.5 s late, and an abandoned checkout costs ~126
|
|
192
|
+
* polls instead of the 360 a flat 2.5 s would have.
|
|
193
|
+
*
|
|
194
|
+
* The three constants are ONE decision — 48 × 2.5 s + 78 × 10 s ≈ 15 min — so
|
|
195
|
+
* the cap is derived rather than typed, and cannot drift from the comment.
|
|
196
|
+
*
|
|
197
|
+
* Fifteen minutes because by then a webhook that was ever coming has come. Past
|
|
198
|
+
* that the answer will not change while the buyer watches: the scheduled
|
|
199
|
+
* reconciliation is what rescues a genuinely late one, and it does that whether
|
|
200
|
+
* the tab is open or not.
|
|
187
201
|
*
|
|
188
202
|
* The card wait is bounded at 90 s (`CARD_AWAITING_POLL_CAP`) because a card
|
|
189
203
|
* authorises inline and a buyer is holding their phone. This leg is the other
|
|
190
204
|
* shape: the buyer has already been off to another site and back, and may
|
|
191
205
|
* legitimately still be finishing there.
|
|
192
206
|
*/
|
|
193
|
-
const
|
|
194
|
-
const
|
|
207
|
+
const HOSTED_RESUME_FAST_MS = 2_500;
|
|
208
|
+
const HOSTED_RESUME_SLOW_MS = 10_000;
|
|
209
|
+
/** Two minutes at the fast rate, before the wait is worth economising on. */
|
|
210
|
+
const HOSTED_RESUME_FAST_POLLS = (2 * 60_000) / HOSTED_RESUME_FAST_MS;
|
|
211
|
+
/** Thirteen more at the slow one — 15 minutes all told. */
|
|
212
|
+
const HOSTED_RESUME_POLL_CAP =
|
|
213
|
+
HOSTED_RESUME_FAST_POLLS + (13 * 60_000) / HOSTED_RESUME_SLOW_MS;
|
|
195
214
|
|
|
196
215
|
/**
|
|
197
216
|
* The leg of checkout that resumes after a hosted provider sent the buyer back
|
|
@@ -218,7 +237,9 @@ function useHostedResume(tenantSlug?: string): {
|
|
|
218
237
|
const [order] = useState(() => takeHostedOrder(tenantSlug));
|
|
219
238
|
const { status, timedOut } = usePaymentPolling(order?.orderId ?? null, {
|
|
220
239
|
enabled: Boolean(order),
|
|
221
|
-
intervalMs:
|
|
240
|
+
intervalMs: HOSTED_RESUME_FAST_MS,
|
|
241
|
+
slowAfterPolls: HOSTED_RESUME_FAST_POLLS,
|
|
242
|
+
slowIntervalMs: HOSTED_RESUME_SLOW_MS,
|
|
222
243
|
maxHealthyPolls: HOSTED_RESUME_POLL_CAP,
|
|
223
244
|
});
|
|
224
245
|
return { order, status, timedOut };
|
|
@@ -14,11 +14,45 @@ interface PollingOptions {
|
|
|
14
14
|
* Undefined ⇒ unbounded, today's behavior (the PIX consumer passes no cap).
|
|
15
15
|
*/
|
|
16
16
|
maxHealthyPolls?: number;
|
|
17
|
+
/**
|
|
18
|
+
* Opt-in BACKOFF: after this many healthy polls, keep asking at
|
|
19
|
+
* {@link slowIntervalMs} instead of {@link intervalMs}.
|
|
20
|
+
*
|
|
21
|
+
* A single interval cannot serve a long wait, because the two things it
|
|
22
|
+
* decides pull opposite ways. It is how fast a buyer WHO PAID learns that
|
|
23
|
+
* they did — every poll is a provider round trip, and the answer lands within
|
|
24
|
+
* seconds of the webhook — and it is also what an abandoned checkout costs
|
|
25
|
+
* for the rest of the window. Tuning one picks the other's loser: a slow
|
|
26
|
+
* interval taxes the common case (the person on this screen almost always
|
|
27
|
+
* paid) to subsidise the rare one.
|
|
28
|
+
*
|
|
29
|
+
* Splitting them costs neither. Both must be set for backoff to apply.
|
|
30
|
+
*/
|
|
31
|
+
slowAfterPolls?: number;
|
|
32
|
+
slowIntervalMs?: number;
|
|
17
33
|
}
|
|
18
34
|
|
|
19
35
|
/** Consecutive poll errors tolerated before giving up (avoids an infinite spinner). */
|
|
20
36
|
const MAX_POLL_ERRORS = 4;
|
|
21
37
|
|
|
38
|
+
/**
|
|
39
|
+
* How long before the next ask, given how many healthy polls have happened.
|
|
40
|
+
*
|
|
41
|
+
* A pure function of the options, so it lives out here rather than inside the
|
|
42
|
+
* effect — the hook is at its size gate, and a scheduling RULE is easier to
|
|
43
|
+
* read (and to test) stated once than threaded through a closure.
|
|
44
|
+
*
|
|
45
|
+
* Reads the count AFTER the poll just made, so the slow phase begins on the
|
|
46
|
+
* poll FOLLOWING the threshold rather than one early: a wait described as "N
|
|
47
|
+
* fast polls" has to actually make N of them.
|
|
48
|
+
*/
|
|
49
|
+
function pollDelay(healthy: number, options: PollingOptions): number {
|
|
50
|
+
const { intervalMs = 2500, slowAfterPolls, slowIntervalMs } = options;
|
|
51
|
+
const backingOff =
|
|
52
|
+
slowAfterPolls !== undefined && slowIntervalMs !== undefined && healthy >= slowAfterPolls;
|
|
53
|
+
return backingOff ? slowIntervalMs : intervalMs;
|
|
54
|
+
}
|
|
55
|
+
|
|
22
56
|
/**
|
|
23
57
|
* Poll an order's payment status until it reaches a terminal state.
|
|
24
58
|
*
|
|
@@ -29,7 +63,13 @@ const MAX_POLL_ERRORS = 4;
|
|
|
29
63
|
*/
|
|
30
64
|
export function usePaymentPolling(
|
|
31
65
|
orderId: string | null,
|
|
32
|
-
{
|
|
66
|
+
{
|
|
67
|
+
intervalMs = 2500,
|
|
68
|
+
enabled = true,
|
|
69
|
+
maxHealthyPolls,
|
|
70
|
+
slowAfterPolls,
|
|
71
|
+
slowIntervalMs,
|
|
72
|
+
}: PollingOptions = {},
|
|
33
73
|
): { status: OrderStatus | null; error: string | null; timedOut: boolean } {
|
|
34
74
|
const [status, setStatus] = useState<OrderStatus | null>(null);
|
|
35
75
|
const [error, setError] = useState<string | null>(null);
|
|
@@ -80,7 +120,7 @@ export function usePaymentPolling(
|
|
|
80
120
|
}
|
|
81
121
|
timer = setTimeout(() => {
|
|
82
122
|
void tick();
|
|
83
|
-
}, intervalMs);
|
|
123
|
+
}, pollDelay(healthyCount, { intervalMs, slowAfterPolls, slowIntervalMs }));
|
|
84
124
|
};
|
|
85
125
|
|
|
86
126
|
void tick();
|
|
@@ -91,7 +131,7 @@ export function usePaymentPolling(
|
|
|
91
131
|
clearTimeout(timer);
|
|
92
132
|
}
|
|
93
133
|
};
|
|
94
|
-
}, [orderId, intervalMs, enabled, maxHealthyPolls, client]);
|
|
134
|
+
}, [orderId, intervalMs, enabled, maxHealthyPolls, slowAfterPolls, slowIntervalMs, client]);
|
|
95
135
|
|
|
96
136
|
return { status, error, timedOut };
|
|
97
137
|
}
|
|
@@ -99,16 +99,22 @@ function buildHostedHandoff(runtime: FlowsRuntime): CheckoutScreens["HostedHando
|
|
|
99
99
|
/**
|
|
100
100
|
* How long this screen keeps asking, and how often — see the twin constants in
|
|
101
101
|
* `use-checkout-controller.ts`, which bounds the same wait for the components
|
|
102
|
-
* layer.
|
|
102
|
+
* layer. 2.5 s for two minutes, then 10 s for thirteen: 126 polls, 15 minutes.
|
|
103
|
+
*
|
|
104
|
+
* Two rates because one cannot serve both ends of this wait — a paying buyer
|
|
105
|
+
* learns within 2.5 s, an abandoned checkout costs a third of what a flat fast
|
|
106
|
+
* rate would. The reasoning is on the twin constants.
|
|
103
107
|
*
|
|
104
108
|
* Stated here rather than imported because the two waits are the same DECISION
|
|
105
|
-
* arrived at twice, not one shared implementation: this screen takes its
|
|
109
|
+
* arrived at twice, not one shared implementation: this screen takes its FAST
|
|
106
110
|
* interval from the host's `polling` config when there is one, and a host that
|
|
107
111
|
* tunes that must not have this package's cap silently mean a different
|
|
108
112
|
* wall-clock window than the constant's comment claims.
|
|
109
113
|
*/
|
|
110
|
-
const
|
|
111
|
-
const
|
|
114
|
+
const RETURN_FAST_MS = 2_500;
|
|
115
|
+
const RETURN_SLOW_MS = 10_000;
|
|
116
|
+
const RETURN_FAST_POLLS = (2 * 60_000) / RETURN_FAST_MS;
|
|
117
|
+
const RETURN_POLL_CAP = RETURN_FAST_POLLS + (13 * 60_000) / RETURN_SLOW_MS;
|
|
112
118
|
|
|
113
119
|
function buildHostedReturn(runtime: FlowsRuntime): CheckoutScreens["HostedReturn"] {
|
|
114
120
|
function HostedReturnBody({
|
|
@@ -125,7 +131,9 @@ function buildHostedReturn(runtime: FlowsRuntime): CheckoutScreens["HostedReturn
|
|
|
125
131
|
// watches until they close the tab.
|
|
126
132
|
const { status, timedOut } = usePaymentPolling(parked?.orderId ?? null, {
|
|
127
133
|
enabled: Boolean(parked),
|
|
128
|
-
intervalMs: runtime.config.polling?.intervalMs ??
|
|
134
|
+
intervalMs: runtime.config.polling?.intervalMs ?? RETURN_FAST_MS,
|
|
135
|
+
slowAfterPolls: RETURN_FAST_POLLS,
|
|
136
|
+
slowIntervalMs: RETURN_SLOW_MS,
|
|
129
137
|
maxHealthyPolls: RETURN_POLL_CAP,
|
|
130
138
|
});
|
|
131
139
|
|