@12-apps/payments-frontend 3.9.0 → 3.11.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 +117 -0
- package/src/card/cpf.ts +12 -5
- package/src/card/fields.tsx +17 -12
- package/src/card/format.ts +33 -17
- package/src/card/index.ts +3 -1
- package/src/card/pt-BR.ts +52 -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 +24 -19
- 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/ProviderSetupGuide.tsx +3 -1
- 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 +312 -0
- package/src/components/settings-pt-BR.ts +172 -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,201 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Every sentence the buyer's checkout SCREENS render (FUT-760).
|
|
3
|
+
*
|
|
4
|
+
* The step wrappers already took theirs through `CheckoutViewCopy`; this is
|
|
5
|
+
* what sits inside them — the method tiles, the PIX and card panes, the wallet
|
|
6
|
+
* buttons, the hosted handover, and the refusals any of them can produce.
|
|
7
|
+
*
|
|
8
|
+
* Required, with no defaults. The split is the one the whole port draws: WHICH
|
|
9
|
+
* situation the buyer is in — a charge still settling, one taking longer than
|
|
10
|
+
* it should, a wallet this store cannot start, a provider page they are about
|
|
11
|
+
* to be sent to — is knowledge of the payment lifecycle, and it stays in the
|
|
12
|
+
* package. The words are the host's.
|
|
13
|
+
*
|
|
14
|
+
* Two members are LOCALES rather than sentences (`pix.expiryLocale`,
|
|
15
|
+
* `wallet.googlePay.buttonLocale`). They are here for the same reason: both
|
|
16
|
+
* were hard-coded to Brazilian Portuguese, and both decide what a buyer reads
|
|
17
|
+
* — one formats the PIX expiry clock, the other is the language Google draws
|
|
18
|
+
* its own button in. A host that translates every string below and still ships
|
|
19
|
+
* `"pt"` to Google has a checkout in two languages.
|
|
20
|
+
*/
|
|
21
|
+
|
|
22
|
+
/** The method tiles, and what each one promises. */
|
|
23
|
+
export interface MethodPickerCopy {
|
|
24
|
+
/** The group's heading, and its `aria-label` — one string, said once. */
|
|
25
|
+
groupLabel: string;
|
|
26
|
+
/**
|
|
27
|
+
* What each method is CALLED. "PIX" is a Brazilian instrument and keeps its
|
|
28
|
+
* name everywhere, the way "Visa" does; "Cartão" is just the word for card,
|
|
29
|
+
* and was the tile's label — so both are stated rather than one assumed.
|
|
30
|
+
*/
|
|
31
|
+
pixLabel: string;
|
|
32
|
+
cardLabel: string;
|
|
33
|
+
/** What each method is like to use — shown under its name. */
|
|
34
|
+
pixDescription: string;
|
|
35
|
+
cardDescription: string;
|
|
36
|
+
/** This store's providers cannot take that method. */
|
|
37
|
+
unavailableHere: string;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* What a charge in flight says, shared by the PIX, card and wallet panes.
|
|
42
|
+
*
|
|
43
|
+
* One slice rather than three, because these ARE the same four states — and
|
|
44
|
+
* three copies of them is how a product ends up telling a buyer two different
|
|
45
|
+
* things about one situation depending on which button they pressed.
|
|
46
|
+
*/
|
|
47
|
+
export interface SettlingCopy {
|
|
48
|
+
/** The provider answered, and the answer was not a payment. */
|
|
49
|
+
cannotConfirm: string;
|
|
50
|
+
/** Still open past the point where a buyer starts to worry. */
|
|
51
|
+
takingLonger: string;
|
|
52
|
+
/**
|
|
53
|
+
* The one sentence in here that prevents money moving twice: a buyer who
|
|
54
|
+
* pays again because the screen went quiet has been charged twice, and only
|
|
55
|
+
* this line stands between them and that.
|
|
56
|
+
*/
|
|
57
|
+
takingLongerHelp: string;
|
|
58
|
+
/** The charge is on its way to the provider. */
|
|
59
|
+
processing: string;
|
|
60
|
+
/** It arrived, and settlement is being confirmed. */
|
|
61
|
+
confirming: string;
|
|
62
|
+
/** It came back refused. */
|
|
63
|
+
cannotPay: string;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/** The PIX pane: the QR, the copyable code, and the wait. */
|
|
67
|
+
export interface PixPaneCopy {
|
|
68
|
+
heading: string;
|
|
69
|
+
/** What to do with the QR, carrying the order total the buyer is paying. */
|
|
70
|
+
instructions(totalLabel: string): string;
|
|
71
|
+
/** The QR image's alternative text — the only thing a screen reader gets. */
|
|
72
|
+
qrAlt: string;
|
|
73
|
+
copyAction: string;
|
|
74
|
+
/** The same button for the two seconds after a successful copy. */
|
|
75
|
+
copiedAction: string;
|
|
76
|
+
/**
|
|
77
|
+
* How long the code lasts. Takes the already-formatted clock time, and
|
|
78
|
+
* {@link expiryLocale} is what formatted it.
|
|
79
|
+
*/
|
|
80
|
+
validUntil(time: string): string;
|
|
81
|
+
/**
|
|
82
|
+
* The BCP-47 tag the expiry clock is formatted in — `'pt-BR'` here, and a
|
|
83
|
+
* hard-coded one before this port. Beside the sentence it feeds rather than
|
|
84
|
+
* in a config elsewhere, because they are one decision.
|
|
85
|
+
*/
|
|
86
|
+
expiryLocale: string;
|
|
87
|
+
/** The live footer while nothing has been paid yet. */
|
|
88
|
+
awaiting: string;
|
|
89
|
+
/** The order came back with no PIX charge on it — nothing to show. */
|
|
90
|
+
chargeMissing: string;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/** The card pane's own heading. Everything else in it is {@link SettlingCopy}. */
|
|
94
|
+
export interface CardPaneCopy {
|
|
95
|
+
heading: string;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/** Who is paying, as the checkout has understood it. */
|
|
99
|
+
export interface PayerSummaryCopy {
|
|
100
|
+
taxId(formatted: string): string;
|
|
101
|
+
/** The store already holds this CPF — nothing to re-enter. */
|
|
102
|
+
taxIdAlreadyKnown: string;
|
|
103
|
+
payingAs(name: string): string;
|
|
104
|
+
payingWithSavedDetails: string;
|
|
105
|
+
/** Reopen the buyer-details step to pay as somebody else. */
|
|
106
|
+
changeAction: string;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/** The refusal panel, and what a buyer can do about it. */
|
|
110
|
+
export interface PaymentErrorCopy {
|
|
111
|
+
confirming: string;
|
|
112
|
+
cannotContinue: string;
|
|
113
|
+
/** Only offered for a refusal that is SAFE to retry — never an unresolved one. */
|
|
114
|
+
retryAction: string;
|
|
115
|
+
emailLabel: string;
|
|
116
|
+
/** Why the store's own address will not do. */
|
|
117
|
+
emailMustDifferHint: string;
|
|
118
|
+
useEmailAction: string;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/** The two wallet buttons — what they say when they cannot run. */
|
|
122
|
+
export interface WalletCopy {
|
|
123
|
+
applePay: {
|
|
124
|
+
/** The line item Apple's own sheet shows above the amount. */
|
|
125
|
+
orderTotal: string;
|
|
126
|
+
/** This store cannot offer Apple Pay at all. */
|
|
127
|
+
cannotStart: string;
|
|
128
|
+
/** It started and failed. */
|
|
129
|
+
cannotComplete: string;
|
|
130
|
+
payAction: string;
|
|
131
|
+
};
|
|
132
|
+
googlePay: {
|
|
133
|
+
cannotComplete: string;
|
|
134
|
+
/**
|
|
135
|
+
* The language Google draws its button's own label in. Google owns those
|
|
136
|
+
* pixels, so this is the only say a host has over them.
|
|
137
|
+
*/
|
|
138
|
+
buttonLocale: string;
|
|
139
|
+
};
|
|
140
|
+
/** The divider under the wallet buttons, pointing at the form below. */
|
|
141
|
+
orPayWithCard: string;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
/** The interstitial before a buyer leaves for a provider's own page. */
|
|
145
|
+
export interface HostedHandoverCopy {
|
|
146
|
+
/**
|
|
147
|
+
* Where they are going. Named when the adapter says who it is, generic
|
|
148
|
+
* otherwise — "a página de pagamento do provedor" is the same sentence with
|
|
149
|
+
* the one useful word removed.
|
|
150
|
+
*/
|
|
151
|
+
destinationNamed(displayName: string): string;
|
|
152
|
+
destinationGeneric: string;
|
|
153
|
+
/** The methods the hosted page will offer, when we know them. */
|
|
154
|
+
methodsChoice(methods: string): string;
|
|
155
|
+
/** Which methods those are — read from what the provider actually declared. */
|
|
156
|
+
pixAndCard: string;
|
|
157
|
+
pixOnly: string;
|
|
158
|
+
cardOnly: string;
|
|
159
|
+
/** The whole sentence, composed from the two above. */
|
|
160
|
+
handoff(destination: string, choice: string): string;
|
|
161
|
+
/** What happens after they pay over there. */
|
|
162
|
+
afterwards: string;
|
|
163
|
+
startAction: string;
|
|
164
|
+
preparing: string;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
/** What the browser says when the wire itself failed. */
|
|
168
|
+
export interface CheckoutTransportCopy {
|
|
169
|
+
/** A refusal whose envelope carried no sentence of its own. */
|
|
170
|
+
failed: string;
|
|
171
|
+
/** A 2xx that was not the shape the route promises. */
|
|
172
|
+
invalidResponse: string;
|
|
173
|
+
/** The request never got out — the one of the three a buyer really meets. */
|
|
174
|
+
offline: string;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
/** The buyer-details validators, field by field. */
|
|
178
|
+
export interface CheckoutValidationCopy {
|
|
179
|
+
taxIdInvalid: string;
|
|
180
|
+
nameRequired: string;
|
|
181
|
+
emailInvalid: string;
|
|
182
|
+
phoneInvalid: string;
|
|
183
|
+
/** Any other declared field the store demands and the buyer left empty. */
|
|
184
|
+
required: string;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
/** Everything above, in the shape the checkout context carries. */
|
|
188
|
+
export interface CheckoutScreensCopy {
|
|
189
|
+
method: MethodPickerCopy;
|
|
190
|
+
settling: SettlingCopy;
|
|
191
|
+
pix: PixPaneCopy;
|
|
192
|
+
card: CardPaneCopy;
|
|
193
|
+
payer: PayerSummaryCopy;
|
|
194
|
+
error: PaymentErrorCopy;
|
|
195
|
+
wallet: WalletCopy;
|
|
196
|
+
hosted: HostedHandoverCopy;
|
|
197
|
+
transport: CheckoutTransportCopy;
|
|
198
|
+
validation: CheckoutValidationCopy;
|
|
199
|
+
/** The step wrapper's own wait, while the charge is being raised. */
|
|
200
|
+
generatingPayment: string;
|
|
201
|
+
}
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
import type { CheckoutScreensCopy } from './screens-copy';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* The pt-BR pack for the buyer's checkout screens — a NAMED constant a host
|
|
5
|
+
* passes by hand, never a default.
|
|
6
|
+
*
|
|
7
|
+
* The filename is what exempts this file from the copy-portability gate:
|
|
8
|
+
* Portuguese may ship, it may not be silent. Every sentence is VERBATIM what
|
|
9
|
+
* the screens used to render, so a host adopting it sees no change — what
|
|
10
|
+
* changes is that the words are chosen in a diff.
|
|
11
|
+
*/
|
|
12
|
+
export const PT_BR_CHECKOUT_SCREENS_COPY: CheckoutScreensCopy = {
|
|
13
|
+
method: {
|
|
14
|
+
groupLabel: 'Forma de pagamento',
|
|
15
|
+
pixLabel: 'PIX',
|
|
16
|
+
cardLabel: 'Cartão',
|
|
17
|
+
pixDescription: 'Aprovação imediata',
|
|
18
|
+
cardDescription: 'Crédito à vista',
|
|
19
|
+
unavailableHere: 'Indisponível nesta loja',
|
|
20
|
+
},
|
|
21
|
+
settling: {
|
|
22
|
+
cannotConfirm: 'Não foi possível confirmar o pagamento',
|
|
23
|
+
takingLonger: 'O pagamento está demorando mais que o esperado',
|
|
24
|
+
takingLongerHelp:
|
|
25
|
+
'Você pode aguardar ou verificar seu pedido em instantes — não realize um novo pagamento.',
|
|
26
|
+
processing: 'Processando pagamento…',
|
|
27
|
+
confirming: 'Estamos confirmando seu pagamento',
|
|
28
|
+
cannotPay: 'Não foi possível pagar',
|
|
29
|
+
},
|
|
30
|
+
pix: {
|
|
31
|
+
heading: 'Pague com PIX',
|
|
32
|
+
instructions: (totalLabel) =>
|
|
33
|
+
`Escaneie o QR code no app do seu banco ou copie o código. Total ${totalLabel}.`,
|
|
34
|
+
qrAlt: 'QR Code PIX para pagamento',
|
|
35
|
+
copyAction: 'Copiar',
|
|
36
|
+
copiedAction: 'Copiado!',
|
|
37
|
+
validUntil: (time) => `Válido até ${time}. A confirmação é automática.`,
|
|
38
|
+
expiryLocale: 'pt-BR',
|
|
39
|
+
awaiting: 'Aguardando pagamento…',
|
|
40
|
+
chargeMissing: 'Não foi possível gerar o código PIX.',
|
|
41
|
+
},
|
|
42
|
+
card: {
|
|
43
|
+
heading: 'Pague com cartão',
|
|
44
|
+
},
|
|
45
|
+
payer: {
|
|
46
|
+
taxId: (formatted) => `CPF ${formatted}`,
|
|
47
|
+
taxIdAlreadyKnown: 'CPF já cadastrado',
|
|
48
|
+
payingAs: (name) => `Pagando como ${name}`,
|
|
49
|
+
payingWithSavedDetails: 'Pagando com os seus dados salvos',
|
|
50
|
+
changeAction: 'Alterar',
|
|
51
|
+
},
|
|
52
|
+
error: {
|
|
53
|
+
confirming: 'Estamos confirmando seu pagamento',
|
|
54
|
+
cannotContinue: 'Não foi possível continuar',
|
|
55
|
+
retryAction: 'Tentar novamente',
|
|
56
|
+
emailLabel: 'E-mail para o pagamento',
|
|
57
|
+
emailMustDifferHint: 'use um e-mail diferente do da loja',
|
|
58
|
+
useEmailAction: 'Usar este e-mail e continuar',
|
|
59
|
+
},
|
|
60
|
+
wallet: {
|
|
61
|
+
applePay: {
|
|
62
|
+
orderTotal: 'Total do pedido',
|
|
63
|
+
cannotStart: 'Não foi possível iniciar o Apple Pay nesta loja. Pague com cartão.',
|
|
64
|
+
cannotComplete:
|
|
65
|
+
'Não foi possível iniciar o Apple Pay. Tente novamente ou pague com cartão.',
|
|
66
|
+
payAction: 'Pagar com Apple Pay',
|
|
67
|
+
},
|
|
68
|
+
googlePay: {
|
|
69
|
+
cannotComplete:
|
|
70
|
+
'Não foi possível concluir o pagamento com o Google Pay. Tente novamente ou pague com cartão.',
|
|
71
|
+
buttonLocale: 'pt',
|
|
72
|
+
},
|
|
73
|
+
orPayWithCard: 'ou pague com cartão',
|
|
74
|
+
},
|
|
75
|
+
hosted: {
|
|
76
|
+
destinationNamed: (displayName) => `à página de pagamento da ${displayName}`,
|
|
77
|
+
destinationGeneric: 'à página de pagamento segura do provedor',
|
|
78
|
+
methodsChoice: (methods) => `, onde você escolhe pagar com ${methods}`,
|
|
79
|
+
pixAndCard: 'PIX ou cartão',
|
|
80
|
+
pixOnly: 'PIX',
|
|
81
|
+
cardOnly: 'cartão',
|
|
82
|
+
handoff: (destination, choice) => `Você será levado ${destination}${choice}.`,
|
|
83
|
+
afterwards:
|
|
84
|
+
'Assim que o pagamento for concluído, você volta para cá e nós confirmamos o pedido.',
|
|
85
|
+
startAction: 'Seguir para o pagamento',
|
|
86
|
+
preparing: 'Preparando o pagamento',
|
|
87
|
+
},
|
|
88
|
+
transport: {
|
|
89
|
+
failed: 'Não foi possível concluir a operação. Tente novamente.',
|
|
90
|
+
invalidResponse: 'Resposta inválida do servidor.',
|
|
91
|
+
offline: 'Não foi possível conectar. Verifique sua conexão e tente novamente.',
|
|
92
|
+
},
|
|
93
|
+
validation: {
|
|
94
|
+
taxIdInvalid: 'CPF inválido.',
|
|
95
|
+
nameRequired: 'Informe seu nome.',
|
|
96
|
+
emailInvalid: 'E-mail inválido.',
|
|
97
|
+
phoneInvalid: 'Telefone inválido.',
|
|
98
|
+
required: 'Campo obrigatório.',
|
|
99
|
+
},
|
|
100
|
+
generatingPayment: 'Gerando pagamento…',
|
|
101
|
+
};
|
|
@@ -20,6 +20,8 @@ import type { BuyerVaultSession, VaultedCardDisplay } from "@12-apps/payments-ba
|
|
|
20
20
|
import type { SavedCard } from "../../card";
|
|
21
21
|
import { err, ok, type Result } from "../../result";
|
|
22
22
|
|
|
23
|
+
import type { CheckoutTransportCopy } from "./screens-copy";
|
|
24
|
+
|
|
23
25
|
import type {
|
|
24
26
|
ChargeCardInput,
|
|
25
27
|
ChargeOutcome,
|
|
@@ -59,6 +61,16 @@ export const DEFAULT_CHECKOUT_BASE_URL = "/api/checkout";
|
|
|
59
61
|
|
|
60
62
|
/** Where the `createPaymentFlowsBE` mount lives, and how to reach it. */
|
|
61
63
|
export interface CheckoutTransport {
|
|
64
|
+
/**
|
|
65
|
+
* What the buyer reads when the WIRE failed and the server sent no sentence
|
|
66
|
+
* of its own (FUT-760) — required, and with no default.
|
|
67
|
+
*
|
|
68
|
+
* Three sentences, and only one of them is common: `offline`, which is what
|
|
69
|
+
* a buyer on a dropped connection sees. That is exactly why it may not be
|
|
70
|
+
* this package's Portuguese — it is the failure most likely to be the first
|
|
71
|
+
* thing an adopter's shopper ever reads from us.
|
|
72
|
+
*/
|
|
73
|
+
copy: CheckoutTransportCopy;
|
|
62
74
|
/**
|
|
63
75
|
* Prefix for `/config`, `/status`, `/charge`, `/cards`, `/cards/begin`,
|
|
64
76
|
* `/cards/complete`, `/refresh-key`.
|
|
@@ -73,6 +85,17 @@ export interface CheckoutTransport {
|
|
|
73
85
|
headers?: () => HeadersInit | Promise<HeadersInit>;
|
|
74
86
|
}
|
|
75
87
|
|
|
88
|
+
/**
|
|
89
|
+
* A transport whose WORDS some surrounding config already answers.
|
|
90
|
+
*
|
|
91
|
+
* `createPaymentFlows` takes one: its `copy` covers the whole checkout, so
|
|
92
|
+
* asking a host to repeat the three transport sentences beside `fetchImpl`
|
|
93
|
+
* would be the same question twice. Naming `copy` here still wins, for a mount
|
|
94
|
+
* that talks to a different surface in a different voice.
|
|
95
|
+
*/
|
|
96
|
+
export type CheckoutTransportBinding = Omit<CheckoutTransport, "copy"> &
|
|
97
|
+
Partial<Pick<CheckoutTransport, "copy">>;
|
|
98
|
+
|
|
76
99
|
/** The eight calls the buyer checkout makes, pre-bound to a {@link CheckoutTransport}. */
|
|
77
100
|
export interface CheckoutClient {
|
|
78
101
|
getConfig(tenantSlug: string): Promise<Result<CheckoutProviderConfig>>;
|
|
@@ -115,8 +138,8 @@ interface ApiEnvelope<T> {
|
|
|
115
138
|
* The message is what the buyer reads; the code is what a surface uses to
|
|
116
139
|
* decide how to PRESENT it, which a message cannot be parsed for.
|
|
117
140
|
*/
|
|
118
|
-
function refused<T>(json: ApiEnvelope<T> | null): Result<T> {
|
|
119
|
-
return err(json?.error ??
|
|
141
|
+
function refused<T>(json: ApiEnvelope<T> | null, copy: CheckoutTransportCopy): Result<T> {
|
|
142
|
+
return err(json?.error ?? copy.failed, json?.code);
|
|
120
143
|
}
|
|
121
144
|
|
|
122
145
|
/**
|
|
@@ -200,11 +223,14 @@ function completeVaultBody(input: CompleteVaultInput): string {
|
|
|
200
223
|
/**
|
|
201
224
|
* The checkout calls, bound to one transport.
|
|
202
225
|
*
|
|
203
|
-
*
|
|
204
|
-
* `client.ts` have always done
|
|
226
|
+
* `baseUrl` and `fetchImpl` still default to what the free functions in
|
|
227
|
+
* `client.ts` have always done — `/api/checkout/**` on the ambient `fetch`.
|
|
228
|
+
* `copy` does NOT default, because there is no language this package could
|
|
229
|
+
* pick that would be right for the next host (FUT-760).
|
|
205
230
|
*/
|
|
206
|
-
export function createCheckoutClient(transport: CheckoutTransport
|
|
231
|
+
export function createCheckoutClient(transport: CheckoutTransport): CheckoutClient {
|
|
207
232
|
const baseUrl = transport.baseUrl ?? DEFAULT_CHECKOUT_BASE_URL;
|
|
233
|
+
const copy = transport.copy;
|
|
208
234
|
|
|
209
235
|
/** Call a checkout route and normalize the response into a {@link Result}. */
|
|
210
236
|
async function call<T>(route: string, init?: RequestInit): Promise<Result<T>> {
|
|
@@ -218,13 +244,13 @@ export function createCheckoutClient(transport: CheckoutTransport = {}): Checkou
|
|
|
218
244
|
headers: { "Content-Type": "application/json", ...extra, ...init?.headers },
|
|
219
245
|
});
|
|
220
246
|
const json = (await res.json().catch(() => null)) as ApiEnvelope<T> | null;
|
|
221
|
-
if (!res.ok) return refused(json);
|
|
247
|
+
if (!res.ok) return refused(json, copy);
|
|
222
248
|
if (!json || json.data === undefined) {
|
|
223
|
-
return err(json?.error ??
|
|
249
|
+
return err(json?.error ?? copy.invalidResponse);
|
|
224
250
|
}
|
|
225
251
|
return ok(json.data);
|
|
226
252
|
} catch {
|
|
227
|
-
return err(
|
|
253
|
+
return err(copy.offline);
|
|
228
254
|
}
|
|
229
255
|
}
|
|
230
256
|
|
|
@@ -28,6 +28,8 @@ import { useCheckoutNavigate, type CheckoutNavigate } from "./navigate-context";
|
|
|
28
28
|
import type { CardChainLink } from "./method-capability";
|
|
29
29
|
import type { BuyerInfo, CheckoutOrder, OrderStatus } from "./types";
|
|
30
30
|
import { usePaymentPolling } from "./use-payment-polling";
|
|
31
|
+
import type { CardCopy } from "../../card/copy";
|
|
32
|
+
import { useCheckoutCopy } from "./copy-context";
|
|
31
33
|
|
|
32
34
|
const EMPTY_CARD: CardDetails = { number: "", holder: "", expiry: "", cvv: "" };
|
|
33
35
|
|
|
@@ -190,6 +192,8 @@ async function resolveInstruments(input: {
|
|
|
190
192
|
providerChain: readonly CardChainLink[];
|
|
191
193
|
/** The bound key-refresh call (FUT-741) — the self-heal must hit OUR mount. */
|
|
192
194
|
refreshKey: RefreshBrowserKey;
|
|
195
|
+
/** The words a failed mint reports with — the host's (FUT-760). */
|
|
196
|
+
copy: CardCopy;
|
|
193
197
|
}): Promise<Result<CardInstruments>> {
|
|
194
198
|
const { form } = input;
|
|
195
199
|
if (!form.usingNewCard) return ok({ token: form.selection });
|
|
@@ -200,7 +204,7 @@ async function resolveInstruments(input: {
|
|
|
200
204
|
input.onKeyRefreshed,
|
|
201
205
|
form.saveCard,
|
|
202
206
|
input.providerChain,
|
|
203
|
-
|
|
207
|
+
input.copy,
|
|
204
208
|
input.refreshKey,
|
|
205
209
|
);
|
|
206
210
|
}
|
|
@@ -228,6 +232,7 @@ function useCardSubmit(
|
|
|
228
232
|
const { publicKey, setPublicKey } = useCardPublicKey(order.orderId, providerConfig);
|
|
229
233
|
const client = useCheckoutClientApi();
|
|
230
234
|
const navigate = useCheckoutNavigate();
|
|
235
|
+
const cardCopy = useCheckoutCopy().card;
|
|
231
236
|
|
|
232
237
|
const { status, error: pollError, timedOut: pollTimedOut } = usePaymentPolling(order.orderId, {
|
|
233
238
|
enabled: submitted,
|
|
@@ -252,6 +257,7 @@ function useCardSubmit(
|
|
|
252
257
|
onKeyRefreshed: setPublicKey,
|
|
253
258
|
providerChain,
|
|
254
259
|
refreshKey: client.refreshBrowserKey,
|
|
260
|
+
copy: cardCopy,
|
|
255
261
|
});
|
|
256
262
|
if (!resolved.ok) {
|
|
257
263
|
setError(resolved.error);
|
|
@@ -315,11 +321,13 @@ export function useCardCheckout(
|
|
|
315
321
|
const brand = detectBrand(onlyDigits(card.number));
|
|
316
322
|
const usingNewCard = selection === NEW_CARD;
|
|
317
323
|
|
|
324
|
+
const cardCopy = useCheckoutCopy().card;
|
|
325
|
+
const fieldCopy = cardCopy.fields;
|
|
318
326
|
const validate = (): CardFieldErrors => ({
|
|
319
|
-
number: validateCardNumber(card.number),
|
|
320
|
-
holder: validateHolder(card.holder),
|
|
321
|
-
expiry: validateExpiry(card.expiry),
|
|
322
|
-
cvv: validateCvv(card.cvv, brand),
|
|
327
|
+
number: validateCardNumber(card.number, fieldCopy),
|
|
328
|
+
holder: validateHolder(card.holder, fieldCopy),
|
|
329
|
+
expiry: validateExpiry(card.expiry, fieldCopy),
|
|
330
|
+
cvv: validateCvv(card.cvv, fieldCopy, brand),
|
|
323
331
|
});
|
|
324
332
|
|
|
325
333
|
const submit = useCardSubmit(
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { useCallback, useEffect, useMemo, useState, type Dispatch, type SetStateAction } from "react";
|
|
2
2
|
|
|
3
|
-
import {
|
|
3
|
+
import { buyerGateError } from "./buyer-gate";
|
|
4
|
+
import { useCheckoutCopy } from "./copy-context";
|
|
4
5
|
|
|
5
6
|
import { rememberHostedOrder, takeHostedOrder } from "./hosted-return";
|
|
6
7
|
import { useCheckoutNavigate, type CheckoutNavigate } from "./navigate-context";
|
|
@@ -88,51 +89,6 @@ function useCheckoutNav(
|
|
|
88
89
|
return { back, editBuyer: taxIdOnFile ? openDados : undefined };
|
|
89
90
|
}
|
|
90
91
|
|
|
91
|
-
/** The message shown when a declared field is missing or malformed. */
|
|
92
|
-
const FIELD_COMPLAINT: Record<string, string> = {
|
|
93
|
-
taxId: "CPF inválido.",
|
|
94
|
-
name: "Informe seu nome.",
|
|
95
|
-
email: "E-mail inválido.",
|
|
96
|
-
phone: "Telefone inválido.",
|
|
97
|
-
};
|
|
98
|
-
|
|
99
|
-
/** Which input to highlight for a declared key. */
|
|
100
|
-
const FIELD_INPUT: Record<string, BuyerField> = {
|
|
101
|
-
taxId: "cpf",
|
|
102
|
-
name: "name",
|
|
103
|
-
email: "email",
|
|
104
|
-
phone: "phone",
|
|
105
|
-
};
|
|
106
|
-
|
|
107
|
-
/**
|
|
108
|
-
* What the "Continuar" gate objects to, or undefined to advance.
|
|
109
|
-
*
|
|
110
|
-
* The fields come from the chain's own declaration (FUT-595) — absent, they
|
|
111
|
-
* degrade to CPF-required, which is exactly what this gate has always demanded.
|
|
112
|
-
*
|
|
113
|
-
* A blank CPF is only an error when the store has NO CPF for this buyer. With
|
|
114
|
-
* one on file the field starts empty by design (the client is never sent the
|
|
115
|
-
* saved CPF), so demanding one here trapped a returning buyer who opened Dados
|
|
116
|
-
* through "Alterar" and changed their mind: they could not reach Pagamento
|
|
117
|
-
* again, and back only led out to the menu. Leaving it blank means "charge me
|
|
118
|
-
* as before", which is exactly what the server's `resolveBuyerTaxId` does.
|
|
119
|
-
*/
|
|
120
|
-
function buyerGateError(
|
|
121
|
-
buyer: BuyerInfo,
|
|
122
|
-
fields: readonly CheckoutCustomerField[],
|
|
123
|
-
taxIdOnFile: boolean,
|
|
124
|
-
): { message: string; field: BuyerField } | undefined {
|
|
125
|
-
const effective = taxIdOnFile
|
|
126
|
-
? fields.filter((field) => !(field.key === "taxId" && !buyer.taxId?.trim()))
|
|
127
|
-
: fields;
|
|
128
|
-
const offending = buyerFormComplete(buyer, effective);
|
|
129
|
-
if (!offending) return undefined;
|
|
130
|
-
return {
|
|
131
|
-
message: FIELD_COMPLAINT[offending.key] ?? "Campo obrigatório.",
|
|
132
|
-
field: FIELD_INPUT[offending.key] ?? "cpf",
|
|
133
|
-
};
|
|
134
|
-
}
|
|
135
|
-
|
|
136
92
|
/**
|
|
137
93
|
* Where checkout opens: on the outcome for a buyer returning from a hosted
|
|
138
94
|
* provider, on Pagamento when their CPF is already on file, else on Dados.
|
|
@@ -319,6 +275,7 @@ export function useCheckoutController(
|
|
|
319
275
|
tenantSlug?: string,
|
|
320
276
|
) {
|
|
321
277
|
const { createOrder, saveBuyerContact, onExitToMenu, onPaid } = ports;
|
|
278
|
+
const validation = useCheckoutCopy().screens.validation;
|
|
322
279
|
const navigate = useCheckoutNavigate();
|
|
323
280
|
const resume = useHostedResume(tenantSlug);
|
|
324
281
|
const [step, setStep] = useState<Step>(initialStep(Boolean(resume.order), taxIdOnFile));
|
|
@@ -341,7 +298,7 @@ export function useCheckoutController(
|
|
|
341
298
|
}, [clearError]);
|
|
342
299
|
const goToPayment = useCallback(() => {
|
|
343
300
|
clearError();
|
|
344
|
-
const complaint = buyerGateError(buyer, buyerFields, taxIdOnFile);
|
|
301
|
+
const complaint = buyerGateError(validation, buyer, buyerFields, taxIdOnFile);
|
|
345
302
|
if (complaint) { failure.fail(complaint); return; }
|
|
346
303
|
// Persist the buyer's details HERE, on "Continuar" — not when a payment is
|
|
347
304
|
// raised. Everything after this step can fail (no provider configured, a
|
|
@@ -354,7 +311,7 @@ export function useCheckoutController(
|
|
|
354
311
|
saveBuyerContact?.({ name: buyer.name, phone: buyer.phone, taxId: buyer.taxId });
|
|
355
312
|
}
|
|
356
313
|
setStep("payment");
|
|
357
|
-
}, [buyer, buyerFields, saveProfile, taxIdOnFile, clearError, saveBuyerContact]);
|
|
314
|
+
}, [buyer, buyerFields, saveProfile, taxIdOnFile, clearError, saveBuyerContact, validation]);
|
|
358
315
|
const startPayment = useCallback(async (chosen: PaymentMethod, override?: BuyerInfo) => {
|
|
359
316
|
clearError();
|
|
360
317
|
setCreating(true);
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import type { CheckoutCopy } from "./copy-context";
|
|
2
|
+
|
|
1
3
|
/**
|
|
2
4
|
* Every string the legacy checkout views render — required props, with NO
|
|
3
5
|
* defaults, deliberately (the payments extraction's own doctrine, FUT-760):
|
|
@@ -87,4 +89,15 @@ export interface CheckoutViewCopy {
|
|
|
87
89
|
dados: DadosStepCopy;
|
|
88
90
|
emptyCart: EmptyCartCopy;
|
|
89
91
|
status: PaymentStatusCopy;
|
|
92
|
+
/**
|
|
93
|
+
* The words the screens BELOW these read — the card fields, the wallet
|
|
94
|
+
* panes, the buyer-details inputs (FUT-760).
|
|
95
|
+
*
|
|
96
|
+
* Carried here rather than as a second prop so a host still passes copy
|
|
97
|
+
* exactly once. `CheckoutFlow` mounts it as `CheckoutCopyProvider`, because
|
|
98
|
+
* threading it down through four intermediate components as props is how a
|
|
99
|
+
* copy port comes to exist, be required, and go unread — which is what
|
|
100
|
+
* happened to `unavailableWithRemedyTitle` before this port was finished.
|
|
101
|
+
*/
|
|
102
|
+
screens: CheckoutCopy;
|
|
90
103
|
}
|
|
@@ -3,6 +3,7 @@ import { useState, type JSX } from "react";
|
|
|
3
3
|
|
|
4
4
|
import { ApplePayButton, applePaySupported } from "./apple-pay-button";
|
|
5
5
|
import { CardView } from "./card-view";
|
|
6
|
+
import { useCheckoutCopy } from "./copy-context";
|
|
6
7
|
import { GooglePayButton } from "./google-pay-button";
|
|
7
8
|
import {
|
|
8
9
|
applePayDeclared,
|
|
@@ -37,11 +38,12 @@ type WalletPaneProps = ProviderCheckoutScreenProps & {
|
|
|
37
38
|
/** Post-submit confirmation, error > timeout > spinner — the card view's order. */
|
|
38
39
|
function WalletProcessing({ wallet }: { wallet: WalletCharge }): JSX.Element {
|
|
39
40
|
const { Alert, LoadingState } = useCheckoutComponents();
|
|
41
|
+
const copy = useCheckoutCopy().screens.settling;
|
|
40
42
|
if (wallet.pollError) {
|
|
41
43
|
return (
|
|
42
44
|
<Alert
|
|
43
45
|
variant="danger"
|
|
44
|
-
title=
|
|
46
|
+
title={copy.cannotConfirm}
|
|
45
47
|
description={wallet.pollError}
|
|
46
48
|
showIcon
|
|
47
49
|
data-testid="wallet-poll-error"
|
|
@@ -52,8 +54,8 @@ function WalletProcessing({ wallet }: { wallet: WalletCharge }): JSX.Element {
|
|
|
52
54
|
return (
|
|
53
55
|
<Alert
|
|
54
56
|
variant="warning"
|
|
55
|
-
title=
|
|
56
|
-
description=
|
|
57
|
+
title={copy.takingLonger}
|
|
58
|
+
description={copy.takingLongerHelp}
|
|
57
59
|
showIcon
|
|
58
60
|
data-testid="wallet-poll-timeout"
|
|
59
61
|
/>
|
|
@@ -63,7 +65,7 @@ function WalletProcessing({ wallet }: { wallet: WalletCharge }): JSX.Element {
|
|
|
63
65
|
<LoadingState
|
|
64
66
|
variant="spinner"
|
|
65
67
|
size="md"
|
|
66
|
-
message=
|
|
68
|
+
message={copy.processing}
|
|
67
69
|
dataTestId="wallet-processing"
|
|
68
70
|
/>
|
|
69
71
|
);
|
|
@@ -77,10 +79,11 @@ function WalletProcessing({ wallet }: { wallet: WalletCharge }): JSX.Element {
|
|
|
77
79
|
*/
|
|
78
80
|
function WalletUnresolved({ message }: { message: string }): JSX.Element {
|
|
79
81
|
const { Alert } = useCheckoutComponents();
|
|
82
|
+
const copy = useCheckoutCopy().screens.settling;
|
|
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="wallet-unresolved"
|
|
@@ -105,6 +108,7 @@ function WalletButtons({
|
|
|
105
108
|
onSheetError: (message: string) => void;
|
|
106
109
|
}): JSX.Element | null {
|
|
107
110
|
const { Text } = useCheckoutComponents();
|
|
111
|
+
const copy = useCheckoutCopy().screens.wallet;
|
|
108
112
|
const [googleReady, setGoogleReady] = useState(false);
|
|
109
113
|
const googlePay = googlePayConfig(props.config);
|
|
110
114
|
const applePay = applePayDeclared(props.config) && applePaySupported();
|
|
@@ -131,7 +135,7 @@ function WalletButtons({
|
|
|
131
135
|
{applePay || googleReady ? (
|
|
132
136
|
<Divider>
|
|
133
137
|
<Text variant="caption" size="xs" color="secondary" as="span">
|
|
134
|
-
|
|
138
|
+
{copy.orPayWithCard}
|
|
135
139
|
</Text>
|
|
136
140
|
</Divider>
|
|
137
141
|
) : null}
|
|
@@ -142,6 +146,7 @@ function WalletButtons({
|
|
|
142
146
|
/** The CARD pane: wallet fast lane above, the card form below. */
|
|
143
147
|
export function WalletCardPane(props: WalletPaneProps): JSX.Element {
|
|
144
148
|
const { Alert } = useCheckoutComponents();
|
|
149
|
+
const copy = useCheckoutCopy().screens.settling;
|
|
145
150
|
const { order, buyer, config, tenantSlug, onResolved, pollIntervalMs } = props;
|
|
146
151
|
const wallet = useWalletCharge(order, buyer, onResolved, pollIntervalMs);
|
|
147
152
|
// A sheet failure the wallet reported before any charge existed (pay.js
|
|
@@ -158,7 +163,7 @@ export function WalletCardPane(props: WalletPaneProps): JSX.Element {
|
|
|
158
163
|
{wallet.error ? (
|
|
159
164
|
<Alert
|
|
160
165
|
variant="danger"
|
|
161
|
-
title=
|
|
166
|
+
title={copy.cannotPay}
|
|
162
167
|
description={wallet.error}
|
|
163
168
|
showIcon
|
|
164
169
|
data-testid="wallet-error"
|
|
@@ -167,7 +172,7 @@ export function WalletCardPane(props: WalletPaneProps): JSX.Element {
|
|
|
167
172
|
{sheetError && !wallet.error ? (
|
|
168
173
|
<Alert
|
|
169
174
|
variant="danger"
|
|
170
|
-
title=
|
|
175
|
+
title={copy.cannotPay}
|
|
171
176
|
description={sheetError}
|
|
172
177
|
showIcon
|
|
173
178
|
data-testid="wallet-sheet-error"
|