@12-apps/payments-frontend 3.8.0 → 3.10.0
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/activation/charge-copy.ts +10 -0
- package/src/activation/use-activation-charge.ts +10 -8
- package/src/card/copy.ts +115 -0
- package/src/card/cpf.ts +12 -5
- package/src/card/fields.tsx +17 -12
- package/src/card/format.ts +32 -16
- package/src/card/index.ts +3 -1
- package/src/card/pt-BR.ts +51 -0
- package/src/card/stripe-token.ts +5 -6
- package/src/card/tokenize.ts +28 -32
- package/src/components/CheckoutPayment.tsx +37 -201
- package/src/components/ConfirmCredentialSave.tsx +8 -10
- package/src/components/ConnectionCard.tsx +23 -18
- package/src/components/ConnectionProbe.tsx +3 -1
- package/src/components/CredentialFieldStack.tsx +8 -2
- package/src/components/CredentialFields.tsx +13 -6
- package/src/components/CredentialFormAlerts.tsx +28 -19
- package/src/components/EnvironmentTabs.tsx +23 -15
- package/src/components/OAuthPanel.tsx +5 -6
- package/src/components/PaymentProviderSettings.tsx +45 -13
- package/src/components/ProviderConnection.tsx +37 -28
- package/src/components/ProviderList.tsx +6 -3
- package/src/components/ProviderPriorityList.tsx +18 -16
- package/src/components/ProviderStatusBar.tsx +24 -24
- package/src/components/SetupGuideSection.tsx +15 -6
- package/src/components/checkout/apple-pay-button.tsx +19 -9
- package/src/components/checkout/buyer-fields.ts +5 -3
- package/src/components/checkout/buyer-gate.ts +68 -0
- package/src/components/checkout/buyer-info-form.tsx +14 -15
- package/src/components/checkout/card-instruments.ts +31 -12
- package/src/components/checkout/card-view.tsx +11 -7
- package/src/components/checkout/checkout-flow.tsx +4 -1
- package/src/components/checkout/checkout-steps.tsx +3 -1
- package/src/components/checkout/client-context.tsx +28 -16
- package/src/components/checkout/client.ts +43 -16
- package/src/components/checkout/copy-context.tsx +83 -0
- package/src/components/checkout/google-pay-button.tsx +19 -7
- package/src/components/checkout/method-picker.tsx +34 -10
- package/src/components/checkout/payer-summary.tsx +8 -6
- package/src/components/checkout/payment-error-panel.tsx +8 -5
- package/src/components/checkout/pix-view.tsx +16 -9
- package/src/components/checkout/providers/hosted-link.tsx +26 -16
- package/src/components/checkout/pt-BR.ts +27 -0
- package/src/components/checkout/screens-copy.ts +201 -0
- package/src/components/checkout/screens-pt-BR.ts +101 -0
- package/src/components/checkout/transport.ts +34 -8
- package/src/components/checkout/use-card-checkout.ts +13 -5
- package/src/components/checkout/use-checkout-controller.ts +5 -48
- package/src/components/checkout/view-copy.ts +13 -0
- package/src/components/checkout/wallet-pane.tsx +13 -8
- package/src/components/checkout-payment-copy.ts +88 -0
- package/src/components/checkout-payment-pt-BR.ts +50 -0
- package/src/components/checkout-payment-types.ts +25 -0
- package/src/components/checkout-payment-views.tsx +225 -0
- package/src/components/connection-state.ts +22 -10
- package/src/components/credential-rules.ts +8 -3
- package/src/components/platform/ConnectApplicationPanel.tsx +6 -2
- package/src/components/platform/ConnectEnvironmentCard.tsx +19 -15
- package/src/components/platform/HomologacaoGuideCard.tsx +23 -19
- package/src/components/platform/HomologacaoOutcomeCard.tsx +24 -16
- package/src/components/platform/PlatformHomologacao.tsx +12 -8
- package/src/components/settings-copy-context.tsx +50 -0
- package/src/components/settings-copy.ts +304 -0
- package/src/components/settings-pt-BR.ts +169 -0
- package/src/flows/copy.ts +12 -0
- package/src/flows/create-payment-flows.tsx +8 -1
- package/src/flows/runtime.tsx +12 -4
- package/src/flows/screens-vault.tsx +10 -10
- package/src/flows/types.ts +5 -2
- package/src/flows/use-add-card.ts +10 -5
- package/src/index.ts +63 -0
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* What the "Continuar" gate objects to (FUT-595), split out of
|
|
3
|
+
* `use-checkout-controller.ts`.
|
|
4
|
+
*
|
|
5
|
+
* Its own module because the answer is a pure function of what the store's
|
|
6
|
+
* chain declared, the buyer's form, and the words a host chose — nothing in
|
|
7
|
+
* here touches React, a network or the controller's state machine, and the
|
|
8
|
+
* controller was over the file-size gate carrying it.
|
|
9
|
+
*/
|
|
10
|
+
import { buyerFormComplete } from "./buyer-info-form";
|
|
11
|
+
import type { CheckoutValidationCopy } from "./screens-copy";
|
|
12
|
+
import type { BuyerField, BuyerInfo, CheckoutCustomerField } from "./types";
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* The message shown when a declared field is missing or malformed — the HOST's
|
|
16
|
+
* words (FUT-760), keyed by the field the chain declared.
|
|
17
|
+
*
|
|
18
|
+
* Built from the copy rather than held as a module constant: a frozen table
|
|
19
|
+
* could only carry the origin host's Portuguese, and it would reach every
|
|
20
|
+
* adopter's buyer on the very first field they got wrong.
|
|
21
|
+
*/
|
|
22
|
+
function fieldComplaints(copy: CheckoutValidationCopy): Record<string, string> {
|
|
23
|
+
return {
|
|
24
|
+
taxId: copy.taxIdInvalid,
|
|
25
|
+
name: copy.nameRequired,
|
|
26
|
+
email: copy.emailInvalid,
|
|
27
|
+
phone: copy.phoneInvalid,
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/** Which input to highlight for a declared key. */
|
|
32
|
+
const FIELD_INPUT: Record<string, BuyerField> = {
|
|
33
|
+
taxId: "cpf",
|
|
34
|
+
name: "name",
|
|
35
|
+
email: "email",
|
|
36
|
+
phone: "phone",
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* What the "Continuar" gate objects to, or undefined to advance.
|
|
41
|
+
*
|
|
42
|
+
* The fields come from the chain's own declaration (FUT-595) — absent, they
|
|
43
|
+
* degrade to CPF-required, which is exactly what this gate has always demanded.
|
|
44
|
+
*
|
|
45
|
+
* A blank CPF is only an error when the store has NO CPF for this buyer. With
|
|
46
|
+
* one on file the field starts empty by design (the client is never sent the
|
|
47
|
+
* saved CPF), so demanding one here trapped a returning buyer who opened Dados
|
|
48
|
+
* through "Alterar" and changed their mind: they could not reach Pagamento
|
|
49
|
+
* again, and back only led out to the menu. Leaving it blank means "charge me
|
|
50
|
+
* as before", which is exactly what the server's `resolveBuyerTaxId` does.
|
|
51
|
+
*/
|
|
52
|
+
export function buyerGateError(
|
|
53
|
+
copy: CheckoutValidationCopy,
|
|
54
|
+
buyer: BuyerInfo,
|
|
55
|
+
fields: readonly CheckoutCustomerField[],
|
|
56
|
+
taxIdOnFile: boolean,
|
|
57
|
+
): { message: string; field: BuyerField } | undefined {
|
|
58
|
+
const effective = taxIdOnFile
|
|
59
|
+
? fields.filter((field) => !(field.key === "taxId" && !buyer.taxId?.trim()))
|
|
60
|
+
: fields;
|
|
61
|
+
const offending = buyerFormComplete(buyer, effective);
|
|
62
|
+
if (!offending) return undefined;
|
|
63
|
+
return {
|
|
64
|
+
message: fieldComplaints(copy)[offending.key] ?? copy.required,
|
|
65
|
+
field: FIELD_INPUT[offending.key] ?? "cpf",
|
|
66
|
+
};
|
|
67
|
+
}
|
|
68
|
+
|
|
@@ -6,6 +6,8 @@ import { formatCpf, validateCpf } from "../../card";
|
|
|
6
6
|
import { fieldSatisfied } from "./buyer-fields";
|
|
7
7
|
import type { BuyerField, BuyerInfo, CheckoutCustomerField } from "./types";
|
|
8
8
|
import { useCheckoutComponents } from "./ui";
|
|
9
|
+
import type { CardFieldCopy } from "../../card/copy";
|
|
10
|
+
import { useCheckoutCopy, type BuyerInfoCopy } from "./copy-context";
|
|
9
11
|
|
|
10
12
|
const EMAIL_PATTERN = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
|
|
11
13
|
|
|
@@ -62,18 +64,20 @@ function deriveErrors(
|
|
|
62
64
|
value: BuyerInfo,
|
|
63
65
|
fieldError: FieldError,
|
|
64
66
|
required: ReadonlySet<InputKey>,
|
|
67
|
+
copy: BuyerInfoCopy,
|
|
68
|
+
cardCopy: CardFieldCopy,
|
|
65
69
|
): BuyerFieldErrors {
|
|
66
70
|
const override = (field: BuyerField): string | undefined =>
|
|
67
71
|
fieldError?.field === field ? fieldError.message : undefined;
|
|
68
72
|
const localEmail =
|
|
69
|
-
value.email && !EMAIL_PATTERN.test(value.email) ?
|
|
73
|
+
value.email && !EMAIL_PATTERN.test(value.email) ? copy.emailInvalid : undefined;
|
|
70
74
|
const missing = (key: InputKey, message: string): string | undefined =>
|
|
71
75
|
required.has(key) && !valueOf(value, key).trim() ? message : undefined;
|
|
72
76
|
return {
|
|
73
|
-
cpf: override("cpf") ?? (value.taxId ? validateCpf(value.taxId) : undefined),
|
|
74
|
-
email: override("email") ?? localEmail ?? missing("email",
|
|
75
|
-
name: override("name") ?? missing("name",
|
|
76
|
-
phone: override("phone") ?? missing("phone",
|
|
77
|
+
cpf: override("cpf") ?? (value.taxId ? validateCpf(value.taxId, cardCopy) : undefined),
|
|
78
|
+
email: override("email") ?? localEmail ?? missing("email", copy.emailRequired),
|
|
79
|
+
name: override("name") ?? missing("name", copy.nameRequired),
|
|
80
|
+
phone: override("phone") ?? missing("phone", copy.phoneRequired),
|
|
77
81
|
};
|
|
78
82
|
}
|
|
79
83
|
|
|
@@ -82,17 +86,11 @@ function deriveErrors(
|
|
|
82
86
|
* declared. It used to name the CPF unconditionally, which is wrong the moment
|
|
83
87
|
* a store's provider wants something else (or nothing).
|
|
84
88
|
*/
|
|
85
|
-
function instructionFor(required: ReadonlySet<InputKey
|
|
89
|
+
function instructionFor(required: ReadonlySet<InputKey>, copy: BuyerInfoCopy): string {
|
|
86
90
|
const names = FIELD_ORDER.filter((key) => required.has(key)).map((key) =>
|
|
87
91
|
key === "taxId" ? "CPF" : INPUTS[key].label.toLowerCase(),
|
|
88
92
|
);
|
|
89
|
-
|
|
90
|
-
return "Nome, e-mail e telefone são opcionais — usados apenas para o comprovante.";
|
|
91
|
-
}
|
|
92
|
-
return (
|
|
93
|
-
`Informe seu ${names.join(", ")} (${names.length === 1 ? "obrigatório" : "obrigatórios"} ` +
|
|
94
|
-
"para o pagamento). Os demais campos são opcionais — usados apenas para o comprovante."
|
|
95
|
-
);
|
|
93
|
+
return copy.fieldsHint(names);
|
|
96
94
|
}
|
|
97
95
|
|
|
98
96
|
/** Which inputs to render, and which of them are required. */
|
|
@@ -184,12 +182,13 @@ export function BuyerInfoForm({
|
|
|
184
182
|
}): JSX.Element {
|
|
185
183
|
const { Text } = useCheckoutComponents();
|
|
186
184
|
const { shown, required } = resolveShape(fields);
|
|
187
|
-
const
|
|
185
|
+
const copy = useCheckoutCopy();
|
|
186
|
+
const errors = deriveErrors(value, fieldError ?? null, required, copy.buyer, copy.card.fields);
|
|
188
187
|
|
|
189
188
|
return (
|
|
190
189
|
<Box sx={{ display: "flex", flexDirection: "column", gap: 2 }}>
|
|
191
190
|
<Text variant="caption" size="xs" color="secondary" as="p">
|
|
192
|
-
{instructionFor(required)}
|
|
191
|
+
{instructionFor(required, copy.buyer)}
|
|
193
192
|
</Text>
|
|
194
193
|
|
|
195
194
|
{shown.map((key) => (
|
|
@@ -7,16 +7,18 @@ import {
|
|
|
7
7
|
} from "../../card";
|
|
8
8
|
import { err, ok, type Result } from "../../result";
|
|
9
9
|
|
|
10
|
-
import { refreshCardPublicKey } from "./client";
|
|
11
10
|
import type { CardChainLink } from "./method-capability";
|
|
12
11
|
import type { SavedCardMeta } from "./types";
|
|
12
|
+
import type { CardCopy } from "../../card/copy";
|
|
13
13
|
|
|
14
14
|
/**
|
|
15
15
|
* The order-scoped key refresh, as a parameter (FUT-741).
|
|
16
16
|
*
|
|
17
17
|
* The self-heal is a call to OUR OWN mount, so it has to go through whichever
|
|
18
|
-
* transport the surrounding checkout was bound to.
|
|
19
|
-
* module
|
|
18
|
+
* transport the surrounding checkout was bound to. Required since FUT-760: the
|
|
19
|
+
* default was the unbound module call, and that now needs words this module
|
|
20
|
+
* has no business choosing. Every real caller already passed the bound
|
|
21
|
+
* `client.refreshBrowserKey`.
|
|
20
22
|
*/
|
|
21
23
|
export type RefreshBrowserKey = (input: {
|
|
22
24
|
orderId: string;
|
|
@@ -51,15 +53,16 @@ async function tokenizeNewCard(
|
|
|
51
53
|
orderId: string,
|
|
52
54
|
onKeyRefreshed: (key: string) => void,
|
|
53
55
|
refreshKey: RefreshBrowserKey,
|
|
56
|
+
copy: CardCopy,
|
|
54
57
|
): Promise<Result<CardToken>> {
|
|
55
|
-
const first = await tokenizeForCheckout(card, config);
|
|
58
|
+
const first = await tokenizeForCheckout(card, config, copy);
|
|
56
59
|
if (first.ok || !config.publicKey) return first;
|
|
57
60
|
if (config.provider === null || tokenizerFor(config.provider) !== "pagbank-sdk") return first;
|
|
58
61
|
|
|
59
62
|
const refreshed = await refreshKey({ orderId });
|
|
60
63
|
if (refreshed.ok && refreshed.data.publicKey && refreshed.data.publicKey !== config.publicKey) {
|
|
61
64
|
onKeyRefreshed(refreshed.data.publicKey);
|
|
62
|
-
return tokenizeForCheckout(card, { ...config, publicKey: refreshed.data.publicKey });
|
|
65
|
+
return tokenizeForCheckout(card, { ...config, publicKey: refreshed.data.publicKey }, copy);
|
|
63
66
|
}
|
|
64
67
|
return first;
|
|
65
68
|
}
|
|
@@ -92,6 +95,7 @@ async function mintChainInstruments(
|
|
|
92
95
|
card: CardDetails,
|
|
93
96
|
chain: readonly CardChainLink[],
|
|
94
97
|
timeoutMs: number,
|
|
98
|
+
copy: CardCopy,
|
|
95
99
|
): Promise<Record<string, CardToken>> {
|
|
96
100
|
const results = await Promise.all(
|
|
97
101
|
chain.map(async (link) => {
|
|
@@ -100,7 +104,7 @@ async function mintChainInstruments(
|
|
|
100
104
|
// stub mode and an error everywhere else. It still travels in `chain`,
|
|
101
105
|
// because the walk will reach it.
|
|
102
106
|
if (!link.provider || !link.mintable) return null;
|
|
103
|
-
const tokenized = await mintWithDeadline(card, link, timeoutMs);
|
|
107
|
+
const tokenized = await mintWithDeadline(card, link, timeoutMs, copy);
|
|
104
108
|
return tokenized.ok ? ([link.provider, tokenized.data] as const) : null;
|
|
105
109
|
}),
|
|
106
110
|
);
|
|
@@ -125,6 +129,7 @@ async function mintWithDeadline(
|
|
|
125
129
|
card: CardDetails,
|
|
126
130
|
link: CardChainLink,
|
|
127
131
|
timeoutMs: number,
|
|
132
|
+
copy: CardCopy,
|
|
128
133
|
): Promise<Result<CardToken>> {
|
|
129
134
|
const controller = new AbortController();
|
|
130
135
|
let timer: ReturnType<typeof setTimeout> | undefined;
|
|
@@ -133,11 +138,14 @@ async function mintWithDeadline(
|
|
|
133
138
|
const deadline = new Promise<Result<CardToken>>((resolve) => {
|
|
134
139
|
timer = setTimeout(() => {
|
|
135
140
|
controller.abort();
|
|
136
|
-
resolve(err(
|
|
141
|
+
resolve(err(copy.tokenize.providerTimedOut));
|
|
137
142
|
}, timeoutMs);
|
|
138
143
|
});
|
|
139
144
|
try {
|
|
140
|
-
return await Promise.race([
|
|
145
|
+
return await Promise.race([
|
|
146
|
+
tokenizeForCheckout(card, link, copy, controller.signal),
|
|
147
|
+
deadline,
|
|
148
|
+
]);
|
|
141
149
|
} finally {
|
|
142
150
|
clearTimeout(timer);
|
|
143
151
|
}
|
|
@@ -197,12 +205,20 @@ async function mintEveryEntry(input: {
|
|
|
197
205
|
onKeyRefreshed: (key: string) => void;
|
|
198
206
|
refreshKey: RefreshBrowserKey;
|
|
199
207
|
timeoutMs: number;
|
|
208
|
+
copy: CardCopy;
|
|
200
209
|
}): Promise<{ headToken: Result<CardToken>; minted: Record<string, CardToken> }> {
|
|
201
210
|
const head = mintingConfig(input.config, input.entries);
|
|
202
211
|
const rest = input.entries.filter((link) => link.provider !== head.provider);
|
|
203
212
|
const [headToken, tail] = await Promise.all([
|
|
204
|
-
tokenizeNewCard(
|
|
205
|
-
|
|
213
|
+
tokenizeNewCard(
|
|
214
|
+
input.card,
|
|
215
|
+
head,
|
|
216
|
+
input.orderId,
|
|
217
|
+
input.onKeyRefreshed,
|
|
218
|
+
input.refreshKey,
|
|
219
|
+
input.copy,
|
|
220
|
+
),
|
|
221
|
+
mintChainInstruments(input.card, rest, input.timeoutMs, input.copy),
|
|
206
222
|
]);
|
|
207
223
|
// A failure in the tail is not fatal: that provider is simply one the walk
|
|
208
224
|
// will skip.
|
|
@@ -226,10 +242,12 @@ export async function resolveNewCardToken(
|
|
|
226
242
|
onKeyRefreshed: (key: string) => void,
|
|
227
243
|
saveCard: boolean,
|
|
228
244
|
chain: readonly CardChainLink[],
|
|
245
|
+
/** The words a failed mint reports with — the host's (FUT-760). */
|
|
246
|
+
copy: CardCopy,
|
|
247
|
+
/** The bound key refresh (FUT-741) — the surrounding checkout's transport. */
|
|
248
|
+
refreshKey: RefreshBrowserKey,
|
|
229
249
|
/** Per-entry mint deadline. Overridable so tests need not wait it out. */
|
|
230
250
|
timeoutMs: number = MINT_TIMEOUT_MS,
|
|
231
|
-
/** The bound key refresh (FUT-741); defaults to the unbound module call. */
|
|
232
|
-
refreshKey: RefreshBrowserKey = refreshCardPublicKey,
|
|
233
251
|
): Promise<Result<CardInstruments>> {
|
|
234
252
|
// No chain served (an older host, or a fetch blip): the active provider
|
|
235
253
|
// alone, exactly the pre-FUT-563 behaviour.
|
|
@@ -242,6 +260,7 @@ export async function resolveNewCardToken(
|
|
|
242
260
|
onKeyRefreshed,
|
|
243
261
|
refreshKey,
|
|
244
262
|
timeoutMs,
|
|
263
|
+
copy,
|
|
245
264
|
});
|
|
246
265
|
|
|
247
266
|
// Refused only when NO entry could be minted for. While one still can, the
|
|
@@ -8,6 +8,7 @@ import {
|
|
|
8
8
|
type CardTokenizationConfig,
|
|
9
9
|
} from "../../card";
|
|
10
10
|
|
|
11
|
+
import { useCheckoutCopy } from "./copy-context";
|
|
11
12
|
import { UNRESOLVED_CODE } from "./failure-codes";
|
|
12
13
|
import type { CardChainLink } from "./method-capability";
|
|
13
14
|
import type { BuyerInfo, CheckoutOrder, OrderStatus } from "./types";
|
|
@@ -28,11 +29,12 @@ function SubmittedState({
|
|
|
28
29
|
pollTimedOut: boolean;
|
|
29
30
|
}): JSX.Element {
|
|
30
31
|
const { Alert, LoadingState } = useCheckoutComponents();
|
|
32
|
+
const copy = useCheckoutCopy().screens.settling;
|
|
31
33
|
if (pollError) {
|
|
32
34
|
return (
|
|
33
35
|
<Alert
|
|
34
36
|
variant="danger"
|
|
35
|
-
title=
|
|
37
|
+
title={copy.cannotConfirm}
|
|
36
38
|
description={pollError}
|
|
37
39
|
showIcon
|
|
38
40
|
data-testid="card-poll-error"
|
|
@@ -43,8 +45,8 @@ function SubmittedState({
|
|
|
43
45
|
return (
|
|
44
46
|
<Alert
|
|
45
47
|
variant="warning"
|
|
46
|
-
title=
|
|
47
|
-
description=
|
|
48
|
+
title={copy.takingLonger}
|
|
49
|
+
description={copy.takingLongerHelp}
|
|
48
50
|
showIcon
|
|
49
51
|
data-testid="card-poll-timeout"
|
|
50
52
|
/>
|
|
@@ -54,7 +56,7 @@ function SubmittedState({
|
|
|
54
56
|
<LoadingState
|
|
55
57
|
variant="spinner"
|
|
56
58
|
size="md"
|
|
57
|
-
message=
|
|
59
|
+
message={copy.processing}
|
|
58
60
|
dataTestId="card-processing"
|
|
59
61
|
/>
|
|
60
62
|
);
|
|
@@ -76,11 +78,12 @@ function ChargeFailure({
|
|
|
76
78
|
unresolved: boolean;
|
|
77
79
|
}): JSX.Element {
|
|
78
80
|
const { Alert } = useCheckoutComponents();
|
|
81
|
+
const copy = useCheckoutCopy().screens.settling;
|
|
79
82
|
if (unresolved) {
|
|
80
83
|
return (
|
|
81
84
|
<Alert
|
|
82
85
|
variant="warning"
|
|
83
|
-
title=
|
|
86
|
+
title={copy.confirming}
|
|
84
87
|
description={message}
|
|
85
88
|
showIcon
|
|
86
89
|
data-testid="card-unresolved"
|
|
@@ -88,7 +91,7 @@ function ChargeFailure({
|
|
|
88
91
|
);
|
|
89
92
|
}
|
|
90
93
|
return (
|
|
91
|
-
<Alert variant="danger" title=
|
|
94
|
+
<Alert variant="danger" title={copy.cannotPay} description={message} showIcon data-testid="card-error" />
|
|
92
95
|
);
|
|
93
96
|
}
|
|
94
97
|
|
|
@@ -127,6 +130,7 @@ export function CardView({
|
|
|
127
130
|
pollIntervalMs?: number;
|
|
128
131
|
}): JSX.Element {
|
|
129
132
|
const { Text } = useCheckoutComponents();
|
|
133
|
+
const copy = useCheckoutCopy().screens.card;
|
|
130
134
|
const cc = useCardCheckout(order, buyer, providerConfig, onResolved, pollIntervalMs, tenantSlug, providerChain);
|
|
131
135
|
// A charge NOBODY can confirm yet is not a decline (FUT-563). Some provider
|
|
132
136
|
// may be holding the buyer's money, so it gets its own presentation: the
|
|
@@ -141,7 +145,7 @@ export function CardView({
|
|
|
141
145
|
return (
|
|
142
146
|
<Box data-testid="card-view" sx={{ display: "flex", flexDirection: "column", gap: 2 }}>
|
|
143
147
|
<Text variant="heading" size="md" weight="bold" as="h2">
|
|
144
|
-
|
|
148
|
+
{copy.heading}
|
|
145
149
|
</Text>
|
|
146
150
|
|
|
147
151
|
{cc.savedCards.length > 0 ? (
|
|
@@ -6,6 +6,7 @@ import { DadosStep, EmptyCart, PaymentStep } from "./checkout-steps";
|
|
|
6
6
|
import { ArrowBackIcon } from "./icons";
|
|
7
7
|
import { PaymentStatus } from "./payment-status";
|
|
8
8
|
import type { BuyerInfo, CheckoutProviderConfig, SettlementCheckout } from "./types";
|
|
9
|
+
import { CheckoutCopyProvider } from "./copy-context";
|
|
9
10
|
import { CheckoutComponentsProvider, useCheckoutComponents, type CheckoutComponents } from "./ui";
|
|
10
11
|
import type { CheckoutViewCopy } from "./view-copy";
|
|
11
12
|
import { useCheckoutController, type CheckoutHostPorts } from "./use-checkout-controller";
|
|
@@ -245,7 +246,9 @@ function CheckoutFlowBody(props: Omit<CheckoutFlowProps, "components">): JSX.Ele
|
|
|
245
246
|
export function CheckoutFlow({ components, ...props }: CheckoutFlowProps): JSX.Element {
|
|
246
247
|
return (
|
|
247
248
|
<CheckoutComponentsProvider components={components}>
|
|
248
|
-
<
|
|
249
|
+
<CheckoutCopyProvider copy={props.copy.screens}>
|
|
250
|
+
<CheckoutFlowBody {...props} />
|
|
251
|
+
</CheckoutCopyProvider>
|
|
249
252
|
</CheckoutComponentsProvider>
|
|
250
253
|
);
|
|
251
254
|
}
|
|
@@ -2,6 +2,7 @@ import { Box } from "@mui/material";
|
|
|
2
2
|
import { useEffect, useRef, type JSX, type ReactNode } from "react";
|
|
3
3
|
|
|
4
4
|
import { BuyerInfoForm } from "./buyer-info-form";
|
|
5
|
+
import { useCheckoutCopy } from "./copy-context";
|
|
5
6
|
import { LockOutlinedIcon } from "./icons";
|
|
6
7
|
import { useMethodChoice } from "./method-choice";
|
|
7
8
|
import { MethodPicker } from "./method-picker";
|
|
@@ -345,6 +346,7 @@ export function PaymentStep({
|
|
|
345
346
|
onResolved,
|
|
346
347
|
}: PaymentStepProps): JSX.Element {
|
|
347
348
|
const { LoadingState } = useCheckoutComponents();
|
|
349
|
+
const screens = useCheckoutCopy().screens;
|
|
348
350
|
const config = providerConfig ?? null;
|
|
349
351
|
const choice = useMethodChoice(config, method, onMethodChange);
|
|
350
352
|
useAutoRaiseOrder(order, method, creating, createError, onGenerate);
|
|
@@ -381,7 +383,7 @@ export function PaymentStep({
|
|
|
381
383
|
raised, and two stacked spinners saying the same thing is what the
|
|
382
384
|
buyer actually saw. */}
|
|
383
385
|
{!order && creating && !choice.atProvider ? (
|
|
384
|
-
<LoadingState variant="spinner" size="md" message=
|
|
386
|
+
<LoadingState variant="spinner" size="md" message={screens.generatingPayment} dataTestId="payment-generating" />
|
|
385
387
|
) : null}
|
|
386
388
|
|
|
387
389
|
{!order && method && createError ? (
|
|
@@ -15,8 +15,14 @@
|
|
|
15
15
|
*
|
|
16
16
|
* The default is built from the `client.ts` bindings LAZILY (inside each
|
|
17
17
|
* arrow), so a suite that `vi.mock`s that module still intercepts the call.
|
|
18
|
+
*
|
|
19
|
+
* Since FUT-760 that default also needs the transport's WORDS, and it takes
|
|
20
|
+
* them from the checkout's copy context — which is why {@link
|
|
21
|
+
* useCheckoutClientApi} builds it instead of holding it as a module constant.
|
|
22
|
+
* No screen passes them: a screen already has the copy provider above it, or
|
|
23
|
+
* it would not render at all.
|
|
18
24
|
*/
|
|
19
|
-
import { createContext, useContext, type JSX, type ReactNode } from "react";
|
|
25
|
+
import { createContext, useContext, useMemo, type JSX, type ReactNode } from "react";
|
|
20
26
|
|
|
21
27
|
import {
|
|
22
28
|
chargeCard,
|
|
@@ -26,22 +32,26 @@ import {
|
|
|
26
32
|
pollOrderStatus,
|
|
27
33
|
refreshCardPublicKey,
|
|
28
34
|
} from "./client";
|
|
35
|
+
import { useCheckoutCopy } from "./copy-context";
|
|
36
|
+
import type { CheckoutTransportCopy } from "./screens-copy";
|
|
29
37
|
import { createCheckoutClient, type CheckoutClient } from "./transport";
|
|
30
38
|
|
|
31
39
|
/** The unbound client: `/api/checkout` on the ambient `fetch`. */
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
40
|
+
function unboundClient(copy: CheckoutTransportCopy): CheckoutClient {
|
|
41
|
+
return {
|
|
42
|
+
getConfig: (tenantSlug) => fetchCheckoutConfig(tenantSlug, copy),
|
|
43
|
+
getStatus: (ref) => pollOrderStatus(ref, copy),
|
|
44
|
+
charge: (input) => chargeCard(input, copy),
|
|
45
|
+
chargeWallet: (input) => chargeWallet(input, copy),
|
|
46
|
+
listInstruments: (tenantSlug) => listSavedCards(copy, tenantSlug),
|
|
47
|
+
// The vault pair (FUT-183) has no `client.ts` free function to bind — it is
|
|
48
|
+
// newer than that module. Built lazily from the default transport instead,
|
|
49
|
+
// which is the same wire: `/api/checkout`, ambient `fetch` resolved per call.
|
|
50
|
+
beginVault: () => createCheckoutClient({ copy }).beginVault(),
|
|
51
|
+
completeVault: (input) => createCheckoutClient({ copy }).completeVault(input),
|
|
52
|
+
refreshBrowserKey: (input) => refreshCardPublicKey(input, copy),
|
|
53
|
+
};
|
|
54
|
+
}
|
|
45
55
|
|
|
46
56
|
const CheckoutClientContext = createContext<CheckoutClient | null>(null);
|
|
47
57
|
|
|
@@ -58,7 +68,9 @@ export function CheckoutClientProvider({
|
|
|
58
68
|
);
|
|
59
69
|
}
|
|
60
70
|
|
|
61
|
-
/** The bound client, or the
|
|
71
|
+
/** The bound client, or the unbound one when no provider sits above. */
|
|
62
72
|
export function useCheckoutClientApi(): CheckoutClient {
|
|
63
|
-
|
|
73
|
+
const provided = useContext(CheckoutClientContext);
|
|
74
|
+
const copy = useCheckoutCopy().screens.transport;
|
|
75
|
+
return useMemo(() => provided ?? unboundClient(copy), [provided, copy]);
|
|
64
76
|
}
|
|
@@ -12,6 +12,12 @@
|
|
|
12
12
|
* injected `fetch` reaches for `createPaymentFlows({ transport })` instead;
|
|
13
13
|
* nothing about these functions changes when it does.
|
|
14
14
|
*
|
|
15
|
+
* Each takes the transport's COPY (FUT-760). Unbound is about the mount, not
|
|
16
|
+
* about the language: a wire failure still has to say something to a buyer,
|
|
17
|
+
* and the one thing this package must not do is pick those words itself. The
|
|
18
|
+
* caller is `client-context.tsx`, which reads them from the checkout's copy
|
|
19
|
+
* context — so no screen passes them by hand.
|
|
20
|
+
*
|
|
15
21
|
* Order CREATION and the buyer-profile save are deliberately NOT here: both
|
|
16
22
|
* are host domain (the cart, the account) and reach the flow as ports
|
|
17
23
|
* (`createOrder` / `saveBuyerContact` on `CheckoutFlowProps`).
|
|
@@ -24,6 +30,7 @@
|
|
|
24
30
|
import type { SavedCard } from "../../card";
|
|
25
31
|
import type { Result } from "../../result";
|
|
26
32
|
|
|
33
|
+
import type { CheckoutTransportCopy } from "./screens-copy";
|
|
27
34
|
import { createCheckoutClient } from "./transport";
|
|
28
35
|
import type {
|
|
29
36
|
ChargeCardInput,
|
|
@@ -34,14 +41,23 @@ import type {
|
|
|
34
41
|
} from "./types";
|
|
35
42
|
|
|
36
43
|
/**
|
|
37
|
-
* The default binding: `/api/checkout` on the ambient `fetch
|
|
38
|
-
*
|
|
44
|
+
* The default binding: `/api/checkout` on the ambient `fetch`, saying what the
|
|
45
|
+
* caller's copy says when the wire fails.
|
|
46
|
+
*
|
|
47
|
+
* Built per call rather than once, because the words are now an argument. It
|
|
48
|
+
* costs one closure and resolves `fetch` per call either way, so a suite that
|
|
49
|
+
* stubs the global still wins.
|
|
39
50
|
*/
|
|
40
|
-
|
|
51
|
+
function defaultClient(copy: CheckoutTransportCopy) {
|
|
52
|
+
return createCheckoutClient({ copy });
|
|
53
|
+
}
|
|
41
54
|
|
|
42
55
|
/** Poll an order's reconciled status (async provider webhook confirmation). */
|
|
43
|
-
export async function pollOrderStatus(
|
|
44
|
-
|
|
56
|
+
export async function pollOrderStatus(
|
|
57
|
+
orderId: string,
|
|
58
|
+
copy: CheckoutTransportCopy,
|
|
59
|
+
): Promise<Result<OrderStatus>> {
|
|
60
|
+
return defaultClient(copy).getStatus(orderId);
|
|
45
61
|
}
|
|
46
62
|
|
|
47
63
|
/**
|
|
@@ -52,8 +68,9 @@ export async function pollOrderStatus(orderId: string): Promise<Result<OrderStat
|
|
|
52
68
|
*/
|
|
53
69
|
export async function fetchCheckoutConfig(
|
|
54
70
|
tenantSlug: string,
|
|
71
|
+
copy: CheckoutTransportCopy,
|
|
55
72
|
): Promise<Result<CheckoutProviderConfig>> {
|
|
56
|
-
return defaultClient.getConfig(tenantSlug);
|
|
73
|
+
return defaultClient(copy).getConfig(tenantSlug);
|
|
57
74
|
}
|
|
58
75
|
|
|
59
76
|
/**
|
|
@@ -63,10 +80,11 @@ export async function fetchCheckoutConfig(
|
|
|
63
80
|
* (the web page resolved it server-side) and for the FUT-174 rotated-key
|
|
64
81
|
* self-heal retry.
|
|
65
82
|
*/
|
|
66
|
-
export async function refreshCardPublicKey(
|
|
67
|
-
orderId: string
|
|
68
|
-
|
|
69
|
-
|
|
83
|
+
export async function refreshCardPublicKey(
|
|
84
|
+
input: { orderId: string },
|
|
85
|
+
copy: CheckoutTransportCopy,
|
|
86
|
+
): Promise<Result<{ publicKey: string | null }>> {
|
|
87
|
+
return defaultClient(copy).refreshBrowserKey(input);
|
|
70
88
|
}
|
|
71
89
|
|
|
72
90
|
/**
|
|
@@ -74,8 +92,11 @@ export async function refreshCardPublicKey(input: {
|
|
|
74
92
|
* `hostedCheckoutUrl` when the provider demands the buyer finish on its own
|
|
75
93
|
* page (3-D Secure, FUT-698) — the caller then hands the buyer over.
|
|
76
94
|
*/
|
|
77
|
-
export async function chargeCard(
|
|
78
|
-
|
|
95
|
+
export async function chargeCard(
|
|
96
|
+
input: ChargeCardInput,
|
|
97
|
+
copy: CheckoutTransportCopy,
|
|
98
|
+
): Promise<Result<ChargeOutcome>> {
|
|
99
|
+
return defaultClient(copy).charge(input);
|
|
79
100
|
}
|
|
80
101
|
|
|
81
102
|
/**
|
|
@@ -83,11 +104,17 @@ export async function chargeCard(input: ChargeCardInput): Promise<Result<ChargeO
|
|
|
83
104
|
* `/charge` route as {@link chargeCard}, carrying `wallet: { type, key }` in
|
|
84
105
|
* place of a card token.
|
|
85
106
|
*/
|
|
86
|
-
export async function chargeWallet(
|
|
87
|
-
|
|
107
|
+
export async function chargeWallet(
|
|
108
|
+
input: ChargeWalletInput,
|
|
109
|
+
copy: CheckoutTransportCopy,
|
|
110
|
+
): Promise<Result<ChargeOutcome>> {
|
|
111
|
+
return defaultClient(copy).chargeWallet(input);
|
|
88
112
|
}
|
|
89
113
|
|
|
90
114
|
/** List saved cards available for reuse (empty on any error — non-blocking). */
|
|
91
|
-
export async function listSavedCards(
|
|
92
|
-
|
|
115
|
+
export async function listSavedCards(
|
|
116
|
+
copy: CheckoutTransportCopy,
|
|
117
|
+
tenantSlug?: string,
|
|
118
|
+
): Promise<SavedCard[]> {
|
|
119
|
+
return defaultClient(copy).listInstruments(tenantSlug);
|
|
93
120
|
}
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import { createContext, useContext, type JSX, type ReactNode } from 'react';
|
|
4
|
+
|
|
5
|
+
import type { CardCopy } from '../../card/copy';
|
|
6
|
+
import type { CheckoutScreensCopy } from './screens-copy';
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* The buyer checkout's words, for the screens too deep to reach by prop
|
|
10
|
+
* (FUT-760).
|
|
11
|
+
*
|
|
12
|
+
* The existing `CheckoutViewCopy` is a required PROP, and that is still the
|
|
13
|
+
* right shape for the flow's own screens — a host wiring the checkout passes
|
|
14
|
+
* it at the one call that mounts them. This context exists for the parts that
|
|
15
|
+
* prop cannot reach without threading it through four intermediate
|
|
16
|
+
* components: the card fields, the wallet panes, the method tiles.
|
|
17
|
+
*
|
|
18
|
+
* It sits beside `CheckoutComponentsProvider` because the two answer the same
|
|
19
|
+
* kind of question — a host's design system there, a host's language here —
|
|
20
|
+
* and a screen that has one always has the other.
|
|
21
|
+
*/
|
|
22
|
+
export interface CheckoutCopy {
|
|
23
|
+
/** The card form and its tokenizers. */
|
|
24
|
+
card: CardCopy;
|
|
25
|
+
/** The buyer-details step's own fields and their refusals. */
|
|
26
|
+
buyer: BuyerInfoCopy;
|
|
27
|
+
/**
|
|
28
|
+
* Everything inside the steps — the method tiles, the PIX and card panes,
|
|
29
|
+
* the wallet buttons, the hosted handover, and the refusals any of them can
|
|
30
|
+
* produce.
|
|
31
|
+
*/
|
|
32
|
+
screens: CheckoutScreensCopy;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* The buyer-details step: what it asks for, and what it says when the answer
|
|
37
|
+
* will not do.
|
|
38
|
+
*/
|
|
39
|
+
export interface BuyerInfoCopy {
|
|
40
|
+
emailInvalid: string;
|
|
41
|
+
emailRequired: string;
|
|
42
|
+
nameRequired: string;
|
|
43
|
+
phoneRequired: string;
|
|
44
|
+
/**
|
|
45
|
+
* The hint above the fields, built from what the store's provider actually
|
|
46
|
+
* DEMANDS — which fields those are is the package's answer, and it changes
|
|
47
|
+
* per store.
|
|
48
|
+
*
|
|
49
|
+
* A function over the required field names, not a template with a slot: the
|
|
50
|
+
* sentence agrees its adjective with how many there are ("obrigatório" /
|
|
51
|
+
* "obrigatórios"), and a `{fields}` hole would have this package deciding
|
|
52
|
+
* that agreement for every language.
|
|
53
|
+
*/
|
|
54
|
+
fieldsHint(requiredFieldNames: readonly string[]): string;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
const CheckoutCopyContext = createContext<CheckoutCopy | null>(null);
|
|
58
|
+
|
|
59
|
+
export function CheckoutCopyProvider({
|
|
60
|
+
copy,
|
|
61
|
+
children,
|
|
62
|
+
}: {
|
|
63
|
+
copy: CheckoutCopy;
|
|
64
|
+
children: ReactNode;
|
|
65
|
+
}): JSX.Element {
|
|
66
|
+
return <CheckoutCopyContext.Provider value={copy}>{children}</CheckoutCopyContext.Provider>;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* The words this checkout renders — THROWS outside a provider rather than
|
|
71
|
+
* falling back.
|
|
72
|
+
*
|
|
73
|
+
* A fallback could only be the origin host's Portuguese, handed silently to
|
|
74
|
+
* the next adopter's shopper. Failing at the mount is the whole point: it is
|
|
75
|
+
* the one moment a host can still be told it forgot.
|
|
76
|
+
*/
|
|
77
|
+
export function useCheckoutCopy(): CheckoutCopy {
|
|
78
|
+
const copy = useContext(CheckoutCopyContext);
|
|
79
|
+
if (!copy) {
|
|
80
|
+
throw new Error('useCheckoutCopy must be rendered inside a <CheckoutCopyProvider>');
|
|
81
|
+
}
|
|
82
|
+
return copy;
|
|
83
|
+
}
|