@12-apps/payments-frontend 3.34.0 → 3.34.2

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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@12-apps/payments-frontend",
3
- "version": "3.34.0",
3
+ "version": "3.34.2",
4
4
  "type": "module",
5
5
  "sideEffects": false,
6
6
  "description": "Browser half of the vendor-agnostic payments platform: plug-and-play MUI components for the per-provider settings page (credential form from each provider's schema, masked hints, verify/enable) and the checkout page (PIX QR + polling, card tokenization, hosted-checkout redirect), plus the headless hooks and fetch clients they build on. Talks only to the host's payments HTTP surface — never to a provider directly. Microfrontend-ready: no app coupling, host injects theme and auth.",
@@ -22,6 +22,9 @@ import { useCheckoutComponents } from "./ui";
22
22
 
23
23
  type StepState = "done" | "current" | "pending";
24
24
 
25
+ /** The way to the card is a link: underlined, so it does not lean on colour alone. */
26
+ const LINK_SX = { "& button": { textDecoration: "underline", textUnderlineOffset: "3px" }, "& button, & button *": { fontWeight: 600 } } as const;
27
+
25
28
  function StepMark({ state, index }: { state: StepState; index: number }): JSX.Element {
26
29
  const size = { width: 26, height: 26, [PIX_WIDE]: { width: 28, height: 28 } };
27
30
  if (state === "done") {
@@ -92,7 +95,7 @@ function Heading({ words, totalLabel }: { words: PixAfterCopyCopy; totalLabel: s
92
95
  </Box>
93
96
  <Box sx={{ display: "flex", flexDirection: "column", gap: 0.5 }}>
94
97
  {/* The screen's title outranks the checklist: 22px, 28px wide (the prototype's scale). */}
95
- <Box sx={{ "& > *": { fontSize: "1.375rem", lineHeight: 1.25 }, [PIX_WIDE]: { "& > *": { fontSize: "1.75rem" } } }}>
98
+ <Box sx={{ "&& > *": { fontSize: "1.375rem", lineHeight: 1.25 }, [PIX_WIDE]: { "&& > *": { fontSize: "1.75rem" } } }}>
96
99
  <Text variant="heading" size="md" weight="bold" as="h2" style={{ margin: 0 }} data-testid="pix-after-copy-title">
97
100
  {words.title}
98
101
  </Text>
@@ -126,7 +129,7 @@ function Actions({ justCopied, onCopyAgain, onShowQr, onPreferCard }: Pick<PixAf
126
129
  const { Button } = useCheckoutComponents();
127
130
  const copy = useCheckoutCopy().screens.pix;
128
131
  return (
129
- <Box sx={{ display: "grid", gridTemplateColumns: "repeat(2, minmax(0, 1fr))", gap: 1.25, "& button": { minHeight: 48, height: "100%" }, [PIX_WIDE]: { display: "flex", flexWrap: "wrap", alignItems: "center", gap: 1.5, "& button": { height: "auto" } } }}>
132
+ <Box sx={{ display: "grid", gridTemplateColumns: "repeat(2, minmax(0, 1fr))", gap: 1.25, "& button": { minHeight: 48, height: "100%" }, "& button, & button *": { fontWeight: 700 }, [PIX_WIDE]: { display: "flex", flexWrap: "wrap", alignItems: "center", gap: 1.5, "& button": { height: "auto" } } }}>
130
133
  {/* Narrow, a grid cell; wide, its own width beside the link (the prototype's row). */}
131
134
  <Box sx={{ minWidth: 0, [PIX_WIDE]: { flex: "none", "& > *": { width: "auto" } } }}>
132
135
  <Button variant="outline" color="neutral" size="md" fullWidth icon={<ContentCopyIcon fontSize="small" />} onClick={onCopyAgain} dataTestId="pix-copy-again">
@@ -139,7 +142,7 @@ function Actions({ justCopied, onCopyAgain, onShowQr, onPreferCard }: Pick<PixAf
139
142
  </Button>
140
143
  </Box>
141
144
  {onPreferCard ? (
142
- <Box sx={{ display: "none", [PIX_WIDE]: { display: "block" } }}>
145
+ <Box sx={{ ...LINK_SX, display: "none", [PIX_WIDE]: { display: "block" } }}>
143
146
  <Button variant="text" color="primary" size="md" icon={<CreditCardIcon fontSize="small" />} onClick={onPreferCard} dataTestId="pix-prefer-card-wide">
144
147
  {copy.preferCardAction}
145
148
  </Button>
@@ -161,7 +164,7 @@ export function PixAfterCopy(props: PixAfterCopyProps): JSX.Element {
161
164
  <Actions {...props} />
162
165
  {footer}
163
166
  {onPreferCard ? (
164
- <Box sx={{ display: "flex", justifyContent: "center", [PIX_WIDE]: { display: "none" } }}>
167
+ <Box sx={{ ...LINK_SX, display: "flex", justifyContent: "center", [PIX_WIDE]: { display: "none" } }}>
165
168
  <Button variant="text" color="primary" size="md" icon={<CreditCardIcon fontSize="small" />} onClick={onPreferCard} dataTestId="pix-prefer-card">
166
169
  {copy.preferCardAction}
167
170
  </Button>
@@ -163,15 +163,28 @@ function CopyPanel({ pix, shown, onCopy, idBase }: { pix: PixCharge; shown: bool
163
163
  return (
164
164
  <Box id={paneIds(idBase, "copy").panel} role="tabpanel" aria-labelledby={paneIds(idBase, "copy").tab} sx={{ ...panelSx(shown), [PIX_WIDE]: { display: "flex", justifyContent: "center" } }}>
165
165
  <ColumnHeading>{copy.copyPasteHeading}</ColumnHeading>
166
+ {/*
167
+ The box IS the code's frame, so whatever the host draws for inline code
168
+ (a tinted chip, its own border and padding) is undone inside it:
169
+ otherwise a wrapped code reads as a box inside a box.
170
+ */}
166
171
  <Box
167
- sx={{ px: 2, py: 1.5, border: "1px solid", borderColor: "divider", borderRadius: 1.5, bgcolor: "action.hover" }}
172
+ sx={{
173
+ px: 2,
174
+ py: 1.5,
175
+ border: "1px solid",
176
+ borderColor: "divider",
177
+ borderRadius: 1.5,
178
+ bgcolor: "action.hover",
179
+ "&& > *": { bgcolor: "transparent", border: 0, borderRadius: 0, p: 0 },
180
+ }}
168
181
  >
169
182
  <Text variant="code" size="xs" as="p" data-testid="pix-code" style={{ margin: 0, wordBreak: "break-all", lineHeight: 1.55 }}>
170
183
  {pix.copyPaste}
171
184
  </Text>
172
185
  </Box>
173
186
  {/* The one action on the pane: the prototype's 56px bar (52 wide), whatever the host's lg is. */}
174
- <Box sx={{ "& > *": { minHeight: 56 }, [PIX_WIDE]: { "& > *": { minHeight: 52 } } }}>
187
+ <Box sx={{ "& > *": { minHeight: 56 }, "& > *, & > * *": { fontWeight: 700 }, [PIX_WIDE]: { "& > *": { minHeight: 52 } } }}>
175
188
  <Button variant="solid" color="primary" size="lg" fullWidth icon={<ContentCopyIcon fontSize="small" />} onClick={onCopy} dataTestId="pix-copy">
176
189
  {copy.copyAction}
177
190
  </Button>
@@ -251,9 +251,12 @@ function PixBody({ order, pix, poll }: { order: CheckoutOrder; pix: PixCharge; p
251
251
  }
252
252
  return (
253
253
  <>
254
- <Text variant="heading" size="md" weight="bold" as="h2" style={{ margin: 0 }}>
255
- {copy.heading}
256
- </Text>
254
+ {/* The pane's title, at the prototype's scale: 20px, 26px wide. */}
255
+ <Box sx={{ "&& > *": { fontSize: "1.25rem", lineHeight: 1.25 }, [PIX_WIDE]: { "&& > *": { fontSize: "1.625rem" } } }}>
256
+ <Text variant="heading" size="md" weight="bold" as="h2" style={{ margin: 0 }}>
257
+ {copy.heading}
258
+ </Text>
259
+ </Box>
257
260
  <PixPane
258
261
  pix={pix}
259
262
  tab={tab}
@@ -293,8 +296,22 @@ export function PixView({
293
296
  );
294
297
  }
295
298
  return (
296
- <Box data-testid="pix-view" sx={{ ...PIX_CONTAINER, display: "flex", flexDirection: "column", gap: 2 }}>
297
- <PixBody order={order} pix={pix} poll={poll} />
299
+ <Box data-testid="pix-view" sx={PIX_CONTAINER}>
300
+ {/*
301
+ Wide, the pane is one white card on the page (the prototype's desktop
302
+ frame): heading, the two columns and the footer read as one unit. The
303
+ card is a child because a container query cannot style its container.
304
+ */}
305
+ <Box
306
+ sx={{
307
+ display: "flex",
308
+ flexDirection: "column",
309
+ gap: 2,
310
+ [PIX_WIDE]: { gap: 2.5, p: 3.5, bgcolor: "background.paper", border: "1px solid", borderColor: "divider", borderRadius: 2 },
311
+ }}
312
+ >
313
+ <PixBody order={order} pix={pix} poll={poll} />
314
+ </Box>
298
315
  </Box>
299
316
  );
300
317
  }
@@ -19,8 +19,9 @@ import { claimRearm, REARM_QUIET_MS } from "./poll-rearm";
19
19
  *
20
20
  * The mark is cleared by `restart`, by a `poke` that supersedes the marked
21
21
  * ask, by sending its follow-up and by a terminal answer. The deadline does
22
- * NOT clear it: a marked ask answering after the deadline is caught by
23
- * {@link owedAfterAnswer} and neither asks nor schedules.
22
+ * NOT clear it: a marked ask answering after the deadline is stopped by the
23
+ * loop's `carriesOn` (FUT-3222) before the mark is read, and neither asks nor
24
+ * schedules; the next `restart` clears the mark.
24
25
  *
25
26
  * Structural state, as in `poll-rearm.ts`: the loop's run satisfies it.
26
27
  */
@@ -47,12 +48,12 @@ export function claimHint(run: HintState): boolean {
47
48
  }
48
49
 
49
50
  /**
50
- * After a non-terminal answer: `"ask"` when a mark owes one more ask now,
51
- * `"ended"` when the wait stopped while that ask was out, `"none"` when nothing
52
- * is owed and the loop schedules as usual. Clears the mark.
51
+ * After a non-terminal answer to a wait that is still running: whether a mark
52
+ * owes one more ask now, rather than the usual schedule. Clears the mark. A
53
+ * wait that stopped while the ask was out never gets here (`carriesOn`).
53
54
  */
54
- export function owedAfterAnswer(run: Pick<HintState, "hinted" | "stopped">): "ask" | "ended" | "none" {
55
- if (!run.hinted) return "none";
55
+ export function owedAfterAnswer(run: Pick<HintState, "hinted">): boolean {
56
+ if (!run.hinted) return false;
56
57
  run.hinted = false;
57
- return run.stopped ? "ended" : "ask";
58
+ return true;
58
59
  }
@@ -280,11 +280,21 @@ function askNow(run: PollRun, tick: () => Promise<void>): void {
280
280
  void tick();
281
281
  }
282
282
 
283
+ /**
284
+ * Whether the ask that just answered may book what comes next. Not when a newer
285
+ * ask superseded it, and not when the wall clock ended the wait while it was out
286
+ * (FUT-3222): live, `outOfTime` never ends a wait, so an ask the deadline
287
+ * overtook kept booking more behind the timed-out panel. Read AFTER `absorb`, so
288
+ * a PAID landing just past the deadline is still written.
289
+ */
290
+ function carriesOn(run: PollRun, mine: number): boolean {
291
+ return run.attempt === mine && !run.stopped;
292
+ }
293
+
283
294
  /** After a non-terminal answer: the ask a hint owes, or the usual schedule. */
284
295
  function continueAfter(run: PollRun, tick: () => Promise<void>, schedule: () => void): void {
285
- const owed = owedAfterAnswer(run);
286
- if (owed === "ask") askNow(run, tick);
287
- else if (owed === "none") schedule();
296
+ if (owedAfterAnswer(run)) askNow(run, tick);
297
+ else schedule();
288
298
  }
289
299
 
290
300
  /** The handle the hook holds on one running wait. */
@@ -332,7 +342,7 @@ export function createPollLoop(
332
342
  clearDeadline(run);
333
343
  return;
334
344
  }
335
- if (run.attempt !== mine) return;
345
+ if (!carriesOn(run, mine)) return;
336
346
  continueAfter(run, tick, () => scheduleNext(run, options, sink, () => void tick()));
337
347
  };
338
348