@12-apps/payments-frontend 3.21.4 → 3.23.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 +387 -0
- package/src/components/checkout/checkout-flow.tsx +126 -24
- 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/confirmation-wait.ts +97 -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 +41 -0
- package/src/components/checkout/failure-codes.ts +82 -0
- package/src/components/checkout/hosted-return.ts +190 -206
- package/src/components/checkout/hosted-store.ts +291 -0
- package/src/components/checkout/payment-error-panel.tsx +9 -3
- package/src/components/checkout/payment-status-parts.tsx +311 -0
- package/src/components/checkout/payment-status.tsx +69 -264
- package/src/components/checkout/pix-view.tsx +97 -8
- package/src/components/checkout/poll-loop.ts +5 -3
- package/src/components/checkout/providers/types.ts +20 -3
- package/src/components/checkout/pt-BR.ts +42 -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 +35 -0
- package/src/components/checkout/use-card-checkout.ts +34 -33
- package/src/components/checkout/use-checkout-controller.ts +68 -274
- package/src/components/checkout/use-hosted-resume.ts +326 -0
- package/src/components/checkout/use-payment-polling.ts +58 -7
- package/src/components/checkout/use-wallet-charge.ts +24 -1
- package/src/components/checkout/view-copy.ts +70 -0
- package/src/components/checkout/wallet-pane.tsx +9 -1
- package/src/flows/catalog-exit.ts +33 -0
- package/src/flows/create-payment-flows.tsx +19 -1
- package/src/flows/pipeline/actions.tsx +104 -0
- package/src/flows/pipeline/admission.ts +55 -0
- package/src/flows/pipeline/context.ts +140 -0
- package/src/flows/pipeline/derive-step.ts +234 -0
- package/src/flows/pipeline/engine-actions.ts +257 -0
- package/src/flows/pipeline/engine-chrome.tsx +123 -0
- package/src/flows/pipeline/engine-state.ts +107 -0
- package/src/flows/pipeline/engine.tsx +377 -0
- package/src/flows/pipeline/methods.ts +71 -0
- package/src/flows/pipeline/refusal-routing.ts +106 -0
- package/src/flows/pipeline/slices.ts +110 -0
- package/src/flows/pipeline/stable-plugins.ts +72 -0
- package/src/flows/pipeline/steps/buyer-steps.tsx +297 -0
- package/src/flows/pipeline/steps/index.ts +54 -0
- package/src/flows/pipeline/steps/pay-steps.tsx +182 -0
- package/src/flows/pipeline/steps/status-step.tsx +41 -0
- package/src/flows/pipeline/types.ts +232 -0
- package/src/flows/public.ts +78 -0
- package/src/flows/screens-hosted.tsx +55 -5
- package/src/flows/screens-pay.tsx +6 -1
- package/src/flows/types.ts +25 -2
- package/src/index.ts +29 -19
|
@@ -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",
|
|
@@ -101,4 +134,12 @@ export const EN_US_CHECKOUT_VIEW_COPY: CheckoutViewCopy = {
|
|
|
101
134
|
action: "See the menu",
|
|
102
135
|
},
|
|
103
136
|
status: EN_US_PAYMENT_STATUS_COPY,
|
|
137
|
+
pipeline: {
|
|
138
|
+
loading: "Loading…",
|
|
139
|
+
// The KEYS are the package's own settlement-method ids, never words.
|
|
140
|
+
awaitingHandover: {
|
|
141
|
+
PIX: "Opening Pix…",
|
|
142
|
+
CARD: "Opening the card payment…",
|
|
143
|
+
},
|
|
144
|
+
},
|
|
104
145
|
};
|
|
@@ -15,3 +15,85 @@
|
|
|
15
15
|
* one and must not word it as a failure.
|
|
16
16
|
*/
|
|
17
17
|
export const UNRESOLVED_CODE = "PAYMENT_UNRESOLVED";
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* REFUSALS RE-SENDING THE SAME REQUEST CANNOT CLEAR (FUT-1182).
|
|
21
|
+
*
|
|
22
|
+
* A different exception from {@link UNRESOLVED_CODE}, and the difference is
|
|
23
|
+
* worth keeping: an unresolved charge withholds the retry because pressing it
|
|
24
|
+
* could take the buyer's money twice. These withhold it because pressing it
|
|
25
|
+
* cannot do anything at all. The order was refused for a fact about the world —
|
|
26
|
+
* the shop is shut, the mode is off, the booked slot is gone, the basket has
|
|
27
|
+
* already been paid for, the merchant has connected no provider — and the
|
|
28
|
+
* identical POST meets the identical fact. So the button was the screen's most
|
|
29
|
+
* prominent control and it was guaranteed to fail, phrased as though the buyer
|
|
30
|
+
* had got something wrong.
|
|
31
|
+
*
|
|
32
|
+
* ## Why a package names a host's vocabulary here
|
|
33
|
+
*
|
|
34
|
+
* Because the alternative is worse in both directions. A prop the host fills
|
|
35
|
+
* would leave every host that has not filled it with the defect this exists to
|
|
36
|
+
* remove; and a rule inferred from the STATUS code cannot separate these from
|
|
37
|
+
* the 400s and 409s that a retry genuinely clears (`CHARGE_MISMATCH` is a 409
|
|
38
|
+
* and re-raising is exactly what fixes it).
|
|
39
|
+
*
|
|
40
|
+
* What makes it safe is that the list is CLOSED and its members are wire
|
|
41
|
+
* constants: this file already names `PAYMENT_UNRESOLVED` for the same reason,
|
|
42
|
+
* and the transport beside it already speaks the routes those codes arrive on.
|
|
43
|
+
* A host that answers none of these loses nothing — an unknown code keeps the
|
|
44
|
+
* retry, which is the pre-1182 behaviour for every code.
|
|
45
|
+
*
|
|
46
|
+
* ## It never SUPPRESSES the message, only the button
|
|
47
|
+
*
|
|
48
|
+
* The host half of this ticket (FUT-1166) acts on the same codes: it refetches
|
|
49
|
+
* whatever was stale, so the screen behind the refusal corrects itself into the
|
|
50
|
+
* gate that says what the buyer can actually do. The refusal's own sentence has
|
|
51
|
+
* to stay on screen while that happens, or a shopper watches a checkout
|
|
52
|
+
* rearrange itself for no stated reason.
|
|
53
|
+
*
|
|
54
|
+
* ## What is deliberately NOT here
|
|
55
|
+
*
|
|
56
|
+
* `BASKET_NOT_FOUND` and `MODE_CATALOG_SCOPE`, because this list is the
|
|
57
|
+
* GUARANTEED half. Both name states a fresh read can legitimately answer
|
|
58
|
+
* differently, and withholding the retry on a maybe is how a buyer with a
|
|
59
|
+
* recoverable problem ends up with no control at all — the same asymmetry
|
|
60
|
+
* `CheckoutDecline.retriable` settles by treating silence as yes.
|
|
61
|
+
*/
|
|
62
|
+
const NO_RETRY_CODES: readonly string[] = [
|
|
63
|
+
// The shop is shut, or shut by the time the slot came round.
|
|
64
|
+
"STORE_CLOSED",
|
|
65
|
+
"SCHEDULE_UNAVAILABLE",
|
|
66
|
+
// The store does not sell this way — a bookmarked URL, or a mode cookie that
|
|
67
|
+
// outlived the config allowing it.
|
|
68
|
+
"MODE_UNAVAILABLE",
|
|
69
|
+
// No provider connected. TWO spellings on purpose: the package's own routes
|
|
70
|
+
// answer `PAYMENT_NOT_CONFIGURED`, and a host that guards its order route
|
|
71
|
+
// before delegating answers its own. A client meets both.
|
|
72
|
+
"PAYMENT_NOT_CONFIGURED",
|
|
73
|
+
"PAYMENTS_NOT_CONFIGURED",
|
|
74
|
+
// Already bought — in another tab, or by whoever was settling alongside them.
|
|
75
|
+
// Retrying cannot un-pay it; the buyer wants their purchases, not this step.
|
|
76
|
+
"CART_ALREADY_PAID",
|
|
77
|
+
"BASKET_ALREADY_BOUGHT",
|
|
78
|
+
// Nothing left to charge for, and this step cannot put anything back.
|
|
79
|
+
"EMPTY_CART",
|
|
80
|
+
"COMANDA_CLOSED",
|
|
81
|
+
// The order cannot be delivered as asked. Fixed where the address is, which
|
|
82
|
+
// is not here.
|
|
83
|
+
"DELIVERY_UNAVAILABLE",
|
|
84
|
+
"DELIVERY_ADDRESS_REQUIRED",
|
|
85
|
+
];
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* Whether the surface may offer to send this refused order again.
|
|
89
|
+
*
|
|
90
|
+
* SILENCE MEANS YES, and so does anything unrecognised: a refusal with no code,
|
|
91
|
+
* or one from a server this bundle is a release behind, keeps the retry. The
|
|
92
|
+
* two packages version independently and a host may answer a vocabulary of its
|
|
93
|
+
* own, so "never heard of it" has to degrade to the old behaviour rather than
|
|
94
|
+
* to a screen with nothing on it.
|
|
95
|
+
*/
|
|
96
|
+
export function retryMayHelp(code: string | null | undefined): boolean {
|
|
97
|
+
if (code === undefined || code === null) return true;
|
|
98
|
+
return !NO_RETRY_CODES.includes(code);
|
|
99
|
+
}
|