@12-apps/payments-frontend 3.4.0 → 3.6.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 +3 -3
- package/src/activation/copy.ts +36 -0
- package/src/activation/mint-charge.ts +77 -0
- package/src/activation/redirect-state.ts +184 -0
- package/src/activation/returned-settlement.ts +117 -0
- package/src/activation/use-redirect-activation.ts +341 -0
- package/src/components/checkout/checkout-steps.tsx +39 -19
- package/src/components/checkout/method-capability.ts +27 -0
- package/src/components/checkout/method-choice.ts +64 -0
- package/src/components/checkout/providers/capability-default.tsx +1 -35
- package/src/components/checkout/providers/hands-over.ts +43 -0
- package/src/components/checkout/providers/hosted-link.tsx +113 -12
- package/src/components/checkout/providers/registry.ts +30 -0
- package/src/components/checkout/providers/types.ts +19 -1
- package/src/components/checkout/types.ts +11 -0
- package/src/index.ts +33 -0
|
@@ -15,26 +15,112 @@
|
|
|
15
15
|
* is leaving this page and parking the payable for the return leg matters more
|
|
16
16
|
* than painting a pane they will not see. `hosted-return.ts` picks it back up.
|
|
17
17
|
*
|
|
18
|
-
* So the whole job here is the moment BEFORE that: the buyer
|
|
19
|
-
*
|
|
20
|
-
*
|
|
21
|
-
*
|
|
22
|
-
*
|
|
18
|
+
* So the whole job here is the moment BEFORE that: telling the buyer where
|
|
19
|
+
* they are about to go, and giving them the one action that takes them.
|
|
20
|
+
*
|
|
21
|
+
* ## Why the ACTION lives here and not in the shell's picker
|
|
22
|
+
*
|
|
23
|
+
* The shell hides its PIX/card picker for this screen (`methodChosenAtProvider`
|
|
24
|
+
* in `./registry.ts`), so the screen owns the affordance that starts the
|
|
25
|
+
* charge. That is the point of the whole arrangement: the method question is
|
|
26
|
+
* binding on the provider's page, not on ours — every answer mints the same
|
|
27
|
+
* link — so asking it twice tells the buyer their first answer was thrown
|
|
28
|
+
* away. One button that says where it leads is the honest version of that
|
|
29
|
+
* screen, and it also removes the moment where the pane rendered `null` under
|
|
30
|
+
* a picker, which reads as a checkout that has stalled.
|
|
23
31
|
*/
|
|
24
32
|
import { Box } from "@mui/material";
|
|
25
33
|
import type { JSX } from "react";
|
|
26
34
|
|
|
35
|
+
import { offeredMethods } from "../method-capability";
|
|
36
|
+
import type { CheckoutProviderConfig } from "../types";
|
|
27
37
|
import { useCheckoutComponents } from "../ui";
|
|
28
38
|
|
|
29
39
|
import type { ProviderCheckoutScreenProps } from "./types";
|
|
30
40
|
|
|
31
|
-
|
|
32
|
-
|
|
41
|
+
/**
|
|
42
|
+
* How the store's own page is named to the buyer — "à página segura da
|
|
43
|
+
* InfinitePay", or the provider-neutral phrasing when no name was published.
|
|
44
|
+
*
|
|
45
|
+
* The neutral form is not a lesser fallback to be tidied away later: a host one
|
|
46
|
+
* release behind serves no `displayName`, and the buyer of that store gets a
|
|
47
|
+
* true sentence rather than our internal id ("infinitepay") dressed up as a
|
|
48
|
+
* brand.
|
|
49
|
+
*/
|
|
50
|
+
function destinationLabel(config: CheckoutProviderConfig | null): string {
|
|
51
|
+
const name = config?.chain?.[0]?.displayName?.trim();
|
|
52
|
+
return name ? `à página de pagamento da ${name}` : "à página de pagamento segura do provedor";
|
|
53
|
+
}
|
|
33
54
|
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
55
|
+
/**
|
|
56
|
+
* What the buyer will be asked to choose between once they get there, so the
|
|
57
|
+
* sentence promises exactly what the provider's page offers.
|
|
58
|
+
*
|
|
59
|
+
* Read from the same declaration the picker used to render, which is what
|
|
60
|
+
* keeps this honest for a provider that takes only one of the two: a hosted
|
|
61
|
+
* PIX-only store must not promise a card.
|
|
62
|
+
*/
|
|
63
|
+
function methodsPhrase(config: CheckoutProviderConfig | null): string | null {
|
|
64
|
+
const offered = offeredMethods(config);
|
|
65
|
+
const pix = offered === null || offered.includes("PIX");
|
|
66
|
+
const card = offered === null || offered.includes("CARD");
|
|
67
|
+
if (pix && card) return "PIX ou cartão";
|
|
68
|
+
if (pix) return "PIX";
|
|
69
|
+
if (card) return "cartão";
|
|
70
|
+
return null;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/** The full "where you are going and what happens there" sentence. */
|
|
74
|
+
function handoffMessage(config: CheckoutProviderConfig | null): string {
|
|
75
|
+
const methods = methodsPhrase(config);
|
|
76
|
+
const choice = methods ? `, onde você escolhe pagar com ${methods}` : "";
|
|
77
|
+
return `Você será levado ${destinationLabel(config)}${choice}.`;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/** The invitation: what happens next, and the one button that starts it. */
|
|
81
|
+
function HandOffInvite({
|
|
82
|
+
config,
|
|
83
|
+
onStart,
|
|
84
|
+
}: {
|
|
85
|
+
config: CheckoutProviderConfig | null;
|
|
86
|
+
onStart: () => void;
|
|
87
|
+
}): JSX.Element {
|
|
88
|
+
const { Button, Text } = useCheckoutComponents();
|
|
89
|
+
return (
|
|
90
|
+
<Box
|
|
91
|
+
data-testid="checkout-handoff-invite"
|
|
92
|
+
sx={{ display: "flex", flexDirection: "column", gap: 2 }}
|
|
93
|
+
>
|
|
94
|
+
<Text variant="body" size="md" as="p">
|
|
95
|
+
{handoffMessage(config)}
|
|
96
|
+
</Text>
|
|
97
|
+
<Text variant="caption" size="sm" as="p" color="secondary">
|
|
98
|
+
Assim que o pagamento for concluído, você volta para cá e nós confirmamos o pedido.
|
|
99
|
+
</Text>
|
|
100
|
+
<Button
|
|
101
|
+
variant="solid"
|
|
102
|
+
color="primary"
|
|
103
|
+
size="lg"
|
|
104
|
+
fullWidth
|
|
105
|
+
onClick={onStart}
|
|
106
|
+
dataTestId="checkout-handoff-start"
|
|
107
|
+
>
|
|
108
|
+
Seguir para o pagamento
|
|
109
|
+
</Button>
|
|
110
|
+
</Box>
|
|
111
|
+
);
|
|
112
|
+
}
|
|
37
113
|
|
|
114
|
+
/**
|
|
115
|
+
* The moment after the button: the charge is being raised, then we navigate.
|
|
116
|
+
*
|
|
117
|
+
* Says the SAME sentence the invite did, from the same helper. It used to word
|
|
118
|
+
* the destination differently — "a página segura do provedor" against the
|
|
119
|
+
* invite's named one — so the screen appeared to change its mind about where
|
|
120
|
+
* the buyer was going at the exact moment they committed to going there.
|
|
121
|
+
*/
|
|
122
|
+
function HandOffPending({ config }: { config: CheckoutProviderConfig | null }): JSX.Element {
|
|
123
|
+
const { Text, LoadingState } = useCheckoutComponents();
|
|
38
124
|
return (
|
|
39
125
|
<Box
|
|
40
126
|
data-testid="checkout-handoff-pending"
|
|
@@ -42,11 +128,26 @@ export function HostedLinkScreen({ method }: ProviderCheckoutScreenProps): JSX.E
|
|
|
42
128
|
>
|
|
43
129
|
<LoadingState variant="spinner" message="Preparando o pagamento" size="md" />
|
|
44
130
|
<Text variant="body" size="md" as="p">
|
|
45
|
-
|
|
131
|
+
{handoffMessage(config)}
|
|
46
132
|
</Text>
|
|
47
133
|
<Text variant="caption" size="sm" as="p" color="secondary">
|
|
48
|
-
Assim que
|
|
134
|
+
Assim que o pagamento for concluído, você volta para cá e nós confirmamos o pedido.
|
|
49
135
|
</Text>
|
|
50
136
|
</Box>
|
|
51
137
|
);
|
|
52
138
|
}
|
|
139
|
+
|
|
140
|
+
export function HostedLinkScreen({
|
|
141
|
+
method,
|
|
142
|
+
config,
|
|
143
|
+
onStart,
|
|
144
|
+
}: ProviderCheckoutScreenProps): JSX.Element | null {
|
|
145
|
+
// A method is only ever set here once the buyer has committed — either by
|
|
146
|
+
// pressing the CTA below, or (on a host whose shell still renders a picker)
|
|
147
|
+
// by choosing a tile. Both mean the same thing: the hand-off is underway.
|
|
148
|
+
if (method) return <HandOffPending config={config} />;
|
|
149
|
+
// No method and no CTA to offer ⇒ the shell is still showing its picker, and
|
|
150
|
+
// the pane stays out of the way exactly as every other screen does.
|
|
151
|
+
if (!onStart) return null;
|
|
152
|
+
return <HandOffInvite config={config} onStart={onStart} />;
|
|
153
|
+
}
|
|
@@ -26,8 +26,10 @@
|
|
|
26
26
|
* to an empty pane.
|
|
27
27
|
*/
|
|
28
28
|
import { CapabilityDefaultScreen } from "./capability-default";
|
|
29
|
+
import { handsBuyerOver } from "./hands-over";
|
|
29
30
|
import { HostedLinkScreen } from "./hosted-link";
|
|
30
31
|
import { PixAndCardScreen } from "./pix-and-card";
|
|
32
|
+
import type { CheckoutProviderConfig } from "../types";
|
|
31
33
|
import type { ProviderCheckoutScreen } from "./types";
|
|
32
34
|
|
|
33
35
|
/**
|
|
@@ -76,3 +78,31 @@ export function resolveCheckoutScreen(
|
|
|
76
78
|
): ProviderCheckoutScreen {
|
|
77
79
|
return screenFor(chainHeadScreen) ?? CapabilityDefaultScreen;
|
|
78
80
|
}
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* Does the buyer choose PIX-or-card on the PROVIDER's page rather than on ours?
|
|
84
|
+
*
|
|
85
|
+
* The shell asks this before rendering its method picker, and hides the picker
|
|
86
|
+
* when the answer is yes (FUT-596 follow-up). A hand-off store asks the
|
|
87
|
+
* question twice otherwise: once here, where the answer changes nothing —
|
|
88
|
+
* every method mints the same checkout link — and again on the provider's own
|
|
89
|
+
* page, where it is finally binding. The first ask is not merely redundant, it
|
|
90
|
+
* is misleading: the buyer who picked PIX here has picked nothing, and finding
|
|
91
|
+
* the same two options waiting for them on another site reads as a checkout
|
|
92
|
+
* that lost their answer.
|
|
93
|
+
*
|
|
94
|
+
* Resolved in the SAME order as {@link resolveCheckoutScreen} — a declared id
|
|
95
|
+
* wins, and an undeclared or unknown one falls back to the capability read —
|
|
96
|
+
* so the picker and the pane can never disagree about which flow this is. That
|
|
97
|
+
* is why the test is an identity check against the hand-off screen rather than
|
|
98
|
+
* a second table of ids: a table would be the copy that drifts, and it would
|
|
99
|
+
* drift the moment a new hand-off adapter declared its own id.
|
|
100
|
+
*/
|
|
101
|
+
export function methodChosenAtProvider(
|
|
102
|
+
chainHeadScreen: string | null | undefined,
|
|
103
|
+
config: CheckoutProviderConfig | null,
|
|
104
|
+
): boolean {
|
|
105
|
+
const declared = screenFor(chainHeadScreen);
|
|
106
|
+
if (declared) return declared === HostedLinkScreen;
|
|
107
|
+
return handsBuyerOver(config);
|
|
108
|
+
}
|
|
@@ -46,8 +46,26 @@ export interface ProviderCheckoutScreenProps {
|
|
|
46
46
|
* those readings call the helpers themselves.
|
|
47
47
|
*/
|
|
48
48
|
config: CheckoutProviderConfig | null;
|
|
49
|
-
/**
|
|
49
|
+
/**
|
|
50
|
+
* What the shell currently has selected; `null` before a choice.
|
|
51
|
+
*
|
|
52
|
+
* `null` is also the resting state of a screen that OWNS the choice (see
|
|
53
|
+
* {@link ProviderCheckoutScreenProps.onStart}): nothing is selected until
|
|
54
|
+
* the buyer presses that screen's own CTA.
|
|
55
|
+
*/
|
|
50
56
|
method: PaymentMethod | null;
|
|
57
|
+
/**
|
|
58
|
+
* Present ⇒ this screen owns the "how do I start paying" affordance, because
|
|
59
|
+
* the shell has hidden its method picker for it (FUT-596 follow-up). Calling
|
|
60
|
+
* it commits the buyer to the store's hand-off method and raises the charge,
|
|
61
|
+
* exactly as choosing a tile in the picker does.
|
|
62
|
+
*
|
|
63
|
+
* Absent for every screen where the picker is still on the page — those
|
|
64
|
+
* screens must not grow a second way to start the same charge.
|
|
65
|
+
*/
|
|
66
|
+
onStart?: () => void;
|
|
67
|
+
/** A charge is being raised right now — the shell's own busy flag. */
|
|
68
|
+
creating?: boolean;
|
|
51
69
|
/** Scopes the saved-card list to the store being paid. */
|
|
52
70
|
tenantSlug?: string;
|
|
53
71
|
/** The shell's polling cadence, passed through so tests can shorten it. */
|
|
@@ -250,6 +250,17 @@ export interface CheckoutCustomerField {
|
|
|
250
250
|
*/
|
|
251
251
|
export interface CheckoutChainLink {
|
|
252
252
|
provider: string;
|
|
253
|
+
/**
|
|
254
|
+
* The provider's own name as a BUYER should read it ("InfinitePay"), which
|
|
255
|
+
* `GET /api/checkout/config` publishes per entry.
|
|
256
|
+
*
|
|
257
|
+
* Optional, and the DEGRADE DIRECTION IS "SAY NOTHING": an older host serves
|
|
258
|
+
* no name, and a hand-off screen then describes where the buyer is going
|
|
259
|
+
* without naming it, rather than printing the internal `provider` id — a
|
|
260
|
+
* buyer shown "infinitepay" learns less than one shown nothing, and learns
|
|
261
|
+
* it in a vocabulary that is ours rather than theirs.
|
|
262
|
+
*/
|
|
263
|
+
displayName?: string | null;
|
|
253
264
|
tokenization: "NONE" | "PUBLIC_KEY" | "SDK" | "REDIRECT";
|
|
254
265
|
publicKey: string | null;
|
|
255
266
|
mockTokenization: boolean;
|
package/src/index.ts
CHANGED
|
@@ -252,6 +252,39 @@ export {
|
|
|
252
252
|
*/
|
|
253
253
|
export type { PaymentEnvironment } from '@12-apps/payments-backend';
|
|
254
254
|
|
|
255
|
+
// ---------------------------------------------------------------------------
|
|
256
|
+
// The REDIRECT ACTIVATION protocol (FUT-463, packaged by FUT-763) — proving a
|
|
257
|
+
// connection can charge, for a provider whose payer pays on its own page.
|
|
258
|
+
//
|
|
259
|
+
// `renderVerification` above stays what it was: the package decides where the
|
|
260
|
+
// step appears and the host owns the screen. What moved is the protocol behind
|
|
261
|
+
// it — resume-on-mount, the return trip's ids, the refusal/expiry/transport
|
|
262
|
+
// distinctions, the bounded wait. Every one of those was learned from a payment
|
|
263
|
+
// that went wrong, and no second host should have to learn them again.
|
|
264
|
+
// ---------------------------------------------------------------------------
|
|
265
|
+
export {
|
|
266
|
+
useRedirectActivation,
|
|
267
|
+
type RedirectActivation,
|
|
268
|
+
type RedirectActivationOptions,
|
|
269
|
+
} from './activation/use-redirect-activation';
|
|
270
|
+
export { type RedirectActivationCopy } from './activation/copy';
|
|
271
|
+
export {
|
|
272
|
+
creationFailure,
|
|
273
|
+
postActivation,
|
|
274
|
+
refusedByProvider,
|
|
275
|
+
settleActivationPoll,
|
|
276
|
+
type ActivationClock,
|
|
277
|
+
type ActivationPendingBody,
|
|
278
|
+
type ActivationPollBody,
|
|
279
|
+
type RedirectActivationState,
|
|
280
|
+
type SettlePollIo,
|
|
281
|
+
} from './activation/redirect-state';
|
|
282
|
+
export {
|
|
283
|
+
clearReturnedSettlement,
|
|
284
|
+
takeReturnedSettlement,
|
|
285
|
+
RETURNED_SETTLEMENT_KEY,
|
|
286
|
+
} from './activation/returned-settlement';
|
|
287
|
+
|
|
255
288
|
// ---------------------------------------------------------------------------
|
|
256
289
|
// The connect ROUND TRIP's other end (FUT-763): what the OAuth callback
|
|
257
290
|
// redirected back with, taken out of the address bar once. The codes are a
|