@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.
@@ -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 spacing={1}>
158
- {step.text ? (
159
- <Typography variant="body2">
160
- {richText(step.text)}{' '}
161
- {step.link ? (
162
- <Link href={step.link.url} target="_blank" rel="noreferrer">
163
- {step.link.label}
164
- </Link>
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
- {step.copy ? (
195
- <CopyField
196
- label={step.copy.label}
197
- text={step.copy.text}
198
- collapsible={step.copy.collapsible}
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={3} data-testid="payments-setup-guide">
214
- <Stepper activeStep={activeStage} alternativeLabel>
215
- {guide.stages.map((stage) => (
216
- <Step key={stage.id}>
217
- <StepLabel>{stage.label}</StepLabel>
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
- <Paper
240
- variant="outlined"
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
- <Stack spacing={2}>
248
- <Typography variant="subtitle1" fontWeight="bold">
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 variant="body2" color="text.secondary">
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
- {section.steps.map((step, index) => (
257
- <StepRow key={index} step={step} actions={actions} />
258
- ))}
259
- {footer}
260
- </Stack>
261
- </Paper>
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, Chip, FormControlLabel, Stack, Switch, Tooltip, Typography } from '@mui/material';
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 spacing={0.5}>
162
- <Stack direction="row" spacing={1} alignItems="center">
163
- <Typography variant="h6">{descriptor.displayName}</Typography>
164
- <Chip size="small" data-testid="payments-status" label={badge.label} color={badge.color} />
165
- {/* The switch belongs at the far edge: it is the one control here that
166
- changes what buyers experience, and crowding it against the status
167
- chip made the two read as one compound widget. */}
168
- <Box sx={{ flexGrow: 1 }} />
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
- <FormControlLabel
173
- control={
174
- <Switch
175
- data-testid="payments-enabled-toggle"
176
- checked={enabled}
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
- </Stack>
186
- {lockedOff ? (
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
- ) : null}
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, ComandaCheckout } from "./types";
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 comanda settlement ignores it). */
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, comanda and order CREATION stay in the host and
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 a comanda, not the cart (FUT-comandas). */
42
- comanda?: ComandaCheckout | null;
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 comanda (else the cart's own totals). */
63
- function comandaTotalOverride(
64
- comanda: ComandaCheckout | null | undefined,
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 comanda ? { label: comanda.totalLabel, items: comanda.totalItems } : undefined;
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 comanda scope's, else the cart's. */
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
- comanda: ComandaCheckout | null | undefined,
84
+ settlement: SettlementCheckout | null | undefined,
85
85
  cart: { totalLabel: string },
86
86
  ): string {
87
- return order?.totalLabel ?? comanda?.totalLabel ?? cart.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, comanda, taxIdOnFile = false, providerConfig, tenantSlug, confirmationExtra, validateApplePayMerchant, ...ports } = props;
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 comanda settlement pays already-sent kitchen items — the cart is
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 (!comanda && cart.empty && !c.order && c.step !== "status") {
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={comandaTotalOverride(comanda)}
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, comanda, cart)}
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 comanda scope's when settling a comanda
117
- * (FUT-comandas), otherwise the cart's own — both supplied by the host, which
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
- /** Comanda settlement (FUT-comandas): totals come from the comanda, not the cart. */
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 comanda: those totals come from the
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;