@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
|
@@ -22,6 +22,8 @@ import type { PaymentsSettingsClient } from '../client';
|
|
|
22
22
|
import { ConnectionFacts, ConnectSteps, DisconnectDialog } from './ConnectionCard';
|
|
23
23
|
import { expiryProximity, isConnected } from './connection-state';
|
|
24
24
|
import { BAR_MSG_SX, BAR_SX, BTN_PRIMARY_SX, BTN_QUIET_DANGER_SX, T } from './panel-tokens';
|
|
25
|
+
import type { OAuthConnectionCopy } from './settings-copy';
|
|
26
|
+
import { usePaymentsSettingsCopy } from './settings-copy-context';
|
|
25
27
|
|
|
26
28
|
/**
|
|
27
29
|
* The `authMode: 'oauth'` half of the settings page: a provider whose
|
|
@@ -56,12 +58,13 @@ export interface ProviderConnectionProps {
|
|
|
56
58
|
* gray line read the same on the eve of an outage as a year out.
|
|
57
59
|
*/
|
|
58
60
|
function ExpiryNote(props: { expiresAt: string }) {
|
|
61
|
+
const copy = usePaymentsSettingsCopy().oauth;
|
|
59
62
|
const when = new Date(props.expiresAt).toLocaleString('pt-BR');
|
|
60
63
|
const proximity = expiryProximity(props.expiresAt);
|
|
61
64
|
if (proximity === 'SAFE' || proximity === null) {
|
|
62
65
|
return (
|
|
63
66
|
<Typography variant="caption" color="text.secondary">
|
|
64
|
-
{
|
|
67
|
+
{copy.validUntil(when)}
|
|
65
68
|
</Typography>
|
|
66
69
|
);
|
|
67
70
|
}
|
|
@@ -73,8 +76,8 @@ function ExpiryNote(props: { expiresAt: string }) {
|
|
|
73
76
|
data-testid="payments-expiry-warning"
|
|
74
77
|
>
|
|
75
78
|
{proximity === 'PAST'
|
|
76
|
-
?
|
|
77
|
-
:
|
|
79
|
+
? copy.expiredAt(when)
|
|
80
|
+
: copy.expiresAt(when)}
|
|
78
81
|
</Typography>
|
|
79
82
|
);
|
|
80
83
|
}
|
|
@@ -96,23 +99,19 @@ function ExpiryNote(props: { expiresAt: string }) {
|
|
|
96
99
|
* up confidently misdescribing an outage.
|
|
97
100
|
*/
|
|
98
101
|
function connectFailure(
|
|
102
|
+
copy: OAuthConnectionCopy,
|
|
99
103
|
message: string,
|
|
100
104
|
displayName: string,
|
|
101
105
|
): { severity: 'error' | 'warning'; text: string } {
|
|
102
106
|
if (/no platform oauth application credentials/i.test(message)) {
|
|
103
|
-
return {
|
|
104
|
-
severity: 'warning',
|
|
105
|
-
text:
|
|
106
|
-
`A conexão automática com ${displayName} não está disponível nesta instalação — ` +
|
|
107
|
-
'o aplicativo de autorização não foi cadastrado. Para conectar agora, abra ' +
|
|
108
|
-
'“Prefiro informar as credenciais manualmente” abaixo e cole as suas próprias chaves.',
|
|
109
|
-
};
|
|
107
|
+
return { severity: 'warning', text: copy.notAvailableHere(displayName) };
|
|
110
108
|
}
|
|
111
109
|
return { severity: 'error', text: message };
|
|
112
110
|
}
|
|
113
111
|
|
|
114
112
|
function ConnectError({ message, displayName }: { message: string; displayName: string }) {
|
|
115
|
-
const
|
|
113
|
+
const copy = usePaymentsSettingsCopy().oauth;
|
|
114
|
+
const { severity, text } = connectFailure(copy, message, displayName);
|
|
116
115
|
return (
|
|
117
116
|
<Alert severity={severity} data-testid="payments-connect-failure">
|
|
118
117
|
{text}
|
|
@@ -120,21 +119,28 @@ function ConnectError({ message, displayName }: { message: string; displayName:
|
|
|
120
119
|
);
|
|
121
120
|
}
|
|
122
121
|
|
|
123
|
-
function connectLabel(
|
|
122
|
+
function connectLabel(
|
|
123
|
+
copy: OAuthConnectionCopy,
|
|
124
|
+
displayName: string,
|
|
125
|
+
connected: boolean,
|
|
126
|
+
busy: string | null,
|
|
127
|
+
) {
|
|
124
128
|
if (busy === 'connect') return <CircularProgress size={18} />;
|
|
125
|
-
return connected ?
|
|
129
|
+
return connected ? copy.reconnectAction : copy.connectAction(displayName);
|
|
126
130
|
}
|
|
127
131
|
|
|
128
132
|
/**
|
|
129
133
|
* pt-BR labels for the scopes a store owner actually sees. Unknown scopes fall
|
|
130
134
|
* back to the provider's own spelling — wrong words are worse than raw ones.
|
|
131
135
|
*/
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
136
|
+
function scopeLabels(copy: OAuthConnectionCopy): Record<string, string> {
|
|
137
|
+
return {
|
|
138
|
+
'payments.read': copy.scopes.read,
|
|
139
|
+
'payments.create': copy.scopes.create,
|
|
140
|
+
'payments.refund': copy.scopes.refund,
|
|
141
|
+
'accounts.read': copy.scopes.account,
|
|
142
|
+
};
|
|
143
|
+
}
|
|
138
144
|
|
|
139
145
|
/**
|
|
140
146
|
* WHICH account is connected (FUT-300): identity, granted scopes and the
|
|
@@ -143,6 +149,7 @@ const SCOPE_LABELS: Record<string, string> = {
|
|
|
143
149
|
* charges into was to disconnect and connect again.
|
|
144
150
|
*/
|
|
145
151
|
function ConnectedAccountDetails(props: { account: ConnectedOAuthAccount }) {
|
|
152
|
+
const copy = usePaymentsSettingsCopy().oauth;
|
|
146
153
|
const { account } = props;
|
|
147
154
|
const identity = account.accountLabel ?? account.accountId;
|
|
148
155
|
return (
|
|
@@ -154,7 +161,7 @@ function ConnectedAccountDetails(props: { account: ConnectedOAuthAccount }) {
|
|
|
154
161
|
) : null}
|
|
155
162
|
{account.connectedAt ? (
|
|
156
163
|
<Typography variant="caption" color="text.secondary">
|
|
157
|
-
{
|
|
164
|
+
{copy.connectedAt(new Date(account.connectedAt).toLocaleString())}
|
|
158
165
|
</Typography>
|
|
159
166
|
) : null}
|
|
160
167
|
{account.grantedScopes.length > 0 ? (
|
|
@@ -164,7 +171,7 @@ function ConnectedAccountDetails(props: { account: ConnectedOAuthAccount }) {
|
|
|
164
171
|
key={scope}
|
|
165
172
|
size="small"
|
|
166
173
|
variant="outlined"
|
|
167
|
-
label={
|
|
174
|
+
label={scopeLabels(copy)[scope] ?? scope}
|
|
168
175
|
/>
|
|
169
176
|
))}
|
|
170
177
|
</Stack>
|
|
@@ -184,6 +191,7 @@ function ConnectionSummary(props: {
|
|
|
184
191
|
environment: PaymentEnvironment;
|
|
185
192
|
connectedAccount: ConnectedOAuthAccount | null;
|
|
186
193
|
}) {
|
|
194
|
+
const copy = usePaymentsSettingsCopy().oauth;
|
|
187
195
|
return (
|
|
188
196
|
<>
|
|
189
197
|
{/*
|
|
@@ -215,8 +223,8 @@ function ConnectionSummary(props: {
|
|
|
215
223
|
*/}
|
|
216
224
|
<Typography sx={{ fontSize: '13px', color: T.ink2, lineHeight: 1.6 }}>
|
|
217
225
|
{props.connected
|
|
218
|
-
?
|
|
219
|
-
:
|
|
226
|
+
? copy.connectedExplainer
|
|
227
|
+
: copy.invitation(props.displayName)}
|
|
220
228
|
</Typography>
|
|
221
229
|
{props.connected ? null : <ConnectSteps displayName={props.displayName} />}
|
|
222
230
|
{props.connected && props.connectedAccount ? (
|
|
@@ -224,7 +232,7 @@ function ConnectionSummary(props: {
|
|
|
224
232
|
) : null}
|
|
225
233
|
{props.status === 'RECONNECT_REQUIRED' ? (
|
|
226
234
|
<Alert severity="warning">
|
|
227
|
-
|
|
235
|
+
{copy.revoked}
|
|
228
236
|
</Alert>
|
|
229
237
|
) : null}
|
|
230
238
|
{props.expiresAt ? <ExpiryNote expiresAt={props.expiresAt} /> : null}
|
|
@@ -249,13 +257,14 @@ function ConnectionActions(props: {
|
|
|
249
257
|
onConnect: () => void;
|
|
250
258
|
onDisconnect: () => void;
|
|
251
259
|
}) {
|
|
260
|
+
const copy = usePaymentsSettingsCopy().oauth;
|
|
252
261
|
const { displayName, connected, busy, onConnect, onDisconnect } = props;
|
|
253
262
|
return (
|
|
254
263
|
<Box sx={BAR_SX} data-testid="payments-connection-bar">
|
|
255
264
|
<Typography sx={BAR_MSG_SX}>
|
|
256
265
|
{connected
|
|
257
|
-
?
|
|
258
|
-
:
|
|
266
|
+
? copy.connectedNote(displayName)
|
|
267
|
+
: copy.roundTripNote}
|
|
259
268
|
</Typography>
|
|
260
269
|
{connected ? (
|
|
261
270
|
<Button
|
|
@@ -264,7 +273,7 @@ function ConnectionActions(props: {
|
|
|
264
273
|
sx={BTN_QUIET_DANGER_SX}
|
|
265
274
|
data-testid="payments-disconnect"
|
|
266
275
|
>
|
|
267
|
-
{busy === 'disconnect' ? <CircularProgress size={18} /> :
|
|
276
|
+
{busy === 'disconnect' ? <CircularProgress size={18} /> : copy.removeAction}
|
|
268
277
|
</Button>
|
|
269
278
|
) : null}
|
|
270
279
|
<Button
|
|
@@ -274,7 +283,7 @@ function ConnectionActions(props: {
|
|
|
274
283
|
onClick={onConnect}
|
|
275
284
|
sx={BTN_PRIMARY_SX}
|
|
276
285
|
>
|
|
277
|
-
{connectLabel(displayName, connected, busy)}
|
|
286
|
+
{connectLabel(copy, displayName, connected, busy)}
|
|
278
287
|
</Button>
|
|
279
288
|
</Box>
|
|
280
289
|
);
|
|
@@ -7,6 +7,7 @@ import type { MaskedProviderConfig, MerchantSettingsView, ProviderDescriptor } f
|
|
|
7
7
|
import type { PaymentsSettingsClient } from '../client';
|
|
8
8
|
import { connectionBadge } from './connection-state';
|
|
9
9
|
import { ProviderPriorityList, chainOf } from './ProviderPriorityList';
|
|
10
|
+
import { usePaymentsSettingsCopy } from './settings-copy-context';
|
|
10
11
|
|
|
11
12
|
/**
|
|
12
13
|
* Provider picker — one card per provider, click to open it.
|
|
@@ -26,6 +27,7 @@ function ProviderPicker({
|
|
|
26
27
|
activeName: string | null;
|
|
27
28
|
onSelect: (name: string) => void;
|
|
28
29
|
}) {
|
|
30
|
+
const copy = usePaymentsSettingsCopy().listBadge;
|
|
29
31
|
return (
|
|
30
32
|
<Box
|
|
31
33
|
data-testid="payments-provider-picker"
|
|
@@ -38,7 +40,7 @@ function ProviderPicker({
|
|
|
38
40
|
>
|
|
39
41
|
{providers.map((provider) => {
|
|
40
42
|
const config = configs.find((c) => c.provider === provider.name) ?? null;
|
|
41
|
-
const badge = connectionBadge(config);
|
|
43
|
+
const badge = connectionBadge(copy, config);
|
|
42
44
|
const selected = provider.name === activeName;
|
|
43
45
|
return (
|
|
44
46
|
<Box
|
|
@@ -93,6 +95,7 @@ export function ProviderList({
|
|
|
93
95
|
reload: () => Promise<void>;
|
|
94
96
|
onSelect: (name: string) => void;
|
|
95
97
|
}) {
|
|
98
|
+
const copy = usePaymentsSettingsCopy().providerList;
|
|
96
99
|
return (
|
|
97
100
|
<Box data-testid="payments-provider-settings">
|
|
98
101
|
{/*
|
|
@@ -111,9 +114,9 @@ export function ProviderList({
|
|
|
111
114
|
/>
|
|
112
115
|
</Box>
|
|
113
116
|
) : null}
|
|
114
|
-
<Typography variant="subtitle1">
|
|
117
|
+
<Typography variant="subtitle1">{copy.heading}</Typography>
|
|
115
118
|
<Typography variant="body2" color="text.secondary" sx={{ mb: 2 }}>
|
|
116
|
-
|
|
119
|
+
{copy.subheading}
|
|
117
120
|
</Typography>
|
|
118
121
|
<ProviderPicker
|
|
119
122
|
providers={view.providers}
|
|
@@ -15,6 +15,8 @@ import { useCallback, useState } from 'react';
|
|
|
15
15
|
import type { MerchantSettingsView, ProviderDescriptor } from '@12-apps/payments-backend';
|
|
16
16
|
|
|
17
17
|
import type { PaymentsSettingsClient } from '../client';
|
|
18
|
+
import { usePaymentsSettingsCopy } from './settings-copy-context';
|
|
19
|
+
import { richText } from './rich-text';
|
|
18
20
|
|
|
19
21
|
/**
|
|
20
22
|
* The store's failover chain, ordered.
|
|
@@ -92,6 +94,7 @@ function PriorityRow({
|
|
|
92
94
|
onDragEnd,
|
|
93
95
|
onMove,
|
|
94
96
|
}: PriorityRowProps) {
|
|
97
|
+
const copy = usePaymentsSettingsCopy().priority;
|
|
95
98
|
return (
|
|
96
99
|
<Paper
|
|
97
100
|
component="li"
|
|
@@ -120,12 +123,12 @@ function PriorityRow({
|
|
|
120
123
|
<Typography sx={{ flexGrow: 1 }}>{label}</Typography>
|
|
121
124
|
{index === 0 ? (
|
|
122
125
|
<Typography variant="caption" color="primary" data-testid="payments-priority-first">
|
|
123
|
-
|
|
126
|
+
{copy.firstInChain}
|
|
124
127
|
</Typography>
|
|
125
128
|
) : null}
|
|
126
129
|
<IconButton
|
|
127
130
|
size="small"
|
|
128
|
-
aria-label={
|
|
131
|
+
aria-label={copy.moveUp(label)}
|
|
129
132
|
disabled={index === 0 || saving}
|
|
130
133
|
onClick={() => onMove(index, index - 1)}
|
|
131
134
|
>
|
|
@@ -133,7 +136,7 @@ function PriorityRow({
|
|
|
133
136
|
</IconButton>
|
|
134
137
|
<IconButton
|
|
135
138
|
size="small"
|
|
136
|
-
aria-label={
|
|
139
|
+
aria-label={copy.moveDown(label)}
|
|
137
140
|
disabled={index === total - 1 || saving}
|
|
138
141
|
onClick={() => onMove(index, index + 1)}
|
|
139
142
|
>
|
|
@@ -154,6 +157,7 @@ function useChainReorder(
|
|
|
154
157
|
client: PaymentsSettingsClient,
|
|
155
158
|
onReordered?: (next: MerchantSettingsView) => void,
|
|
156
159
|
) {
|
|
160
|
+
const copy = usePaymentsSettingsCopy().priority;
|
|
157
161
|
const [chain, setChain] = useState<string[]>(() => chainOf(view));
|
|
158
162
|
const [error, setError] = useState<string | null>(null);
|
|
159
163
|
const [saving, setSaving] = useState(false);
|
|
@@ -174,7 +178,7 @@ function useChainReorder(
|
|
|
174
178
|
})
|
|
175
179
|
.catch((err: unknown) => {
|
|
176
180
|
setChain(previous);
|
|
177
|
-
setError(err instanceof Error ? err.message :
|
|
181
|
+
setError(err instanceof Error ? err.message : copy.saveFailed);
|
|
178
182
|
})
|
|
179
183
|
.finally(() => setSaving(false));
|
|
180
184
|
},
|
|
@@ -201,6 +205,7 @@ function DeclinePolicyControl({
|
|
|
201
205
|
client: PaymentsSettingsClient;
|
|
202
206
|
onChanged?: (next: MerchantSettingsView) => void;
|
|
203
207
|
}) {
|
|
208
|
+
const copy = usePaymentsSettingsCopy().priority;
|
|
204
209
|
const [saving, setSaving] = useState(false);
|
|
205
210
|
const cascades = view.failoverPolicy === 'TECHNICAL_AND_DECLINE';
|
|
206
211
|
|
|
@@ -212,7 +217,9 @@ function DeclinePolicyControl({
|
|
|
212
217
|
size="small"
|
|
213
218
|
checked={cascades}
|
|
214
219
|
disabled={saving}
|
|
215
|
-
|
|
220
|
+
// The accessible name is the same sentence with its emphasis
|
|
221
|
+
// markers stripped: a screen reader must not spell out asterisks.
|
|
222
|
+
inputProps={{ 'aria-label': copy.retryDeclinedLabel.replace(/\*\*/g, '') }}
|
|
216
223
|
onChange={(e) => {
|
|
217
224
|
setSaving(true);
|
|
218
225
|
void client
|
|
@@ -223,28 +230,25 @@ function DeclinePolicyControl({
|
|
|
223
230
|
/>
|
|
224
231
|
}
|
|
225
232
|
label={
|
|
226
|
-
<Typography variant="body2">
|
|
227
|
-
Tentar cartão <strong>recusado</strong> no próximo provedor
|
|
228
|
-
</Typography>
|
|
233
|
+
<Typography variant="body2">{richText(copy.retryDeclinedLabel)}</Typography>
|
|
229
234
|
}
|
|
230
235
|
/>
|
|
231
236
|
<Typography variant="caption" color="text.secondary" display="block">
|
|
232
|
-
{cascades
|
|
233
|
-
? 'Uma recusa passa para o próximo da lista. Isso pode aumentar custos de transação e sinais de fraude.'
|
|
234
|
-
: 'Padrão: uma recusa encerra a cobrança. Só falhas técnicas passam para o próximo.'}
|
|
237
|
+
{cascades ? copy.retryDeclinedOn : copy.retryDeclinedOff}
|
|
235
238
|
</Typography>
|
|
236
239
|
</Box>
|
|
237
240
|
);
|
|
238
241
|
}
|
|
239
242
|
|
|
240
243
|
export function ProviderPriorityList({ view, client, onReordered }: ProviderPriorityListProps) {
|
|
244
|
+
const copy = usePaymentsSettingsCopy().priority;
|
|
241
245
|
const { chain, error, saving, move } = useChainReorder(view, client, onReordered);
|
|
242
246
|
const [dragging, setDragging] = useState<number | null>(null);
|
|
243
247
|
|
|
244
248
|
if (chain.length === 0) {
|
|
245
249
|
return (
|
|
246
250
|
<Alert severity="warning" data-testid="payments-priority-empty">
|
|
247
|
-
|
|
251
|
+
{copy.noneActive}
|
|
248
252
|
</Alert>
|
|
249
253
|
);
|
|
250
254
|
}
|
|
@@ -252,12 +256,10 @@ export function ProviderPriorityList({ view, client, onReordered }: ProviderPrio
|
|
|
252
256
|
return (
|
|
253
257
|
<Box data-testid="payments-priority-list">
|
|
254
258
|
<Typography variant="subtitle2" gutterBottom>
|
|
255
|
-
|
|
259
|
+
{copy.orderHeading}
|
|
256
260
|
</Typography>
|
|
257
261
|
<Typography variant="body2" color="text.secondary" sx={{ mb: 1.5 }}>
|
|
258
|
-
|
|
259
|
-
cobrança falhar por motivo técnico, ele tenta o próximo da lista — mas só quando é possível
|
|
260
|
-
comprovar que a tentativa anterior não gerou cobrança.
|
|
262
|
+
{richText(copy.chainExplainer(labelOf(view.providers, chain[0] ?? '')))}
|
|
261
263
|
</Typography>
|
|
262
264
|
|
|
263
265
|
{error ? (
|
|
@@ -31,6 +31,7 @@ import {
|
|
|
31
31
|
} from './panel-tokens';
|
|
32
32
|
|
|
33
33
|
import { richText } from './rich-text';
|
|
34
|
+
import { usePaymentsSettingsCopy } from './settings-copy-context';
|
|
34
35
|
|
|
35
36
|
/**
|
|
36
37
|
* Renders a provider's step-by-step onboarding walkthrough — the reusable
|
|
@@ -104,6 +105,7 @@ function CopyField({
|
|
|
104
105
|
}
|
|
105
106
|
|
|
106
107
|
function CopyRow({ label, text }: { label: string; text: string }) {
|
|
108
|
+
const copy = usePaymentsSettingsCopy().setupGuide;
|
|
107
109
|
const [copied, setCopied] = useState(false);
|
|
108
110
|
return (
|
|
109
111
|
<Stack direction="row" spacing={1} alignItems="center" sx={{ width: '100%' }}>
|
|
@@ -114,7 +116,7 @@ function CopyRow({ label, text }: { label: string; text: string }) {
|
|
|
114
116
|
*/}
|
|
115
117
|
<TextField fullWidth size="small" disabled value={text} />
|
|
116
118
|
<IconButton
|
|
117
|
-
aria-label={copied ?
|
|
119
|
+
aria-label={copied ? copy.copied : copy.copyValue(label)}
|
|
118
120
|
onClick={() => {
|
|
119
121
|
void navigator.clipboard.writeText(text).then(() => setCopied(true));
|
|
120
122
|
}}
|
|
@@ -6,6 +6,8 @@ import type { MaskedProviderConfig, ProviderDescriptor } from '@12-apps/payments
|
|
|
6
6
|
|
|
7
7
|
import { isConnected } from './connection-state';
|
|
8
8
|
import { T } from './panel-tokens';
|
|
9
|
+
import type { ConnectionStatusCopy } from './settings-copy';
|
|
10
|
+
import { usePaymentsSettingsCopy } from './settings-copy-context';
|
|
9
11
|
|
|
10
12
|
/**
|
|
11
13
|
* The provider's headline: what state it is in, and the switch that decides
|
|
@@ -37,19 +39,22 @@ interface StatusBadge {
|
|
|
37
39
|
* — a real payment that actually landed — and is the only one painted green.
|
|
38
40
|
*/
|
|
39
41
|
export function statusBadge(
|
|
42
|
+
copy: ConnectionStatusCopy,
|
|
40
43
|
config: MaskedProviderConfig | null,
|
|
41
44
|
descriptor?: ProviderDescriptor,
|
|
42
45
|
): StatusBadge {
|
|
43
|
-
if (config?.chargeVerifiedAt) return { label:
|
|
46
|
+
if (config?.chargeVerifiedAt) return { label: copy.verified, color: 'success' };
|
|
44
47
|
// A store that has entered nothing has not FAILED at anything. The stored
|
|
45
48
|
// status outlives the credential it described — clearing the field leaves the
|
|
46
49
|
// last verdict behind — so a red FALHOU sat above an empty form, accusing the
|
|
47
50
|
// owner of a mistake they had not yet had the chance to make.
|
|
48
51
|
if (descriptor && !anyCredentialStored(config, descriptor)) {
|
|
49
|
-
return { label:
|
|
52
|
+
return { label: copy.unverified, color: 'default' };
|
|
50
53
|
}
|
|
51
|
-
if (config?.status === 'RECONNECT_REQUIRED')
|
|
52
|
-
|
|
54
|
+
if (config?.status === 'RECONNECT_REQUIRED') {
|
|
55
|
+
return { label: copy.reconnectRequired, color: 'error' };
|
|
56
|
+
}
|
|
57
|
+
if (isConnected(config)) return { label: copy.connectionOk, color: 'info' };
|
|
53
58
|
// A FAILED probe lands here, and it says NÃO VERIFICADO like every other
|
|
54
59
|
// not-yet-connected state. `FALHOU` was a third word for the same fact — the
|
|
55
60
|
// store cannot take money through this provider yet — and it was the only
|
|
@@ -59,7 +64,7 @@ export function statusBadge(
|
|
|
59
64
|
//
|
|
60
65
|
// `RECONNECT_REQUIRED` stays red on purpose: that one is a connection that
|
|
61
66
|
// WAS working and stopped, which is news rather than a step.
|
|
62
|
-
return { label:
|
|
67
|
+
return { label: copy.unverified, color: 'default' };
|
|
63
68
|
}
|
|
64
69
|
|
|
65
70
|
/**
|
|
@@ -107,6 +112,7 @@ function anyCredentialStored(
|
|
|
107
112
|
* does not have, and quoting it from here could only ever be a guess.
|
|
108
113
|
*/
|
|
109
114
|
function toggleGate(
|
|
115
|
+
copy: ConnectionStatusCopy,
|
|
110
116
|
descriptor: ProviderDescriptor,
|
|
111
117
|
config: MaskedProviderConfig | null,
|
|
112
118
|
enabled: boolean,
|
|
@@ -119,9 +125,7 @@ function toggleGate(
|
|
|
119
125
|
if (!lockedOff) return { lockedOff, hint: '' };
|
|
120
126
|
return {
|
|
121
127
|
lockedOff,
|
|
122
|
-
hint: provable
|
|
123
|
-
? 'Este provedor só passa a receber vendas depois dos 3 passos abaixo.'
|
|
124
|
-
: 'Conecte e verifique o provedor antes de ativar as vendas.',
|
|
128
|
+
hint: provable ? copy.threeStepsAhead : copy.connectAndVerifyFirst,
|
|
125
129
|
};
|
|
126
130
|
}
|
|
127
131
|
|
|
@@ -133,24 +137,19 @@ function toggleGate(
|
|
|
133
137
|
* opposite situations: the first is a step outstanding, the second a decision
|
|
134
138
|
* the owner made and can undo in one click.
|
|
135
139
|
*/
|
|
136
|
-
function headline(
|
|
140
|
+
function headline(
|
|
141
|
+
copy: ConnectionStatusCopy,
|
|
142
|
+
io: { enabled: boolean; paused: boolean; lockedOff: boolean; hint: string },
|
|
143
|
+
) {
|
|
137
144
|
if (io.enabled) {
|
|
138
|
-
return {
|
|
139
|
-
state: 'Recebendo vendas',
|
|
140
|
-
sub: 'Sua loja está recebendo por este provedor.',
|
|
141
|
-
tone: T.ok,
|
|
142
|
-
};
|
|
145
|
+
return { ...copy.receiving, tone: T.ok };
|
|
143
146
|
}
|
|
144
147
|
if (io.paused) {
|
|
145
|
-
return {
|
|
146
|
-
state: 'Pausado',
|
|
147
|
-
sub: 'Conexão pronta e pausada por você — nenhum pedido novo é cobrado aqui.',
|
|
148
|
-
tone: T.warn,
|
|
149
|
-
};
|
|
148
|
+
return { ...copy.pausedByOwner, tone: T.warn };
|
|
150
149
|
}
|
|
151
150
|
return {
|
|
152
|
-
state:
|
|
153
|
-
sub: io.lockedOff ? io.hint :
|
|
151
|
+
state: copy.readyNotReceiving.state,
|
|
152
|
+
sub: io.lockedOff ? io.hint : copy.readyNotReceiving.sub,
|
|
154
153
|
tone: T.ink3,
|
|
155
154
|
};
|
|
156
155
|
}
|
|
@@ -231,12 +230,13 @@ export function ProviderStatusBar({
|
|
|
231
230
|
onToggle: (enabled: boolean) => void;
|
|
232
231
|
}) {
|
|
233
232
|
const enabled = config?.enabled ?? false;
|
|
234
|
-
const
|
|
235
|
-
const
|
|
233
|
+
const copy = usePaymentsSettingsCopy().status;
|
|
234
|
+
const { lockedOff, hint } = toggleGate(copy, descriptor, config, enabled);
|
|
235
|
+
const badge = statusBadge(copy, config, descriptor);
|
|
236
236
|
|
|
237
237
|
const proven = Boolean(config?.chargeVerifiedAt);
|
|
238
238
|
const paused = proven && !enabled;
|
|
239
|
-
const { state, sub, tone } = headline({ enabled, paused, lockedOff, hint });
|
|
239
|
+
const { state, sub, tone } = headline(copy, { enabled, paused, lockedOff, hint });
|
|
240
240
|
|
|
241
241
|
return (
|
|
242
242
|
<Stack direction="row" alignItems="flex-start" gap="14px" flexWrap="wrap">
|
|
@@ -7,6 +7,7 @@ import type { ProviderSetupGuide as Guide, SetupSection } from '@12-apps/payment
|
|
|
7
7
|
|
|
8
8
|
import { DoneRow } from './CredentialFields';
|
|
9
9
|
import { ProviderSetupGuide } from './ProviderSetupGuide';
|
|
10
|
+
import { usePaymentsSettingsCopy } from './settings-copy-context';
|
|
10
11
|
|
|
11
12
|
/**
|
|
12
13
|
* The walkthrough, plus the one step whose completion only the OWNER can
|
|
@@ -34,9 +35,10 @@ export const CHECKOUT_CONFIRM_ACTION = 'checkout-integrado-confirmado';
|
|
|
34
35
|
* and Stone both own a step no API can report, and neither of them has a
|
|
35
36
|
* "Checkout Integrado" to have enabled. Sections say what they are asking about
|
|
36
37
|
* via `SetupSection.confirmLabel`; this fallback keeps InfinitePay's rendered
|
|
37
|
-
* guide identical.
|
|
38
|
+
* guide identical. It is `setupGuide.defaultConfirmLabel` on the host's pack
|
|
39
|
+
* now, so the vendor's product name is at least chosen rather than compiled in.
|
|
38
40
|
*/
|
|
39
|
-
|
|
41
|
+
|
|
40
42
|
|
|
41
43
|
/** Does this section end in a question only the owner can answer? */
|
|
42
44
|
function isConfirmable(section: SetupSection): boolean {
|
|
@@ -134,12 +136,13 @@ export interface SetupGuideSectionProps {
|
|
|
134
136
|
* still there to press Revisar on.
|
|
135
137
|
*/
|
|
136
138
|
function ConfirmedRow({ section, onReopen }: { section: SetupSection; onReopen: () => void }) {
|
|
139
|
+
const copy = usePaymentsSettingsCopy().setupGuide;
|
|
137
140
|
return (
|
|
138
141
|
<DoneRow
|
|
139
142
|
testId="payments-setup-confirmed"
|
|
140
143
|
label={section.doneSummary?.label ?? section.title}
|
|
141
|
-
value={section.doneSummary?.value ??
|
|
142
|
-
editLabel=
|
|
144
|
+
value={section.doneSummary?.value ?? copy.confirmedByYou}
|
|
145
|
+
editLabel={copy.reviewAction}
|
|
143
146
|
onEdit={onReopen}
|
|
144
147
|
/>
|
|
145
148
|
);
|
|
@@ -157,12 +160,13 @@ function ConfirmedRow({ section, onReopen }: { section: SetupSection; onReopen:
|
|
|
157
160
|
function confirmationOf(
|
|
158
161
|
guide: Guide,
|
|
159
162
|
{ stage, confirmed, editing }: { stage: number; confirmed: boolean; editing: boolean },
|
|
163
|
+
fallbackLabel: string,
|
|
160
164
|
): { label: string; settledSection: SetupSection | null } {
|
|
161
165
|
const asking = confirmableStage(guide);
|
|
162
166
|
const section = asking >= 0 ? sectionAt(guide, asking) : null;
|
|
163
167
|
const settled = confirmed && !editing && asking >= 0 && stage > asking;
|
|
164
168
|
return {
|
|
165
|
-
label: section?.confirmLabel ??
|
|
169
|
+
label: section?.confirmLabel ?? fallbackLabel,
|
|
166
170
|
settledSection: settled ? section : null,
|
|
167
171
|
};
|
|
168
172
|
}
|
|
@@ -193,7 +197,12 @@ export function SetupGuideSection({
|
|
|
193
197
|
|
|
194
198
|
const stage = effectiveStage(guide, confirmed, editing, stored);
|
|
195
199
|
const open = sectionAt(guide, stage);
|
|
196
|
-
const
|
|
200
|
+
const guideCopy = usePaymentsSettingsCopy().setupGuide;
|
|
201
|
+
const { label, settledSection } = confirmationOf(
|
|
202
|
+
guide,
|
|
203
|
+
{ stage, confirmed, editing },
|
|
204
|
+
guideCopy.defaultConfirmLabel,
|
|
205
|
+
);
|
|
197
206
|
|
|
198
207
|
return (
|
|
199
208
|
<Box data-testid="payments-setup">
|
|
@@ -1,8 +1,13 @@
|
|
|
1
1
|
import { Box } from "@mui/material";
|
|
2
2
|
import { useRef, type JSX } from "react";
|
|
3
3
|
|
|
4
|
+
import { useCheckoutCopy } from "./copy-context";
|
|
5
|
+
import type { WalletCopy } from "./screens-copy";
|
|
4
6
|
import type { CheckoutOrder } from "./types";
|
|
5
7
|
|
|
8
|
+
/** The Apple half of {@link WalletCopy} — what this button and its sheet say. */
|
|
9
|
+
type ApplePayCopy = WalletCopy["applePay"];
|
|
10
|
+
|
|
6
11
|
/**
|
|
7
12
|
* The Apple-owned pay button (FUT-472), on `ApplePaySession` — the API Safari
|
|
8
13
|
* ships natively; there is no script to load. This component owns token
|
|
@@ -85,13 +90,16 @@ export function applePaySupported(): boolean {
|
|
|
85
90
|
}
|
|
86
91
|
|
|
87
92
|
/** The sheet's payment request, from the server-authoritative order total. */
|
|
88
|
-
function paymentRequest(order: CheckoutOrder): ApplePayPaymentRequest {
|
|
93
|
+
function paymentRequest(order: CheckoutOrder, copy: ApplePayCopy): ApplePayPaymentRequest {
|
|
89
94
|
return {
|
|
90
95
|
countryCode: "BR",
|
|
91
96
|
currencyCode: "BRL",
|
|
92
97
|
supportedNetworks: APPLE_PAY_SUPPORTED_NETWORKS,
|
|
93
98
|
merchantCapabilities: ["supports3DS"],
|
|
94
|
-
|
|
99
|
+
// The one line Apple's own sheet renders from us, so it is the host's
|
|
100
|
+
// (FUT-760) — the rest of that sheet is Apple's, in the buyer's own
|
|
101
|
+
// system language.
|
|
102
|
+
total: { label: copy.orderTotal, amount: (order.totalCents / 100).toFixed(2) },
|
|
95
103
|
};
|
|
96
104
|
}
|
|
97
105
|
|
|
@@ -109,8 +117,8 @@ export interface ApplePayButtonProps {
|
|
|
109
117
|
* The host's merchant-validation port: exchange `validationURL` for an
|
|
110
118
|
* Apple merchant session, SERVER-SIDE (the merchant identity certificate
|
|
111
119
|
* must never reach a browser). Absent — the external prerequisites are not
|
|
112
|
-
* done, or the host has not wired it — the session aborts with
|
|
113
|
-
*
|
|
120
|
+
* done, or the host has not wired it — the session aborts with the host's
|
|
121
|
+
* `cannotStart` sentence and the card form remains the way to pay.
|
|
114
122
|
*/
|
|
115
123
|
validateMerchant?: (validationURL: string) => Promise<unknown>;
|
|
116
124
|
}
|
|
@@ -120,20 +128,21 @@ function runSession(
|
|
|
120
128
|
Session: ApplePaySessionClass,
|
|
121
129
|
order: CheckoutOrder,
|
|
122
130
|
handlers: ApplePayButtonProps,
|
|
131
|
+
copy: ApplePayCopy,
|
|
123
132
|
): void {
|
|
124
|
-
const session = new Session(APPLE_PAY_VERSION, paymentRequest(order));
|
|
133
|
+
const session = new Session(APPLE_PAY_VERSION, paymentRequest(order, copy));
|
|
125
134
|
session.onvalidatemerchant = (event) => {
|
|
126
135
|
const validate = handlers.validateMerchant;
|
|
127
136
|
if (!validate) {
|
|
128
137
|
session.abort();
|
|
129
|
-
handlers.onError(
|
|
138
|
+
handlers.onError(copy.cannotStart);
|
|
130
139
|
return;
|
|
131
140
|
}
|
|
132
141
|
validate(event.validationURL)
|
|
133
142
|
.then((merchantSession) => session.completeMerchantValidation(merchantSession))
|
|
134
143
|
.catch(() => {
|
|
135
144
|
session.abort();
|
|
136
|
-
handlers.onError(
|
|
145
|
+
handlers.onError(copy.cannotComplete);
|
|
137
146
|
});
|
|
138
147
|
};
|
|
139
148
|
session.onpaymentauthorized = (event) => {
|
|
@@ -162,15 +171,16 @@ export function ApplePayButton(props: ApplePayButtonProps): JSX.Element | null {
|
|
|
162
171
|
// over a stale charge target.
|
|
163
172
|
const current = useRef(props);
|
|
164
173
|
current.current = props;
|
|
174
|
+
const copy = useCheckoutCopy().screens.wallet.applePay;
|
|
165
175
|
const Session = applePaySessionClass();
|
|
166
176
|
if (!Session || !applePaySupported()) return null;
|
|
167
177
|
return (
|
|
168
178
|
<Box
|
|
169
179
|
component="button"
|
|
170
180
|
type="button"
|
|
171
|
-
aria-label=
|
|
181
|
+
aria-label={copy.payAction}
|
|
172
182
|
data-testid="apple-pay-button"
|
|
173
|
-
onClick={() => runSession(Session, current.current.order, current.current)}
|
|
183
|
+
onClick={() => runSession(Session, current.current.order, current.current, copy)}
|
|
174
184
|
sx={{
|
|
175
185
|
WebkitAppearance: "-apple-pay-button",
|
|
176
186
|
// Apple draws the button; these only give it room. The fallback
|
|
@@ -28,7 +28,7 @@
|
|
|
28
28
|
* the two must not disagree about what a chain needs.
|
|
29
29
|
*/
|
|
30
30
|
|
|
31
|
-
import {
|
|
31
|
+
import { isValidCpf } from "../../card";
|
|
32
32
|
|
|
33
33
|
import type { CheckoutChainLink, CheckoutCustomerField, PaymentMethod } from "./types";
|
|
34
34
|
|
|
@@ -105,8 +105,10 @@ const RULES: Record<CheckoutCustomerField["type"], (value: string) => boolean> =
|
|
|
105
105
|
PHONE: (value) => /^\d{10,11}$/.test(value.replace(/\D/g, "")),
|
|
106
106
|
MOBILE: (value) => /^\d{2}9\d{8}$/.test(value.replace(/\D/g, "")),
|
|
107
107
|
// The real check-digit rule, not a length test: the same validator the CPF
|
|
108
|
-
// input has always used, so the form and the gate cannot disagree.
|
|
109
|
-
|
|
108
|
+
// input has always used, so the form and the gate cannot disagree. The
|
|
109
|
+
// PREDICATE rather than the message — this is a gate, and it would only be
|
|
110
|
+
// throwing the sentence away (FUT-760).
|
|
111
|
+
CPF: (value) => isValidCpf(value),
|
|
110
112
|
};
|
|
111
113
|
|
|
112
114
|
/** Whether a declared field is satisfied by what the buyer typed. */
|