@12-apps/payments-frontend 3.8.0 → 3.10.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +2 -2
- package/src/activation/charge-copy.ts +10 -0
- package/src/activation/use-activation-charge.ts +10 -8
- package/src/card/copy.ts +115 -0
- package/src/card/cpf.ts +12 -5
- package/src/card/fields.tsx +17 -12
- package/src/card/format.ts +32 -16
- package/src/card/index.ts +3 -1
- package/src/card/pt-BR.ts +51 -0
- package/src/card/stripe-token.ts +5 -6
- package/src/card/tokenize.ts +28 -32
- package/src/components/CheckoutPayment.tsx +37 -201
- package/src/components/ConfirmCredentialSave.tsx +8 -10
- package/src/components/ConnectionCard.tsx +23 -18
- package/src/components/ConnectionProbe.tsx +3 -1
- package/src/components/CredentialFieldStack.tsx +8 -2
- package/src/components/CredentialFields.tsx +13 -6
- package/src/components/CredentialFormAlerts.tsx +28 -19
- package/src/components/EnvironmentTabs.tsx +23 -15
- package/src/components/OAuthPanel.tsx +5 -6
- package/src/components/PaymentProviderSettings.tsx +45 -13
- package/src/components/ProviderConnection.tsx +37 -28
- package/src/components/ProviderList.tsx +6 -3
- package/src/components/ProviderPriorityList.tsx +18 -16
- package/src/components/ProviderStatusBar.tsx +24 -24
- package/src/components/SetupGuideSection.tsx +15 -6
- package/src/components/checkout/apple-pay-button.tsx +19 -9
- package/src/components/checkout/buyer-fields.ts +5 -3
- package/src/components/checkout/buyer-gate.ts +68 -0
- package/src/components/checkout/buyer-info-form.tsx +14 -15
- package/src/components/checkout/card-instruments.ts +31 -12
- package/src/components/checkout/card-view.tsx +11 -7
- package/src/components/checkout/checkout-flow.tsx +4 -1
- package/src/components/checkout/checkout-steps.tsx +3 -1
- package/src/components/checkout/client-context.tsx +28 -16
- package/src/components/checkout/client.ts +43 -16
- package/src/components/checkout/copy-context.tsx +83 -0
- package/src/components/checkout/google-pay-button.tsx +19 -7
- package/src/components/checkout/method-picker.tsx +34 -10
- package/src/components/checkout/payer-summary.tsx +8 -6
- package/src/components/checkout/payment-error-panel.tsx +8 -5
- package/src/components/checkout/pix-view.tsx +16 -9
- package/src/components/checkout/providers/hosted-link.tsx +26 -16
- package/src/components/checkout/pt-BR.ts +27 -0
- package/src/components/checkout/screens-copy.ts +201 -0
- package/src/components/checkout/screens-pt-BR.ts +101 -0
- package/src/components/checkout/transport.ts +34 -8
- package/src/components/checkout/use-card-checkout.ts +13 -5
- package/src/components/checkout/use-checkout-controller.ts +5 -48
- package/src/components/checkout/view-copy.ts +13 -0
- package/src/components/checkout/wallet-pane.tsx +13 -8
- package/src/components/checkout-payment-copy.ts +88 -0
- package/src/components/checkout-payment-pt-BR.ts +50 -0
- package/src/components/checkout-payment-types.ts +25 -0
- package/src/components/checkout-payment-views.tsx +225 -0
- package/src/components/connection-state.ts +22 -10
- package/src/components/credential-rules.ts +8 -3
- package/src/components/platform/ConnectApplicationPanel.tsx +6 -2
- package/src/components/platform/ConnectEnvironmentCard.tsx +19 -15
- package/src/components/platform/HomologacaoGuideCard.tsx +23 -19
- package/src/components/platform/HomologacaoOutcomeCard.tsx +24 -16
- package/src/components/platform/PlatformHomologacao.tsx +12 -8
- package/src/components/settings-copy-context.tsx +50 -0
- package/src/components/settings-copy.ts +304 -0
- package/src/components/settings-pt-BR.ts +169 -0
- package/src/flows/copy.ts +12 -0
- package/src/flows/create-payment-flows.tsx +8 -1
- package/src/flows/runtime.tsx +12 -4
- package/src/flows/screens-vault.tsx +10 -10
- package/src/flows/types.ts +5 -2
- package/src/flows/use-add-card.ts +10 -5
- package/src/index.ts +63 -0
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Every sentence the LEGACY `CheckoutPayment` step renders (FUT-760).
|
|
3
|
+
*
|
|
4
|
+
* A required prop rather than a context, unlike the factory's screens: this
|
|
5
|
+
* component is one plug-and-play step a host drops in itself, and there is no
|
|
6
|
+
* tree of pieces below it to reach. The host already passes `reference`,
|
|
7
|
+
* `amount` and `tokenizeCard` at the same call; the words go beside them.
|
|
8
|
+
*
|
|
9
|
+
* The split is the one the whole port draws. WHICH state the charge is in — a
|
|
10
|
+
* PIX code waiting to be scanned, a hosted page the buyer is about to be sent
|
|
11
|
+
* to, a store with no provider at all — is knowledge of the payments
|
|
12
|
+
* lifecycle, and it stays here. The words are the host's.
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
/** The money line, and the locale that renders it. */
|
|
16
|
+
export interface LegacyMoneyCopy {
|
|
17
|
+
/** The heading above the amount being paid. */
|
|
18
|
+
totalLabel(formattedAmount: string): string;
|
|
19
|
+
/** The button that authorizes it, on both the saved-card and hosted paths. */
|
|
20
|
+
payAction(formattedAmount: string): string;
|
|
21
|
+
/**
|
|
22
|
+
* The BCP-47 tag the amount is formatted in — `'pt-BR'` here, and hard-coded
|
|
23
|
+
* before this port. The CURRENCY is not here: it arrives on the `Money` the
|
|
24
|
+
* host passes, which is the store's own, not a translator's.
|
|
25
|
+
*/
|
|
26
|
+
amountLocale: string;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/** The two method cards, before a charge exists. */
|
|
30
|
+
export interface LegacyMethodCopy {
|
|
31
|
+
groupLabel: string;
|
|
32
|
+
pixTitle: string;
|
|
33
|
+
pixSubtitle: string;
|
|
34
|
+
cardTitle: string;
|
|
35
|
+
cardSubtitle: string;
|
|
36
|
+
/** PIX's own action — this step generates the code rather than charging. */
|
|
37
|
+
generatePixAction: string;
|
|
38
|
+
/** A REDIRECT provider offers no choice: one button, and the buyer leaves. */
|
|
39
|
+
continueToPaymentAction: string;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/** The PIX panel: the code, and the wait. */
|
|
43
|
+
export interface LegacyPixCopy {
|
|
44
|
+
qrAlt: string;
|
|
45
|
+
/** The label on the copy-and-paste payload field. */
|
|
46
|
+
copyPasteLabel: string;
|
|
47
|
+
copyAction: string;
|
|
48
|
+
copiedAction: string;
|
|
49
|
+
awaiting: string;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/** The card form, and the saved cards above it. */
|
|
53
|
+
export interface LegacyCardCopy {
|
|
54
|
+
/** The heading over the saved-card radios. */
|
|
55
|
+
heading: string;
|
|
56
|
+
numberLabel: string;
|
|
57
|
+
holderLabel: string;
|
|
58
|
+
/** Its parenthetical spells the ORDER — "MM/AA" in pt-BR, "MM/YY" in en. */
|
|
59
|
+
expiryLabel: string;
|
|
60
|
+
cvvLabel: string;
|
|
61
|
+
payAction: string;
|
|
62
|
+
/** The trailing radio that opens the form. */
|
|
63
|
+
newCard: string;
|
|
64
|
+
/**
|
|
65
|
+
* How a saved card reads. Takes the parts rather than a sentence, because
|
|
66
|
+
* the expiry is optional — a provider that shared none leaves it out.
|
|
67
|
+
*/
|
|
68
|
+
savedCard(brand: string, last4: string, expiry?: string): string;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/** What this step says when it cannot take a payment at all. */
|
|
72
|
+
export interface LegacyRefusalCopy {
|
|
73
|
+
/** No provider is configured — nothing the buyer can do. */
|
|
74
|
+
paymentsOff: string;
|
|
75
|
+
/** The host wired no `tokenizeCard`, so a card cannot be minted here. */
|
|
76
|
+
cardUnavailable: string;
|
|
77
|
+
/** The interstitial before a hosted provider's own page. */
|
|
78
|
+
redirectNotice: string;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/** Everything above, in one object a host passes at the mount. */
|
|
82
|
+
export interface CheckoutPaymentCopy {
|
|
83
|
+
money: LegacyMoneyCopy;
|
|
84
|
+
method: LegacyMethodCopy;
|
|
85
|
+
pix: LegacyPixCopy;
|
|
86
|
+
card: LegacyCardCopy;
|
|
87
|
+
refusal: LegacyRefusalCopy;
|
|
88
|
+
}
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import type { CheckoutPaymentCopy } from './checkout-payment-copy';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* The pt-BR pack for the legacy `CheckoutPayment` step — a NAMED constant a
|
|
5
|
+
* host 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 step used to render, so a host adopting it sees no change — what changes
|
|
10
|
+
* is that the words are chosen in a diff.
|
|
11
|
+
*/
|
|
12
|
+
export const PT_BR_CHECKOUT_PAYMENT_COPY: CheckoutPaymentCopy = {
|
|
13
|
+
money: {
|
|
14
|
+
totalLabel: (formattedAmount) => `Total: ${formattedAmount}`,
|
|
15
|
+
payAction: (formattedAmount) => `Pagar ${formattedAmount}`,
|
|
16
|
+
amountLocale: 'pt-BR',
|
|
17
|
+
},
|
|
18
|
+
method: {
|
|
19
|
+
groupLabel: 'Forma de pagamento',
|
|
20
|
+
pixTitle: 'PIX',
|
|
21
|
+
pixSubtitle: 'Aprovação imediata',
|
|
22
|
+
cardTitle: 'Cartão',
|
|
23
|
+
cardSubtitle: 'Crédito à vista',
|
|
24
|
+
generatePixAction: 'Gerar QR Code PIX',
|
|
25
|
+
continueToPaymentAction: 'Continuar para o pagamento',
|
|
26
|
+
},
|
|
27
|
+
pix: {
|
|
28
|
+
qrAlt: 'QR Code PIX',
|
|
29
|
+
copyPasteLabel: 'PIX copia e cola',
|
|
30
|
+
copyAction: 'Copiar código',
|
|
31
|
+
copiedAction: 'Copiado!',
|
|
32
|
+
awaiting: 'Aguardando pagamento…',
|
|
33
|
+
},
|
|
34
|
+
card: {
|
|
35
|
+
heading: 'Pague com cartão',
|
|
36
|
+
numberLabel: 'Número do cartão',
|
|
37
|
+
holderLabel: 'Nome impresso no cartão',
|
|
38
|
+
expiryLabel: 'Validade (MM/AA)',
|
|
39
|
+
cvvLabel: 'CVV',
|
|
40
|
+
payAction: 'Pagar com cartão',
|
|
41
|
+
newCard: 'Novo cartão — inserir outro cartão',
|
|
42
|
+
savedCard: (brand, last4, expiry) =>
|
|
43
|
+
`${brand} •••• ${last4}${expiry ? ` — Validade ${expiry}` : ''}`,
|
|
44
|
+
},
|
|
45
|
+
refusal: {
|
|
46
|
+
paymentsOff: 'Esta loja ainda não aceita pagamentos online.',
|
|
47
|
+
cardUnavailable: 'Pagamento com cartão indisponível.',
|
|
48
|
+
redirectNotice: 'Você será direcionado para concluir o pagamento com segurança.',
|
|
49
|
+
},
|
|
50
|
+
};
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The two shapes the legacy `CheckoutPayment` step and its views both name.
|
|
3
|
+
*
|
|
4
|
+
* Their own module so the views can import them without importing the
|
|
5
|
+
* container that renders the views — the circular import that would otherwise
|
|
6
|
+
* follow from splitting the file (FUT-760). Both stay exported from the
|
|
7
|
+
* package root, where they have always been.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
/** What the card form collects, before anything is tokenized. */
|
|
11
|
+
export interface CardFormValues {
|
|
12
|
+
number: string;
|
|
13
|
+
holder: string;
|
|
14
|
+
expiry: string;
|
|
15
|
+
cvv: string;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/** A provider-vaulted card the buyer may reuse ("Mastercard •••• 7599"). */
|
|
19
|
+
export interface SavedCardOption {
|
|
20
|
+
savedCardToken: string;
|
|
21
|
+
brand: string;
|
|
22
|
+
last4: string;
|
|
23
|
+
/** e.g. "02/2034" — shown beside the brand, worded by the host. */
|
|
24
|
+
expiry?: string;
|
|
25
|
+
}
|
|
@@ -0,0 +1,225 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* The legacy `CheckoutPayment` step's PRESENTATION — the PIX panel, the card
|
|
5
|
+
* form, the saved-card list and the two method cards.
|
|
6
|
+
*
|
|
7
|
+
* Split from `CheckoutPayment.tsx` when the copy port (FUT-760) pushed that
|
|
8
|
+
* file past the size gate, along the seam it already had: everything here
|
|
9
|
+
* takes props and renders, and nothing here holds a client, a charge or an
|
|
10
|
+
* effect. The container keeps those.
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import {
|
|
14
|
+
Box,
|
|
15
|
+
Button,
|
|
16
|
+
CircularProgress,
|
|
17
|
+
FormControlLabel,
|
|
18
|
+
Radio,
|
|
19
|
+
RadioGroup,
|
|
20
|
+
Stack,
|
|
21
|
+
TextField,
|
|
22
|
+
Typography,
|
|
23
|
+
} from '@mui/material';
|
|
24
|
+
import { useState } from 'react';
|
|
25
|
+
|
|
26
|
+
import type { ClientChargeView, Money } from '@12-apps/payments-backend';
|
|
27
|
+
|
|
28
|
+
import type { ClientPaymentsConfig } from '../client';
|
|
29
|
+
|
|
30
|
+
import type {
|
|
31
|
+
CheckoutPaymentCopy,
|
|
32
|
+
LegacyCardCopy,
|
|
33
|
+
LegacyMethodCopy,
|
|
34
|
+
LegacyMoneyCopy,
|
|
35
|
+
LegacyPixCopy,
|
|
36
|
+
} from './checkout-payment-copy';
|
|
37
|
+
import type { CardFormValues, SavedCardOption } from './checkout-payment-types';
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* The amount, in the host's locale and the store's currency.
|
|
41
|
+
*
|
|
42
|
+
* The locale is the host's say (FUT-760) — it decides the decimal mark, the
|
|
43
|
+
* grouping and where the symbol sits. The currency is not: it rides on the
|
|
44
|
+
* `Money` the host already passes, and is a fact about the charge.
|
|
45
|
+
*/
|
|
46
|
+
export function formatAmount(amount: Money, copy: LegacyMoneyCopy): string {
|
|
47
|
+
return (amount.amountCents / 100).toLocaleString(copy.amountLocale, {
|
|
48
|
+
style: 'currency',
|
|
49
|
+
currency: amount.currency,
|
|
50
|
+
});
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export function PixPanel({ charge, copy }: { charge: ClientChargeView; copy: LegacyPixCopy }) {
|
|
54
|
+
const [copied, setCopied] = useState(false);
|
|
55
|
+
if (!charge.pix) return null;
|
|
56
|
+
return (
|
|
57
|
+
<Stack spacing={1} alignItems="flex-start">
|
|
58
|
+
{charge.pix.qrImageUrl ? (
|
|
59
|
+
<Box component="img" src={charge.pix.qrImageUrl} alt={copy.qrAlt} sx={{ width: 220 }} />
|
|
60
|
+
) : null}
|
|
61
|
+
<TextField fullWidth multiline size="small" label={copy.copyPasteLabel} value={charge.pix.qrText} />
|
|
62
|
+
<Button
|
|
63
|
+
size="small"
|
|
64
|
+
onClick={() => {
|
|
65
|
+
void navigator.clipboard.writeText(charge.pix?.qrText ?? '').then(() => setCopied(true));
|
|
66
|
+
}}
|
|
67
|
+
>
|
|
68
|
+
{copied ? copy.copiedAction : copy.copyAction}
|
|
69
|
+
</Button>
|
|
70
|
+
<Stack direction="row" spacing={1} alignItems="center">
|
|
71
|
+
<CircularProgress size={16} />
|
|
72
|
+
<Typography variant="body2">{copy.awaiting}</Typography>
|
|
73
|
+
</Stack>
|
|
74
|
+
</Stack>
|
|
75
|
+
);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
function CardPanel({
|
|
79
|
+
disabled,
|
|
80
|
+
onSubmit,
|
|
81
|
+
copy,
|
|
82
|
+
}: {
|
|
83
|
+
disabled: boolean;
|
|
84
|
+
onSubmit: (values: CardFormValues) => void;
|
|
85
|
+
copy: LegacyCardCopy;
|
|
86
|
+
}) {
|
|
87
|
+
const [values, setValues] = useState<CardFormValues>({ number: '', holder: '', expiry: '', cvv: '' });
|
|
88
|
+
const field = (key: keyof CardFormValues, label: string, width?: number) => (
|
|
89
|
+
<TextField
|
|
90
|
+
size="small"
|
|
91
|
+
label={label}
|
|
92
|
+
value={values[key]}
|
|
93
|
+
sx={width ? { width } : undefined}
|
|
94
|
+
onChange={(e) => setValues((v) => ({ ...v, [key]: e.target.value }))}
|
|
95
|
+
/>
|
|
96
|
+
);
|
|
97
|
+
return (
|
|
98
|
+
<Stack spacing={2}>
|
|
99
|
+
{field('number', copy.numberLabel)}
|
|
100
|
+
{field('holder', copy.holderLabel)}
|
|
101
|
+
<Stack direction="row" spacing={2}>
|
|
102
|
+
{field('expiry', copy.expiryLabel, 140)}
|
|
103
|
+
{field('cvv', copy.cvvLabel, 100)}
|
|
104
|
+
</Stack>
|
|
105
|
+
<Button variant="contained" disabled={disabled} onClick={() => onSubmit(values)}>
|
|
106
|
+
{copy.payAction}
|
|
107
|
+
</Button>
|
|
108
|
+
</Stack>
|
|
109
|
+
);
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
interface CardSectionProps {
|
|
113
|
+
amount: Money;
|
|
114
|
+
savedCards: SavedCardOption[];
|
|
115
|
+
disabled: boolean;
|
|
116
|
+
onNewCard: (values: CardFormValues) => void;
|
|
117
|
+
onSavedCard: (card: SavedCardOption) => void;
|
|
118
|
+
copy: CheckoutPaymentCopy;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/** "Pague com cartão": saved cards as radios + "Novo cartão" fallback. */
|
|
122
|
+
function CardSection({
|
|
123
|
+
amount,
|
|
124
|
+
savedCards,
|
|
125
|
+
disabled,
|
|
126
|
+
onNewCard,
|
|
127
|
+
onSavedCard,
|
|
128
|
+
copy,
|
|
129
|
+
}: CardSectionProps) {
|
|
130
|
+
const [selected, setSelected] = useState(savedCards[0]?.savedCardToken ?? 'new');
|
|
131
|
+
const chosen = savedCards.find((c) => c.savedCardToken === selected);
|
|
132
|
+
if (savedCards.length === 0) {
|
|
133
|
+
return <CardPanel disabled={disabled} onSubmit={onNewCard} copy={copy.card} />;
|
|
134
|
+
}
|
|
135
|
+
return (
|
|
136
|
+
<Stack spacing={2}>
|
|
137
|
+
<Typography variant="subtitle2">{copy.card.heading}</Typography>
|
|
138
|
+
<RadioGroup value={selected} onChange={(_, v) => setSelected(v)}>
|
|
139
|
+
{savedCards.map((card) => (
|
|
140
|
+
<FormControlLabel
|
|
141
|
+
key={card.savedCardToken}
|
|
142
|
+
value={card.savedCardToken}
|
|
143
|
+
control={<Radio />}
|
|
144
|
+
label={copy.card.savedCard(card.brand, card.last4, card.expiry)}
|
|
145
|
+
/>
|
|
146
|
+
))}
|
|
147
|
+
<FormControlLabel value="new" control={<Radio />} label={copy.card.newCard} />
|
|
148
|
+
</RadioGroup>
|
|
149
|
+
{chosen ? (
|
|
150
|
+
<Button variant="contained" disabled={disabled} onClick={() => onSavedCard(chosen)}>
|
|
151
|
+
{copy.money.payAction(formatAmount(amount, copy.money))}
|
|
152
|
+
</Button>
|
|
153
|
+
) : (
|
|
154
|
+
<CardPanel disabled={disabled} onSubmit={onNewCard} copy={copy.card} />
|
|
155
|
+
)}
|
|
156
|
+
</Stack>
|
|
157
|
+
);
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
interface MethodChooserProps {
|
|
161
|
+
config: ClientPaymentsConfig;
|
|
162
|
+
amount: Money;
|
|
163
|
+
savedCards: SavedCardOption[];
|
|
164
|
+
loading: boolean;
|
|
165
|
+
startPix: () => void;
|
|
166
|
+
startCard: (values: CardFormValues) => void;
|
|
167
|
+
startSavedCard: (card: SavedCardOption) => void;
|
|
168
|
+
copy: CheckoutPaymentCopy;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
/** One "Forma de pagamento" option card (PIX / Cartão). */
|
|
172
|
+
function MethodCard(props: { title: string; subtitle: string; selected: boolean; onClick: () => void }) {
|
|
173
|
+
return (
|
|
174
|
+
<Button
|
|
175
|
+
variant="outlined"
|
|
176
|
+
onClick={props.onClick}
|
|
177
|
+
sx={{ flex: 1, justifyContent: 'flex-start', textAlign: 'left', borderWidth: props.selected ? 2 : 1 }}
|
|
178
|
+
>
|
|
179
|
+
<Stack alignItems="flex-start">
|
|
180
|
+
<Typography variant="subtitle2">{props.title}</Typography>
|
|
181
|
+
<Typography variant="caption" color="text.secondary">
|
|
182
|
+
{props.subtitle}
|
|
183
|
+
</Typography>
|
|
184
|
+
</Stack>
|
|
185
|
+
</Button>
|
|
186
|
+
);
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
/** Pre-charge phase: "Forma de pagamento" method cards, then the flow. */
|
|
190
|
+
export function MethodChooser(props: MethodChooserProps) {
|
|
191
|
+
const { config, amount, savedCards, loading, startPix, startCard, startSavedCard, copy } = props;
|
|
192
|
+
const words: LegacyMethodCopy = copy.method;
|
|
193
|
+
const [method, setMethod] = useState<'PIX' | 'CARD'>('PIX');
|
|
194
|
+
if (config.tokenization === 'REDIRECT') {
|
|
195
|
+
return (
|
|
196
|
+
<Button variant="contained" disabled={loading} onClick={startPix}>
|
|
197
|
+
{words.continueToPaymentAction}
|
|
198
|
+
</Button>
|
|
199
|
+
);
|
|
200
|
+
}
|
|
201
|
+
return (
|
|
202
|
+
<>
|
|
203
|
+
<Typography variant="subtitle2">{words.groupLabel}</Typography>
|
|
204
|
+
<Stack direction="row" spacing={2}>
|
|
205
|
+
<MethodCard title={words.pixTitle} subtitle={words.pixSubtitle} selected={method === 'PIX'} onClick={() => setMethod('PIX')} />
|
|
206
|
+
<MethodCard title={words.cardTitle} subtitle={words.cardSubtitle} selected={method === 'CARD'} onClick={() => setMethod('CARD')} />
|
|
207
|
+
</Stack>
|
|
208
|
+
{method === 'PIX' ? (
|
|
209
|
+
<Button variant="contained" disabled={loading} onClick={startPix}>
|
|
210
|
+
{words.generatePixAction}
|
|
211
|
+
</Button>
|
|
212
|
+
) : (
|
|
213
|
+
<CardSection
|
|
214
|
+
amount={amount}
|
|
215
|
+
savedCards={savedCards}
|
|
216
|
+
disabled={loading}
|
|
217
|
+
onNewCard={startCard}
|
|
218
|
+
onSavedCard={startSavedCard}
|
|
219
|
+
copy={copy}
|
|
220
|
+
/>
|
|
221
|
+
)}
|
|
222
|
+
</>
|
|
223
|
+
);
|
|
224
|
+
}
|
|
225
|
+
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { MaskedProviderConfig } from '@12-apps/payments-backend';
|
|
2
|
+
import type { ConnectionBadgeCopy } from './settings-copy';
|
|
2
3
|
|
|
3
4
|
/**
|
|
4
5
|
* Is there actually a connection here?
|
|
@@ -77,21 +78,32 @@ export function expiryProximity(
|
|
|
77
78
|
return at - now.getTime() <= EXPIRY_WARNING_WINDOW_MS ? 'NEAR' : 'SAFE';
|
|
78
79
|
}
|
|
79
80
|
|
|
80
|
-
/**
|
|
81
|
+
/**
|
|
82
|
+
* What a provider card reports at a glance.
|
|
83
|
+
*
|
|
84
|
+
* The union used to spell its four labels in Portuguese — an exported TYPE
|
|
85
|
+
* carrying one product's words, which is the structural half of the same leak
|
|
86
|
+
* `CardBrand` had (FUT-760). It names the STATE now; the word is the host's.
|
|
87
|
+
*/
|
|
81
88
|
type ConnectionBadge =
|
|
82
|
-
| { label: '
|
|
83
|
-
| { label: '
|
|
84
|
-
| { label: '
|
|
85
|
-
| { label:
|
|
89
|
+
| { label: string; state: 'ACTIVE'; color: 'success' }
|
|
90
|
+
| { label: string; state: 'CONNECTED'; color: 'default' }
|
|
91
|
+
| { label: string; state: 'RECONNECT'; color: 'error' }
|
|
92
|
+
| { label: string; state: 'NOT_CONNECTED'; color: 'warning' };
|
|
86
93
|
|
|
87
94
|
/**
|
|
88
95
|
* `enabled` is deliberately checked first: it is the only state that means
|
|
89
96
|
* money can move, and a store reading `Ativo` has already passed everything
|
|
90
97
|
* below it.
|
|
91
98
|
*/
|
|
92
|
-
export function connectionBadge(
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
return { label:
|
|
99
|
+
export function connectionBadge(
|
|
100
|
+
copy: ConnectionBadgeCopy,
|
|
101
|
+
config: MaskedProviderConfig | null,
|
|
102
|
+
): ConnectionBadge {
|
|
103
|
+
if (config?.enabled) return { label: copy.active, state: 'ACTIVE', color: 'success' };
|
|
104
|
+
if (config?.status === 'RECONNECT_REQUIRED') {
|
|
105
|
+
return { label: copy.reconnect, state: 'RECONNECT', color: 'error' };
|
|
106
|
+
}
|
|
107
|
+
if (isConnected(config)) return { label: copy.connected, state: 'CONNECTED', color: 'default' };
|
|
108
|
+
return { label: copy.notConnected, state: 'NOT_CONNECTED', color: 'warning' };
|
|
97
109
|
}
|
|
@@ -6,6 +6,7 @@ import type {
|
|
|
6
6
|
} from '@12-apps/payments-backend';
|
|
7
7
|
|
|
8
8
|
import type { PendingSave } from './ConfirmCredentialSave';
|
|
9
|
+
import type { CredentialFormCopy } from './settings-copy';
|
|
9
10
|
|
|
10
11
|
/**
|
|
11
12
|
* The DECISIONS the credential form makes, with no JSX anywhere near them.
|
|
@@ -91,11 +92,15 @@ export function fieldsWellFormed(
|
|
|
91
92
|
* is the more useful of the two truths. Naming one of Stripe's four would be a
|
|
92
93
|
* lie about what the button writes, which is why only the fallback changes.
|
|
93
94
|
*/
|
|
94
|
-
export function saveLabel(
|
|
95
|
+
export function saveLabel(
|
|
96
|
+
copy: CredentialFormCopy,
|
|
97
|
+
descriptor: ProviderDescriptor,
|
|
98
|
+
complete: boolean,
|
|
99
|
+
): string {
|
|
95
100
|
const required = descriptor.credentialSchema.filter((field) => field.required);
|
|
96
101
|
const only = required.length === 1 ? required[0] : undefined;
|
|
97
|
-
if (!only) return complete ?
|
|
98
|
-
return
|
|
102
|
+
if (!only) return complete ? copy.saveAndTest : copy.save;
|
|
103
|
+
return copy.saveOnly(only.label.replace(/\s*\([^)]*\)\s*$/, ''));
|
|
99
104
|
}
|
|
100
105
|
|
|
101
106
|
/**
|
|
@@ -24,6 +24,10 @@ import { CARD_SX, ConnectEnvironmentCard } from './ConnectEnvironmentCard';
|
|
|
24
24
|
* (`consultConnectApplications` in `@12-apps/payments-backend`) and passes it
|
|
25
25
|
* here, so the host page is a thin mount — page chrome, auth and loading
|
|
26
26
|
* belong to the host; the screen itself lives in this package.
|
|
27
|
+
*
|
|
28
|
+
* English, like the rest of this platform surface (FUT-760): the reader is the
|
|
29
|
+
* deployment's own integrator, reading redirect URIs and environment variable
|
|
30
|
+
* names, and everything a developer reads in this repo is English.
|
|
27
31
|
*/
|
|
28
32
|
export interface ConnectApplicationPanelProps {
|
|
29
33
|
/** The consult report, as the backend's `consultConnectApplications` answers. */
|
|
@@ -44,7 +48,7 @@ export function ConnectApplicationPanel(props: ConnectApplicationPanelProps): Re
|
|
|
44
48
|
<Stack spacing={2} data-testid="connect-application-panel">
|
|
45
49
|
<Stack spacing={0.5} data-testid="connect-expected-redirect" sx={CARD_SX}>
|
|
46
50
|
<Typography variant="caption" color="text.secondary" fontWeight={600}>
|
|
47
|
-
Callback
|
|
51
|
+
Callback this deployment uses (the value that must be registered)
|
|
48
52
|
</Typography>
|
|
49
53
|
<Box
|
|
50
54
|
component="code"
|
|
@@ -68,7 +72,7 @@ export function ConnectApplicationPanel(props: ConnectApplicationPanelProps): Re
|
|
|
68
72
|
onClick={() => onRefresh()}
|
|
69
73
|
data-testid="connect-refresh"
|
|
70
74
|
>
|
|
71
|
-
|
|
75
|
+
Consult again
|
|
72
76
|
</Button>
|
|
73
77
|
</Box>
|
|
74
78
|
) : null}
|
|
@@ -7,15 +7,18 @@ import type { ConnectApplicationStatus, PaymentEnvironment } from '@12-apps/paym
|
|
|
7
7
|
|
|
8
8
|
/**
|
|
9
9
|
* One environment's Connect application (FUT-479, packaged by FUT-573).
|
|
10
|
-
* Sandbox and
|
|
10
|
+
* Sandbox and production are SEPARATE applications with separate id/secret
|
|
11
11
|
* pairs, so each card stands on its own: configured or not, what PagBank says
|
|
12
12
|
* is registered, and whether the registered redirect URI matches the
|
|
13
13
|
* deployment's callback.
|
|
14
|
+
*
|
|
15
|
+
* English, like the rest of this platform surface (FUT-760) — see
|
|
16
|
+
* `ConnectApplicationPanel` for why.
|
|
14
17
|
*/
|
|
15
18
|
|
|
16
19
|
const ENV_LABEL: Record<PaymentEnvironment, string> = {
|
|
17
20
|
SANDBOX: 'Sandbox',
|
|
18
|
-
PRODUCTION: '
|
|
21
|
+
PRODUCTION: 'Production',
|
|
19
22
|
};
|
|
20
23
|
|
|
21
24
|
/** The bordered-card look every block of these screens shares. */
|
|
@@ -47,22 +50,23 @@ function MismatchAlert({ status }: { status: ConnectApplicationStatus }): ReactN
|
|
|
47
50
|
if (status.redirectUriMismatch === true) {
|
|
48
51
|
return (
|
|
49
52
|
<Alert severity="error" data-testid={`connect-mismatch-${status.environment}`}>
|
|
50
|
-
|
|
51
|
-
|
|
53
|
+
The redirect_uri registered with PagBank differs from the callback this deployment
|
|
54
|
+
uses. The OAuth authorization flow fails silently until the registration is corrected
|
|
55
|
+
at PagBank.
|
|
52
56
|
</Alert>
|
|
53
57
|
);
|
|
54
58
|
}
|
|
55
59
|
if (status.redirectUriMismatch === false) {
|
|
56
60
|
return (
|
|
57
61
|
<Alert severity="success" data-testid={`connect-match-${status.environment}`}>
|
|
58
|
-
|
|
62
|
+
The registered redirect_uri matches the callback this deployment uses.
|
|
59
63
|
</Alert>
|
|
60
64
|
);
|
|
61
65
|
}
|
|
62
66
|
return (
|
|
63
67
|
<Alert severity="warning" data-testid={`connect-unknown-${status.environment}`}>
|
|
64
|
-
|
|
65
|
-
callback
|
|
68
|
+
The PagBank response carried no redirect_uri, so it could not be compared with the
|
|
69
|
+
callback this deployment uses.
|
|
66
70
|
</Alert>
|
|
67
71
|
);
|
|
68
72
|
}
|
|
@@ -75,16 +79,16 @@ function ApplicationFields({ status }: { status: ConnectApplicationStatus }): Re
|
|
|
75
79
|
return (
|
|
76
80
|
<Stack spacing={1.5}>
|
|
77
81
|
<Box sx={{ display: 'flex', flexWrap: 'wrap', gap: 2.5 }}>
|
|
78
|
-
<Field label="
|
|
82
|
+
<Field label="Name (shown to the merchant)">{app.name ?? '—'}</Field>
|
|
79
83
|
<Field label="Site">{app.site ?? '—'}</Field>
|
|
80
|
-
<Field label="
|
|
84
|
+
<Field label="Description">{app.description ?? '—'}</Field>
|
|
81
85
|
<Field label="Logo">{app.logo ?? '—'}</Field>
|
|
82
|
-
<Field label="redirect_uri
|
|
86
|
+
<Field label="Registered redirect_uri">{app.redirectUri ?? 'not reported'}</Field>
|
|
83
87
|
</Box>
|
|
84
88
|
{extraKeys.length > 0 ? (
|
|
85
89
|
<Box data-testid={`connect-extra-${status.environment}`}>
|
|
86
90
|
<Typography variant="caption" color="text.secondary" fontWeight={600}>
|
|
87
|
-
|
|
91
|
+
Other fields returned (undocumented schema)
|
|
88
92
|
</Typography>
|
|
89
93
|
<Box component="pre" sx={{ m: 0, fontSize: 12, overflowX: 'auto' }}>
|
|
90
94
|
{JSON.stringify(app.extra, null, 2)}
|
|
@@ -117,13 +121,13 @@ function ConfigHelp({
|
|
|
117
121
|
onClick={() => setOpen((value) => !value)}
|
|
118
122
|
data-testid={`connect-config-toggle-${environment}`}
|
|
119
123
|
>
|
|
120
|
-
{open ? '
|
|
124
|
+
{open ? 'Hide environment variables' : 'Show environment variables'}
|
|
121
125
|
</Button>
|
|
122
126
|
{open ? (
|
|
123
127
|
<Box data-testid={`connect-config-details-${environment}`}>
|
|
124
128
|
<Typography variant="caption" color="text.secondary" component="p">
|
|
125
|
-
|
|
126
|
-
fallback
|
|
129
|
+
This environment's application is resolved strictly from these variables, with no
|
|
130
|
+
fallback between environments:
|
|
127
131
|
</Typography>
|
|
128
132
|
<Box component="ul" sx={{ m: 0, pl: 2.5 }}>
|
|
129
133
|
{configVars.map((name) => (
|
|
@@ -159,7 +163,7 @@ export function ConnectEnvironmentCard({
|
|
|
159
163
|
</Box>
|
|
160
164
|
{!status.configured ? (
|
|
161
165
|
<Typography variant="body2" color="text.secondary">
|
|
162
|
-
|
|
166
|
+
No application configured in this environment.
|
|
163
167
|
</Typography>
|
|
164
168
|
) : null}
|
|
165
169
|
{status.error !== null ? (
|
|
@@ -11,6 +11,16 @@ import { CARD_SX } from './ConnectEnvironmentCard';
|
|
|
11
11
|
* The paste-ready homologação answers (FUT-483, packaged by FUT-573) — the
|
|
12
12
|
* Pipefy form with every deployment-specific value already computed, and the
|
|
13
13
|
* services list naming BOTH Order and Connect.
|
|
14
|
+
*
|
|
15
|
+
* ## Why this screen is English and its field labels are not (FUT-760)
|
|
16
|
+
*
|
|
17
|
+
* The reader is the PLATFORM's own integrator — the person registering a
|
|
18
|
+
* Connect application and chasing a `403 ACCESS_DENIED` — so the prose here
|
|
19
|
+
* follows the same rule as every other developer-facing sentence in this
|
|
20
|
+
* repo. The field names beside each value are quotes of PagBank's Pipefy
|
|
21
|
+
* form, and they arrive on `guide.fieldLabels` rather than being written
|
|
22
|
+
* here: this card is USED by matching each block to the box it goes in, and a
|
|
23
|
+
* translated box name has nothing to match.
|
|
14
24
|
*/
|
|
15
25
|
|
|
16
26
|
/** One paste-ready value, in a copyable code block. */
|
|
@@ -42,42 +52,36 @@ export function HomologacaoGuideCard({ guide }: { guide: HomologacaoGuide }): Re
|
|
|
42
52
|
return (
|
|
43
53
|
<Stack spacing={1.5} data-testid="homologacao-guide-card" sx={CARD_SX}>
|
|
44
54
|
<Typography variant="body2" fontWeight={600}>
|
|
45
|
-
|
|
55
|
+
Homologation form — answers ready to paste
|
|
46
56
|
</Typography>
|
|
47
57
|
<Typography variant="body2" color="text.secondary" component="p">
|
|
48
|
-
|
|
58
|
+
Open the{' '}
|
|
49
59
|
<Link
|
|
50
60
|
href={guide.formUrl}
|
|
51
61
|
target="_blank"
|
|
52
62
|
rel="noreferrer"
|
|
53
63
|
data-testid="homologacao-form-link"
|
|
54
64
|
>
|
|
55
|
-
|
|
65
|
+
official homologation form
|
|
56
66
|
</Link>{' '}
|
|
57
|
-
|
|
67
|
+
and fill it in with the values below. In parallel, open a ticket with{' '}
|
|
58
68
|
<Link href={guide.supportFormUrl} target="_blank" rel="noreferrer">
|
|
59
|
-
SIP —
|
|
69
|
+
SIP — PagBank integration support
|
|
60
70
|
</Link>{' '}
|
|
61
|
-
|
|
62
|
-
|
|
71
|
+
quoting the 403 ACCESS_DENIED: whichever answers first settles whether the form
|
|
72
|
+
covers Connect. Documentation:{' '}
|
|
63
73
|
<Link href={guide.docsUrl} target="_blank" rel="noreferrer">
|
|
64
|
-
|
|
74
|
+
requesting homologation
|
|
65
75
|
</Link>
|
|
66
76
|
.
|
|
67
77
|
</Typography>
|
|
68
|
-
<Answer label=
|
|
78
|
+
<Answer label={guide.fieldLabels.integrationType}>{guide.integrationType}</Answer>
|
|
69
79
|
<Box data-testid="homologacao-services">
|
|
70
|
-
<Answer label=
|
|
71
|
-
{guide.services.join('\n')}
|
|
72
|
-
</Answer>
|
|
80
|
+
<Answer label={guide.fieldLabels.services}>{guide.services.join('\n')}</Answer>
|
|
73
81
|
</Box>
|
|
74
|
-
<Answer label=
|
|
75
|
-
|
|
76
|
-
</Answer>
|
|
77
|
-
<Answer label="URL do site">{guide.siteUrl}</Answer>
|
|
78
|
-
<Answer label="Detalhe quais produtos/serviços serão comercializados">
|
|
79
|
-
{guide.productsDescription}
|
|
80
|
-
</Answer>
|
|
82
|
+
<Answer label={guide.fieldLabels.accessInstructions}>{guide.accessInstructions}</Answer>
|
|
83
|
+
<Answer label={guide.fieldLabels.siteUrl}>{guide.siteUrl}</Answer>
|
|
84
|
+
<Answer label={guide.fieldLabels.productsDescription}>{guide.productsDescription}</Answer>
|
|
81
85
|
<Typography variant="caption" color="text.secondary" component="p">
|
|
82
86
|
{guide.slaText}
|
|
83
87
|
</Typography>
|