@12-apps/payments-frontend 3.20.0 → 3.20.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/checkout/card-view.tsx +30 -28
- package/src/components/checkout/checkout-flow.tsx +5 -0
- package/src/components/checkout/en-US.ts +9 -0
- package/src/components/checkout/payment-status.tsx +130 -38
- package/src/components/checkout/pix-view.tsx +34 -12
- package/src/components/checkout/pt-BR.ts +7 -0
- package/src/components/checkout/screens-copy.ts +19 -0
- package/src/components/checkout/screens-en-US.ts +4 -0
- package/src/components/checkout/screens-pt-BR.ts +2 -0
- package/src/components/checkout/stalled-wait.tsx +58 -0
- package/src/components/checkout/use-card-checkout.ts +45 -11
- package/src/components/checkout/use-checkout-controller.ts +48 -13
- package/src/components/checkout/use-payment-polling.ts +250 -64
- package/src/components/checkout/use-wallet-charge.ts +20 -6
- package/src/components/checkout/view-copy.ts +15 -0
- package/src/components/checkout/wallet-pane.tsx +17 -16
- package/src/flows/copy.ts +19 -0
- package/src/flows/screens-hosted.tsx +72 -17
|
@@ -12,6 +12,7 @@ import {
|
|
|
12
12
|
googlePayConfig,
|
|
13
13
|
} from "./method-capability";
|
|
14
14
|
import type { ProviderCheckoutScreenProps } from "./providers/types";
|
|
15
|
+
import { StalledWait } from "./stalled-wait";
|
|
15
16
|
import { useCheckoutComponents } from "./ui";
|
|
16
17
|
import { useWalletCharge, type WalletCharge } from "./use-wallet-charge";
|
|
17
18
|
|
|
@@ -35,29 +36,29 @@ type WalletPaneProps = ProviderCheckoutScreenProps & {
|
|
|
35
36
|
order: NonNullable<ProviderCheckoutScreenProps["order"]>;
|
|
36
37
|
};
|
|
37
38
|
|
|
38
|
-
/** Post-submit confirmation,
|
|
39
|
+
/** Post-submit confirmation, timeout > error > spinner — the card view's order. */
|
|
39
40
|
function WalletProcessing({ wallet }: { wallet: WalletCharge }): JSX.Element {
|
|
40
|
-
const {
|
|
41
|
+
const { LoadingState } = useCheckoutComponents();
|
|
41
42
|
const copy = useCheckoutCopy().screens.settling;
|
|
42
|
-
if (wallet.
|
|
43
|
+
if (wallet.pollTimedOut) {
|
|
43
44
|
return (
|
|
44
|
-
<
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
45
|
+
<StalledWait
|
|
46
|
+
title={copy.takingLonger}
|
|
47
|
+
description={copy.takingLongerHelp}
|
|
48
|
+
onCheckAgain={wallet.pollCheckAgain}
|
|
49
|
+
testId="wallet-poll-timeout"
|
|
50
|
+
actionTestId="wallet-check-again"
|
|
50
51
|
/>
|
|
51
52
|
);
|
|
52
53
|
}
|
|
53
|
-
if (wallet.
|
|
54
|
+
if (wallet.pollError) {
|
|
54
55
|
return (
|
|
55
|
-
<
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
56
|
+
<StalledWait
|
|
57
|
+
title={copy.connectionLost}
|
|
58
|
+
description={wallet.pollError}
|
|
59
|
+
onCheckAgain={wallet.pollCheckAgain}
|
|
60
|
+
testId="wallet-poll-error"
|
|
61
|
+
actionTestId="wallet-check-again"
|
|
61
62
|
/>
|
|
62
63
|
);
|
|
63
64
|
}
|
package/src/flows/copy.ts
CHANGED
|
@@ -59,6 +59,25 @@ export interface CheckoutCopyFE {
|
|
|
59
59
|
* part only a host can own.
|
|
60
60
|
*/
|
|
61
61
|
returnTimedOut?: string;
|
|
62
|
+
/**
|
|
63
|
+
* The return leg while the poll cannot REACH us (FUT-1144) — a dropped
|
|
64
|
+
* connection, or a browser that aborted our requests while the buyer was on
|
|
65
|
+
* the provider's page. The wait is still running and re-arms the moment the
|
|
66
|
+
* tab or the signal comes back, so the sentence must say "still trying", not
|
|
67
|
+
* "failed".
|
|
68
|
+
*
|
|
69
|
+
* OPTIONAL, on the {@link returnTimedOut} precedent: without it the screen
|
|
70
|
+
* shows the transport's own sentence — which the host already owns, through
|
|
71
|
+
* `views.screens.transport` — under `returnPending` as a warning.
|
|
72
|
+
*/
|
|
73
|
+
returnUnreachable?: string;
|
|
74
|
+
/**
|
|
75
|
+
* The label on the buyer's "ask now" (FUT-1144), offered whenever the wait is
|
|
76
|
+
* unreachable or has elapsed. OPTIONAL for the same reason, and its ABSENCE
|
|
77
|
+
* costs the button: a control with no host-written label could only carry
|
|
78
|
+
* this package's Portuguese.
|
|
79
|
+
*/
|
|
80
|
+
returnCheckAgain?: string;
|
|
62
81
|
/** The Dados step's primary action. */
|
|
63
82
|
continueAction: string;
|
|
64
83
|
/**
|
|
@@ -108,13 +108,60 @@ function buildHostedHandoff(runtime: FlowsRuntime): CheckoutScreens["HostedHando
|
|
|
108
108
|
* Stated here rather than imported because the two waits are the same DECISION
|
|
109
109
|
* arrived at twice, not one shared implementation: this screen takes its FAST
|
|
110
110
|
* interval from the host's `polling` config when there is one, and a host that
|
|
111
|
-
* tunes that must not have this package's
|
|
112
|
-
* wall-clock window than the constant's comment claims
|
|
111
|
+
* tunes that must not have this package's bound silently mean a different
|
|
112
|
+
* wall-clock window than the constant's comment claims — which is exactly what
|
|
113
|
+
* a bound counted in POLLS did, and why it is counted in milliseconds now
|
|
114
|
+
* (FUT-1144).
|
|
113
115
|
*/
|
|
114
116
|
const RETURN_FAST_MS = 2_500;
|
|
115
117
|
const RETURN_SLOW_MS = 10_000;
|
|
116
118
|
const RETURN_FAST_POLLS = (2 * 60_000) / RETURN_FAST_MS;
|
|
117
|
-
const
|
|
119
|
+
const RETURN_WINDOW_MS = 15 * 60_000;
|
|
120
|
+
|
|
121
|
+
/**
|
|
122
|
+
* The two ways this wait stops looking like progress, said as a warning with
|
|
123
|
+
* the buyer's own "ask now" under it (FUT-1144).
|
|
124
|
+
*
|
|
125
|
+
* The button is drawn only when the host wrote a label for it. That is the
|
|
126
|
+
* `returnTimedOut` precedent one step further: a bound with no copy still
|
|
127
|
+
* stops the spinner, and an action with no copy could only be labelled in this
|
|
128
|
+
* package's Portuguese, so it is the one half that stands down.
|
|
129
|
+
*/
|
|
130
|
+
function ReturnStalled({
|
|
131
|
+
runtime,
|
|
132
|
+
description,
|
|
133
|
+
onCheckAgain,
|
|
134
|
+
testId,
|
|
135
|
+
}: {
|
|
136
|
+
runtime: FlowsRuntime;
|
|
137
|
+
description: string | undefined;
|
|
138
|
+
onCheckAgain: () => void;
|
|
139
|
+
testId: string;
|
|
140
|
+
}): JSX.Element {
|
|
141
|
+
const { Alert, Button } = useCheckoutComponents();
|
|
142
|
+
return (
|
|
143
|
+
<Box data-testid="checkout-hosted-return" sx={{ py: 4, display: "flex", flexDirection: "column", gap: 2 }}>
|
|
144
|
+
<Alert
|
|
145
|
+
variant="warning"
|
|
146
|
+
title={runtime.copy.returnPending}
|
|
147
|
+
{...(description === undefined ? {} : { description })}
|
|
148
|
+
showIcon
|
|
149
|
+
data-testid={testId}
|
|
150
|
+
/>
|
|
151
|
+
{runtime.copy.returnCheckAgain === undefined ? null : (
|
|
152
|
+
<Button
|
|
153
|
+
variant="outline"
|
|
154
|
+
color="neutral"
|
|
155
|
+
size="md"
|
|
156
|
+
onClick={onCheckAgain}
|
|
157
|
+
dataTestId="checkout-hosted-return-check-again"
|
|
158
|
+
>
|
|
159
|
+
{runtime.copy.returnCheckAgain}
|
|
160
|
+
</Button>
|
|
161
|
+
)}
|
|
162
|
+
</Box>
|
|
163
|
+
);
|
|
164
|
+
}
|
|
118
165
|
|
|
119
166
|
function buildHostedReturn(runtime: FlowsRuntime): CheckoutScreens["HostedReturn"] {
|
|
120
167
|
function HostedReturnBody({
|
|
@@ -126,15 +173,15 @@ function buildHostedReturn(runtime: FlowsRuntime): CheckoutScreens["HostedReturn
|
|
|
126
173
|
// Read-and-clear, once, on first render: the resumed view belongs to
|
|
127
174
|
// exactly one return trip.
|
|
128
175
|
const [parked] = useState(takeHostedOrder);
|
|
129
|
-
// Bounded, for the reason on
|
|
176
|
+
// Bounded, for the reason on RETURN_WINDOW_MS: nothing here can ever reach a
|
|
130
177
|
// terminal state on its own, so an unbounded poll is a spinner the buyer
|
|
131
178
|
// watches until they close the tab.
|
|
132
|
-
const { status, timedOut } = usePaymentPolling(parked?.orderId ?? null, {
|
|
179
|
+
const { status, timedOut, error, checkAgain } = usePaymentPolling(parked?.orderId ?? null, {
|
|
133
180
|
enabled: Boolean(parked),
|
|
134
181
|
intervalMs: runtime.config.polling?.intervalMs ?? RETURN_FAST_MS,
|
|
135
182
|
slowAfterPolls: RETURN_FAST_POLLS,
|
|
136
183
|
slowIntervalMs: RETURN_SLOW_MS,
|
|
137
|
-
|
|
184
|
+
maxWaitMs: RETURN_WINDOW_MS,
|
|
138
185
|
});
|
|
139
186
|
|
|
140
187
|
useEffect(() => {
|
|
@@ -152,19 +199,27 @@ function buildHostedReturn(runtime: FlowsRuntime): CheckoutScreens["HostedReturn
|
|
|
152
199
|
/>
|
|
153
200
|
);
|
|
154
201
|
}
|
|
202
|
+
// STOPPED beats STILL TRYING. A wait that failed its way to the wall clock
|
|
203
|
+
// carries both, and the elapsed state is the one that stops asking — and the
|
|
204
|
+
// one whose sentence says not to pay again.
|
|
155
205
|
if (timedOut) {
|
|
156
206
|
return (
|
|
157
|
-
<
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
207
|
+
<ReturnStalled
|
|
208
|
+
runtime={runtime}
|
|
209
|
+
description={runtime.copy.returnTimedOut}
|
|
210
|
+
onCheckAgain={checkAgain}
|
|
211
|
+
testId="checkout-hosted-return-timeout"
|
|
212
|
+
/>
|
|
213
|
+
);
|
|
214
|
+
}
|
|
215
|
+
if (error !== null) {
|
|
216
|
+
return (
|
|
217
|
+
<ReturnStalled
|
|
218
|
+
runtime={runtime}
|
|
219
|
+
description={runtime.copy.returnUnreachable ?? error}
|
|
220
|
+
onCheckAgain={checkAgain}
|
|
221
|
+
testId="checkout-hosted-return-unreachable"
|
|
222
|
+
/>
|
|
168
223
|
);
|
|
169
224
|
}
|
|
170
225
|
return (
|