@12-apps/payments-frontend 3.21.3 → 3.22.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/components/checkout/basket.ts +85 -0
- package/src/components/checkout/card-outcome.ts +81 -0
- package/src/components/checkout/card-view.tsx +62 -22
- package/src/components/checkout/checkout-actions.ts +341 -0
- package/src/components/checkout/checkout-flow.tsx +112 -18
- package/src/components/checkout/checkout-steps.tsx +149 -174
- package/src/components/checkout/checkout-totals.tsx +51 -0
- package/src/components/checkout/client-context.tsx +3 -0
- package/src/components/checkout/dados-step.tsx +141 -0
- package/src/components/checkout/decline.ts +48 -0
- package/src/components/checkout/en-US.ts +33 -0
- package/src/components/checkout/hosted-return.ts +190 -206
- package/src/components/checkout/hosted-store.ts +269 -0
- package/src/components/checkout/payment-status-parts.tsx +311 -0
- package/src/components/checkout/payment-status.tsx +69 -264
- package/src/components/checkout/poll-loop.ts +10 -12
- package/src/components/checkout/poll-rearm.ts +86 -0
- package/src/components/checkout/providers/types.ts +20 -3
- package/src/components/checkout/pt-BR.ts +33 -0
- package/src/components/checkout/screens-copy.ts +14 -0
- package/src/components/checkout/screens-en-US.ts +1 -0
- package/src/components/checkout/screens-pt-BR.ts +3 -0
- package/src/components/checkout/transport.ts +21 -1
- package/src/components/checkout/types.ts +24 -0
- package/src/components/checkout/use-card-checkout.ts +31 -31
- package/src/components/checkout/use-checkout-controller.ts +51 -271
- package/src/components/checkout/use-hosted-resume.ts +326 -0
- package/src/components/checkout/view-copy.ts +32 -0
- package/src/components/checkout/wallet-pane.tsx +3 -0
- package/src/flows/create-payment-flows.tsx +7 -0
- package/src/flows/screens-hosted.tsx +19 -2
- package/src/index.ts +22 -0
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
import { Box } from "@mui/material";
|
|
2
|
+
import type { JSX, ReactNode } from "react";
|
|
3
|
+
|
|
4
|
+
import { BuyerInfoForm } from "./buyer-info-form";
|
|
5
|
+
import { LockOutlinedIcon } from "./icons";
|
|
6
|
+
import { displayTotals, PayBarTotal } from "./checkout-totals";
|
|
7
|
+
import type { BuyerField, BuyerInfo, CheckoutCustomerField } from "./types";
|
|
8
|
+
import { useCheckoutComponents } from "./ui";
|
|
9
|
+
import type { DadosStepCopy } from "./view-copy";
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Step 1 of the buyer checkout, and its sticky bar.
|
|
13
|
+
*
|
|
14
|
+
* Split out of `./checkout-steps.tsx` when the Pagamento step grew a money line
|
|
15
|
+
* of its own (FUT-1179) and took that file past its size gate. The seam is the
|
|
16
|
+
* one the flow already renders on: one step per module, with the totals both of
|
|
17
|
+
* them show living in `./checkout-totals.tsx`.
|
|
18
|
+
*/
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Step 1 "Dados" — the buyer's register info (CPF plus optional name/email/
|
|
22
|
+
* phone; contact pre-filled from the saved buyer profile). NO payment method
|
|
23
|
+
* here (that's step 2); nav lives in the slim checkout header. "Continuar"
|
|
24
|
+
* (sticky, with the live total) validates the CPF and advances to "Pagamento"
|
|
25
|
+
* — no charge yet.
|
|
26
|
+
*/
|
|
27
|
+
export function DadosStep({
|
|
28
|
+
copy,
|
|
29
|
+
buyer,
|
|
30
|
+
onBuyerChange,
|
|
31
|
+
saveProfile,
|
|
32
|
+
onSaveProfileChange,
|
|
33
|
+
createError,
|
|
34
|
+
errorField,
|
|
35
|
+
onContinue,
|
|
36
|
+
cartTotals,
|
|
37
|
+
buyerFields,
|
|
38
|
+
discountLines,
|
|
39
|
+
totalOverride,
|
|
40
|
+
}: {
|
|
41
|
+
/** The step's own sentences — the HOST's words (see `./view-copy`). */
|
|
42
|
+
copy: DadosStepCopy;
|
|
43
|
+
buyer: BuyerInfo;
|
|
44
|
+
onBuyerChange: (buyer: BuyerInfo) => void;
|
|
45
|
+
saveProfile: boolean;
|
|
46
|
+
onSaveProfileChange: (value: boolean) => void;
|
|
47
|
+
createError: string | null;
|
|
48
|
+
errorField: BuyerField | null;
|
|
49
|
+
onContinue: () => void;
|
|
50
|
+
/** The host cart's own totals — what the pay bar shows in cart mode. */
|
|
51
|
+
cartTotals: { totalLabel: string; totalItems: number };
|
|
52
|
+
/** What the store's chain declares it needs (FUT-595); absent ⇒ CPF-required. */
|
|
53
|
+
buyerFields?: readonly CheckoutCustomerField[];
|
|
54
|
+
/**
|
|
55
|
+
* The saving, itemized under the total the buyer is about to authorize
|
|
56
|
+
* (FUT-246) — RENDERED BY THE HOST from its cart (the storefront passes its
|
|
57
|
+
* cart footer's money block), never re-implemented here, so the two surfaces
|
|
58
|
+
* can never word the same discount differently.
|
|
59
|
+
*/
|
|
60
|
+
discountLines?: ReactNode;
|
|
61
|
+
/** Settling an open balance: totals come from the settlement, not the cart. */
|
|
62
|
+
totalOverride?: { label: string; items: number };
|
|
63
|
+
}): JSX.Element {
|
|
64
|
+
const { Checkbox } = useCheckoutComponents();
|
|
65
|
+
const { label: totalLabel, items: totalItems } = displayTotals(totalOverride, cartTotals);
|
|
66
|
+
|
|
67
|
+
return (
|
|
68
|
+
<>
|
|
69
|
+
<Box sx={{ display: "flex", flexDirection: "column", gap: 3, pb: { xs: createError ? 22 : 14, sm: createError ? 20 : 12 } }}>
|
|
70
|
+
<BuyerInfoForm
|
|
71
|
+
value={buyer}
|
|
72
|
+
onChange={onBuyerChange}
|
|
73
|
+
fields={buyerFields}
|
|
74
|
+
fieldError={errorField && createError ? { field: errorField, message: createError } : null}
|
|
75
|
+
/>
|
|
76
|
+
<Checkbox
|
|
77
|
+
checked={saveProfile}
|
|
78
|
+
onChange={(_event, checked) => onSaveProfileChange(checked)}
|
|
79
|
+
label={copy.saveProfile}
|
|
80
|
+
data-testid="buyer-save-profile"
|
|
81
|
+
/>
|
|
82
|
+
</Box>
|
|
83
|
+
|
|
84
|
+
<DadosPayBar
|
|
85
|
+
copy={copy}
|
|
86
|
+
totalLabel={totalLabel}
|
|
87
|
+
totalItems={totalItems}
|
|
88
|
+
createError={createError}
|
|
89
|
+
onContinue={onContinue}
|
|
90
|
+
>
|
|
91
|
+
{/* Suppressed while settling a balance: those totals come from the
|
|
92
|
+
frozen ticket, not the cart. */}
|
|
93
|
+
{totalOverride ? null : discountLines}
|
|
94
|
+
</DadosPayBar>
|
|
95
|
+
</>
|
|
96
|
+
);
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/** The sticky "Continuar" bar: the refusal, the money, and the one action. */
|
|
100
|
+
function DadosPayBar({
|
|
101
|
+
copy,
|
|
102
|
+
totalLabel,
|
|
103
|
+
totalItems,
|
|
104
|
+
createError,
|
|
105
|
+
onContinue,
|
|
106
|
+
children,
|
|
107
|
+
}: {
|
|
108
|
+
copy: DadosStepCopy;
|
|
109
|
+
totalLabel: string;
|
|
110
|
+
totalItems: number;
|
|
111
|
+
createError: string | null;
|
|
112
|
+
onContinue: () => void;
|
|
113
|
+
children?: ReactNode;
|
|
114
|
+
}): JSX.Element {
|
|
115
|
+
const { ActionBar, Alert, Button, Text } = useCheckoutComponents();
|
|
116
|
+
return (
|
|
117
|
+
<ActionBar dataTestId="checkout-pay-bar">
|
|
118
|
+
<Box sx={{ width: "100%", display: "flex", flexDirection: "column", gap: 1.5 }}>
|
|
119
|
+
{createError ? (
|
|
120
|
+
<Alert variant="danger" title={copy.cannotContinueTitle} description={createError} showIcon data-testid="checkout-error" />
|
|
121
|
+
) : null}
|
|
122
|
+
<Box sx={{ display: "flex", alignItems: "center", gap: 2 }}>
|
|
123
|
+
<PayBarTotal totalLabel={totalLabel} totalItems={totalItems}>{children}</PayBarTotal>
|
|
124
|
+
<Box sx={{ flex: 1, display: "flex", flexDirection: "column", alignItems: "flex-end", gap: 0.5, minWidth: 0 }}>
|
|
125
|
+
<Button variant="solid" color="primary" size="lg" fullWidth onClick={onContinue} dataTestId="checkout-continue">
|
|
126
|
+
{copy.continueAction}
|
|
127
|
+
</Button>
|
|
128
|
+
{copy.secureNotice ? (
|
|
129
|
+
<Box sx={{ display: "flex", alignItems: "center", gap: 0.5, color: "text.secondary" }}>
|
|
130
|
+
<LockOutlinedIcon sx={{ fontSize: 13 }} />
|
|
131
|
+
<Text variant="caption" size="xs" color="secondary" as="span">
|
|
132
|
+
{copy.secureNotice}
|
|
133
|
+
</Text>
|
|
134
|
+
</Box>
|
|
135
|
+
) : null}
|
|
136
|
+
</Box>
|
|
137
|
+
</Box>
|
|
138
|
+
</Box>
|
|
139
|
+
</ActionBar>
|
|
140
|
+
);
|
|
141
|
+
}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* HOW A REFUSED CARD IS DESCRIBED (FUT-1145).
|
|
3
|
+
*
|
|
4
|
+
* Its own module rather than another block in `./types.ts`, which is at its
|
|
5
|
+
* size ceiling — and because this is one idea with two halves that must travel
|
|
6
|
+
* together: what happened, and whether trying again could ever work.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* WHY a card was refused, in the cross-provider vocabulary the server
|
|
11
|
+
* normalizes every acquirer's own code into (FUT-1145).
|
|
12
|
+
*
|
|
13
|
+
* A MIRROR of `@12-apps/payments-backend`'s `DeclineReason`, restated rather
|
|
14
|
+
* than imported for the reason every other mirror in `./types.ts` is: the two
|
|
15
|
+
* packages version independently, so a server one release ahead can answer a
|
|
16
|
+
* reason this bundle has never heard of. Everything that reads it degrades to
|
|
17
|
+
* the generic refusal rather than rendering nothing — see
|
|
18
|
+
* `PaymentStatusCopy.declined`. `declines.test.ts` pins the mirror against the
|
|
19
|
+
* backend union, so a reason ADDED there fails a test here rather than going
|
|
20
|
+
* quietly unworded.
|
|
21
|
+
*/
|
|
22
|
+
export type CheckoutDeclineReason =
|
|
23
|
+
| "INSUFFICIENT_FUNDS"
|
|
24
|
+
| "CARD_DECLINED"
|
|
25
|
+
| "INVALID_CARD"
|
|
26
|
+
| "EXPIRED_CARD"
|
|
27
|
+
| "FRAUD_SUSPECTED"
|
|
28
|
+
| "PROVIDER_ERROR"
|
|
29
|
+
| "UNKNOWN";
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* A refusal as the checkout has to present it (FUT-1145): what happened, and
|
|
33
|
+
* whether another attempt could ever work.
|
|
34
|
+
*
|
|
35
|
+
* The two answer different questions and neither can carry the other. "Attempts
|
|
36
|
+
* exhausted — do not retry" and "the standing authorization was cancelled at
|
|
37
|
+
* the bank" are both terminal for completely different causes, and a
|
|
38
|
+
* seven-value taxonomy has to flatten one of them into a lie; meanwhile a
|
|
39
|
+
* screen deciding whether to OFFER a retry needs the verdict and not the cause.
|
|
40
|
+
*
|
|
41
|
+
* `retriable` UNDEFINED means the provider offered no guidance, which is not
|
|
42
|
+
* the same as "no": the retry stays on offer, because withholding it on
|
|
43
|
+
* silence would strand a buyer whose card is fine.
|
|
44
|
+
*/
|
|
45
|
+
export interface CheckoutDecline {
|
|
46
|
+
reason?: CheckoutDeclineReason;
|
|
47
|
+
retriable?: boolean;
|
|
48
|
+
}
|
|
@@ -48,9 +48,42 @@ export const EN_US_PAYMENT_STATUS_COPY: PaymentStatusCopy = {
|
|
|
48
48
|
"We are still trying. If you have already paid, do not pay again — " +
|
|
49
49
|
"the order is confirmed as soon as the provider tells us.",
|
|
50
50
|
},
|
|
51
|
+
/**
|
|
52
|
+
* One refusal at a time, in the cardholder's own terms (FUT-1145).
|
|
53
|
+
*
|
|
54
|
+
* `UNKNOWN` is deliberately absent: with no recognised reason there is
|
|
55
|
+
* nothing specific to say, and `failed` above is already that sentence.
|
|
56
|
+
*/
|
|
57
|
+
declined: {
|
|
58
|
+
INSUFFICIENT_FUNDS: {
|
|
59
|
+
heading: "There were not enough funds",
|
|
60
|
+
support: "Nothing was charged. Try another card.",
|
|
61
|
+
},
|
|
62
|
+
CARD_DECLINED: {
|
|
63
|
+
heading: "Your bank did not authorise the payment",
|
|
64
|
+
support: "Nothing was charged. Try another card, or talk to your bank.",
|
|
65
|
+
},
|
|
66
|
+
INVALID_CARD: {
|
|
67
|
+
heading: "The card details were not accepted",
|
|
68
|
+
support: "Nothing was charged. Check the number, the expiry and the CVV, or use another card.",
|
|
69
|
+
},
|
|
70
|
+
EXPIRED_CARD: {
|
|
71
|
+
heading: "That card has expired",
|
|
72
|
+
support: "Nothing was charged. Use a card that is still in date.",
|
|
73
|
+
},
|
|
74
|
+
FRAUD_SUSPECTED: {
|
|
75
|
+
heading: "Your bank blocked this purchase for security",
|
|
76
|
+
support: "Nothing was charged. Talk to your bank, or use another card.",
|
|
77
|
+
},
|
|
78
|
+
PROVIDER_ERROR: {
|
|
79
|
+
heading: "We could not process the payment right now",
|
|
80
|
+
support: "Nothing was charged. Try again in a moment.",
|
|
81
|
+
},
|
|
82
|
+
},
|
|
51
83
|
retryAction: "Try again",
|
|
52
84
|
regenerateAction: "Generate a new code",
|
|
53
85
|
checkAgainAction: "Check again",
|
|
86
|
+
notPaidAction: "I could not pay",
|
|
54
87
|
backAction: "Back to the menu",
|
|
55
88
|
amountLabel: "Amount paid",
|
|
56
89
|
referenceLabel: "Order",
|