@12-apps/payments-frontend 1.21.1 → 2.1.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/client.ts +31 -1
- package/src/components/ConnectionCard.tsx +234 -0
- package/src/components/CredentialFieldStack.tsx +55 -0
- package/src/components/CredentialFields.tsx +131 -19
- package/src/components/CredentialFormAlerts.tsx +37 -15
- package/src/components/EnvironmentTabs.tsx +72 -23
- package/src/components/OAuthPanel.tsx +166 -0
- package/src/components/PaymentProviderSettings.tsx +23 -3
- package/src/components/ProviderConnection.tsx +119 -77
- package/src/components/ProviderCredentialForm.tsx +40 -56
- package/src/components/ProviderPanel.tsx +54 -111
- package/src/components/ProviderSetupGuide.tsx +174 -54
- package/src/components/ProviderStatusBar.tsx +104 -24
- package/src/components/checkout/checkout-flow.tsx +18 -18
- package/src/components/checkout/checkout-steps.tsx +8 -8
- package/src/components/checkout/types.ts +20 -8
- package/src/components/checkout/use-checkout-controller.ts +4 -4
- package/src/components/credential-rules.ts +48 -12
- package/src/components/panel-tokens.ts +166 -0
- package/src/flows/copy.ts +7 -32
- package/src/flows/create-payment-flows.tsx +6 -7
- package/src/flows/types.ts +18 -5
- package/src/index.ts +4 -5
|
@@ -92,7 +92,7 @@ export interface CheckoutOrder {
|
|
|
92
92
|
totalCents: number;
|
|
93
93
|
/**
|
|
94
94
|
* GROSS total before discounts, or null when the discount engine never ran
|
|
95
|
-
* for this order (every order predating FUT-235, and the
|
|
95
|
+
* for this order (every order predating FUT-235, and the settlement path): the
|
|
96
96
|
* subtotal then simply IS {@link totalCents}.
|
|
97
97
|
*/
|
|
98
98
|
subtotalCents: number | null;
|
|
@@ -117,13 +117,25 @@ export interface CheckoutOrder {
|
|
|
117
117
|
}
|
|
118
118
|
|
|
119
119
|
/**
|
|
120
|
-
*
|
|
121
|
-
* the
|
|
122
|
-
*
|
|
123
|
-
*
|
|
120
|
+
* SETTLEMENT context: this checkout pays an already-open balance rather than
|
|
121
|
+
* the cart.
|
|
122
|
+
*
|
|
123
|
+
* Named for what it does, not for the one product it came from. It was
|
|
124
|
+
* `SettlementCheckout` with `scope: "MINE" | "TABLE"` — a restaurant tab and its
|
|
125
|
+
* two ways of splitting one — which is a real concept but that host's, and it
|
|
126
|
+
* arrived in every adopter's type surface. The shape underneath is general: a
|
|
127
|
+
* host has resolved SOME balance, by whatever rule it likes, and the flow shows
|
|
128
|
+
* that total in place of the cart's.
|
|
129
|
+
*
|
|
130
|
+
* `scope` is an opaque string the library only echoes back. It exists so a host
|
|
131
|
+
* can tell its own two settlement modes apart in the events it receives; this
|
|
132
|
+
* package neither reads it nor enumerates it, which is exactly why it must not
|
|
133
|
+
* publish a closed set of somebody else's words.
|
|
134
|
+
*
|
|
135
|
+
* HOW a balance is resolved is the host's business; the flow renders the answer.
|
|
124
136
|
*/
|
|
125
|
-
export interface
|
|
126
|
-
scope:
|
|
137
|
+
export interface SettlementCheckout {
|
|
138
|
+
scope: string;
|
|
127
139
|
totalLabel: string;
|
|
128
140
|
totalItems: number;
|
|
129
141
|
}
|
|
@@ -153,7 +165,7 @@ export type CreateOrderResult =
|
|
|
153
165
|
|
|
154
166
|
/**
|
|
155
167
|
* What the flow hands the host's `createOrder` port. Everything else an order
|
|
156
|
-
* needs — WHICH cart, WHICH
|
|
168
|
+
* needs — WHICH cart, WHICH settlement scope, WHICH tenant — is the host's own
|
|
157
169
|
* context, closed over by its port implementation.
|
|
158
170
|
*/
|
|
159
171
|
export interface CreateOrderRequest {
|
|
@@ -35,15 +35,15 @@ const CPF_ONLY: readonly CheckoutCustomerField[] = [
|
|
|
35
35
|
export interface CheckoutHostPorts {
|
|
36
36
|
/**
|
|
37
37
|
* Raise the order (and its first charge). The host closes over everything
|
|
38
|
-
* the flow must not know: WHICH cart, WHICH tenant, WHICH
|
|
39
|
-
|
|
38
|
+
* the flow must not know: WHICH cart, WHICH tenant, WHICH settlement scope.
|
|
39
|
+
*/
|
|
40
40
|
createOrder: (input: CreateOrderRequest) => Promise<CreateOrderResult>;
|
|
41
41
|
/**
|
|
42
42
|
* Persist the buyer's contact when they press "Continuar" on Dados — the
|
|
43
43
|
* host's account surface owns the write (and the blank-CPF-never-clears
|
|
44
44
|
* rule). Fire-and-forget by contract: the flow advances regardless of the
|
|
45
45
|
* outcome, and only calls this under the "salvar meus dados" consent.
|
|
46
|
-
|
|
46
|
+
*/
|
|
47
47
|
saveBuyerContact?: (contact: BuyerContact) => void;
|
|
48
48
|
/** Leave checkout for the host's menu/catalog. */
|
|
49
49
|
onExitToMenu: () => void;
|
|
@@ -52,7 +52,7 @@ export interface CheckoutHostPorts {
|
|
|
52
52
|
* server emptied it inside the confirmation transaction (FUT-601) and
|
|
53
53
|
* nothing else tells the SPA. Never fired for FAILED/EXPIRED: that shopper
|
|
54
54
|
* still has a basket to retry with.
|
|
55
|
-
|
|
55
|
+
*/
|
|
56
56
|
onPaid?: () => void;
|
|
57
57
|
}
|
|
58
58
|
|
|
@@ -18,21 +18,36 @@ import type { PendingSave } from './ConfirmCredentialSave';
|
|
|
18
18
|
*/
|
|
19
19
|
|
|
20
20
|
/**
|
|
21
|
-
* Is there
|
|
21
|
+
* Is there enough on record to make the probe WORTH RUNNING?
|
|
22
22
|
*
|
|
23
|
-
*
|
|
24
|
-
*
|
|
25
|
-
*
|
|
23
|
+
* A required-field test cannot answer this, and Stripe is why: every field in its
|
|
24
|
+
* schema is `required: false`, because under authorization the access token
|
|
25
|
+
* fulfils the secret key and nothing needs typing at all. On the credentials
|
|
26
|
+
* path that makes the required-field test vacuously true, so a save with one
|
|
27
|
+
* key pasted and three boxes empty went straight to the probe and came back
|
|
28
|
+
* "Credenciais recusadas pela Stripe." — a rejection reported for a request
|
|
29
|
+
* that was never worth making, blaming the owner mid-way through typing.
|
|
30
|
+
*
|
|
31
|
+
* So completeness is measured over the schema MINUS its `advanced` fields.
|
|
32
|
+
* Not the whole schema: that asked for Stripe's `connectedAccountId`, which
|
|
33
|
+
* ordinary stores must leave empty, and owners filled it with their own account
|
|
34
|
+
* id to satisfy the button — producing exactly the refusal this was meant to
|
|
35
|
+
* prevent. A partial save is still a perfectly good save (blank fields preserve
|
|
36
|
+
* what is stored); it simply is not yet a connection worth testing.
|
|
26
37
|
*/
|
|
27
|
-
export function
|
|
38
|
+
export function credentialsComplete(
|
|
28
39
|
descriptor: ProviderDescriptor,
|
|
29
|
-
config: MaskedProviderConfig,
|
|
40
|
+
config: MaskedProviderConfig | null,
|
|
30
41
|
environment: PaymentEnvironment,
|
|
42
|
+
values: Record<string, string>,
|
|
31
43
|
): boolean {
|
|
32
|
-
const stored = config
|
|
33
|
-
return descriptor.credentialSchema
|
|
34
|
-
|
|
35
|
-
|
|
44
|
+
const stored = config?.environments[environment] ?? {};
|
|
45
|
+
return descriptor.credentialSchema.every((spec) => {
|
|
46
|
+
if (spec.advanced) return true;
|
|
47
|
+
const typed = values[spec.key];
|
|
48
|
+
if (typed !== undefined && typed.trim() !== '') return true;
|
|
49
|
+
return stored[spec.key]?.configured === true;
|
|
50
|
+
});
|
|
36
51
|
}
|
|
37
52
|
|
|
38
53
|
/**
|
|
@@ -55,10 +70,31 @@ export function fieldsWellFormed(
|
|
|
55
70
|
});
|
|
56
71
|
}
|
|
57
72
|
|
|
58
|
-
|
|
73
|
+
/**
|
|
74
|
+
* What the one button says it will do — and it does BOTH halves.
|
|
75
|
+
*
|
|
76
|
+
* Saving IS testing (see `FormActions`): the write is followed straight away by
|
|
77
|
+
* the probe, which is what reaches the provider with the pasted keys and, on a
|
|
78
|
+
* pass, is the only thing that makes the activation charge appear. A button
|
|
79
|
+
* reading "Salvar" claims half of that. On the credentials path the owner has
|
|
80
|
+
* no other control, so they were left looking for the one that sends the keys
|
|
81
|
+
* to the provider — there isn't one, because this is it.
|
|
82
|
+
*
|
|
83
|
+
* …but only once there is a connection to test. Half a credential set cannot
|
|
84
|
+
* be, so a partly-filled form says plain "Salvar" and the save skips the probe:
|
|
85
|
+
* promising a test that is then reported as "Credenciais recusadas" is worse
|
|
86
|
+
* than promising nothing, because it reads as a verdict on what was typed.
|
|
87
|
+
*
|
|
88
|
+
* A provider whose whole connection is ONE field keeps naming that field
|
|
89
|
+
* instead. The label then says what is about to be committed rather than merely
|
|
90
|
+
* that something is, and on a step whose entire content is that single box that
|
|
91
|
+
* is the more useful of the two truths. Naming one of Stripe's four would be a
|
|
92
|
+
* lie about what the button writes, which is why only the fallback changes.
|
|
93
|
+
*/
|
|
94
|
+
export function saveLabel(descriptor: ProviderDescriptor, complete: boolean): string {
|
|
59
95
|
const required = descriptor.credentialSchema.filter((field) => field.required);
|
|
60
96
|
const only = required.length === 1 ? required[0] : undefined;
|
|
61
|
-
if (!only) return 'Salvar';
|
|
97
|
+
if (!only) return complete ? 'Salvar e testar conexão' : 'Salvar';
|
|
62
98
|
return `Salvar ${only.label.replace(/\s*\([^)]*\)\s*$/, '')}`;
|
|
63
99
|
}
|
|
64
100
|
|
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The payments screen's own palette and geometry, taken verbatim from the
|
|
3
|
+
* design prototype.
|
|
4
|
+
*
|
|
5
|
+
* Local rather than theme-level on purpose. `@repo/spa-shared/theme` says it in
|
|
6
|
+
* as many words — "re-deriving them here would quietly restyle every admin
|
|
7
|
+
* screen from a function nobody thinks of as owning that" — and the platform
|
|
8
|
+
* primary (`#6366F1`) is a decision already made for the rest of the app. This
|
|
9
|
+
* screen is a designed surface with its own greens, ambers and hairlines, and
|
|
10
|
+
* matching it must not move a single pixel anywhere else.
|
|
11
|
+
*
|
|
12
|
+
* Every value here is a hex the prototype states, not an approximation: a
|
|
13
|
+
* "pixel perfect" screen that rounds its own line colour to `divider` is a
|
|
14
|
+
* screen that looks nearly right and reviews as wrong.
|
|
15
|
+
*/
|
|
16
|
+
export const T = {
|
|
17
|
+
ink: '#111318',
|
|
18
|
+
ink2: '#3d4350',
|
|
19
|
+
ink3: '#6b7280',
|
|
20
|
+
ink4: '#9aa1ad',
|
|
21
|
+
line: '#e4e7ec',
|
|
22
|
+
line2: '#eef0f4',
|
|
23
|
+
bg: '#ffffff',
|
|
24
|
+
bg2: '#f7f8fa',
|
|
25
|
+
|
|
26
|
+
brand: '#5b5bd6',
|
|
27
|
+
brandInk: '#4a4ac4',
|
|
28
|
+
brandSoft: '#eeeefc',
|
|
29
|
+
brandLine: '#c9c9f2',
|
|
30
|
+
|
|
31
|
+
ok: '#0f7a4d',
|
|
32
|
+
okSoft: '#e8f6ef',
|
|
33
|
+
okLine: '#bfe5d3',
|
|
34
|
+
okInk: '#0b5c3a',
|
|
35
|
+
|
|
36
|
+
warn: '#b45309',
|
|
37
|
+
warnSoft: '#fdf1e3',
|
|
38
|
+
warnLine: '#f3d9b5',
|
|
39
|
+
warnInk: '#7c3d06',
|
|
40
|
+
|
|
41
|
+
bad: '#c02626',
|
|
42
|
+
badSoft: '#fdeceb',
|
|
43
|
+
badLine: '#f5c6c2',
|
|
44
|
+
badInk: '#8f1d1d',
|
|
45
|
+
|
|
46
|
+
info: '#1d6fa5',
|
|
47
|
+
infoSoft: '#e8f3fb',
|
|
48
|
+
infoLine: '#bfdcf0',
|
|
49
|
+
infoInk: '#14496b',
|
|
50
|
+
|
|
51
|
+
mono: '"SFMono-Regular",ui-monospace,Menlo,Consolas,monospace',
|
|
52
|
+
} as const;
|
|
53
|
+
|
|
54
|
+
/** The card that bounds one provider: hairline, 12px, nothing clipped. */
|
|
55
|
+
export const CARD_SX = {
|
|
56
|
+
border: `1px solid ${T.line}`,
|
|
57
|
+
borderRadius: '12px',
|
|
58
|
+
background: T.bg,
|
|
59
|
+
overflow: 'hidden',
|
|
60
|
+
} as const;
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* A step's panel — the bordered block a single step lives in.
|
|
64
|
+
*
|
|
65
|
+
* `overflow: hidden` matters: the action bar below sticks to its bottom edge
|
|
66
|
+
* and would otherwise paint over the rounded corner.
|
|
67
|
+
*/
|
|
68
|
+
export const PANEL_SX = {
|
|
69
|
+
border: `1px solid ${T.line}`,
|
|
70
|
+
borderRadius: '11px',
|
|
71
|
+
overflow: 'hidden',
|
|
72
|
+
mx: '20px',
|
|
73
|
+
mb: '20px',
|
|
74
|
+
} as const;
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* The action bar: the primary control, always the last thing in the block the
|
|
78
|
+
* owner just filled.
|
|
79
|
+
*
|
|
80
|
+
* Sticky rather than merely last, so on a long step (the four credential boxes,
|
|
81
|
+
* the card form) the button the owner is working toward stays on screen instead
|
|
82
|
+
* of being something they have to go and find.
|
|
83
|
+
*/
|
|
84
|
+
export const BAR_SX = {
|
|
85
|
+
position: 'sticky',
|
|
86
|
+
bottom: 0,
|
|
87
|
+
background: 'rgba(255,255,255,.94)',
|
|
88
|
+
backdropFilter: 'blur(6px)',
|
|
89
|
+
borderTop: `1px solid ${T.line}`,
|
|
90
|
+
px: '18px',
|
|
91
|
+
py: '12px',
|
|
92
|
+
display: 'flex',
|
|
93
|
+
alignItems: 'center',
|
|
94
|
+
gap: '12px',
|
|
95
|
+
flexWrap: 'wrap',
|
|
96
|
+
} as const;
|
|
97
|
+
|
|
98
|
+
/** The sentence beside the button — why it is enabled, or what just happened. */
|
|
99
|
+
export const BAR_MSG_SX = {
|
|
100
|
+
fontSize: '12.5px',
|
|
101
|
+
color: T.ink3,
|
|
102
|
+
flex: 1,
|
|
103
|
+
minWidth: '140px',
|
|
104
|
+
lineHeight: 1.4,
|
|
105
|
+
} as const;
|
|
106
|
+
|
|
107
|
+
/** Primary: the one thing this step is for. */
|
|
108
|
+
export const BTN_PRIMARY_SX = {
|
|
109
|
+
borderRadius: '8px',
|
|
110
|
+
px: '18px',
|
|
111
|
+
py: '10px',
|
|
112
|
+
fontSize: '13.5px',
|
|
113
|
+
fontWeight: 650,
|
|
114
|
+
textTransform: 'none',
|
|
115
|
+
boxShadow: 'none',
|
|
116
|
+
background: T.brand,
|
|
117
|
+
color: '#fff',
|
|
118
|
+
'&:hover': { background: T.brandInk, boxShadow: 'none' },
|
|
119
|
+
'&.Mui-disabled': { background: '#c9cad6', color: '#fff' },
|
|
120
|
+
} as const;
|
|
121
|
+
|
|
122
|
+
/** Secondary: a real alternative, not a lesser primary. */
|
|
123
|
+
export const BTN_SECONDARY_SX = {
|
|
124
|
+
borderRadius: '8px',
|
|
125
|
+
px: '18px',
|
|
126
|
+
py: '10px',
|
|
127
|
+
fontSize: '13.5px',
|
|
128
|
+
fontWeight: 650,
|
|
129
|
+
textTransform: 'none',
|
|
130
|
+
background: T.bg,
|
|
131
|
+
border: `1px solid ${T.line}`,
|
|
132
|
+
color: T.ink2,
|
|
133
|
+
'&:hover': { borderColor: T.ink4, background: T.bg },
|
|
134
|
+
} as const;
|
|
135
|
+
|
|
136
|
+
/**
|
|
137
|
+
* Destructive, stated quietly.
|
|
138
|
+
*
|
|
139
|
+
* "Remover conexão" sits beside the step's own button and must not compete with
|
|
140
|
+
* it — an owner reaches this deliberately or not at all, and a red filled button
|
|
141
|
+
* on every step is an invitation to misclick.
|
|
142
|
+
*/
|
|
143
|
+
export const BTN_QUIET_DANGER_SX = {
|
|
144
|
+
background: 'none',
|
|
145
|
+
border: 0,
|
|
146
|
+
color: T.bad,
|
|
147
|
+
px: '6px',
|
|
148
|
+
py: '10px',
|
|
149
|
+
fontSize: '13.5px',
|
|
150
|
+
fontWeight: 600,
|
|
151
|
+
textTransform: 'none',
|
|
152
|
+
'&:hover': { background: 'none', textDecoration: 'underline' },
|
|
153
|
+
} as const;
|
|
154
|
+
|
|
155
|
+
/** An inline text control — the way between the two connection paths. */
|
|
156
|
+
export const LINKISH_SX = {
|
|
157
|
+
background: 'none',
|
|
158
|
+
border: 0,
|
|
159
|
+
color: T.brand,
|
|
160
|
+
fontSize: '12.5px',
|
|
161
|
+
p: 0,
|
|
162
|
+
minWidth: 0,
|
|
163
|
+
textTransform: 'none',
|
|
164
|
+
verticalAlign: 'baseline',
|
|
165
|
+
'&:hover': { background: 'none', textDecoration: 'underline' },
|
|
166
|
+
} as const;
|
package/src/flows/copy.ts
CHANGED
|
@@ -9,8 +9,13 @@
|
|
|
9
9
|
* keep it; moving all of it here in the same change that introduces the factory
|
|
10
10
|
* would be a copy rewrite disguised as an API.
|
|
11
11
|
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
12
|
+
* THE DEFAULTS ARE GONE (FUT-760). `DEFAULT_CHECKOUT_COPY_FE` held one
|
|
13
|
+
* product's RESTAURANT vocabulary — "Pagamento com o garçom", "Chame o garçom
|
|
14
|
+
* para fechar a conta na mesa", "Ver cardápio" — and was spread into every
|
|
15
|
+
* adopter that passed no `copy`. A host selling insurance got a waiter, and
|
|
16
|
+
* nothing failed, because saying nothing is exactly how a host silently adopts
|
|
17
|
+
* another product's voice. `copy` is required on the config now; this file is
|
|
18
|
+
* the port and nothing else.
|
|
14
19
|
*/
|
|
15
20
|
|
|
16
21
|
/** Every buyer-facing string the factory's own screens render. */
|
|
@@ -49,33 +54,3 @@ export interface CheckoutCopyFE {
|
|
|
49
54
|
manageCardsEmpty: string;
|
|
50
55
|
manageCardsAdd: string;
|
|
51
56
|
}
|
|
52
|
-
|
|
53
|
-
export const DEFAULT_CHECKOUT_COPY_FE: CheckoutCopyFE = {
|
|
54
|
-
unavailableTitle: "Pagamento online indisponível",
|
|
55
|
-
unavailableBody:
|
|
56
|
-
"Esta loja não recebe pagamentos pelo site. Combine o pagamento diretamente com a loja para concluir seu pedido.",
|
|
57
|
-
unavailableWithRemedyTitle: "Pagamento com o garçom",
|
|
58
|
-
unavailableWithRemedyBody:
|
|
59
|
-
"Esta loja não recebe pagamentos pelo site. Chame o garçom para fechar a conta na mesa.",
|
|
60
|
-
handoffTitle: "Você será levado ao pagamento",
|
|
61
|
-
handoffBody:
|
|
62
|
-
"Estamos abrindo a página segura do meio de pagamento. Se ela não abrir sozinha, use o link abaixo.",
|
|
63
|
-
handoffLink: "Abrir a página de pagamento",
|
|
64
|
-
handoffCancel: "Voltar",
|
|
65
|
-
returnPending: "Confirmando seu pagamento…",
|
|
66
|
-
returnUnknown:
|
|
67
|
-
"Não encontramos um pagamento em andamento nesta sessão. Verifique seus pedidos em instantes.",
|
|
68
|
-
emptyCartTitle: "Seu carrinho está vazio.",
|
|
69
|
-
emptyCartAction: "Ver cardápio",
|
|
70
|
-
continueAction: "Continuar",
|
|
71
|
-
addCardTitle: "Adicionar cartão",
|
|
72
|
-
addCardAction: "Salvar cartão",
|
|
73
|
-
addCardPreparing: "Preparando o formulário…",
|
|
74
|
-
addCardSavedTitle: "Cartão salvo",
|
|
75
|
-
addCardSavedBody: "Você poderá usá-lo nas próximas compras.",
|
|
76
|
-
addCardFailedTitle: "Não foi possível salvar o cartão",
|
|
77
|
-
addCardUnavailable: "Esta loja não aceita salvar cartões no momento.",
|
|
78
|
-
manageCardsTitle: "Meus cartões",
|
|
79
|
-
manageCardsEmpty: "Você ainda não tem cartões salvos.",
|
|
80
|
-
manageCardsAdd: "Adicionar cartão",
|
|
81
|
-
};
|
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
*
|
|
8
8
|
* ## Why the scope arrives as HOOKS
|
|
9
9
|
*
|
|
10
|
-
* `useScope`, `useCart`, `useBuyerDefaults`, `
|
|
10
|
+
* `useScope`, `useCart`, `useBuyerDefaults`, `useSettlement` and
|
|
11
11
|
* `ports.useAvailability` are hooks, not values, and they are invoked in a
|
|
12
12
|
* component BODY — never read at factory time. The factory runs once, at module
|
|
13
13
|
* evaluation, so a value-shaped config would freeze the first store's slug and
|
|
@@ -19,10 +19,9 @@ import { useCallback, type JSX, type ReactNode } from "react";
|
|
|
19
19
|
import { buyerFieldsFor } from "../components/checkout/buyer-fields";
|
|
20
20
|
import { CheckoutFlow } from "../components/checkout/checkout-flow";
|
|
21
21
|
import { createCheckoutClient } from "../components/checkout/transport";
|
|
22
|
-
import type { CheckoutProviderConfig,
|
|
22
|
+
import type { CheckoutProviderConfig, SettlementCheckout } from "../components/checkout/types";
|
|
23
23
|
import { useCheckoutController } from "../components/checkout/use-checkout-controller";
|
|
24
24
|
|
|
25
|
-
import { DEFAULT_CHECKOUT_COPY_FE } from "./copy";
|
|
26
25
|
import { FlowsProvider, useResolvedConfig, type FlowsRuntime } from "./runtime";
|
|
27
26
|
import { buyerScreens } from "./screens-buyer";
|
|
28
27
|
import { hostedScreens } from "./screens-hosted";
|
|
@@ -50,7 +49,7 @@ function buildRuntime(config: PaymentFlowsConfig): FlowsRuntime {
|
|
|
50
49
|
return {
|
|
51
50
|
config,
|
|
52
51
|
client,
|
|
53
|
-
copy:
|
|
52
|
+
copy: config.copy,
|
|
54
53
|
navigate,
|
|
55
54
|
// Both of these are HOOKS. They are called from a component body on every
|
|
56
55
|
// render, so the slug follows the host's router and the availability vote
|
|
@@ -68,10 +67,10 @@ function buildCheckout(
|
|
|
68
67
|
const { ports } = runtime.config;
|
|
69
68
|
const Unavailable = screens.PaymentsUnavailable;
|
|
70
69
|
|
|
71
|
-
function CheckoutBody({
|
|
70
|
+
function CheckoutBody({ settlement }: { settlement?: SettlementCheckout | null }): JSX.Element {
|
|
72
71
|
const cart = runtime.config.useCart();
|
|
73
72
|
const defaults = runtime.config.useBuyerDefaults?.() ?? {};
|
|
74
|
-
const
|
|
73
|
+
const hostSettlement = runtime.config.useSettlement?.() ?? null;
|
|
75
74
|
const { config, pending } = useResolvedConfig(runtime);
|
|
76
75
|
const availability = runtime.useAvailability();
|
|
77
76
|
const tenantSlug = runtime.useTenantSlug();
|
|
@@ -93,7 +92,7 @@ function buildCheckout(
|
|
|
93
92
|
onPaid={ports.onPaid}
|
|
94
93
|
defaultBuyer={defaults.buyer}
|
|
95
94
|
taxIdOnFile={defaults.taxIdOnFile ?? false}
|
|
96
|
-
|
|
95
|
+
settlement={settlement ?? hostSettlement}
|
|
97
96
|
providerConfig={config}
|
|
98
97
|
tenantSlug={tenantSlug}
|
|
99
98
|
confirmationExtra={runtime.config.confirmation?.extra}
|
package/src/flows/types.ts
CHANGED
|
@@ -31,7 +31,7 @@ import type {
|
|
|
31
31
|
ChargeOutcome,
|
|
32
32
|
CheckoutOrder,
|
|
33
33
|
CheckoutProviderConfig,
|
|
34
|
-
|
|
34
|
+
SettlementCheckout,
|
|
35
35
|
CreateOrderRequest,
|
|
36
36
|
CreateOrderResult,
|
|
37
37
|
OrderStatus,
|
|
@@ -100,8 +100,8 @@ export interface PaymentFlowsConfig {
|
|
|
100
100
|
useCart(): CheckoutCartView;
|
|
101
101
|
/** The buyer's saved details, and whether a CPF is already on file. */
|
|
102
102
|
useBuyerDefaults?(): { buyer?: BuyerInfo; taxIdOnFile?: boolean; pending?: boolean };
|
|
103
|
-
/** Present ⇒ this checkout settles a
|
|
104
|
-
|
|
103
|
+
/** Present ⇒ this checkout settles a settlement rather than the cart. */
|
|
104
|
+
useSettlement?(): SettlementCheckout | null;
|
|
105
105
|
|
|
106
106
|
/** Design-system slots, filled ONCE instead of per screen. */
|
|
107
107
|
components?: Partial<CheckoutComponents>;
|
|
@@ -117,7 +117,20 @@ export interface PaymentFlowsConfig {
|
|
|
117
117
|
// "what happens WHEN it fires": a timed-out mint must decide whether the walk
|
|
118
118
|
// advances to the next entry or the whole charge refuses, and that is a money
|
|
119
119
|
// rule (FUT-563), not a wire-up.
|
|
120
|
-
|
|
120
|
+
/**
|
|
121
|
+
* Every buyer-facing sentence the factory's own screens render.
|
|
122
|
+
*
|
|
123
|
+
* REQUIRED, and not partial. It used to be `Partial<…>` over a pt-BR default
|
|
124
|
+
* spread in at `create-payment-flows.tsx`, and that default was one product's
|
|
125
|
+
* RESTAURANT vocabulary — "Pagamento com o garçom", "Chame o garçom para
|
|
126
|
+
* fechar a conta na mesa", "Ver cardápio" — reaching every adopter that said
|
|
127
|
+
* nothing. A host selling insurance got a waiter.
|
|
128
|
+
*
|
|
129
|
+
* That is the failure a default cannot warn about: saying nothing is exactly
|
|
130
|
+
* how a host adopts another product's voice, and nothing fails. Required
|
|
131
|
+
* makes a new host answer once, at the one call site that knows the answer.
|
|
132
|
+
*/
|
|
133
|
+
copy: CheckoutCopyFE;
|
|
121
134
|
/** Host content under the paid receipt (the storefront's PWA install invite). */
|
|
122
135
|
confirmation?: { extra?: ReactNode };
|
|
123
136
|
/**
|
|
@@ -187,7 +200,7 @@ export interface CheckoutConfigState {
|
|
|
187
200
|
/** What `createPaymentFlows` returns. */
|
|
188
201
|
export interface PaymentFlows {
|
|
189
202
|
/** THE mount: a complete buyer checkout in one line. */
|
|
190
|
-
Checkout: ComponentType<{
|
|
203
|
+
Checkout: ComponentType<{ settlement?: SettlementCheckout | null }>;
|
|
191
204
|
/** Slots + transport + scope + the fetched config, for a nesting host. */
|
|
192
205
|
Provider: ComponentType<{ children: ReactNode; config?: CheckoutProviderConfig | null }>;
|
|
193
206
|
screens: CheckoutScreens;
|
package/src/index.ts
CHANGED
|
@@ -56,10 +56,9 @@ export {
|
|
|
56
56
|
// hand-composing path is unchanged.
|
|
57
57
|
// ---------------------------------------------------------------------------
|
|
58
58
|
export { createPaymentFlows } from './flows/create-payment-flows';
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
} from './flows/copy';
|
|
59
|
+
// A type and nothing else now: `DEFAULT_CHECKOUT_COPY_FE` used to sit beside it
|
|
60
|
+
// and was the only value this module ever published (FUT-760).
|
|
61
|
+
export type { CheckoutCopyFE } from './flows/copy';
|
|
63
62
|
export {
|
|
64
63
|
type BoundCheckoutClient,
|
|
65
64
|
type BuyerDetailsProps,
|
|
@@ -143,7 +142,7 @@ export {
|
|
|
143
142
|
type CheckoutOrder,
|
|
144
143
|
type CheckoutProviderConfig,
|
|
145
144
|
type CheckoutWalletType,
|
|
146
|
-
type
|
|
145
|
+
type SettlementCheckout,
|
|
147
146
|
type CreateOrderRequest,
|
|
148
147
|
type CreateOrderResult,
|
|
149
148
|
type OrderStatus,
|