@12-apps/payments-frontend 3.9.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
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { Box } from "@mui/material";
|
|
2
2
|
import { useEffect, useRef, useState, type JSX } from "react";
|
|
3
3
|
|
|
4
|
+
import { useCheckoutCopy } from "./copy-context";
|
|
4
5
|
import type { CheckoutOrder } from "./types";
|
|
5
6
|
|
|
6
7
|
/**
|
|
@@ -219,11 +220,13 @@ export function GooglePayButton({
|
|
|
219
220
|
onReady,
|
|
220
221
|
}: GooglePayButtonProps): JSX.Element | null {
|
|
221
222
|
const client = useGooglePayClient(api, environment);
|
|
223
|
+
const copy = useCheckoutCopy().screens.wallet.googlePay;
|
|
222
224
|
const container = useRef<HTMLDivElement | null>(null);
|
|
223
225
|
// The latest handlers/order, so the Google-rendered button — mounted once —
|
|
224
|
-
// never closes over a stale charge target.
|
|
225
|
-
|
|
226
|
-
current
|
|
226
|
+
// never closes over a stale charge target. The words ride along for the same
|
|
227
|
+
// reason: the sheet's failure is reported from inside that one-time closure.
|
|
228
|
+
const current = useRef({ order, params, onKey, onError, onReady, copy });
|
|
229
|
+
current.current = { order, params, onKey, onError, onReady, copy };
|
|
227
230
|
|
|
228
231
|
useEffect(() => {
|
|
229
232
|
if (client) current.current.onReady?.();
|
|
@@ -234,23 +237,32 @@ export function GooglePayButton({
|
|
|
234
237
|
if (!client || !mount) return undefined;
|
|
235
238
|
const button = client.createButton({
|
|
236
239
|
onClick: () => {
|
|
237
|
-
const {
|
|
240
|
+
const {
|
|
241
|
+
order: forOrder,
|
|
242
|
+
params: forParams,
|
|
243
|
+
onKey: emit,
|
|
244
|
+
onError: fail,
|
|
245
|
+
copy: words,
|
|
246
|
+
} = current.current;
|
|
238
247
|
client
|
|
239
248
|
.loadPaymentData(paymentDataRequest(forParams, forOrder))
|
|
240
249
|
.then((data) => emit(data.paymentMethodData.tokenizationData.token))
|
|
241
250
|
.catch((error: unknown) => {
|
|
242
251
|
if (sheetDismissed(error)) return;
|
|
243
|
-
fail(
|
|
252
|
+
fail(words.cannotComplete);
|
|
244
253
|
});
|
|
245
254
|
},
|
|
246
255
|
buttonSizeMode: "fill",
|
|
247
|
-
|
|
256
|
+
// Google draws this button's own label, so the language it draws it in
|
|
257
|
+
// is the host's say (FUT-760) — a translated checkout with a Portuguese
|
|
258
|
+
// pay button is two languages on one screen.
|
|
259
|
+
buttonLocale: copy.buttonLocale,
|
|
248
260
|
});
|
|
249
261
|
mount.replaceChildren(button);
|
|
250
262
|
return () => {
|
|
251
263
|
mount.replaceChildren();
|
|
252
264
|
};
|
|
253
|
-
}, [client]);
|
|
265
|
+
}, [client, copy.buttonLocale]);
|
|
254
266
|
|
|
255
267
|
if (!client) return null;
|
|
256
268
|
return (
|
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
import { Box } from "@mui/material";
|
|
2
2
|
import type { JSX } from "react";
|
|
3
3
|
|
|
4
|
+
import { useCheckoutCopy } from "./copy-context";
|
|
4
5
|
import { CreditCardIcon, PixIcon } from "./icons";
|
|
6
|
+
import type { MethodPickerCopy } from "./screens-copy";
|
|
5
7
|
import type { PaymentMethod } from "./types";
|
|
6
8
|
import { useCheckoutComponents } from "./ui";
|
|
7
9
|
|
|
@@ -12,10 +14,25 @@ interface MethodOption {
|
|
|
12
14
|
icon: JSX.Element;
|
|
13
15
|
}
|
|
14
16
|
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
17
|
+
/**
|
|
18
|
+
* The two tiles, named by the HOST (FUT-760).
|
|
19
|
+
*
|
|
20
|
+
* Built per render rather than as a module constant, because the words are no
|
|
21
|
+
* longer this package's: a frozen `OPTIONS` array could only hold the origin
|
|
22
|
+
* host's Portuguese. The order and the icons are still ours — that is layout,
|
|
23
|
+
* not language.
|
|
24
|
+
*/
|
|
25
|
+
function methodOptions(copy: MethodPickerCopy): MethodOption[] {
|
|
26
|
+
return [
|
|
27
|
+
{ value: "PIX", label: copy.pixLabel, description: copy.pixDescription, icon: <PixIcon /> },
|
|
28
|
+
{
|
|
29
|
+
value: "CARD",
|
|
30
|
+
label: copy.cardLabel,
|
|
31
|
+
description: copy.cardDescription,
|
|
32
|
+
icon: <CreditCardIcon />,
|
|
33
|
+
},
|
|
34
|
+
];
|
|
35
|
+
}
|
|
19
36
|
|
|
20
37
|
/** Text/icon color for a tile's state — one place, so the ternaries stay flat. */
|
|
21
38
|
function tileColor(unavailable: boolean, selected: boolean, muted: string): string {
|
|
@@ -36,6 +53,7 @@ function MethodTile({
|
|
|
36
53
|
onSelect: () => void;
|
|
37
54
|
}): JSX.Element {
|
|
38
55
|
const { Text } = useCheckoutComponents();
|
|
56
|
+
const copy = useCheckoutCopy().screens.method;
|
|
39
57
|
const cursor = unavailable ? "not-allowed" : "pointer";
|
|
40
58
|
return (
|
|
41
59
|
<Box
|
|
@@ -79,7 +97,7 @@ function MethodTile({
|
|
|
79
97
|
{option.label}
|
|
80
98
|
</Text>
|
|
81
99
|
<Text variant="caption" size="xs" color="secondary" as="p">
|
|
82
|
-
{unavailable ?
|
|
100
|
+
{unavailable ? copy.unavailableHere : option.description}
|
|
83
101
|
</Text>
|
|
84
102
|
</Box>
|
|
85
103
|
</Box>
|
|
@@ -104,8 +122,13 @@ function MethodTile({
|
|
|
104
122
|
* leave an unexplained one-option "choice". The caller preselects the sole
|
|
105
123
|
* remaining method, so no extra tap is demanded either.
|
|
106
124
|
*/
|
|
107
|
-
function visibleOptions(
|
|
108
|
-
|
|
125
|
+
function visibleOptions(
|
|
126
|
+
copy: MethodPickerCopy,
|
|
127
|
+
offered: PaymentMethod[] | null,
|
|
128
|
+
): MethodOption[] {
|
|
129
|
+
return methodOptions(copy).filter(
|
|
130
|
+
(option) => offered === null || offered.includes(option.value),
|
|
131
|
+
);
|
|
109
132
|
}
|
|
110
133
|
|
|
111
134
|
export function MethodPicker({
|
|
@@ -123,15 +146,16 @@ export function MethodPicker({
|
|
|
123
146
|
offered?: PaymentMethod[] | null;
|
|
124
147
|
}): JSX.Element {
|
|
125
148
|
const { Text } = useCheckoutComponents();
|
|
126
|
-
const
|
|
149
|
+
const copy = useCheckoutCopy().screens.method;
|
|
150
|
+
const options = visibleOptions(copy, offered);
|
|
127
151
|
return (
|
|
128
152
|
<Box>
|
|
129
153
|
<Text variant="body" size="sm" weight="bold" as="p" style={{ marginBottom: 8 }}>
|
|
130
|
-
|
|
154
|
+
{copy.groupLabel}
|
|
131
155
|
</Text>
|
|
132
156
|
<Box
|
|
133
157
|
role="radiogroup"
|
|
134
|
-
aria-label=
|
|
158
|
+
aria-label={copy.groupLabel}
|
|
135
159
|
data-testid="checkout-method"
|
|
136
160
|
sx={{ display: "flex", gap: 1 }}
|
|
137
161
|
>
|
|
@@ -3,13 +3,14 @@ import type { JSX } from "react";
|
|
|
3
3
|
|
|
4
4
|
import { formatCpf } from "../../card";
|
|
5
5
|
|
|
6
|
+
import { useCheckoutCopy } from "./copy-context";
|
|
6
7
|
import { PersonOutlineIcon } from "./icons";
|
|
8
|
+
import type { PayerSummaryCopy } from "./screens-copy";
|
|
7
9
|
import { useCheckoutComponents } from "./ui";
|
|
8
10
|
|
|
9
|
-
|
|
10
11
|
/** What the CPF line reads, given whether the buyer typed one this time. */
|
|
11
|
-
function cpfLabel(taxId: string | undefined): string {
|
|
12
|
-
return taxId?.trim() ?
|
|
12
|
+
function cpfLabel(copy: PayerSummaryCopy, taxId: string | undefined): string {
|
|
13
|
+
return taxId?.trim() ? copy.taxId(formatCpf(taxId)) : copy.taxIdAlreadyKnown;
|
|
13
14
|
}
|
|
14
15
|
|
|
15
16
|
/**
|
|
@@ -42,6 +43,7 @@ export function PayerSummary({
|
|
|
42
43
|
onEdit?: () => void;
|
|
43
44
|
}): JSX.Element | null {
|
|
44
45
|
const { Button, Text } = useCheckoutComponents();
|
|
46
|
+
const copy = useCheckoutCopy().screens.payer;
|
|
45
47
|
if (!onEdit) return null;
|
|
46
48
|
|
|
47
49
|
return (
|
|
@@ -61,10 +63,10 @@ export function PayerSummary({
|
|
|
61
63
|
<PersonOutlineIcon sx={{ fontSize: 20, color: "text.secondary", flex: "0 0 auto" }} />
|
|
62
64
|
<Box sx={{ flex: 1, minWidth: 0 }}>
|
|
63
65
|
<Text variant="body" size="sm" as="p" data-testid="checkout-payer-name">
|
|
64
|
-
{name ?
|
|
66
|
+
{name ? copy.payingAs(name) : copy.payingWithSavedDetails}
|
|
65
67
|
</Text>
|
|
66
68
|
<Text variant="caption" size="xs" color="secondary" as="p" data-testid="checkout-payer-cpf">
|
|
67
|
-
{cpfLabel(taxId)}
|
|
69
|
+
{cpfLabel(copy, taxId)}
|
|
68
70
|
</Text>
|
|
69
71
|
</Box>
|
|
70
72
|
<Button
|
|
@@ -74,7 +76,7 @@ export function PayerSummary({
|
|
|
74
76
|
onClick={onEdit}
|
|
75
77
|
dataTestId="checkout-payer-edit"
|
|
76
78
|
>
|
|
77
|
-
|
|
79
|
+
{copy.changeAction}
|
|
78
80
|
</Button>
|
|
79
81
|
</Box>
|
|
80
82
|
);
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { Box } from "@mui/material";
|
|
2
2
|
import { useState, type JSX } from "react";
|
|
3
3
|
|
|
4
|
+
import { useCheckoutCopy } from "./copy-context";
|
|
4
5
|
import { UNRESOLVED_CODE } from "./failure-codes";
|
|
5
6
|
import { useCheckoutComponents } from "./ui";
|
|
6
7
|
|
|
@@ -35,13 +36,14 @@ export function PaymentErrorPanel({
|
|
|
35
36
|
onRetry: () => void;
|
|
36
37
|
}): JSX.Element {
|
|
37
38
|
const { Alert } = useCheckoutComponents();
|
|
39
|
+
const copy = useCheckoutCopy().screens.error;
|
|
38
40
|
const unresolved = code === UNRESOLVED_CODE;
|
|
39
41
|
|
|
40
42
|
return (
|
|
41
43
|
<Box sx={{ display: "flex", flexDirection: "column", gap: 1.5 }}>
|
|
42
44
|
<Alert
|
|
43
45
|
variant={unresolved ? "warning" : "danger"}
|
|
44
|
-
title={unresolved ?
|
|
46
|
+
title={unresolved ? copy.confirming : copy.cannotContinue}
|
|
45
47
|
description={message}
|
|
46
48
|
showIcon
|
|
47
49
|
data-testid={unresolved ? "checkout-unresolved" : "checkout-error"}
|
|
@@ -62,13 +64,14 @@ function RetryAffordance({
|
|
|
62
64
|
onRetry: () => void;
|
|
63
65
|
}): JSX.Element {
|
|
64
66
|
const { Button, Input } = useCheckoutComponents();
|
|
67
|
+
const copy = useCheckoutCopy().screens.error;
|
|
65
68
|
const [altEmail, setAltEmail] = useState("");
|
|
66
69
|
|
|
67
70
|
if (!emailFlagged) {
|
|
68
71
|
return (
|
|
69
72
|
<Box>
|
|
70
73
|
<Button variant="solid" color="primary" size="md" onClick={onRetry} dataTestId="checkout-retry-payment">
|
|
71
|
-
|
|
74
|
+
{copy.retryAction}
|
|
72
75
|
</Button>
|
|
73
76
|
</Box>
|
|
74
77
|
);
|
|
@@ -76,20 +79,20 @@ function RetryAffordance({
|
|
|
76
79
|
return (
|
|
77
80
|
<Box sx={{ display: "flex", flexDirection: "column", gap: 1.5 }}>
|
|
78
81
|
<Input
|
|
79
|
-
label=
|
|
82
|
+
label={copy.emailLabel}
|
|
80
83
|
type="email"
|
|
81
84
|
variant="outlined"
|
|
82
85
|
size="md"
|
|
83
86
|
fullWidth
|
|
84
87
|
autoComplete="email"
|
|
85
|
-
placeholder=
|
|
88
|
+
placeholder={copy.emailMustDifferHint}
|
|
86
89
|
value={altEmail}
|
|
87
90
|
onChange={(event) => setAltEmail(event.target.value)}
|
|
88
91
|
data-testid="checkout-alt-email"
|
|
89
92
|
/>
|
|
90
93
|
<Box>
|
|
91
94
|
<Button variant="solid" color="primary" size="md" disabled={!altEmail.trim()} onClick={() => onUseEmail(altEmail.trim())} dataTestId="checkout-use-alt-email">
|
|
92
|
-
|
|
95
|
+
{copy.useEmailAction}
|
|
93
96
|
</Button>
|
|
94
97
|
</Box>
|
|
95
98
|
</Box>
|
|
@@ -2,6 +2,7 @@ import { Box } from "@mui/material";
|
|
|
2
2
|
import { useEffect, useState, type JSX } from "react";
|
|
3
3
|
import QRCode from "react-qr-code";
|
|
4
4
|
|
|
5
|
+
import { useCheckoutCopy } from "./copy-context";
|
|
5
6
|
import { ContentCopyIcon } from "./icons";
|
|
6
7
|
import type { CheckoutOrder, OrderStatus, PixCharge } from "./types";
|
|
7
8
|
import { useCheckoutComponents } from "./ui";
|
|
@@ -17,6 +18,7 @@ import { usePaymentPolling } from "./use-payment-polling";
|
|
|
17
18
|
/** The copyable "copia e cola" strip with its copy button. */
|
|
18
19
|
function PixCodeBox({ pix }: { pix: PixCharge }): JSX.Element {
|
|
19
20
|
const { Button, Text } = useCheckoutComponents();
|
|
21
|
+
const copy = useCheckoutCopy().screens.pix;
|
|
20
22
|
const [copied, setCopied] = useState(false);
|
|
21
23
|
|
|
22
24
|
const copyCode = async (): Promise<void> => {
|
|
@@ -63,7 +65,7 @@ function PixCodeBox({ pix }: { pix: PixCharge }): JSX.Element {
|
|
|
63
65
|
}}
|
|
64
66
|
dataTestId="pix-copy"
|
|
65
67
|
>
|
|
66
|
-
{copied ?
|
|
68
|
+
{copied ? copy.copiedAction : copy.copyAction}
|
|
67
69
|
</Button>
|
|
68
70
|
</Box>
|
|
69
71
|
);
|
|
@@ -72,11 +74,12 @@ function PixCodeBox({ pix }: { pix: PixCharge }): JSX.Element {
|
|
|
72
74
|
/** The live footer: poll error, or the pulsing "awaiting payment" indicator. */
|
|
73
75
|
function PixPollFooter({ error }: { error: string | null }): JSX.Element {
|
|
74
76
|
const { Alert, Text } = useCheckoutComponents();
|
|
77
|
+
const { pix, settling } = useCheckoutCopy().screens;
|
|
75
78
|
if (error) {
|
|
76
79
|
return (
|
|
77
80
|
<Alert
|
|
78
81
|
variant="danger"
|
|
79
|
-
title=
|
|
82
|
+
title={settling.cannotConfirm}
|
|
80
83
|
description={error}
|
|
81
84
|
showIcon
|
|
82
85
|
data-testid="pix-poll-error"
|
|
@@ -87,7 +90,7 @@ function PixPollFooter({ error }: { error: string | null }): JSX.Element {
|
|
|
87
90
|
<Box sx={{ display: "flex", alignItems: "center", gap: 1, color: "text.secondary" }}>
|
|
88
91
|
<LoadingDot />
|
|
89
92
|
<Text variant="caption" size="xs" color="secondary" as="span" data-testid="pix-awaiting">
|
|
90
|
-
|
|
93
|
+
{pix.awaiting}
|
|
91
94
|
</Text>
|
|
92
95
|
</Box>
|
|
93
96
|
);
|
|
@@ -103,6 +106,7 @@ export function PixView({
|
|
|
103
106
|
pollIntervalMs?: number;
|
|
104
107
|
}): JSX.Element {
|
|
105
108
|
const { Text } = useCheckoutComponents();
|
|
109
|
+
const copy = useCheckoutCopy().screens.pix;
|
|
106
110
|
const { status, error } = usePaymentPolling(order.orderId, { intervalMs: pollIntervalMs });
|
|
107
111
|
|
|
108
112
|
// Bubble a terminal status up once, so the parent can advance to the status step.
|
|
@@ -116,12 +120,15 @@ export function PixView({
|
|
|
116
120
|
if (!pix) {
|
|
117
121
|
return (
|
|
118
122
|
<Text variant="body" size="sm" color="danger" as="p" data-testid="pix-missing">
|
|
119
|
-
|
|
123
|
+
{copy.chargeMissing}
|
|
120
124
|
</Text>
|
|
121
125
|
);
|
|
122
126
|
}
|
|
123
127
|
|
|
124
|
-
|
|
128
|
+
// The locale is the HOST's (FUT-760): it decides what a buyer reads off the
|
|
129
|
+
// clock, so it travels with the sentence it feeds rather than being frozen
|
|
130
|
+
// to the origin host's here.
|
|
131
|
+
const validUntil = new Date(pix.expiresAt).toLocaleTimeString(copy.expiryLocale, {
|
|
125
132
|
hour: "2-digit",
|
|
126
133
|
minute: "2-digit",
|
|
127
134
|
});
|
|
@@ -132,16 +139,16 @@ export function PixView({
|
|
|
132
139
|
sx={{ display: "flex", flexDirection: "column", alignItems: "center", gap: 2, textAlign: "center" }}
|
|
133
140
|
>
|
|
134
141
|
<Text variant="heading" size="md" weight="bold" as="h2">
|
|
135
|
-
|
|
142
|
+
{copy.heading}
|
|
136
143
|
</Text>
|
|
137
144
|
<Text variant="body" size="sm" color="secondary" as="p">
|
|
138
|
-
|
|
145
|
+
{copy.instructions(order.totalLabel)}
|
|
139
146
|
</Text>
|
|
140
147
|
|
|
141
148
|
<Box
|
|
142
149
|
data-testid="pix-qr"
|
|
143
150
|
role="img"
|
|
144
|
-
aria-label=
|
|
151
|
+
aria-label={copy.qrAlt}
|
|
145
152
|
sx={{ bgcolor: "background.paper", p: 2, borderRadius: 2, border: "1px solid", borderColor: "divider" }}
|
|
146
153
|
>
|
|
147
154
|
<QRCode value={pix.copyPaste} size={200} />
|
|
@@ -150,7 +157,7 @@ export function PixView({
|
|
|
150
157
|
<PixCodeBox pix={pix} />
|
|
151
158
|
|
|
152
159
|
<Text variant="caption" size="xs" color="secondary" as="p" data-testid="pix-expiry">
|
|
153
|
-
|
|
160
|
+
{copy.validUntil(validUntil)}
|
|
154
161
|
</Text>
|
|
155
162
|
|
|
156
163
|
<PixPollFooter error={error} />
|
|
@@ -32,7 +32,9 @@
|
|
|
32
32
|
import { Box } from "@mui/material";
|
|
33
33
|
import type { JSX } from "react";
|
|
34
34
|
|
|
35
|
+
import { useCheckoutCopy } from "../copy-context";
|
|
35
36
|
import { offeredMethods } from "../method-capability";
|
|
37
|
+
import type { HostedHandoverCopy } from "../screens-copy";
|
|
36
38
|
import type { CheckoutProviderConfig } from "../types";
|
|
37
39
|
import { useCheckoutComponents } from "../ui";
|
|
38
40
|
|
|
@@ -47,9 +49,9 @@ import type { ProviderCheckoutScreenProps } from "./types";
|
|
|
47
49
|
* true sentence rather than our internal id ("infinitepay") dressed up as a
|
|
48
50
|
* brand.
|
|
49
51
|
*/
|
|
50
|
-
function destinationLabel(config: CheckoutProviderConfig | null): string {
|
|
52
|
+
function destinationLabel(copy: HostedHandoverCopy, config: CheckoutProviderConfig | null): string {
|
|
51
53
|
const name = config?.chain?.[0]?.displayName?.trim();
|
|
52
|
-
return name ?
|
|
54
|
+
return name ? copy.destinationNamed(name) : copy.destinationGeneric;
|
|
53
55
|
}
|
|
54
56
|
|
|
55
57
|
/**
|
|
@@ -60,21 +62,27 @@ function destinationLabel(config: CheckoutProviderConfig | null): string {
|
|
|
60
62
|
* keeps this honest for a provider that takes only one of the two: a hosted
|
|
61
63
|
* PIX-only store must not promise a card.
|
|
62
64
|
*/
|
|
63
|
-
function methodsPhrase(
|
|
65
|
+
function methodsPhrase(
|
|
66
|
+
copy: HostedHandoverCopy,
|
|
67
|
+
config: CheckoutProviderConfig | null,
|
|
68
|
+
): string | null {
|
|
64
69
|
const offered = offeredMethods(config);
|
|
65
70
|
const pix = offered === null || offered.includes("PIX");
|
|
66
71
|
const card = offered === null || offered.includes("CARD");
|
|
67
|
-
if (pix && card) return
|
|
68
|
-
if (pix) return
|
|
69
|
-
if (card) return
|
|
72
|
+
if (pix && card) return copy.pixAndCard;
|
|
73
|
+
if (pix) return copy.pixOnly;
|
|
74
|
+
if (card) return copy.cardOnly;
|
|
70
75
|
return null;
|
|
71
76
|
}
|
|
72
77
|
|
|
73
78
|
/** The full "where you are going and what happens there" sentence. */
|
|
74
|
-
function handoffMessage(
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
79
|
+
function handoffMessage(
|
|
80
|
+
copy: HostedHandoverCopy,
|
|
81
|
+
config: CheckoutProviderConfig | null,
|
|
82
|
+
): string {
|
|
83
|
+
const methods = methodsPhrase(copy, config);
|
|
84
|
+
const choice = methods ? copy.methodsChoice(methods) : "";
|
|
85
|
+
return copy.handoff(destinationLabel(copy, config), choice);
|
|
78
86
|
}
|
|
79
87
|
|
|
80
88
|
/** The invitation: what happens next, and the one button that starts it. */
|
|
@@ -86,16 +94,17 @@ function HandOffInvite({
|
|
|
86
94
|
onStart: () => void;
|
|
87
95
|
}): JSX.Element {
|
|
88
96
|
const { Button, Text } = useCheckoutComponents();
|
|
97
|
+
const copy = useCheckoutCopy().screens.hosted;
|
|
89
98
|
return (
|
|
90
99
|
<Box
|
|
91
100
|
data-testid="checkout-handoff-invite"
|
|
92
101
|
sx={{ display: "flex", flexDirection: "column", gap: 2 }}
|
|
93
102
|
>
|
|
94
103
|
<Text variant="body" size="md" as="p">
|
|
95
|
-
{handoffMessage(config)}
|
|
104
|
+
{handoffMessage(copy, config)}
|
|
96
105
|
</Text>
|
|
97
106
|
<Text variant="caption" size="sm" as="p" color="secondary">
|
|
98
|
-
|
|
107
|
+
{copy.afterwards}
|
|
99
108
|
</Text>
|
|
100
109
|
<Button
|
|
101
110
|
variant="solid"
|
|
@@ -105,7 +114,7 @@ function HandOffInvite({
|
|
|
105
114
|
onClick={onStart}
|
|
106
115
|
dataTestId="checkout-handoff-start"
|
|
107
116
|
>
|
|
108
|
-
|
|
117
|
+
{copy.startAction}
|
|
109
118
|
</Button>
|
|
110
119
|
</Box>
|
|
111
120
|
);
|
|
@@ -121,17 +130,18 @@ function HandOffInvite({
|
|
|
121
130
|
*/
|
|
122
131
|
function HandOffPending({ config }: { config: CheckoutProviderConfig | null }): JSX.Element {
|
|
123
132
|
const { Text, LoadingState } = useCheckoutComponents();
|
|
133
|
+
const copy = useCheckoutCopy().screens.hosted;
|
|
124
134
|
return (
|
|
125
135
|
<Box
|
|
126
136
|
data-testid="checkout-handoff-pending"
|
|
127
137
|
sx={{ display: "flex", flexDirection: "column", gap: 2, alignItems: "center", py: 4 }}
|
|
128
138
|
>
|
|
129
|
-
<LoadingState variant="spinner" message=
|
|
139
|
+
<LoadingState variant="spinner" message={copy.preparing} size="md" />
|
|
130
140
|
<Text variant="body" size="md" as="p">
|
|
131
|
-
{handoffMessage(config)}
|
|
141
|
+
{handoffMessage(copy, config)}
|
|
132
142
|
</Text>
|
|
133
143
|
<Text variant="caption" size="sm" as="p" color="secondary">
|
|
134
|
-
|
|
144
|
+
{copy.afterwards}
|
|
135
145
|
</Text>
|
|
136
146
|
</Box>
|
|
137
147
|
);
|
|
@@ -1,3 +1,6 @@
|
|
|
1
|
+
import { PT_BR_CARD_COPY } from "../../card/pt-BR";
|
|
2
|
+
import { PT_BR_CHECKOUT_SCREENS_COPY } from "./screens-pt-BR";
|
|
3
|
+
import type { CheckoutCopy } from "./copy-context";
|
|
1
4
|
import type { CheckoutViewCopy, PaymentStatusCopy } from "./view-copy";
|
|
2
5
|
|
|
3
6
|
/**
|
|
@@ -39,7 +42,31 @@ export const PT_BR_PAYMENT_STATUS_COPY: PaymentStatusCopy = {
|
|
|
39
42
|
receiptEmailLabel: "Comprovante enviado para",
|
|
40
43
|
};
|
|
41
44
|
|
|
45
|
+
/**
|
|
46
|
+
* The words the checkout's deeper screens read from context — the card form,
|
|
47
|
+
* its tokenizers, and the buyer-details step's own fields.
|
|
48
|
+
*
|
|
49
|
+
* Same rule as everything else in this file: a NAMED pack a host passes by
|
|
50
|
+
* hand at the mount, never a default the package reaches for.
|
|
51
|
+
*/
|
|
52
|
+
export const PT_BR_CHECKOUT_COPY: CheckoutCopy = {
|
|
53
|
+
card: PT_BR_CARD_COPY,
|
|
54
|
+
screens: PT_BR_CHECKOUT_SCREENS_COPY,
|
|
55
|
+
buyer: {
|
|
56
|
+
emailInvalid: "E-mail inválido.",
|
|
57
|
+
emailRequired: "E-mail obrigatório.",
|
|
58
|
+
nameRequired: "Nome obrigatório.",
|
|
59
|
+
phoneRequired: "Telefone obrigatório.",
|
|
60
|
+
fieldsHint: (names) =>
|
|
61
|
+
names.length === 0
|
|
62
|
+
? "Nome, e-mail e telefone são opcionais — usados apenas para o comprovante."
|
|
63
|
+
: `Informe seu ${names.join(", ")} (${names.length === 1 ? "obrigatório" : "obrigatórios"} ` +
|
|
64
|
+
"para o pagamento). Os demais campos são opcionais — usados apenas para o comprovante.",
|
|
65
|
+
},
|
|
66
|
+
};
|
|
67
|
+
|
|
42
68
|
export const PT_BR_CHECKOUT_VIEW_COPY: CheckoutViewCopy = {
|
|
69
|
+
screens: PT_BR_CHECKOUT_COPY,
|
|
43
70
|
steps: {
|
|
44
71
|
dados: "Dados",
|
|
45
72
|
payment: "Pagamento",
|