@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
|
@@ -6,11 +6,14 @@ import {
|
|
|
6
6
|
Button,
|
|
7
7
|
IconButton,
|
|
8
8
|
Link,
|
|
9
|
-
Paper,
|
|
10
9
|
Stack,
|
|
11
10
|
Step,
|
|
11
|
+
StepConnector,
|
|
12
|
+
stepConnectorClasses,
|
|
12
13
|
StepLabel,
|
|
13
14
|
Stepper,
|
|
15
|
+
styled,
|
|
16
|
+
type StepIconProps,
|
|
14
17
|
TextField,
|
|
15
18
|
Typography,
|
|
16
19
|
} from '@mui/material';
|
|
@@ -18,6 +21,15 @@ import { useState, type ReactNode } from 'react';
|
|
|
18
21
|
|
|
19
22
|
import type { ProviderSetupGuide as Guide, SetupSection, SetupStep } from '@12-apps/payments-backend';
|
|
20
23
|
|
|
24
|
+
import {
|
|
25
|
+
BAR_MSG_SX,
|
|
26
|
+
BAR_SX,
|
|
27
|
+
BTN_PRIMARY_SX,
|
|
28
|
+
BTN_SECONDARY_SX,
|
|
29
|
+
PANEL_SX,
|
|
30
|
+
T,
|
|
31
|
+
} from './panel-tokens';
|
|
32
|
+
|
|
21
33
|
import { richText } from './rich-text';
|
|
22
34
|
|
|
23
35
|
/**
|
|
@@ -122,6 +134,39 @@ const BUTTON_SX = { textTransform: 'none' } as const;
|
|
|
122
134
|
|
|
123
135
|
type StepActions = ProviderSetupGuideProps['actions'];
|
|
124
136
|
|
|
137
|
+
/** A step's sentence, with the provider's own reference inline after it. */
|
|
138
|
+
function StepText({ text, link }: { text?: string; link?: SetupStep['link'] }) {
|
|
139
|
+
if (!text) return null;
|
|
140
|
+
return (
|
|
141
|
+
<Typography sx={{ fontSize: '13px', color: T.ink2, lineHeight: 1.5 }}>
|
|
142
|
+
{richText(text)}{' '}
|
|
143
|
+
{link ? (
|
|
144
|
+
<Link href={link.url} target="_blank" rel="noreferrer">
|
|
145
|
+
{link.label}
|
|
146
|
+
</Link>
|
|
147
|
+
) : null}
|
|
148
|
+
</Typography>
|
|
149
|
+
);
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
/**
|
|
153
|
+
* The panel's action bar: what this step is asking, and the button that answers.
|
|
154
|
+
*
|
|
155
|
+
* The sentence is not decoration. This is the one step no API can report, so
|
|
156
|
+
* the owner is being asked to vouch for work done somewhere else — and a bare
|
|
157
|
+
* button gives them nothing to weigh that against.
|
|
158
|
+
*/
|
|
159
|
+
function ConfirmBar({ action }: { action: { label: string; run: () => void } }) {
|
|
160
|
+
return (
|
|
161
|
+
<Box sx={BAR_SX} data-testid="payments-setup-confirm-bar">
|
|
162
|
+
<Typography sx={BAR_MSG_SX}>Confirme quando terminar do lado do provedor.</Typography>
|
|
163
|
+
<Button variant="contained" disableElevation sx={BTN_PRIMARY_SX} onClick={() => action.run()}>
|
|
164
|
+
{action.label}
|
|
165
|
+
</Button>
|
|
166
|
+
</Box>
|
|
167
|
+
);
|
|
168
|
+
}
|
|
169
|
+
|
|
125
170
|
interface StepRowProps {
|
|
126
171
|
step: SetupStep;
|
|
127
172
|
actions: StepActions;
|
|
@@ -153,37 +198,21 @@ function WarningRow({ text }: { text: string }) {
|
|
|
153
198
|
function StepRow({ step, actions }: StepRowProps) {
|
|
154
199
|
const action = step.action ? actions?.[step.action] : undefined;
|
|
155
200
|
if (step.tone === 'warning') return <WarningRow text={step.text ?? ''} />;
|
|
201
|
+
// An action-only step IS the panel's action bar — see `SectionCard`.
|
|
202
|
+
if (action && !step.text) return <ConfirmBar action={action} />;
|
|
203
|
+
// A bordered row with the work on the left and the way to do it on the
|
|
204
|
+
// right, so a step reads as a thing to tick off rather than as a paragraph.
|
|
205
|
+
// The instructions on this screen are a CHECKLIST — each one is a piece of
|
|
206
|
+
// work the owner does somewhere else and comes back from.
|
|
156
207
|
return (
|
|
157
|
-
<Stack
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
) : null}
|
|
166
|
-
</Typography>
|
|
167
|
-
) : null}
|
|
168
|
-
{step.button ? (
|
|
169
|
-
<Box>
|
|
170
|
-
<Button
|
|
171
|
-
variant="outlined"
|
|
172
|
-
size="small"
|
|
173
|
-
href={step.button.url}
|
|
174
|
-
target="_blank"
|
|
175
|
-
rel="noreferrer"
|
|
176
|
-
sx={BUTTON_SX}
|
|
177
|
-
// The mark is the promise: this leaves the store and opens the
|
|
178
|
-
// provider's site. A button that reads the same as the in-page ones
|
|
179
|
-
// and then navigates away is a small betrayal, and here it lands on
|
|
180
|
-
// a screen that can CHANGE the tag.
|
|
181
|
-
endIcon={<Box component="span" aria-hidden sx={{ fontSize: '0.9em' }}>↗</Box>}
|
|
182
|
-
>
|
|
183
|
-
{step.button.label}
|
|
184
|
-
</Button>
|
|
185
|
-
</Box>
|
|
186
|
-
) : null}
|
|
208
|
+
<Stack
|
|
209
|
+
direction="row"
|
|
210
|
+
gap="12px"
|
|
211
|
+
alignItems="flex-start"
|
|
212
|
+
sx={{ border: `1px solid ${T.line}`, borderRadius: '9px', px: '14px', py: '12px' }}
|
|
213
|
+
>
|
|
214
|
+
<Stack spacing={1} sx={{ flex: 1, minWidth: 0 }}>
|
|
215
|
+
<StepText text={step.text} link={step.link} />
|
|
187
216
|
{action ? (
|
|
188
217
|
<Box>
|
|
189
218
|
<Button variant="contained" size="small" sx={BUTTON_SX} onClick={() => void action.run()}>
|
|
@@ -191,17 +220,88 @@ function StepRow({ step, actions }: StepRowProps) {
|
|
|
191
220
|
</Button>
|
|
192
221
|
</Box>
|
|
193
222
|
) : null}
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
223
|
+
{step.copy ? (
|
|
224
|
+
<CopyField
|
|
225
|
+
label={step.copy.label}
|
|
226
|
+
text={step.copy.text}
|
|
227
|
+
collapsible={step.copy.collapsible}
|
|
228
|
+
/>
|
|
229
|
+
) : null}
|
|
230
|
+
</Stack>
|
|
231
|
+
{step.button ? (
|
|
232
|
+
<Button
|
|
233
|
+
size="small"
|
|
234
|
+
href={step.button.url}
|
|
235
|
+
target="_blank"
|
|
236
|
+
rel="noreferrer"
|
|
237
|
+
sx={{ ...BTN_SECONDARY_SX, px: '12px', py: '7px', fontSize: '12px', flexShrink: 0 }}
|
|
238
|
+
// The mark is the promise: this leaves the store and opens the
|
|
239
|
+
// provider's site. A button that reads the same as the in-page ones
|
|
240
|
+
// and then navigates away is a small betrayal, and here it lands on
|
|
241
|
+
// a screen that can CHANGE where the money goes.
|
|
242
|
+
endIcon={
|
|
243
|
+
<Box component="span" aria-hidden sx={{ fontSize: '0.9em' }}>
|
|
244
|
+
↗
|
|
245
|
+
</Box>
|
|
246
|
+
}
|
|
247
|
+
>
|
|
248
|
+
{step.button.label}
|
|
249
|
+
</Button>
|
|
200
250
|
) : null}
|
|
201
251
|
</Stack>
|
|
202
252
|
);
|
|
203
253
|
}
|
|
204
254
|
|
|
255
|
+
/**
|
|
256
|
+
* The numbered dot: 24px, filled once the store is ON or PAST the step.
|
|
257
|
+
*
|
|
258
|
+
* MUI's own icon is a 24px circle with the number inside and the same fill for
|
|
259
|
+
* active and completed, which is nearly the prototype — the differences are the
|
|
260
|
+
* exact greys and the ✓ on a finished step, and on a screen whose whole job is
|
|
261
|
+
* "where am I" those are the two things that carry the answer.
|
|
262
|
+
*/
|
|
263
|
+
function StageIcon({ active, completed, icon }: StepIconProps) {
|
|
264
|
+
const filled = active || completed;
|
|
265
|
+
return (
|
|
266
|
+
<Box
|
|
267
|
+
sx={{
|
|
268
|
+
width: 24,
|
|
269
|
+
height: 24,
|
|
270
|
+
borderRadius: '50%',
|
|
271
|
+
background: filled ? T.brand : '#d9dbe1',
|
|
272
|
+
color: '#fff',
|
|
273
|
+
fontSize: '12px',
|
|
274
|
+
fontWeight: 700,
|
|
275
|
+
display: 'grid',
|
|
276
|
+
placeItems: 'center',
|
|
277
|
+
}}
|
|
278
|
+
>
|
|
279
|
+
{completed ? '✓' : icon}
|
|
280
|
+
</Box>
|
|
281
|
+
);
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
/** A 2px rule that turns brand-coloured behind the steps already passed. */
|
|
285
|
+
const StageConnector = styled(StepConnector)({
|
|
286
|
+
top: 11,
|
|
287
|
+
[`& .${stepConnectorClasses.line}`]: { borderTopWidth: 2, borderColor: T.line },
|
|
288
|
+
[`&.${stepConnectorClasses.active} .${stepConnectorClasses.line}`]: { borderColor: T.brandLine },
|
|
289
|
+
[`&.${stepConnectorClasses.completed} .${stepConnectorClasses.line}`]: {
|
|
290
|
+
borderColor: T.brandLine,
|
|
291
|
+
},
|
|
292
|
+
});
|
|
293
|
+
|
|
294
|
+
const STAGE_LABEL_SX = {
|
|
295
|
+
'& .MuiStepLabel-label': {
|
|
296
|
+
fontSize: '12px',
|
|
297
|
+
color: T.ink3,
|
|
298
|
+
lineHeight: 1.25,
|
|
299
|
+
mt: '7px !important',
|
|
300
|
+
'&.Mui-active': { color: T.ink, fontWeight: 650 },
|
|
301
|
+
'&.Mui-completed': { color: T.ink3, fontWeight: 400 },
|
|
302
|
+
},
|
|
303
|
+
} as const;
|
|
304
|
+
|
|
205
305
|
export function ProviderSetupGuide({
|
|
206
306
|
guide,
|
|
207
307
|
activeStage = 0,
|
|
@@ -210,11 +310,18 @@ export function ProviderSetupGuide({
|
|
|
210
310
|
sectionFooter,
|
|
211
311
|
}: ProviderSetupGuideProps) {
|
|
212
312
|
return (
|
|
213
|
-
<Stack spacing={
|
|
214
|
-
<Stepper
|
|
215
|
-
{
|
|
216
|
-
|
|
217
|
-
|
|
313
|
+
<Stack spacing={0} data-testid="payments-setup-guide">
|
|
314
|
+
<Stepper
|
|
315
|
+
activeStep={activeStage}
|
|
316
|
+
alternativeLabel
|
|
317
|
+
connector={<StageConnector />}
|
|
318
|
+
sx={{ px: '20px', pt: '6px', pb: '18px' }}
|
|
319
|
+
>
|
|
320
|
+
{guide.stages.map((stage, index) => (
|
|
321
|
+
<Step key={stage.id} completed={index < activeStage}>
|
|
322
|
+
<StepLabel slots={{ stepIcon: StageIcon }} sx={STAGE_LABEL_SX}>
|
|
323
|
+
{stage.label}
|
|
324
|
+
</StepLabel>
|
|
218
325
|
</Step>
|
|
219
326
|
))}
|
|
220
327
|
</Stepper>
|
|
@@ -235,29 +342,42 @@ function SectionCard({
|
|
|
235
342
|
actions: StepActions;
|
|
236
343
|
footer?: ReactNode;
|
|
237
344
|
}) {
|
|
345
|
+
// The step whose completion only the OWNER can report is not a row among the
|
|
346
|
+
// instructions — it is what this panel is FOR. It moves to the action bar, so
|
|
347
|
+
// the control the owner is working toward is the last thing in the block and
|
|
348
|
+
// stays on screen while they read the steps above it.
|
|
349
|
+
const asks = section.steps.filter((step) => step.action !== undefined);
|
|
350
|
+
const reads = section.steps.filter((step) => step.action === undefined);
|
|
351
|
+
|
|
238
352
|
return (
|
|
239
|
-
<
|
|
240
|
-
|
|
241
|
-
sx={{ p: 2 }}
|
|
353
|
+
<Box
|
|
354
|
+
sx={PANEL_SX}
|
|
242
355
|
// Which section is showing is now a FACT about the store's progress, not
|
|
243
356
|
// a constant, so it needs to be assertable by id rather than by matching
|
|
244
357
|
// the prose inside it.
|
|
245
358
|
data-testid={`payments-setup-section-${section.id}`}
|
|
246
359
|
>
|
|
247
|
-
<
|
|
248
|
-
<Typography
|
|
360
|
+
<Box sx={{ px: '18px', pt: '15px' }}>
|
|
361
|
+
<Typography sx={{ fontSize: '14.5px', fontWeight: 700, color: T.ink }}>
|
|
249
362
|
{section.title}
|
|
250
363
|
</Typography>
|
|
251
364
|
{section.intro ? (
|
|
252
|
-
<Typography
|
|
365
|
+
<Typography sx={{ fontSize: '12.5px', color: T.ink3, mt: '5px', lineHeight: 1.5 }}>
|
|
253
366
|
{richText(section.intro)}
|
|
254
367
|
</Typography>
|
|
255
368
|
) : null}
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
369
|
+
</Box>
|
|
370
|
+
<Box sx={{ px: '18px', pt: '14px', pb: '18px' }}>
|
|
371
|
+
<Stack spacing={1.5}>
|
|
372
|
+
{reads.map((step, index) => (
|
|
373
|
+
<StepRow key={index} step={step} actions={actions} />
|
|
374
|
+
))}
|
|
375
|
+
{footer}
|
|
376
|
+
</Stack>
|
|
377
|
+
</Box>
|
|
378
|
+
{asks.map((step, index) => (
|
|
379
|
+
<StepRow key={`ask-${index}`} step={step} actions={actions} />
|
|
380
|
+
))}
|
|
381
|
+
</Box>
|
|
262
382
|
);
|
|
263
383
|
}
|
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
'use client';
|
|
2
2
|
|
|
3
|
-
import { Box,
|
|
3
|
+
import { Box, Stack, Switch, Tooltip, Typography } from '@mui/material';
|
|
4
4
|
|
|
5
5
|
import type { MaskedProviderConfig, ProviderDescriptor } from '@12-apps/payments-backend';
|
|
6
6
|
|
|
7
7
|
import { isConnected } from './connection-state';
|
|
8
|
+
import { T } from './panel-tokens';
|
|
8
9
|
|
|
9
10
|
/**
|
|
10
11
|
* The provider's headline: what state it is in, and the switch that decides
|
|
@@ -124,6 +125,36 @@ function toggleGate(
|
|
|
124
125
|
};
|
|
125
126
|
}
|
|
126
127
|
|
|
128
|
+
/**
|
|
129
|
+
* The three sentences the header can be saying, chosen once.
|
|
130
|
+
*
|
|
131
|
+
* Proven-but-off is its OWN state. "Não está recebendo" is true of a store that
|
|
132
|
+
* never finished setup AND of one that finished and paused, and those are
|
|
133
|
+
* opposite situations: the first is a step outstanding, the second a decision
|
|
134
|
+
* the owner made and can undo in one click.
|
|
135
|
+
*/
|
|
136
|
+
function headline(io: { enabled: boolean; paused: boolean; lockedOff: boolean; hint: string }) {
|
|
137
|
+
if (io.enabled) {
|
|
138
|
+
return {
|
|
139
|
+
state: 'Recebendo vendas',
|
|
140
|
+
sub: 'Sua loja está recebendo por este provedor.',
|
|
141
|
+
tone: T.ok,
|
|
142
|
+
};
|
|
143
|
+
}
|
|
144
|
+
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
|
+
};
|
|
150
|
+
}
|
|
151
|
+
return {
|
|
152
|
+
state: 'Ainda não está recebendo',
|
|
153
|
+
sub: io.lockedOff ? io.hint : 'Tudo pronto — ligue a chave para começar a receber.',
|
|
154
|
+
tone: T.ink3,
|
|
155
|
+
};
|
|
156
|
+
}
|
|
157
|
+
|
|
127
158
|
/**
|
|
128
159
|
* Status chip + the "recebendo vendas" switch, hoisted OUT of the credential
|
|
129
160
|
* form.
|
|
@@ -142,6 +173,52 @@ function toggleGate(
|
|
|
142
173
|
* you hover it. The reason is printed underneath instead of hidden in a
|
|
143
174
|
* tooltip, since the owner who most needs it is the one who cannot click.
|
|
144
175
|
*/
|
|
176
|
+
/**
|
|
177
|
+
* The provider's name and its status chip, side by side.
|
|
178
|
+
*
|
|
179
|
+
* The name is a REAL heading, not a div sized to look like one: it is what this
|
|
180
|
+
* screen is about, and it is how a screen reader — and the harness — identifies
|
|
181
|
+
* the page it landed on. The restyle set the size by hand and took the element
|
|
182
|
+
* with it, which no unit test could see and the harness caught at once.
|
|
183
|
+
*/
|
|
184
|
+
function ProviderName({
|
|
185
|
+
displayName,
|
|
186
|
+
label,
|
|
187
|
+
proven,
|
|
188
|
+
}: {
|
|
189
|
+
displayName: string;
|
|
190
|
+
label: string;
|
|
191
|
+
proven: boolean;
|
|
192
|
+
}) {
|
|
193
|
+
return (
|
|
194
|
+
<Stack direction="row" alignItems="center" gap="9px">
|
|
195
|
+
<Typography
|
|
196
|
+
component="h2"
|
|
197
|
+
sx={{ m: 0, fontSize: '19px', fontWeight: 700, letterSpacing: '-.01em', color: T.ink }}
|
|
198
|
+
>
|
|
199
|
+
{displayName}
|
|
200
|
+
</Typography>
|
|
201
|
+
<Box
|
|
202
|
+
component="span"
|
|
203
|
+
data-testid="payments-status"
|
|
204
|
+
sx={{
|
|
205
|
+
fontSize: '10px',
|
|
206
|
+
fontWeight: 800,
|
|
207
|
+
letterSpacing: '.06em',
|
|
208
|
+
textTransform: 'uppercase',
|
|
209
|
+
borderRadius: '5px',
|
|
210
|
+
px: '7px',
|
|
211
|
+
py: '3px',
|
|
212
|
+
background: proven ? T.okSoft : '#f0f1f4',
|
|
213
|
+
color: proven ? T.ok : T.ink3,
|
|
214
|
+
}}
|
|
215
|
+
>
|
|
216
|
+
{label}
|
|
217
|
+
</Box>
|
|
218
|
+
</Stack>
|
|
219
|
+
);
|
|
220
|
+
}
|
|
221
|
+
|
|
145
222
|
export function ProviderStatusBar({
|
|
146
223
|
descriptor,
|
|
147
224
|
config,
|
|
@@ -157,37 +234,40 @@ export function ProviderStatusBar({
|
|
|
157
234
|
const { lockedOff, hint } = toggleGate(descriptor, config, enabled);
|
|
158
235
|
const badge = statusBadge(config, descriptor);
|
|
159
236
|
|
|
237
|
+
const proven = Boolean(config?.chargeVerifiedAt);
|
|
238
|
+
const paused = proven && !enabled;
|
|
239
|
+
const { state, sub, tone } = headline({ enabled, paused, lockedOff, hint });
|
|
240
|
+
|
|
160
241
|
return (
|
|
161
|
-
<Stack
|
|
162
|
-
<
|
|
163
|
-
<
|
|
164
|
-
<
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
242
|
+
<Stack direction="row" alignItems="flex-start" gap="14px" flexWrap="wrap">
|
|
243
|
+
<Box>
|
|
244
|
+
<ProviderName displayName={descriptor.displayName} label={badge.label} proven={proven} />
|
|
245
|
+
<Typography
|
|
246
|
+
sx={{ fontSize: '12.5px', color: T.ink3, mt: '5px', maxWidth: '52ch', lineHeight: 1.5 }}
|
|
247
|
+
data-testid="payments-enable-hint"
|
|
248
|
+
>
|
|
249
|
+
{sub}
|
|
250
|
+
</Typography>
|
|
251
|
+
</Box>
|
|
252
|
+
{/* The switch belongs at the far edge: it is the one control here that
|
|
253
|
+
changes what buyers experience, and crowding it against the status
|
|
254
|
+
chip made the two read as one compound widget. */}
|
|
255
|
+
<Stack direction="row" alignItems="center" gap="10px" sx={{ ml: 'auto' }}>
|
|
169
256
|
<Tooltip title={hint}>
|
|
170
257
|
{/* A disabled control fires no events, so the tooltip needs a live wrapper. */}
|
|
171
258
|
<span>
|
|
172
|
-
<
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
disabled={busy || lockedOff}
|
|
178
|
-
onChange={(_, next) => onToggle(next)}
|
|
179
|
-
/>
|
|
180
|
-
}
|
|
181
|
-
label={enabled ? 'Recebendo vendas' : 'Não está recebendo'}
|
|
259
|
+
<Switch
|
|
260
|
+
data-testid="payments-enabled-toggle"
|
|
261
|
+
checked={enabled}
|
|
262
|
+
disabled={busy || lockedOff}
|
|
263
|
+
onChange={(_, next) => onToggle(next)}
|
|
182
264
|
/>
|
|
183
265
|
</span>
|
|
184
266
|
</Tooltip>
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
<Typography variant="caption" color="text.secondary" data-testid="payments-enable-hint">
|
|
188
|
-
{hint}
|
|
267
|
+
<Typography sx={{ fontSize: '12.5px', fontWeight: tone === T.ink3 ? 400 : 600, color: tone }}>
|
|
268
|
+
{state}
|
|
189
269
|
</Typography>
|
|
190
|
-
|
|
270
|
+
</Stack>
|
|
191
271
|
</Stack>
|
|
192
272
|
);
|
|
193
273
|
}
|
|
@@ -5,7 +5,7 @@ import { buyerFieldsFor } from "./buyer-fields";
|
|
|
5
5
|
import { DadosStep, EmptyCart, PaymentStep } from "./checkout-steps";
|
|
6
6
|
import { ArrowBackIcon } from "./icons";
|
|
7
7
|
import { PaymentStatus } from "./payment-status";
|
|
8
|
-
import type { BuyerInfo, CheckoutProviderConfig,
|
|
8
|
+
import type { BuyerInfo, CheckoutProviderConfig, SettlementCheckout } from "./types";
|
|
9
9
|
import { CheckoutComponentsProvider, useCheckoutComponents, type CheckoutComponents } from "./ui";
|
|
10
10
|
import { useCheckoutController, type CheckoutHostPorts } from "./use-checkout-controller";
|
|
11
11
|
|
|
@@ -17,7 +17,7 @@ const STEPPER_STEPS = [
|
|
|
17
17
|
|
|
18
18
|
/** What the flow reads off the host's cart — display facts, never money math. */
|
|
19
19
|
export interface CheckoutCartView {
|
|
20
|
-
/** Nothing to check out (cart mode only; a
|
|
20
|
+
/** Nothing to check out (cart mode only; a settlement settlement ignores it). */
|
|
21
21
|
empty: boolean;
|
|
22
22
|
totalLabel: string;
|
|
23
23
|
totalItems: number;
|
|
@@ -30,7 +30,7 @@ export interface CheckoutCartView {
|
|
|
30
30
|
* three-step flow — Dados → Pagamento → Confirmação — with the payment step
|
|
31
31
|
* speaking the store's ACTIVE provider protocol (PagBank PIX + card, Stone
|
|
32
32
|
* card, InfinitePay hosted redirect) against the host-mounted `/api/checkout*`
|
|
33
|
-
* surface. Cart, catalog,
|
|
33
|
+
* surface. Cart, catalog, settlement and order CREATION stay in the host and
|
|
34
34
|
* arrive through {@link CheckoutHostPorts} + {@link CheckoutCartView};
|
|
35
35
|
* pixels render through the slot contract (`components`, see `ui.tsx`).
|
|
36
36
|
*/
|
|
@@ -38,8 +38,8 @@ export interface CheckoutFlowProps extends CheckoutHostPorts {
|
|
|
38
38
|
/** The host's cart, reduced to what the flow displays. */
|
|
39
39
|
cart: CheckoutCartView;
|
|
40
40
|
defaultBuyer?: BuyerInfo;
|
|
41
|
-
/** Present ⇒ this checkout settles
|
|
42
|
-
|
|
41
|
+
/** Present ⇒ this checkout settles an open balance, not the cart . */
|
|
42
|
+
settlement?: SettlementCheckout | null;
|
|
43
43
|
/** The buyer has a CPF saved ⇒ open on Pagamento, skipping Dados (FUT-465). */
|
|
44
44
|
taxIdOnFile?: boolean;
|
|
45
45
|
/** The store's active payment protocol (FUT-697); absent while loading. */
|
|
@@ -51,7 +51,7 @@ export interface CheckoutFlowProps extends CheckoutHostPorts {
|
|
|
51
51
|
* session's `validationURL` for an Apple merchant session, SERVER-SIDE.
|
|
52
52
|
* Optional — without it the Apple Pay sheet cannot start, and the card form
|
|
53
53
|
* remains the way to pay.
|
|
54
|
-
|
|
54
|
+
*/
|
|
55
55
|
validateApplePayMerchant?: (validationURL: string) => Promise<unknown>;
|
|
56
56
|
/** Host content shown on the paid confirmation (the storefront's install invite). */
|
|
57
57
|
confirmationExtra?: ReactNode;
|
|
@@ -59,11 +59,11 @@ export interface CheckoutFlowProps extends CheckoutHostPorts {
|
|
|
59
59
|
components?: Partial<CheckoutComponents>;
|
|
60
60
|
}
|
|
61
61
|
|
|
62
|
-
/** The pay-bar total override when settling a
|
|
63
|
-
function
|
|
64
|
-
|
|
62
|
+
/** The pay-bar total override when settling a settlement (else the cart's own totals). */
|
|
63
|
+
function settlementTotalOverride(
|
|
64
|
+
settlement: SettlementCheckout | null | undefined,
|
|
65
65
|
): { label: string; items: number } | undefined {
|
|
66
|
-
return
|
|
66
|
+
return settlement ? { label: settlement.totalLabel, items: settlement.totalItems } : undefined;
|
|
67
67
|
}
|
|
68
68
|
|
|
69
69
|
/**
|
|
@@ -78,13 +78,13 @@ function confirmationFacts(
|
|
|
78
78
|
return { orderId: order?.orderId, buyerEmail: buyer.email };
|
|
79
79
|
}
|
|
80
80
|
|
|
81
|
-
/** The confirmation total: the created order's, else the
|
|
81
|
+
/** The confirmation total: the created order's, else the settlement scope's, else the cart's. */
|
|
82
82
|
function statusTotalLabel(
|
|
83
83
|
order: { totalLabel: string } | null,
|
|
84
|
-
|
|
84
|
+
settlement: SettlementCheckout | null | undefined,
|
|
85
85
|
cart: { totalLabel: string },
|
|
86
86
|
): string {
|
|
87
|
-
return order?.totalLabel ??
|
|
87
|
+
return order?.totalLabel ?? settlement?.totalLabel ?? cart.totalLabel;
|
|
88
88
|
}
|
|
89
89
|
|
|
90
90
|
/**
|
|
@@ -125,7 +125,7 @@ function ProgressHeader({ step, completed }: { step: string; completed: Set<stri
|
|
|
125
125
|
* card public key are loaded lazily by the card path (order-scoped REST).
|
|
126
126
|
*/
|
|
127
127
|
function CheckoutFlowBody(props: Omit<CheckoutFlowProps, "components">): JSX.Element {
|
|
128
|
-
const { cart, defaultBuyer,
|
|
128
|
+
const { cart, defaultBuyer, settlement, taxIdOnFile = false, providerConfig, tenantSlug, confirmationExtra, validateApplePayMerchant, ...ports } = props;
|
|
129
129
|
// Resolved for NO method on purpose (FUT-595): the Dados step opens before
|
|
130
130
|
// the picker, and the form is filled once — so it asks for the union of what
|
|
131
131
|
// any chain member may need rather than re-opening after the choice. A chain
|
|
@@ -133,14 +133,14 @@ function CheckoutFlowBody(props: Omit<CheckoutFlowProps, "components">): JSX.Ele
|
|
|
133
133
|
const buyerFields = useMemo(() => buyerFieldsFor(providerConfig?.chain, null), [providerConfig]);
|
|
134
134
|
const c = useCheckoutController(ports, defaultBuyer, taxIdOnFile, buyerFields);
|
|
135
135
|
|
|
136
|
-
// A
|
|
136
|
+
// A settlement settlement pays already-sent kitchen items — the cart is
|
|
137
137
|
// legitimately empty here, so the empty-cart guard only applies to cart mode.
|
|
138
138
|
//
|
|
139
139
|
// The guard cannot key on the Dados step any more: skipping it (FUT-465) makes
|
|
140
140
|
// Pagamento the first screen, so an empty cart would otherwise reach the
|
|
141
141
|
// method picker. It holds until an order exists — once one does, its lines are
|
|
142
142
|
// snapshotted server-side and the cart no longer speaks for it.
|
|
143
|
-
if (!
|
|
143
|
+
if (!settlement && cart.empty && !c.order && c.step !== "status") {
|
|
144
144
|
return <EmptyCart onBack={c.goToMenu} />;
|
|
145
145
|
}
|
|
146
146
|
|
|
@@ -162,7 +162,7 @@ function CheckoutFlowBody(props: Omit<CheckoutFlowProps, "components">): JSX.Ele
|
|
|
162
162
|
cartTotals={cart}
|
|
163
163
|
buyerFields={buyerFields}
|
|
164
164
|
discountLines={cart.discountLines}
|
|
165
|
-
totalOverride={
|
|
165
|
+
totalOverride={settlementTotalOverride(settlement)}
|
|
166
166
|
/>
|
|
167
167
|
) : null}
|
|
168
168
|
|
|
@@ -191,7 +191,7 @@ function CheckoutFlowBody(props: Omit<CheckoutFlowProps, "components">): JSX.Ele
|
|
|
191
191
|
{c.step === "status" ? (
|
|
192
192
|
<PaymentStatus
|
|
193
193
|
status={c.finalStatus}
|
|
194
|
-
totalLabel={statusTotalLabel(c.order,
|
|
194
|
+
totalLabel={statusTotalLabel(c.order, settlement, cart)}
|
|
195
195
|
{...confirmationFacts(c.order, c.buyer)}
|
|
196
196
|
onRetry={c.retry}
|
|
197
197
|
onRegenerate={() => { c.setStep("payment"); void c.startPayment("PIX"); }}
|
|
@@ -113,8 +113,8 @@ export function EmptyCart({ onBack }: { onBack: () => void }): JSX.Element {
|
|
|
113
113
|
}
|
|
114
114
|
|
|
115
115
|
/**
|
|
116
|
-
* The totals shown on the pay bar: the
|
|
117
|
-
*
|
|
116
|
+
* The totals shown on the pay bar: the settled balance's when settling a settlement
|
|
117
|
+
* one, otherwise the cart's own — both supplied by the host, which
|
|
118
118
|
* is the only side that knows either.
|
|
119
119
|
*/
|
|
120
120
|
function displayTotals(
|
|
@@ -184,9 +184,9 @@ export function DadosStep({
|
|
|
184
184
|
* (FUT-246) — RENDERED BY THE HOST from its cart (the storefront passes its
|
|
185
185
|
* cart footer's money block), never re-implemented here, so the two surfaces
|
|
186
186
|
* can never word the same discount differently.
|
|
187
|
-
|
|
187
|
+
*/
|
|
188
188
|
discountLines?: ReactNode;
|
|
189
|
-
/**
|
|
189
|
+
/** Settling an open balance: totals come from the settlement, not the cart. */
|
|
190
190
|
totalOverride?: { label: string; items: number };
|
|
191
191
|
}): JSX.Element {
|
|
192
192
|
const { Checkbox } = useCheckoutComponents();
|
|
@@ -215,7 +215,7 @@ export function DadosStep({
|
|
|
215
215
|
createError={createError}
|
|
216
216
|
onContinue={onContinue}
|
|
217
217
|
>
|
|
218
|
-
{/* Suppressed while settling a
|
|
218
|
+
{/* Suppressed while settling a balance: those totals come from the
|
|
219
219
|
frozen ticket, not the cart. */}
|
|
220
220
|
{totalOverride ? null : discountLines}
|
|
221
221
|
</DadosPayBar>
|
|
@@ -275,21 +275,21 @@ interface PaymentStepProps {
|
|
|
275
275
|
/**
|
|
276
276
|
* The refusal's machine code (FUT-563). An UNRESOLVED charge is not a failed
|
|
277
277
|
* one — the panel below must not offer to raise a second.
|
|
278
|
-
|
|
278
|
+
*/
|
|
279
279
|
errorCode?: string | null;
|
|
280
280
|
onGenerate: (method: PaymentMethod) => void;
|
|
281
281
|
onUseEmail: (email: string) => void;
|
|
282
282
|
/**
|
|
283
283
|
* Present ⇒ the buyer reached this step without a Dados step (FUT-465), so
|
|
284
284
|
* the payer block states who is being charged and reopens Dados to change it.
|
|
285
|
-
|
|
285
|
+
*/
|
|
286
286
|
onEditBuyer?: () => void;
|
|
287
287
|
/**
|
|
288
288
|
* The store's active payment protocol (`GET /api/checkout/config`, FUT-697).
|
|
289
289
|
* `null` while loading or on a transient fetch failure — methods then render
|
|
290
290
|
* as before and the card path degrades to the PagBank per-order key refresh,
|
|
291
291
|
* WITHOUT mock permission (fail-open for the UI, fail-closed for the money).
|
|
292
|
-
|
|
292
|
+
*/
|
|
293
293
|
providerConfig?: CheckoutProviderConfig | null;
|
|
294
294
|
/** Scopes the saved-card list to the store being paid (host routing owns it). */
|
|
295
295
|
tenantSlug?: string;
|