@12-apps/payments-frontend 1.0.0 → 1.2.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 +7 -4
- package/src/__tests__/provider-priority-list.test.tsx +2 -2
- package/src/__tests__/slugged-provider.test.tsx +108 -0
- package/src/card/cpf.ts +42 -0
- package/src/card/fields.tsx +254 -0
- package/src/card/format.ts +103 -0
- package/src/card/index.ts +42 -0
- package/src/card/stripe-token.ts +81 -0
- package/src/card/tokenize.test.ts +194 -0
- package/src/card/tokenize.ts +327 -0
- package/src/card/types.ts +54 -0
- package/src/components/PaymentProviderSettings.tsx +30 -4
- package/src/components/checkout/__tests__/card-3ds-handover.test.tsx +147 -0
- package/src/components/checkout/__tests__/clear-cart-on-paid.test.tsx +64 -0
- package/src/components/checkout/__tests__/hosted-return.test.ts +109 -0
- package/src/components/checkout/__tests__/method-capability.test.tsx +120 -0
- package/src/components/checkout/__tests__/payments-unavailable.test.tsx +53 -0
- package/src/components/checkout/__tests__/save-on-continue.test.tsx +165 -0
- package/src/components/checkout/__tests__/second-host.test.tsx +86 -0
- package/src/components/checkout/buyer-info-form.tsx +138 -0
- package/src/components/checkout/card-view.tsx +128 -0
- package/src/components/checkout/checkout-flow.tsx +201 -0
- package/src/components/checkout/checkout-steps.tsx +366 -0
- package/src/components/checkout/client.ts +157 -0
- package/src/components/checkout/hosted-return.ts +92 -0
- package/src/components/checkout/icons.tsx +61 -0
- package/src/components/checkout/method-capability.ts +69 -0
- package/src/components/checkout/method-picker.tsx +153 -0
- package/src/components/checkout/mui-defaults.tsx +218 -0
- package/src/components/checkout/payer-summary.tsx +81 -0
- package/src/components/checkout/payment-status.tsx +256 -0
- package/src/components/checkout/payments-unavailable.tsx +79 -0
- package/src/components/checkout/pix-view.tsx +179 -0
- package/src/components/checkout/types.ts +223 -0
- package/src/components/checkout/ui.tsx +171 -0
- package/src/components/checkout/use-card-checkout.ts +346 -0
- package/src/components/checkout/use-checkout-controller.ts +252 -0
- package/src/components/checkout/use-payment-polling.ts +93 -0
- package/src/components/settings-state.ts +45 -2
- package/src/index.ts +74 -1
- package/src/result.ts +11 -0
- package/src/components/CheckoutFlow.tsx +0 -169
|
@@ -126,6 +126,11 @@ export function useSetupConfirmation(
|
|
|
126
126
|
* BEFORE the loading and error returns — the guide hook depends on them — and
|
|
127
127
|
* that is exactly the position where a component accumulates the branches that
|
|
128
128
|
* make it unreadable.
|
|
129
|
+
*
|
|
130
|
+
* `selected` may be the provider's NAME or its `urlSlug` — a controlled host
|
|
131
|
+
* stores the selection in a path segment, and the segment is the adapter's URL
|
|
132
|
+
* spelling. The raw name stays a working alias so a link built before the
|
|
133
|
+
* adapter declared a slug does not 404.
|
|
129
134
|
*/
|
|
130
135
|
export function useOpenProvider(
|
|
131
136
|
view: MerchantSettingsView | null,
|
|
@@ -135,7 +140,7 @@ export function useOpenProvider(
|
|
|
135
140
|
// providers, not one arbitrary provider's configuration.
|
|
136
141
|
const active = useMemo(() => {
|
|
137
142
|
if (!view || !selected) return null;
|
|
138
|
-
return view.providers.find((p) => p.name === selected) ?? null;
|
|
143
|
+
return view.providers.find((p) => p.name === selected || p.urlSlug === selected) ?? null;
|
|
139
144
|
}, [view, selected]);
|
|
140
145
|
|
|
141
146
|
// A stored connection is what makes the manual walkthrough steps redundant —
|
|
@@ -148,6 +153,44 @@ export function useOpenProvider(
|
|
|
148
153
|
return { active, activeConfig };
|
|
149
154
|
}
|
|
150
155
|
|
|
156
|
+
/**
|
|
157
|
+
* How a host is asked to move its selection. `options.replace` marks a
|
|
158
|
+
* CORRECTION rather than a navigation: the host should rewrite its current
|
|
159
|
+
* history entry (react-router's `replace`), so Voltar never revisits the
|
|
160
|
+
* spelling being corrected.
|
|
161
|
+
*/
|
|
162
|
+
export type ProviderChangeHandler = (
|
|
163
|
+
provider: string | null,
|
|
164
|
+
options?: { replace?: boolean },
|
|
165
|
+
) => void;
|
|
166
|
+
|
|
167
|
+
/**
|
|
168
|
+
* Ask the HOST to respell its path segment once the catalog can (FUT-557).
|
|
169
|
+
*
|
|
170
|
+
* The OAuth callback hands the host a raw provider NAME (`?connected=`), and a
|
|
171
|
+
* controlled host writes it into the URL verbatim — it holds no slug map, on
|
|
172
|
+
* purpose. The alias already resolves, so the SCREEN is right either way; what
|
|
173
|
+
* stays wrong is the ADDRESS BAR, which is exactly what a reload or a shared
|
|
174
|
+
* link uses. Once the view knows the provider's declared spelling, hand the
|
|
175
|
+
* canonical slug back — with `replace`, so the alias spelling never becomes a
|
|
176
|
+
* history entry of its own.
|
|
177
|
+
*
|
|
178
|
+
* Controlled hosts only: with internal selection there is no URL to fix, and
|
|
179
|
+
* `controlled === undefined` is the one test for that (see
|
|
180
|
+
* {@link useSelectedProvider}).
|
|
181
|
+
*/
|
|
182
|
+
export function useCanonicalProviderSegment(
|
|
183
|
+
controlled: string | null | undefined,
|
|
184
|
+
active: ProviderDescriptor | null,
|
|
185
|
+
onProviderChange: ProviderChangeHandler | undefined,
|
|
186
|
+
): void {
|
|
187
|
+
useEffect(() => {
|
|
188
|
+
if (controlled === undefined || controlled === null) return;
|
|
189
|
+
if (!active?.urlSlug || active.urlSlug === controlled) return;
|
|
190
|
+
onProviderChange?.(active.urlSlug, { replace: true });
|
|
191
|
+
}, [controlled, active, onProviderChange]);
|
|
192
|
+
}
|
|
193
|
+
|
|
151
194
|
export function useSettingsState(client: PaymentsSettingsClient) {
|
|
152
195
|
const [view, setView] = useState<MerchantSettingsView | null>(null);
|
|
153
196
|
const [error, setError] = useState<string | null>(null);
|
|
@@ -187,7 +230,7 @@ export function useSettingsState(client: PaymentsSettingsClient) {
|
|
|
187
230
|
export function useSelectedProvider(
|
|
188
231
|
initialProvider: string | null,
|
|
189
232
|
controlled: string | null | undefined,
|
|
190
|
-
onProviderChange:
|
|
233
|
+
onProviderChange: ProviderChangeHandler | undefined,
|
|
191
234
|
) {
|
|
192
235
|
const isControlled = controlled !== undefined;
|
|
193
236
|
const [internal, setInternal] = useState<string | null>(initialProvider);
|
package/src/index.ts
CHANGED
|
@@ -37,7 +37,80 @@ export {
|
|
|
37
37
|
type CheckoutPaymentProps,
|
|
38
38
|
type SavedCardOption,
|
|
39
39
|
} from './components/CheckoutPayment';
|
|
40
|
-
|
|
40
|
+
|
|
41
|
+
// ---------------------------------------------------------------------------
|
|
42
|
+
// The buyer checkout surface (FUT-564) — the storefront's three-step flow,
|
|
43
|
+
// mounted by a host in one line. See ADOPTING.md §3 for the ports and the
|
|
44
|
+
// design-system slot contract.
|
|
45
|
+
// ---------------------------------------------------------------------------
|
|
46
|
+
export {
|
|
47
|
+
CheckoutFlow,
|
|
48
|
+
type CheckoutCartView,
|
|
49
|
+
type CheckoutFlowProps,
|
|
50
|
+
} from './components/checkout/checkout-flow';
|
|
51
|
+
export { type CheckoutHostPorts } from './components/checkout/use-checkout-controller';
|
|
52
|
+
export { PaymentsUnavailable } from './components/checkout/payments-unavailable';
|
|
53
|
+
export { fetchCheckoutConfig } from './components/checkout/client';
|
|
54
|
+
export {
|
|
55
|
+
CheckoutComponentsProvider,
|
|
56
|
+
type CheckoutActionBarProps,
|
|
57
|
+
type CheckoutAlertProps,
|
|
58
|
+
type CheckoutButtonProps,
|
|
59
|
+
type CheckoutCheckboxProps,
|
|
60
|
+
type CheckoutComponents,
|
|
61
|
+
type CheckoutInputProps,
|
|
62
|
+
type CheckoutLoadingStateProps,
|
|
63
|
+
type CheckoutRadioGroupProps,
|
|
64
|
+
type CheckoutRadioOption,
|
|
65
|
+
type CheckoutStepperProps,
|
|
66
|
+
type CheckoutStepperStep,
|
|
67
|
+
type CheckoutTextProps,
|
|
68
|
+
} from './components/checkout/ui';
|
|
69
|
+
export {
|
|
70
|
+
type BuyerContact,
|
|
71
|
+
type BuyerField,
|
|
72
|
+
type BuyerInfo,
|
|
73
|
+
type CheckoutError,
|
|
74
|
+
type CheckoutOrder,
|
|
75
|
+
type CheckoutProviderConfig,
|
|
76
|
+
type ComandaCheckout,
|
|
77
|
+
type CreateOrderRequest,
|
|
78
|
+
type CreateOrderResult,
|
|
79
|
+
type OrderStatus,
|
|
80
|
+
type PaymentMethod,
|
|
81
|
+
} from './components/checkout/types';
|
|
82
|
+
|
|
83
|
+
// The shared card-entry surface (form + tokenizer), used by the checkout above
|
|
84
|
+
// and by the admin's provider-activation charge (FUT-463) — same fields, same
|
|
85
|
+
// validation, same tokenization, or the activation proves nothing.
|
|
86
|
+
export {
|
|
87
|
+
CardPayBar,
|
|
88
|
+
NewCardForm,
|
|
89
|
+
SavedCardsPicker,
|
|
90
|
+
cvvLength,
|
|
91
|
+
detectBrand,
|
|
92
|
+
formatCardNumber,
|
|
93
|
+
formatCpf,
|
|
94
|
+
formatCvv,
|
|
95
|
+
formatExpiry,
|
|
96
|
+
onlyDigits,
|
|
97
|
+
tokenizeCard,
|
|
98
|
+
tokenizeForCheckout,
|
|
99
|
+
tokenizerFor,
|
|
100
|
+
validateCardNumber,
|
|
101
|
+
validateCpf,
|
|
102
|
+
validateCvv,
|
|
103
|
+
validateExpiry,
|
|
104
|
+
validateHolder,
|
|
105
|
+
NEW_CARD,
|
|
106
|
+
type CardBrand,
|
|
107
|
+
type CardDetails,
|
|
108
|
+
type CardFieldErrors,
|
|
109
|
+
type CardToken,
|
|
110
|
+
type CardTokenizationConfig,
|
|
111
|
+
type CardTokenizer,
|
|
112
|
+
type SavedCard,
|
|
113
|
+
} from './card';
|
|
41
114
|
export {
|
|
42
115
|
ProviderConnection,
|
|
43
116
|
type ProviderConnectionProps,
|
package/src/result.ts
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Minimal `Result` shape for the browser payment surfaces. Lives in this
|
|
3
|
+
* package (moved from `@12-apps/spa-shared` with the checkout, FUT-564) so the
|
|
4
|
+
* portable frontend never reaches back into a repo-specific package for a
|
|
5
|
+
* ten-line type.
|
|
6
|
+
*/
|
|
7
|
+
export type Result<T> = { ok: true; data: T } | { ok: false; error: string };
|
|
8
|
+
|
|
9
|
+
export const ok = <T,>(data: T): Result<T> => ({ ok: true, data });
|
|
10
|
+
|
|
11
|
+
export const err = <T,>(error: string): Result<T> => ({ ok: false, error });
|
|
@@ -1,169 +0,0 @@
|
|
|
1
|
-
'use client';
|
|
2
|
-
|
|
3
|
-
import {
|
|
4
|
-
Alert,
|
|
5
|
-
Button,
|
|
6
|
-
Checkbox,
|
|
7
|
-
FormControlLabel,
|
|
8
|
-
Stack,
|
|
9
|
-
Step,
|
|
10
|
-
StepLabel,
|
|
11
|
-
Stepper,
|
|
12
|
-
TextField,
|
|
13
|
-
Typography,
|
|
14
|
-
} from '@mui/material';
|
|
15
|
-
import { useState } from 'react';
|
|
16
|
-
|
|
17
|
-
import type { ClientChargeView, CustomerInfo, Money } from '@12-apps/payments-backend';
|
|
18
|
-
|
|
19
|
-
import { CheckoutPayment, type CheckoutPaymentProps } from './CheckoutPayment';
|
|
20
|
-
|
|
21
|
-
/**
|
|
22
|
-
* The full plug-and-play checkout: the three-step flow of the buyer-facing
|
|
23
|
-
* screens — Dados (CPF required; name/email/phone optional, "save my data")
|
|
24
|
-
* → Pagamento (method cards, saved cards, pay button — `CheckoutPayment`)
|
|
25
|
-
* → Confirmação. The host supplies cart totals, prefilled customer data,
|
|
26
|
-
* saved cards, and reacts to `onCustomerConfirmed` / `onPaid`.
|
|
27
|
-
*/
|
|
28
|
-
export interface CheckoutFlowProps
|
|
29
|
-
extends Omit<CheckoutPaymentProps, 'customer' | 'onPaid'> {
|
|
30
|
-
initialCustomer?: Partial<CustomerInfo>;
|
|
31
|
-
/** Default state of the "save my data for next time" checkbox. */
|
|
32
|
-
saveDataDefault?: boolean;
|
|
33
|
-
/** Fired when the buyer completes step 1 (persist profile if saveData). */
|
|
34
|
-
onCustomerConfirmed?: (customer: CustomerInfo, saveData: boolean) => void;
|
|
35
|
-
onPaid?: (charge: ClientChargeView) => void;
|
|
36
|
-
/** e.g. "1 item" — shown next to the total in the footer. */
|
|
37
|
-
itemsLabel?: string;
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
const STEPS = ['Dados', 'Pagamento', 'Confirmação'];
|
|
41
|
-
|
|
42
|
-
/** CPF: 11 digits (checksum stays server/provider-side; this is UX-level). */
|
|
43
|
-
function cpfDigits(value: string): string {
|
|
44
|
-
return value.replace(/\D/g, '');
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
interface CustomerStepProps {
|
|
48
|
-
amount: Money;
|
|
49
|
-
itemsLabel?: string;
|
|
50
|
-
initial: Partial<CustomerInfo>;
|
|
51
|
-
saveDataDefault: boolean;
|
|
52
|
-
onContinue: (customer: CustomerInfo, saveData: boolean) => void;
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
function CustomerStep({ amount, itemsLabel, initial, saveDataDefault, onContinue }: CustomerStepProps) {
|
|
56
|
-
const [taxId, setTaxId] = useState(initial.taxId ?? '');
|
|
57
|
-
const [name, setName] = useState(initial.name ?? '');
|
|
58
|
-
const [email, setEmail] = useState(initial.email ?? '');
|
|
59
|
-
const [phone, setPhone] = useState(initial.phone ?? '');
|
|
60
|
-
const [saveData, setSaveData] = useState(saveDataDefault);
|
|
61
|
-
const [touched, setTouched] = useState(false);
|
|
62
|
-
const cpfInvalid = cpfDigits(taxId).length !== 11;
|
|
63
|
-
|
|
64
|
-
return (
|
|
65
|
-
<Stack spacing={2}>
|
|
66
|
-
<Typography variant="body2" color="text.secondary">
|
|
67
|
-
Informe seu CPF (obrigatório para o pagamento). Nome, e-mail e telefone são opcionais —
|
|
68
|
-
usados apenas para o comprovante.
|
|
69
|
-
</Typography>
|
|
70
|
-
<TextField
|
|
71
|
-
size="small"
|
|
72
|
-
label="CPF"
|
|
73
|
-
required
|
|
74
|
-
value={taxId}
|
|
75
|
-
error={touched && cpfInvalid}
|
|
76
|
-
helperText={touched && cpfInvalid ? 'Informe um CPF com 11 dígitos.' : undefined}
|
|
77
|
-
onBlur={() => setTouched(true)}
|
|
78
|
-
onChange={(e) => setTaxId(e.target.value)}
|
|
79
|
-
/>
|
|
80
|
-
<TextField size="small" label="Nome" value={name} onChange={(e) => setName(e.target.value)} />
|
|
81
|
-
<TextField size="small" label="E-mail" value={email} onChange={(e) => setEmail(e.target.value)} />
|
|
82
|
-
<TextField size="small" label="Telefone" value={phone} onChange={(e) => setPhone(e.target.value)} />
|
|
83
|
-
<FormControlLabel
|
|
84
|
-
control={<Checkbox checked={saveData} onChange={(_, v) => setSaveData(v)} />}
|
|
85
|
-
label="Salvar meus dados para a próxima compra"
|
|
86
|
-
/>
|
|
87
|
-
<Stack direction="row" spacing={2} alignItems="center">
|
|
88
|
-
<Typography variant="body2" color="text.secondary">
|
|
89
|
-
{itemsLabel ? `Total · ${itemsLabel} — ` : 'Total: '}
|
|
90
|
-
{(amount.amountCents / 100).toLocaleString('pt-BR', { style: 'currency', currency: amount.currency })}
|
|
91
|
-
</Typography>
|
|
92
|
-
<Button
|
|
93
|
-
variant="contained"
|
|
94
|
-
fullWidth
|
|
95
|
-
disabled={cpfInvalid}
|
|
96
|
-
onClick={() => onContinue({ name, email, taxId: cpfDigits(taxId), phone }, saveData)}
|
|
97
|
-
>
|
|
98
|
-
Continuar
|
|
99
|
-
</Button>
|
|
100
|
-
</Stack>
|
|
101
|
-
<Typography variant="caption" color="text.secondary" textAlign="right">
|
|
102
|
-
🔒 Pagamento seguro
|
|
103
|
-
</Typography>
|
|
104
|
-
</Stack>
|
|
105
|
-
);
|
|
106
|
-
}
|
|
107
|
-
|
|
108
|
-
export function CheckoutFlow(props: CheckoutFlowProps) {
|
|
109
|
-
const {
|
|
110
|
-
initialCustomer = {},
|
|
111
|
-
saveDataDefault = true,
|
|
112
|
-
onCustomerConfirmed,
|
|
113
|
-
onPaid,
|
|
114
|
-
itemsLabel,
|
|
115
|
-
...payment
|
|
116
|
-
} = props;
|
|
117
|
-
const [step, setStep] = useState(0);
|
|
118
|
-
const [customer, setCustomer] = useState<CustomerInfo | null>(null);
|
|
119
|
-
const [paid, setPaid] = useState<ClientChargeView | null>(null);
|
|
120
|
-
|
|
121
|
-
return (
|
|
122
|
-
<Stack spacing={3}>
|
|
123
|
-
<Stepper activeStep={step} alternativeLabel>
|
|
124
|
-
{STEPS.map((label) => (
|
|
125
|
-
<Step key={label}>
|
|
126
|
-
<StepLabel>{label}</StepLabel>
|
|
127
|
-
</Step>
|
|
128
|
-
))}
|
|
129
|
-
</Stepper>
|
|
130
|
-
|
|
131
|
-
{step === 0 ? (
|
|
132
|
-
<CustomerStep
|
|
133
|
-
amount={payment.amount}
|
|
134
|
-
itemsLabel={itemsLabel}
|
|
135
|
-
initial={initialCustomer}
|
|
136
|
-
saveDataDefault={saveDataDefault}
|
|
137
|
-
onContinue={(confirmed, saveData) => {
|
|
138
|
-
setCustomer(confirmed);
|
|
139
|
-
onCustomerConfirmed?.(confirmed, saveData);
|
|
140
|
-
setStep(1);
|
|
141
|
-
}}
|
|
142
|
-
/>
|
|
143
|
-
) : null}
|
|
144
|
-
|
|
145
|
-
{step === 1 && customer ? (
|
|
146
|
-
<Stack spacing={1}>
|
|
147
|
-
<Button size="small" sx={{ alignSelf: 'flex-start' }} onClick={() => setStep(0)}>
|
|
148
|
-
← Voltar
|
|
149
|
-
</Button>
|
|
150
|
-
<CheckoutPayment
|
|
151
|
-
{...payment}
|
|
152
|
-
customer={customer}
|
|
153
|
-
onPaid={(charge) => {
|
|
154
|
-
setPaid(charge);
|
|
155
|
-
setStep(2);
|
|
156
|
-
onPaid?.(charge);
|
|
157
|
-
}}
|
|
158
|
-
/>
|
|
159
|
-
</Stack>
|
|
160
|
-
) : null}
|
|
161
|
-
|
|
162
|
-
{step === 2 && paid ? (
|
|
163
|
-
<Alert severity="success">
|
|
164
|
-
Pagamento confirmado — {paid.provider} · {paid.providerChargeId}
|
|
165
|
-
</Alert>
|
|
166
|
-
) : null}
|
|
167
|
-
</Stack>
|
|
168
|
-
);
|
|
169
|
-
}
|