@12-apps/payments-frontend 3.47.0 → 3.47.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.47.0",
3
+ "version": "3.47.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.",
@@ -300,6 +300,24 @@ function noPick(): void {
300
300
  // No bank flow: nothing to remember.
301
301
  }
302
302
 
303
+ /**
304
+ * The way to the bank app on the after-copy face, or `undefined` off a phone.
305
+ * With a bank chosen, one line says what to do there, in place of the
306
+ * checklist (FUT-3612) — unless the STORE confirms this Pix: its checklist's
307
+ * "A loja confirma o pagamento — até 14:32" is then the only place that says
308
+ * who confirms and by when, so it stays — under the six banks too. No line in
309
+ * the host's copy: the checklist stays too. Otherwise the six banks replace it.
310
+ */
311
+ function bankFace(flow: BankFlow | null, pix: PixCharge, stale: boolean): { node: JSX.Element; replacesChecklist: boolean } | undefined {
312
+ if (!flow?.os) return undefined;
313
+ const storeConfirms = pix.confirmation === "MANUAL";
314
+ const oneLine = Boolean(flow.bank.copy.appHint) && !storeConfirms;
315
+ return {
316
+ node: <PixBanks bank={flow.bank} state={flow.state} stale={stale} hint={oneLine} />,
317
+ replacesChecklist: oneLine || (flow.state.chosen === null && !storeConfirms),
318
+ };
319
+ }
320
+
303
321
  /** Both faces of the list's Pix, and the bar under them. */
304
322
  export function ListPixFace(props: ListPixFaceProps & { mode: PaymentListMode }): JSX.Element {
305
323
  const { Text } = useCheckoutComponents();
@@ -311,7 +329,7 @@ export function ListPixFace(props: ListPixFaceProps & { mode: PaymentListMode })
311
329
  // On a phone the bank flow takes the copy: "Copiar e abrir o Nubank", the
312
330
  // toast, the app. Without it — or on a computer — the copy is today's.
313
331
  const onCopy = flow ? copies.copy : props.onCopy;
314
- const banks = flow?.os ? { node: <PixBanks bank={flow.bank} state={flow.state} stale={stale} />, replacesChecklist: true } : undefined;
332
+ const banks = bankFace(flow, pix, stale);
315
333
  return (
316
334
  <Box data-testid="list-pix" sx={{ display: "flex", flexDirection: "column", gap: 1.5 }}>
317
335
  <Text variant="body" size="md" weight="bold" as="p" style={{ margin: 0 }} data-testid="list-pix-title">
@@ -182,13 +182,16 @@ function AppHint({ text }: { text: string }): JSX.Element {
182
182
  );
183
183
  }
184
184
 
185
- /** The grid, or the bank being opened and the line that says what to do there. */
186
- export function PixBanks({ bank, state, stale }: { bank: CheckoutPixBank; state: PixBankState; stale: boolean }): JSX.Element {
185
+ /**
186
+ * The grid, or the bank being opened — with, when `hint`, the line that says
187
+ * what to do there (the face then drops its checklist; see `ListPixFace`).
188
+ */
189
+ export function PixBanks({ bank, state, stale, hint }: { bank: CheckoutPixBank; state: PixBankState; stale: boolean; hint: boolean }): JSX.Element {
187
190
  if (!state.chosen) return <Grid bank={bank} state={state} stale={stale} />;
188
191
  return (
189
192
  <Box sx={{ display: "flex", flexDirection: "column", gap: 1 }}>
190
193
  <Opening bank={bank} state={state} chosen={state.chosen} />
191
- {bank.copy.appHint ? <AppHint text={bank.copy.appHint} /> : null}
194
+ {hint && bank.copy.appHint ? <AppHint text={bank.copy.appHint} /> : null}
192
195
  </Box>
193
196
  );
194
197
  }
@@ -126,12 +126,28 @@ interface PixAfterCopyProps {
126
126
  /**
127
127
  * The way to the bank app, on a phone (FUT-3612): drawn where the heading
128
128
  * was — the toast already said the code was copied and the bar says what it
129
- * waits for — and, while it is the six banks, in place of the checklist.
129
+ * waits for — and, when `replacesChecklist`, in place of the checklist. The
130
+ * column then sits as tight as the prototype's (8px).
130
131
  */
131
132
  banks?: { node: ReactNode; replacesChecklist: boolean };
132
133
  }
133
134
 
134
- /** The narrow layout's two secondaries side by side; wide, the card link joins the row. */
135
+ /**
136
+ * The pair's leading icons. On a phone's card (the pair under 360px) they go,
137
+ * as in the prototype: beside the icon, "Copiar de novo" took two lines in its
138
+ * 159px column ("novo" alone). A computer's column is wider and keeps them.
139
+ * `button` in the selector outranks the library Button's own icon rule, which
140
+ * otherwise wins or loses on style insertion order.
141
+ */
142
+ const ICON_SLOT = "& button .MuiButton-startIcon, & button .MuiButton-start";
143
+ /** The pair's own width, never the pane's. */
144
+ const ACTIONS_NARROW = "@container pixactions (max-width: 359.95px)";
145
+
146
+ /**
147
+ * The narrow layout's two secondaries side by side — 140px columns, so the pair
148
+ * fits a 390px phone's card (two of 180px did not, and stacked) and stacks only
149
+ * at 320 — wide, the card link joins the row.
150
+ */
135
151
  function Actions({ justCopied, onCopyAgain, onShowQr, onPreferCard }: Pick<PixAfterCopyProps, "justCopied" | "onCopyAgain" | "onShowQr" | "onPreferCard">): JSX.Element {
136
152
  const { Button } = useCheckoutComponents();
137
153
  const copy = useCheckoutCopy().screens.pix;
@@ -139,7 +155,7 @@ function Actions({ justCopied, onCopyAgain, onShowQr, onPreferCard }: Pick<PixAf
139
155
  // (FUT-3535): copying it again would only hand the buyer the wrong amount.
140
156
  const stale = usePaymentStale();
141
157
  return (
142
- <Box sx={{ display: "grid", gridTemplateColumns: "repeat(auto-fit, minmax(180px, 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" } } }}>
158
+ <Box sx={{ display: "grid", gridTemplateColumns: "repeat(auto-fit, minmax(140px, 1fr))", gap: 1.25, "& button": { minHeight: 48, height: "100%" }, "& button, & button *": { fontWeight: 700 }, containerType: "inline-size", containerName: "pixactions", [ACTIONS_NARROW]: { [ICON_SLOT]: { display: "none" } }, [PIX_WIDE]: { display: "flex", flexWrap: "wrap", alignItems: "center", gap: 1.5, "& button": { height: "auto" } } }}>
143
159
  {/* Narrow, a grid cell; wide, its own width beside the link (the prototype's row). */}
144
160
  <Box sx={{ minWidth: 0, [PIX_WIDE]: { flex: "none", "& > *": { width: "auto" } } }}>
145
161
  <Button variant="outline" color="neutral" size="md" fullWidth icon={<ContentCopyIcon fontSize="small" />} onClick={onCopyAgain} disabled={stale} dataTestId="pix-copy-again">
@@ -169,7 +185,7 @@ export function PixAfterCopy(props: PixAfterCopyProps): JSX.Element {
169
185
  const stale = usePaymentStale();
170
186
  return (
171
187
  <Box data-testid="pix-after-copy" sx={{ display: "flex", flexWrap: "wrap", gap: 4, alignItems: "flex-start" }}>
172
- <Box sx={{ flex: "999 1 420px", minWidth: 0, display: "flex", flexDirection: "column", gap: 2.5 }}>
188
+ <Box sx={{ flex: "999 1 420px", minWidth: 0, display: "flex", flexDirection: "column", gap: props.banks?.replacesChecklist ? 1 : 2.5 }}>
173
189
  {props.banks ? props.banks.node : <Heading words={words} totalLabel={totalLabel} />}
174
190
  {props.banks?.replacesChecklist ? null : <Checklist words={words} deadline={deadline} />}
175
191
  <Actions {...props} />
@@ -25,7 +25,7 @@ import {
25
25
  type GooglePaymentData,
26
26
  type GooglePaymentsClient,
27
27
  } from '../checkout/google-pay-button';
28
- import type { HomologacaoEvidenceWalletsCopy } from './copy';
28
+ import type { AppleAbsence, HomologacaoEvidenceWalletsCopy } from './copy';
29
29
  import { usePlatformCopy } from './copy-context';
30
30
 
31
31
  /**
@@ -171,9 +171,10 @@ function notOffered(wallet: EvidenceWallet, input: EvidenceWalletsInput, googleR
171
171
  }
172
172
 
173
173
  /** Why Apple Pay has no button on a screen whose host carries wallets — null when it has one, or no wallets at all. */
174
- function appleAbsence(input: EvidenceWalletsInput, offered: boolean): BrowserWalletFailure | null {
174
+ function appleAbsence(input: EvidenceWalletsInput, offered: boolean): AppleAbsence | null {
175
175
  if (offered || !input.config) return null;
176
- return notOffered('APPLE_PAY', input, false)?.failure ?? null;
176
+ const failure = notOffered('APPLE_PAY', input, false)?.failure;
177
+ return failure === 'NOT_CONFIGURED' || failure === 'NOT_AVAILABLE' || failure === 'MERCHANT_VALIDATION_FAILED' ? failure : null;
177
178
  }
178
179
 
179
180
  /** The wallet step's state: the sheets' answers, the presses that open them, and what phase 2 gets. */
@@ -216,7 +217,7 @@ function GooglePayMount({ client, onPress, locale }: { client: GooglePaymentsCli
216
217
  element.replaceChildren(client.createButton({ onClick: () => latest.current(), buttonSizeMode: 'fill', buttonLocale: locale }));
217
218
  return () => element.replaceChildren();
218
219
  }, [client, locale]);
219
- return <Box ref={mount} data-testid="evidence-google-pay" sx={{ minHeight: 40, width: 240, '& > *': { width: '100%' } }} />;
220
+ return <Box ref={mount} data-testid="evidence-google-pay" sx={{ minHeight: 40, width: { xs: '100%', sm: 240 }, '& > *': { width: '100%' } }} />;
220
221
  }
221
222
 
222
223
  /** Apple's button: Safari draws it (`-apple-pay-button`); elsewhere the name on a black pill. */
@@ -231,7 +232,7 @@ function ApplePayPress({ label, onPress }: { label: string; onPress: () => void
231
232
  sx={{
232
233
  WebkitAppearance: '-apple-pay-button',
233
234
  height: 40,
234
- width: 240,
235
+ width: { xs: '100%', sm: 240 },
235
236
  border: 0,
236
237
  borderRadius: 1,
237
238
  cursor: 'pointer',
@@ -270,20 +271,22 @@ export function EvidenceWalletBlock({ wallets }: { wallets: EvidenceWallets }):
270
271
  return result ? [result] : [];
271
272
  });
272
273
  return (
273
- <Stack spacing={1} data-testid="evidence-wallets">
274
+ <Stack spacing={1} data-testid="evidence-wallets" sx={{ maxWidth: '72ch' }}>
274
275
  <Typography variant="body2" fontWeight={600}>
275
276
  {copy.heading}
276
277
  </Typography>
277
- <Typography variant="caption" component="p" color="text.secondary">
278
- {copy.hint}
279
- </Typography>
278
+ {wallets.google || wallets.apple ? (
279
+ <Typography variant="caption" component="p" color="text.secondary">
280
+ {copy.hint}
281
+ </Typography>
282
+ ) : null}
280
283
  <Stack direction={{ xs: 'column', sm: 'row' }} spacing={1}>
281
284
  {wallets.google ? (
282
285
  <GooglePayMount client={wallets.google.client} onPress={wallets.google.press} locale={copy.googlePayButtonLocale} />
283
286
  ) : null}
284
287
  {wallets.apple ? <ApplePayPress label={copy.applePayButton} onPress={wallets.apple.press} /> : null}
285
288
  </Stack>
286
- {wallets.appleAbsent === 'NOT_CONFIGURED' || wallets.appleAbsent === 'NOT_AVAILABLE' ? (
289
+ {wallets.appleAbsent ? (
287
290
  <Typography variant="caption" component="p" color="text.secondary" data-testid="evidence-apple-pay-absent">
288
291
  {copy.applePayAbsent[wallets.appleAbsent]}
289
292
  </Typography>
@@ -156,6 +156,9 @@ export interface HomologacaoEvidenceCardCopy {
156
156
  wallets: HomologacaoEvidenceWalletsCopy;
157
157
  }
158
158
 
159
+ /** Why Apple Pay has no button on the evidence card (FUT-3755). */
160
+ export type AppleAbsence = 'NOT_CONFIGURED' | 'NOT_AVAILABLE' | 'MERCHANT_VALIDATION_FAILED';
161
+
159
162
  /** The Google Pay / Apple Pay step of the evidence card (FUT-3755). */
160
163
  export interface HomologacaoEvidenceWalletsCopy {
161
164
  heading: string;
@@ -174,7 +177,7 @@ export interface HomologacaoEvidenceWalletsCopy {
174
177
  /** Apple Pay started and failed. */
175
178
  applePayCannotComplete: string;
176
179
  /** Why Apple Pay has no button here: the platform has no Apple Pay identity yet, or this browser has no sheet. */
177
- applePayAbsent: { NOT_CONFIGURED: string; NOT_AVAILABLE: string };
180
+ applePayAbsent: Record<AppleAbsence, string>;
178
181
  /** A wallet holds its token: `card` is the network and last four the wallet showed — never the token. */
179
182
  ready: (wallet: string, card: string) => string;
180
183
  /** The sheet answered no token; pressing again retries. */
@@ -147,9 +147,7 @@ export const EN_US_PLATFORM_HOMOLOGACAO_COPY: PlatformHomologacaoCopy = {
147
147
  wallets: {
148
148
  heading: 'Digital wallets (before generating)',
149
149
  names: { GOOGLE_PAY: 'Google Pay', APPLE_PAY: 'Apple Pay' },
150
- hint:
151
- 'Google Pay and Apple Pay open their own sheet, and only on your tap. Tap each one before generating the file; ' +
152
- "without it, the wallets' sections stay pending. Test only: nothing is charged.",
150
+ hint: "Tap each wallet before generating; without it, that wallet's section stays pending.",
153
151
  googlePayButtonLocale: 'en',
154
152
  applePayButton: 'Pay with Apple Pay',
155
153
  applePaySheetTotal: 'Homologation test',
@@ -158,6 +156,7 @@ export const EN_US_PLATFORM_HOMOLOGACAO_COPY: PlatformHomologacaoCopy = {
158
156
  applePayAbsent: {
159
157
  NOT_CONFIGURED: 'Apple Pay: waiting for the Apple Developer account. Its section stays pending.',
160
158
  NOT_AVAILABLE: 'Apple Pay: open this screen in Safari on an iPhone or Mac to test it.',
159
+ MERCHANT_VALIDATION_FAILED: "Apple Pay: the merchant validation is not set up. Its section stays pending.",
161
160
  },
162
161
  ready: (wallet, card) => `${wallet} ready${card ? ` — ${card}` : ''}. It will be used when the file is generated.`,
163
162
  failed: (wallet) => `${wallet} returned no token. Tap again to retry.`,
@@ -131,9 +131,7 @@ export const PT_BR_PLATFORM_HOMOLOGACAO_COPY: PlatformHomologacaoCopy = {
131
131
  wallets: {
132
132
  heading: 'Carteiras digitais (antes de gerar)',
133
133
  names: { GOOGLE_PAY: 'Google Pay', APPLE_PAY: 'Apple Pay' },
134
- hint:
135
- 'O Google Pay e o Apple Pay abrem a própria folha e só com o seu toque. Toque em cada um antes de gerar o arquivo; ' +
136
- 'sem isso, as seções das carteiras ficam pendentes. Somente teste: nada é cobrado.',
134
+ hint: 'Toque em cada carteira antes de gerar; sem isso, a seção dela fica pendente.',
137
135
  googlePayButtonLocale: 'pt',
138
136
  applePayButton: 'Pagar com Apple Pay',
139
137
  applePaySheetTotal: 'Teste de homologação',
@@ -142,6 +140,7 @@ export const PT_BR_PLATFORM_HOMOLOGACAO_COPY: PlatformHomologacaoCopy = {
142
140
  applePayAbsent: {
143
141
  NOT_CONFIGURED: 'Apple Pay: aguardando a conta Apple Developer. A seção fica pendente.',
144
142
  NOT_AVAILABLE: 'Apple Pay: abra esta tela no Safari de um iPhone ou Mac para testar.',
143
+ MERCHANT_VALIDATION_FAILED: 'Apple Pay: a validação do comerciante não está configurada. A seção fica pendente.',
145
144
  },
146
145
  ready: (wallet, card) => `${wallet} pronto${card ? ` — ${card}` : ''}. Será usado ao gerar o arquivo.`,
147
146
  failed: (wallet) => `O ${wallet} não devolveu o token. Toque de novo para tentar.`,