@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.
Files changed (42) hide show
  1. package/package.json +7 -4
  2. package/src/__tests__/provider-priority-list.test.tsx +2 -2
  3. package/src/__tests__/slugged-provider.test.tsx +108 -0
  4. package/src/card/cpf.ts +42 -0
  5. package/src/card/fields.tsx +254 -0
  6. package/src/card/format.ts +103 -0
  7. package/src/card/index.ts +42 -0
  8. package/src/card/stripe-token.ts +81 -0
  9. package/src/card/tokenize.test.ts +194 -0
  10. package/src/card/tokenize.ts +327 -0
  11. package/src/card/types.ts +54 -0
  12. package/src/components/PaymentProviderSettings.tsx +30 -4
  13. package/src/components/checkout/__tests__/card-3ds-handover.test.tsx +147 -0
  14. package/src/components/checkout/__tests__/clear-cart-on-paid.test.tsx +64 -0
  15. package/src/components/checkout/__tests__/hosted-return.test.ts +109 -0
  16. package/src/components/checkout/__tests__/method-capability.test.tsx +120 -0
  17. package/src/components/checkout/__tests__/payments-unavailable.test.tsx +53 -0
  18. package/src/components/checkout/__tests__/save-on-continue.test.tsx +165 -0
  19. package/src/components/checkout/__tests__/second-host.test.tsx +86 -0
  20. package/src/components/checkout/buyer-info-form.tsx +138 -0
  21. package/src/components/checkout/card-view.tsx +128 -0
  22. package/src/components/checkout/checkout-flow.tsx +201 -0
  23. package/src/components/checkout/checkout-steps.tsx +366 -0
  24. package/src/components/checkout/client.ts +157 -0
  25. package/src/components/checkout/hosted-return.ts +92 -0
  26. package/src/components/checkout/icons.tsx +61 -0
  27. package/src/components/checkout/method-capability.ts +69 -0
  28. package/src/components/checkout/method-picker.tsx +153 -0
  29. package/src/components/checkout/mui-defaults.tsx +218 -0
  30. package/src/components/checkout/payer-summary.tsx +81 -0
  31. package/src/components/checkout/payment-status.tsx +256 -0
  32. package/src/components/checkout/payments-unavailable.tsx +79 -0
  33. package/src/components/checkout/pix-view.tsx +179 -0
  34. package/src/components/checkout/types.ts +223 -0
  35. package/src/components/checkout/ui.tsx +171 -0
  36. package/src/components/checkout/use-card-checkout.ts +346 -0
  37. package/src/components/checkout/use-checkout-controller.ts +252 -0
  38. package/src/components/checkout/use-payment-polling.ts +93 -0
  39. package/src/components/settings-state.ts +45 -2
  40. package/src/index.ts +74 -1
  41. package/src/result.ts +11 -0
  42. 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: ((provider: string | null) => void) | undefined,
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
- export { CheckoutFlow, type CheckoutFlowProps } from './components/CheckoutFlow';
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
- }