@12-apps/payments-frontend 3.31.2 → 3.32.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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@12-apps/payments-frontend",
3
- "version": "3.31.2",
3
+ "version": "3.32.0",
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.",
@@ -233,6 +233,9 @@ function StatusStep({
233
233
  // FAILED only, where it picks the sentence and decides whether a retry
234
234
  // could work at all.
235
235
  decline={c.decline}
236
+ // A Pix the store confirms by hand ends without proof either way
237
+ // (FUT-3232), so its refusal and lapse get their own sentences.
238
+ manual={c.order?.pix?.confirmation === "MANUAL"}
236
239
  />
237
240
  );
238
241
  }
@@ -34,6 +34,16 @@ export const EN_US_PAYMENT_STATUS_COPY: PaymentStatusCopy = {
34
34
  heading: "The code expired",
35
35
  support: "Nothing was charged. Generate a new code to carry on.",
36
36
  },
37
+ manual: {
38
+ failed: {
39
+ heading: "The store did not find your Pix",
40
+ support: "If you have already paid, talk to the store before paying again.",
41
+ },
42
+ expired: {
43
+ heading: "The store did not confirm the payment in time",
44
+ support: "If you have already paid, talk to the store before paying again.",
45
+ },
46
+ },
37
47
  awaitingTimedOut: {
38
48
  heading: "We have not had the confirmation yet",
39
49
  support:
@@ -0,0 +1,81 @@
1
+ import type { JSX } from "react";
2
+
3
+ import type { CheckoutDecline } from "./decline";
4
+ import type { OrderStatus } from "./types";
5
+ import { useCheckoutComponents } from "./ui";
6
+ import type { PaymentStatusCopy } from "./view-copy";
7
+
8
+ /**
9
+ * The confirmation screen's "pay again" action — retry or regenerate — split
10
+ * out of `./payment-status-parts.tsx` when the Pix the store confirms
11
+ * (FUT-3232) gave it a second look and took that file past its size gate.
12
+ */
13
+
14
+ /**
15
+ * Whether "Tentar novamente" may be offered for a refusal (FUT-1145).
16
+ *
17
+ * `retriable === false` is the provider's OWN verdict that another attempt with
18
+ * this instrument cannot succeed — attempts exhausted (10001), a cancelled
19
+ * recurring mandate (20118), a malformed request. Offering a retry there is
20
+ * offering a button that mints another failed order and shows the same screen
21
+ * again; on a card the issuer is already counting, it is worse than useless.
22
+ *
23
+ * SILENCE MEANS YES. An undefined verdict is a provider that offered no
24
+ * guidance, not a refusal to retry, and withholding the button on silence
25
+ * would strand a buyer whose card is fine.
26
+ */
27
+ function retryable(decline: CheckoutDecline | null): boolean {
28
+ return decline?.retriable !== false;
29
+ }
30
+
31
+ /**
32
+ * How "Tentar novamente" / "Gerar novo código" look. Under a Pix the store
33
+ * confirms they step back to outline: the sentence above them asks a buyer who
34
+ * may already have paid to talk to the store first, and a filled button would
35
+ * say the opposite.
36
+ */
37
+ function againLook(byStore: boolean): { variant: "solid" | "outline"; color: "primary" | "neutral" } {
38
+ return byStore ? { variant: "outline", color: "neutral" } : { variant: "solid", color: "primary" };
39
+ }
40
+
41
+ /** "Tentar novamente" on a retryable FAILED, "Gerar novo código" on EXPIRED, else nothing. */
42
+ export function PayAgain({
43
+ copy,
44
+ status,
45
+ decline,
46
+ onRetry,
47
+ onRegenerate,
48
+ byStore,
49
+ }: {
50
+ copy: PaymentStatusCopy;
51
+ status: OrderStatus;
52
+ decline: CheckoutDecline | null;
53
+ onRetry?: () => void;
54
+ onRegenerate?: () => void;
55
+ byStore: boolean;
56
+ }): JSX.Element | null {
57
+ const { Button } = useCheckoutComponents();
58
+ const again = againLook(byStore);
59
+ if (status === "FAILED" && onRetry && retryable(decline)) {
60
+ return (
61
+ <Button variant={again.variant} color={again.color} size="lg" onClick={onRetry} dataTestId="payment-retry">
62
+ {copy.retryAction}
63
+ </Button>
64
+ );
65
+ }
66
+ if (status === "EXPIRED" && onRegenerate) {
67
+ return (
68
+ <Button
69
+ variant={again.variant}
70
+ color={again.color}
71
+ size="lg"
72
+ onClick={onRegenerate}
73
+ dataTestId="payment-regenerate"
74
+ >
75
+ {copy.regenerateAction}
76
+ </Button>
77
+ );
78
+ }
79
+ return null;
80
+ }
81
+
@@ -4,6 +4,7 @@ import type { JSX, ReactNode } from "react";
4
4
  import { CheckCircleOutlineIcon, ErrorOutlineIcon, ScheduleIcon } from "./icons";
5
5
  import type { CheckoutDecline } from "./decline";
6
6
  import type { OrderStatus } from "./types";
7
+ import { PayAgain } from "./payment-status-again";
7
8
  import { useCheckoutComponents } from "./ui";
8
9
  import type { PaymentStatusCopy, StatusOutcomeCopy } from "./view-copy";
9
10
 
@@ -120,6 +121,23 @@ function failedOutcome(copy: PaymentStatusCopy, decline: CheckoutDecline | null)
120
121
  return (reason ? copy.declined?.[reason] : undefined) ?? copy.failed;
121
122
  }
122
123
 
124
+ /**
125
+ * A SETTLED outcome's sentences. A Pix only the store confirms swaps FAILED and
126
+ * EXPIRED for its own pair when the host wrote one (`copy.manual`): neither the
127
+ * store's "não recebi" nor its lapsed window proves no money moved.
128
+ */
129
+ function settledOutcome(
130
+ copy: PaymentStatusCopy,
131
+ status: OrderStatus,
132
+ decline: CheckoutDecline | null,
133
+ manual: boolean,
134
+ ): StatusOutcomeCopy {
135
+ const byStore = manual ? copy.manual : undefined;
136
+ if (status === "FAILED") return byStore?.failed ?? failedOutcome(copy, decline);
137
+ if (status === "EXPIRED") return byStore?.expired ?? copy.expired;
138
+ return copy[OUTCOME_COPY_KEY[status]];
139
+ }
140
+
123
141
  /**
124
142
  * The headline block: icon, outcome, and one supporting line.
125
143
  *
@@ -135,23 +153,22 @@ export function OutcomeHero({
135
153
  wait,
136
154
  decline,
137
155
  hero,
156
+ manual = false,
138
157
  }: {
139
158
  copy: PaymentStatusCopy;
140
159
  status: OrderStatus;
141
160
  wait: WaitState;
142
161
  decline: CheckoutDecline | null;
143
162
  hero?: ReactNode;
163
+ /** The payment was a Pix only the store confirms — see {@link settledOutcome}. */
164
+ manual?: boolean;
144
165
  }): JSX.Element {
145
166
  const { Text } = useCheckoutComponents();
146
167
  const face = status === "AWAITING_PAYMENT" ? awaitingFace(copy, wait) : null;
147
168
  const visual = face
148
169
  ? { icon: OUTCOME_VISUAL.AWAITING_PAYMENT.icon, tone: face.tone }
149
170
  : OUTCOME_VISUAL[status];
150
- const outcome = face
151
- ? face.outcome
152
- : status === "FAILED"
153
- ? failedOutcome(copy, decline)
154
- : copy[OUTCOME_COPY_KEY[status]];
171
+ const outcome = face ? face.outcome : settledOutcome(copy, status, decline, manual);
155
172
  return (
156
173
  <Box
157
174
  // `payment-paid` is load-bearing for the storefront journeys — it is how
@@ -233,23 +250,6 @@ export function PaidFacts({
233
250
  );
234
251
  }
235
252
 
236
- /**
237
- * Whether "Tentar novamente" may be offered for a refusal (FUT-1145).
238
- *
239
- * `retriable === false` is the provider's OWN verdict that another attempt with
240
- * this instrument cannot succeed — attempts exhausted (10001), a cancelled
241
- * recurring mandate (20118), a malformed request. Offering a retry there is
242
- * offering a button that mints another failed order and shows the same screen
243
- * again; on a card the issuer is already counting, it is worse than useless.
244
- *
245
- * SILENCE MEANS YES. An undefined verdict is a provider that offered no
246
- * guidance, not a refusal to retry, and withholding the button on silence
247
- * would strand a buyer whose card is fine.
248
- */
249
- function retryable(decline: CheckoutDecline | null): boolean {
250
- return decline?.retriable !== false;
251
- }
252
-
253
253
  /**
254
254
  * How much of the eye the way out asks for on a PAID confirmation.
255
255
  *
@@ -293,6 +293,7 @@ export function StatusActions({
293
293
  onNotPaid,
294
294
  onBackToMenu,
295
295
  backActionEmphasis = "primary",
296
+ manual,
296
297
  }: {
297
298
  copy: PaymentStatusCopy;
298
299
  status: OrderStatus;
@@ -310,6 +311,8 @@ export function StatusActions({
310
311
  onBackToMenu: () => void;
311
312
  /** Whether back-to-menu leads a PAID screen. See {@link BackActionEmphasis}. */
312
313
  backActionEmphasis?: BackActionEmphasis;
314
+ /** The payment was a Pix only the store confirms — see `PayAgain`. */
315
+ manual?: boolean;
313
316
  }): JSX.Element {
314
317
  const { Button } = useCheckoutComponents();
315
318
  const back = backLook(status, backActionEmphasis);
@@ -337,16 +340,14 @@ export function StatusActions({
337
340
  {copy.notPaidAction}
338
341
  </Button>
339
342
  ) : null}
340
- {status === "FAILED" && onRetry && retryable(decline) ? (
341
- <Button variant="solid" color="primary" size="lg" onClick={onRetry} dataTestId="payment-retry">
342
- {copy.retryAction}
343
- </Button>
344
- ) : null}
345
- {status === "EXPIRED" && onRegenerate ? (
346
- <Button variant="solid" color="primary" size="lg" onClick={onRegenerate} dataTestId="payment-regenerate">
347
- {copy.regenerateAction}
348
- </Button>
349
- ) : null}
343
+ <PayAgain
344
+ copy={copy}
345
+ status={status}
346
+ decline={decline}
347
+ onRetry={onRetry}
348
+ onRegenerate={onRegenerate}
349
+ byStore={manual === true}
350
+ />
350
351
  <Button
351
352
  // Full width and last, so the thumb lands on the same place in every
352
353
  // outcome instead of hunting a button that moves with the state.
@@ -117,6 +117,12 @@ interface PaymentStatusProps {
117
117
  * sentence, and it decides whether a retry is offered at all.
118
118
  */
119
119
  decline?: CheckoutDecline | null;
120
+ /**
121
+ * The payment was a Pix only the STORE confirms (FUT-3232): a FAILED or
122
+ * EXPIRED screen then says the store did not find it, never that nothing
123
+ * was charged.
124
+ */
125
+ manual?: boolean;
120
126
  }
121
127
 
122
128
  /**
@@ -198,6 +204,7 @@ export function PaymentStatus(props: PaymentStatusProps): JSX.Element {
198
204
  wait={view.wait}
199
205
  decline={view.decline}
200
206
  hero={props.paidHero}
207
+ manual={props.manual === true}
201
208
  />
202
209
 
203
210
  {view.paid ? (
@@ -220,6 +227,7 @@ export function PaymentStatus(props: PaymentStatusProps): JSX.Element {
220
227
  onNotPaid={view.notPaid}
221
228
  onBackToMenu={onBackToMenu}
222
229
  backActionEmphasis={props.backActionEmphasis}
230
+ manual={props.manual === true}
223
231
  />
224
232
 
225
233
  {view.paid ? props.paidFooter : null}
@@ -28,6 +28,16 @@ export const PT_BR_PAYMENT_STATUS_COPY: PaymentStatusCopy = {
28
28
  heading: "O código expirou",
29
29
  support: "Nenhum valor foi cobrado. Gere um novo código para continuar.",
30
30
  },
31
+ manual: {
32
+ failed: {
33
+ heading: "A loja não identificou o seu Pix",
34
+ support: "Se você já pagou, fale com a loja antes de pagar de novo.",
35
+ },
36
+ expired: {
37
+ heading: "A loja não confirmou o pagamento a tempo",
38
+ support: "Se você já pagou, fale com a loja antes de pagar de novo.",
39
+ },
40
+ },
31
41
  awaitingTimedOut: {
32
42
  heading: "Ainda não recebemos a confirmação",
33
43
  support:
@@ -93,6 +93,14 @@ export interface PaymentStatusCopy {
93
93
  */
94
94
  declined: Partial<Record<CheckoutDeclineReason, StatusOutcomeCopy>>;
95
95
  awaitingTimedOut: StatusOutcomeCopy;
96
+ /**
97
+ * FAILED and EXPIRED for a Pix only the STORE confirms (FUT-3232). The store
98
+ * answering "não recebi", or letting its window lapse, says nothing about
99
+ * whether the buyer paid — a static key takes money whatever the store
100
+ * later says — so "nothing was charged" would be a guess, and "pay again" an
101
+ * invitation to pay twice. Absent, those two keep their ordinary sentences.
102
+ */
103
+ manual?: { failed: StatusOutcomeCopy; expired: StatusOutcomeCopy };
96
104
  /**
97
105
  * The wait cannot reach the payment right now (FUT-1144) — and is STILL
98
106
  * ASKING, which is the whole difference between this outcome and the one