@12-apps/payments-frontend 1.20.2 → 1.21.1
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/ConnectionProbe.tsx +4 -1
- package/src/components/CredentialFormAlerts.tsx +67 -1
- package/src/components/ProviderConnection.tsx +14 -1
- package/src/components/ProviderCredentialForm.tsx +6 -0
- package/src/components/checkout/hosted-return.ts +53 -7
- package/src/index.ts +7 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@12-apps/payments-frontend",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.21.1",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"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.",
|
|
6
6
|
"exports": {
|
|
@@ -17,7 +17,7 @@
|
|
|
17
17
|
"storybook:build": "storybook build"
|
|
18
18
|
},
|
|
19
19
|
"dependencies": {
|
|
20
|
-
"@12-apps/payments-backend": "^2.0
|
|
20
|
+
"@12-apps/payments-backend": "^2.1.0",
|
|
21
21
|
"react-qr-code": "^2.2.0"
|
|
22
22
|
},
|
|
23
23
|
"peerDependencies": {
|
|
@@ -7,7 +7,7 @@ import type { MaskedProviderConfig, ProviderDescriptor } from '@12-apps/payments
|
|
|
7
7
|
|
|
8
8
|
import type { PaymentsSettingsClient } from '../client';
|
|
9
9
|
import { isConnected } from './connection-state';
|
|
10
|
-
import { ProbeAlert, type VerifyProbe } from './CredentialFormAlerts';
|
|
10
|
+
import { ProbeAlert, ProbeChecklist, type VerifyProbe } from './CredentialFormAlerts';
|
|
11
11
|
|
|
12
12
|
/**
|
|
13
13
|
* "Testar conexão" for a store whose connection is a GRANT, not a form.
|
|
@@ -73,6 +73,9 @@ export function ConnectionProbe({
|
|
|
73
73
|
</Button>
|
|
74
74
|
{error ? <Alert severity="error">{error}</Alert> : null}
|
|
75
75
|
{probe ? <ProbeAlert probe={probe} busy={busy} onRetry={() => void verify()} /> : null}
|
|
76
|
+
{/* Shown on a pass too — see `ProbeChecklist`: what a green probe did
|
|
77
|
+
NOT check is the half an owner cannot otherwise find out. */}
|
|
78
|
+
{probe ? <ProbeChecklist probe={probe} /> : null}
|
|
76
79
|
</Stack>
|
|
77
80
|
);
|
|
78
81
|
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
'use client';
|
|
2
2
|
|
|
3
|
-
import { Alert, Button, CircularProgress, Stack, Typography } from '@mui/material';
|
|
3
|
+
import { Alert, Box, Button, CircularProgress, Stack, Typography } from '@mui/material';
|
|
4
4
|
|
|
5
5
|
import type { VerifiedProviderConfig } from '@12-apps/payments-backend';
|
|
6
6
|
|
|
@@ -78,6 +78,72 @@ export function ProbeAlert({
|
|
|
78
78
|
);
|
|
79
79
|
}
|
|
80
80
|
|
|
81
|
+
/**
|
|
82
|
+
* Off-screen but read aloud. Inline rather than `@mui/utils`'s `visuallyHidden`,
|
|
83
|
+
* which is not a dependency of this package — one style object is not worth a
|
|
84
|
+
* new one on a package that ships to every host.
|
|
85
|
+
*/
|
|
86
|
+
const SCREEN_READER_ONLY = {
|
|
87
|
+
position: 'absolute',
|
|
88
|
+
width: 1,
|
|
89
|
+
height: 1,
|
|
90
|
+
overflow: 'hidden',
|
|
91
|
+
clip: 'rect(0 0 0 0)',
|
|
92
|
+
whiteSpace: 'nowrap',
|
|
93
|
+
} as const;
|
|
94
|
+
|
|
95
|
+
/** How each verdict reads at a glance — the mark, and what it is called. */
|
|
96
|
+
const CHECK_MARKS = {
|
|
97
|
+
PASS: { mark: '✓', color: 'success.main', label: 'Verificado' },
|
|
98
|
+
FAIL: { mark: '✕', color: 'error.main', label: 'Corrigir' },
|
|
99
|
+
UNCHECKED: { mark: '–', color: 'text.secondary', label: 'Não verificável' },
|
|
100
|
+
} as const;
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* What the probe established, credential by credential (FUT-796).
|
|
104
|
+
*
|
|
105
|
+
* Rendered on a PASS as well as a failure, which is the point. One boolean over
|
|
106
|
+
* a four-field form told an owner their connection was fine when only the
|
|
107
|
+
* secret key had been checked — the publishable key and the signing secret
|
|
108
|
+
* failed later, at a buyer's card and at a payment that never confirmed, where
|
|
109
|
+
* neither looks like a credential problem.
|
|
110
|
+
*
|
|
111
|
+
* The `UNCHECKED` row is the one that earns this component. It says a
|
|
112
|
+
* credential could NOT be verified and why, which a green tick would deny and a
|
|
113
|
+
* red cross would misattribute — and it tells the owner precisely which part of
|
|
114
|
+
* a passing result not to lean on.
|
|
115
|
+
*/
|
|
116
|
+
export function ProbeChecklist({ probe }: { probe: VerifyProbe }) {
|
|
117
|
+
if (!probe.checks?.length) return null;
|
|
118
|
+
return (
|
|
119
|
+
<Stack spacing={0.5} data-testid="payments-probe-checks">
|
|
120
|
+
{probe.checks.map((check) => {
|
|
121
|
+
const { mark, color, label } = CHECK_MARKS[check.status];
|
|
122
|
+
return (
|
|
123
|
+
<Stack
|
|
124
|
+
key={check.key}
|
|
125
|
+
direction="row"
|
|
126
|
+
spacing={1}
|
|
127
|
+
alignItems="flex-start"
|
|
128
|
+
data-testid={`payments-probe-check-${check.key}`}
|
|
129
|
+
data-status={check.status}
|
|
130
|
+
>
|
|
131
|
+
<Typography component="span" sx={{ color, fontWeight: 700, lineHeight: 1.5 }}>
|
|
132
|
+
<span aria-hidden>{mark}</span>
|
|
133
|
+
{/* The mark alone is colour-only information; the state has to be
|
|
134
|
+
readable to anyone not seeing the colour. */}
|
|
135
|
+
<Box component="span" sx={SCREEN_READER_ONLY}>{` ${label}: `}</Box>
|
|
136
|
+
</Typography>
|
|
137
|
+
<Typography variant="body2" color="text.secondary">
|
|
138
|
+
{check.message}
|
|
139
|
+
</Typography>
|
|
140
|
+
</Stack>
|
|
141
|
+
);
|
|
142
|
+
})}
|
|
143
|
+
</Stack>
|
|
144
|
+
);
|
|
145
|
+
}
|
|
146
|
+
|
|
81
147
|
/**
|
|
82
148
|
* Changing the tag on a store that has ALREADY proved it can receive.
|
|
83
149
|
*
|
|
@@ -163,9 +163,22 @@ function ConnectionSummary(props: {
|
|
|
163
163
|
/>
|
|
164
164
|
</Stack>
|
|
165
165
|
) : null}
|
|
166
|
+
{/*
|
|
167
|
+
The connected sentence names NO platform. It used to open with one
|
|
168
|
+
adopter's product name, hard-coded — so every other host installing this
|
|
169
|
+
package told its own merchants that somebody else's product was creating
|
|
170
|
+
their charges, with no prop to change it. The tell that it was an
|
|
171
|
+
oversight rather than a decision is the branch right below, which already
|
|
172
|
+
templates the provider from props.
|
|
173
|
+
|
|
174
|
+
Nothing was lost by removing it. The sentence exists to answer ONE
|
|
175
|
+
question — why no API key had to be copied — and the answer is the OAuth
|
|
176
|
+
grant, not whose logo is on the page. The subject is the connection
|
|
177
|
+
itself, which is true for every host and needs no new configuration.
|
|
178
|
+
*/}
|
|
166
179
|
<Typography variant="body2" color="text.secondary">
|
|
167
180
|
{props.connected
|
|
168
|
-
? 'Sua conta está conectada.
|
|
181
|
+
? 'Sua conta está conectada. As cobranças são criadas em seu nome — nenhuma chave precisa ser copiada.'
|
|
169
182
|
: `Conecte sua conta ${props.displayName} autorizando o acesso no site do provedor. Nenhuma chave precisa ser copiada.`}
|
|
170
183
|
</Typography>
|
|
171
184
|
{props.connected && props.connectedAccount ? (
|
|
@@ -25,6 +25,7 @@ import {
|
|
|
25
25
|
import {
|
|
26
26
|
FormActions,
|
|
27
27
|
ProbeAlert,
|
|
28
|
+
ProbeChecklist,
|
|
28
29
|
ReverifyWarning,
|
|
29
30
|
type VerifyProbe,
|
|
30
31
|
} from './CredentialFormAlerts';
|
|
@@ -375,6 +376,11 @@ export function ProviderForm(props: ProviderFormProps) {
|
|
|
375
376
|
{form.probe ? (
|
|
376
377
|
<ProbeAlert probe={form.probe} busy={form.busy !== null} onRetry={form.verify} />
|
|
377
378
|
) : null}
|
|
379
|
+
{/* The pasted-key path, and the one FUT-796 is about: this form is where
|
|
380
|
+
the credentials the probe reports on were typed, so its findings
|
|
381
|
+
belong beside them — including on a pass, where the unchecked rows are
|
|
382
|
+
the only place an owner learns what was NOT established. */}
|
|
383
|
+
{form.probe ? <ProbeChecklist probe={form.probe} /> : null}
|
|
378
384
|
|
|
379
385
|
<ConfirmCredentialSave
|
|
380
386
|
pending={form.pending}
|
|
@@ -18,7 +18,31 @@ import type { CheckoutOrder } from "./types";
|
|
|
18
18
|
* unrelated session.
|
|
19
19
|
*/
|
|
20
20
|
|
|
21
|
-
|
|
21
|
+
/**
|
|
22
|
+
* Where the parked order lives, namespaced to this PACKAGE.
|
|
23
|
+
*
|
|
24
|
+
* It used to carry one adopter's brand as its namespace, written into every
|
|
25
|
+
* adopter's browser. A storage key is not a private detail: it is observable
|
|
26
|
+
* surface, asserted on by `@12-apps/payments-e2e` and visible in devtools to
|
|
27
|
+
* anyone running the host. The sibling handover in this same folder already got
|
|
28
|
+
* this right with a `payments:` prefix; this one did not.
|
|
29
|
+
*
|
|
30
|
+
* Exported so a host or a spec names it rather than retyping it.
|
|
31
|
+
*/
|
|
32
|
+
export const HOSTED_ORDER_STORAGE_KEY = "payments.checkout.hostedOrder";
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* The key before the rename, READ ONLY.
|
|
36
|
+
*
|
|
37
|
+
* A buyer who left for the provider's page on the old bundle comes back to the
|
|
38
|
+
* new one with their order parked under the old name. Without this they land on
|
|
39
|
+
* the plain return screen — the order still settles, because the webhook does
|
|
40
|
+
* that and never depended on any of this, but the confirmation they were
|
|
41
|
+
* promised is missing for a reason they could not possibly understand.
|
|
42
|
+
*
|
|
43
|
+
* Delete once no session can still be mid-redirect across that deploy.
|
|
44
|
+
*/
|
|
45
|
+
const LEGACY_KEY = "futurepay.checkout.hostedOrder";
|
|
22
46
|
|
|
23
47
|
/**
|
|
24
48
|
* What a hosted provider appends to the return URL. InfinitePay sends the
|
|
@@ -46,7 +70,7 @@ function isReturnTrip(): boolean {
|
|
|
46
70
|
/** Park the raised order before handing the buyer to the provider's page. */
|
|
47
71
|
export function rememberHostedOrder(order: CheckoutOrder): void {
|
|
48
72
|
try {
|
|
49
|
-
window.sessionStorage?.setItem(
|
|
73
|
+
window.sessionStorage?.setItem(HOSTED_ORDER_STORAGE_KEY, JSON.stringify(order));
|
|
50
74
|
} catch {
|
|
51
75
|
// Storage disabled or full. The redirect must still happen: the webhook
|
|
52
76
|
// settles the order either way, and refusing to send the buyer to pay
|
|
@@ -62,15 +86,37 @@ export function rememberHostedOrder(order: CheckoutOrder): void {
|
|
|
62
86
|
* of resuming one they never paid. Read-and-clear for the same reason: the
|
|
63
87
|
* resumed view belongs to exactly one return.
|
|
64
88
|
*/
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
89
|
+
/**
|
|
90
|
+
* The raw parked payload under either key, cleared as it is read.
|
|
91
|
+
*
|
|
92
|
+
* Split out from {@link takeHostedOrder} so the storage handling and the
|
|
93
|
+
* parsing stay separately readable — reading two keys and clearing both put the
|
|
94
|
+
* combined function over the complexity gate, and the two halves fail for
|
|
95
|
+
* unrelated reasons anyway (storage disabled vs. a value that is not an order).
|
|
96
|
+
*
|
|
97
|
+
* BOTH keys are cleared whichever one answered: this is read-and-clear, and a
|
|
98
|
+
* legacy entry left behind would let a later return trip resume an order that
|
|
99
|
+
* was already consumed.
|
|
100
|
+
*/
|
|
101
|
+
function takeParkedPayload(): string | null {
|
|
68
102
|
try {
|
|
69
|
-
raw =
|
|
70
|
-
|
|
103
|
+
const raw =
|
|
104
|
+
window.sessionStorage?.getItem(HOSTED_ORDER_STORAGE_KEY) ??
|
|
105
|
+
window.sessionStorage?.getItem(LEGACY_KEY) ??
|
|
106
|
+
null;
|
|
107
|
+
window.sessionStorage?.removeItem(HOSTED_ORDER_STORAGE_KEY);
|
|
108
|
+
window.sessionStorage?.removeItem(LEGACY_KEY);
|
|
109
|
+
return raw;
|
|
71
110
|
} catch {
|
|
111
|
+
// Storage disabled or unavailable — the same "no parked order" as an empty
|
|
112
|
+
// slot, and the webhook still settles the order regardless.
|
|
72
113
|
return null;
|
|
73
114
|
}
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
export function takeHostedOrder(): CheckoutOrder | null {
|
|
118
|
+
if (!isReturnTrip()) return null;
|
|
119
|
+
const raw = takeParkedPayload();
|
|
74
120
|
if (!raw) return null;
|
|
75
121
|
try {
|
|
76
122
|
const parsed: unknown = JSON.parse(raw);
|
package/src/index.ts
CHANGED
|
@@ -87,6 +87,13 @@ export {
|
|
|
87
87
|
export { type CheckoutHostPorts } from './components/checkout/use-checkout-controller';
|
|
88
88
|
export { PaymentsUnavailable } from './components/checkout/payments-unavailable';
|
|
89
89
|
export { fetchCheckoutConfig } from './components/checkout/client';
|
|
90
|
+
/**
|
|
91
|
+
* The `sessionStorage` key the hosted-checkout return leg parks the raised
|
|
92
|
+
* order under. Public because it is already observable — a spec asserting the
|
|
93
|
+
* handover, or a host clearing storage on sign-out, otherwise retypes the
|
|
94
|
+
* literal and silently drifts when it changes.
|
|
95
|
+
*/
|
|
96
|
+
export { HOSTED_ORDER_STORAGE_KEY } from './components/checkout/hosted-return';
|
|
90
97
|
// ---------------------------------------------------------------------------
|
|
91
98
|
// Digital wallets (FUT-471/472) — the Google-branded button and the capability
|
|
92
99
|
// read it is gated on. `CheckoutFlow` wires these automatically; they are
|