@12-apps/payments-frontend 1.19.0 → 1.20.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": "1.
|
|
3
|
+
"version": "1.20.0",
|
|
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": "^1.
|
|
20
|
+
"@12-apps/payments-backend": "^1.23.0",
|
|
21
21
|
"react-qr-code": "^2.2.0"
|
|
22
22
|
},
|
|
23
23
|
"peerDependencies": {
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import { Alert, Button, CircularProgress, Stack } from '@mui/material';
|
|
4
|
+
import { useState } from 'react';
|
|
5
|
+
|
|
6
|
+
import type { MaskedProviderConfig, ProviderDescriptor } from '@12-apps/payments-backend';
|
|
7
|
+
|
|
8
|
+
import type { PaymentsSettingsClient } from '../client';
|
|
9
|
+
import { isConnected } from './connection-state';
|
|
10
|
+
import { ProbeAlert, type VerifyProbe } from './CredentialFormAlerts';
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* "Testar conexão" for a store whose connection is a GRANT, not a form.
|
|
14
|
+
*
|
|
15
|
+
* On the credentials path the probe runs off the save button — saving IS
|
|
16
|
+
* testing — so it lives inside the credential form. An OAuth store never opens
|
|
17
|
+
* that form: it sits behind the "prefiro informar as credenciais manualmente"
|
|
18
|
+
* disclosure, which the connect card above it says there is no reason to open.
|
|
19
|
+
* The store's own guide told the owner to press "Testar conexão", and the
|
|
20
|
+
* screen offered no such button anywhere they would look (FUT-691).
|
|
21
|
+
*
|
|
22
|
+
* Rendered only once there is a connection to test (`isConnected`, which
|
|
23
|
+
* includes RECONNECT_REQUIRED — a dead grant is exactly what an owner wants to
|
|
24
|
+
* probe). A passing probe is reported by the persistent status chip in the
|
|
25
|
+
* header, exactly as the form's probe is (see {@link ProbeAlert} for why a
|
|
26
|
+
* green banner would say nothing the chip does not); a failure gets the
|
|
27
|
+
* adapter's own sentence.
|
|
28
|
+
*/
|
|
29
|
+
export function ConnectionProbe({
|
|
30
|
+
descriptor,
|
|
31
|
+
config,
|
|
32
|
+
client,
|
|
33
|
+
reload,
|
|
34
|
+
}: {
|
|
35
|
+
descriptor: ProviderDescriptor;
|
|
36
|
+
config: MaskedProviderConfig | null;
|
|
37
|
+
client: PaymentsSettingsClient;
|
|
38
|
+
/** Refresh the settings view — the probe may have moved the stored status. */
|
|
39
|
+
reload: () => void;
|
|
40
|
+
}) {
|
|
41
|
+
const [busy, setBusy] = useState(false);
|
|
42
|
+
const [error, setError] = useState<string | null>(null);
|
|
43
|
+
const [probe, setProbe] = useState<VerifyProbe | null>(null);
|
|
44
|
+
if (!isConnected(config)) return null;
|
|
45
|
+
|
|
46
|
+
const verify = async () => {
|
|
47
|
+
setBusy(true);
|
|
48
|
+
setError(null);
|
|
49
|
+
try {
|
|
50
|
+
// The ACTIVE environment: it is the one the grant was sealed into and
|
|
51
|
+
// the one the connection card above describes.
|
|
52
|
+
const verified = await client.verify(descriptor.name, config?.environment);
|
|
53
|
+
setProbe(verified.probe);
|
|
54
|
+
reload();
|
|
55
|
+
} catch (err) {
|
|
56
|
+
setError(err instanceof Error ? err.message : String(err));
|
|
57
|
+
} finally {
|
|
58
|
+
setBusy(false);
|
|
59
|
+
}
|
|
60
|
+
};
|
|
61
|
+
|
|
62
|
+
return (
|
|
63
|
+
<Stack spacing={1} alignItems="flex-start" data-testid="payments-connection-probe">
|
|
64
|
+
<Button
|
|
65
|
+
variant="outlined"
|
|
66
|
+
size="small"
|
|
67
|
+
data-testid="payments-oauth-verify"
|
|
68
|
+
disabled={busy}
|
|
69
|
+
onClick={() => void verify()}
|
|
70
|
+
sx={{ textTransform: 'none' }}
|
|
71
|
+
>
|
|
72
|
+
{busy ? <CircularProgress size={18} /> : 'Testar conexão'}
|
|
73
|
+
</Button>
|
|
74
|
+
{error ? <Alert severity="error">{error}</Alert> : null}
|
|
75
|
+
{probe ? <ProbeAlert probe={probe} busy={busy} onRetry={() => void verify()} /> : null}
|
|
76
|
+
</Stack>
|
|
77
|
+
);
|
|
78
|
+
}
|
|
@@ -19,6 +19,7 @@ import type {
|
|
|
19
19
|
} from '@12-apps/payments-backend';
|
|
20
20
|
|
|
21
21
|
import type { PaymentsSettingsClient } from '../client';
|
|
22
|
+
import { ConnectionProbe } from './ConnectionProbe';
|
|
22
23
|
import { EnvironmentNotice, EnvironmentSelector } from './EnvironmentTabs';
|
|
23
24
|
import { ProviderConnection } from './ProviderConnection';
|
|
24
25
|
import { ProviderForm } from './ProviderCredentialForm';
|
|
@@ -57,10 +58,13 @@ export interface ActivePanelProps {
|
|
|
57
58
|
* physically contains the field for that step, because "informe sua
|
|
58
59
|
* InfiniteTag" and the box you type it into are one thing.
|
|
59
60
|
*
|
|
60
|
-
* On the OAUTH path
|
|
61
|
-
*
|
|
62
|
-
*
|
|
63
|
-
*
|
|
61
|
+
* On the OAUTH path with a working connect button the walkthrough renders
|
|
62
|
+
* OUTSIDE the manual disclosure (slots empty — the credential FORM stays
|
|
63
|
+
* inside it). It used to be stuffed in there whole, which buried the
|
|
64
|
+
* provider's own setup guide behind "prefiro informar as credenciais
|
|
65
|
+
* manualmente" — a label the connect card above explicitly says there is no
|
|
66
|
+
* reason to open (FUT-691). Deliberately shared behavior: every OAuth
|
|
67
|
+
* provider's screen changes shape, PagBank's included.
|
|
64
68
|
*/
|
|
65
69
|
guide?: (slots: {
|
|
66
70
|
rows: ReactNode;
|
|
@@ -166,6 +170,35 @@ function requiredStored(
|
|
|
166
170
|
.every((spec) => stored[spec.key]?.configured === true);
|
|
167
171
|
}
|
|
168
172
|
|
|
173
|
+
/**
|
|
174
|
+
* Does this provider get the connect-button branch of {@link OAuthPanel}?
|
|
175
|
+
* With a working connect button the walkthrough leaves the form; without one
|
|
176
|
+
* the form IS the path and keeps the guide interleaved.
|
|
177
|
+
*/
|
|
178
|
+
function oauthWithConnect(props: ActivePanelProps): boolean {
|
|
179
|
+
return props.descriptor.authMode === 'oauth' && Boolean(props.prepareConnect);
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
/**
|
|
183
|
+
* The walkthrough as the OAUTH connect branch renders it: standalone, slots
|
|
184
|
+
* empty (null on every other branch, where the form carries the guide).
|
|
185
|
+
*
|
|
186
|
+
* The credential form stays inside the manual disclosure, so the guide gets no
|
|
187
|
+
* rows and no live field — and `stored` answers for the ACTIVE environment,
|
|
188
|
+
* which is the one the connect card and the server-computed stage describe
|
|
189
|
+
* (the tabs inside the disclosure govern the form, not this).
|
|
190
|
+
*/
|
|
191
|
+
function oauthWalkthrough(props: ActivePanelProps): ReactNode {
|
|
192
|
+
const { descriptor, config, guide } = props;
|
|
193
|
+
if (!guide || !oauthWithConnect(props)) return null;
|
|
194
|
+
return guide({
|
|
195
|
+
rows: null,
|
|
196
|
+
sectionFooter: null,
|
|
197
|
+
editing: false,
|
|
198
|
+
stored: requiredStored(descriptor, config, config?.environment ?? 'SANDBOX'),
|
|
199
|
+
});
|
|
200
|
+
}
|
|
201
|
+
|
|
169
202
|
export function ActivePanel(props: ActivePanelProps) {
|
|
170
203
|
const { descriptor, config, client, onChanged, reload, guide, verification } = props;
|
|
171
204
|
const statusBar = <EnableBar {...props} />;
|
|
@@ -180,6 +213,10 @@ export function ActivePanel(props: ActivePanelProps) {
|
|
|
180
213
|
// and the one that costs money is the one you did not ask for.
|
|
181
214
|
const [editing, setEditing] = useState(false);
|
|
182
215
|
|
|
216
|
+
// On the connect branch the walkthrough leaves the form (see the `guide`
|
|
217
|
+
// prop): the form then stands alone inside the manual disclosure.
|
|
218
|
+
const oauthConnect = oauthWithConnect(props);
|
|
219
|
+
|
|
183
220
|
const credentials = (
|
|
184
221
|
<ProviderForm
|
|
185
222
|
descriptor={descriptor}
|
|
@@ -191,7 +228,7 @@ export function ActivePanel(props: ActivePanelProps) {
|
|
|
191
228
|
onChanged={onChanged}
|
|
192
229
|
onSaved={reload}
|
|
193
230
|
onCredentialsReplaced={props.onCredentialsReplaced}
|
|
194
|
-
renderGuide={guide}
|
|
231
|
+
renderGuide={oauthConnect ? undefined : guide}
|
|
195
232
|
/>
|
|
196
233
|
);
|
|
197
234
|
|
|
@@ -226,6 +263,7 @@ export function ActivePanel(props: ActivePanelProps) {
|
|
|
226
263
|
<OAuthPanel
|
|
227
264
|
{...props}
|
|
228
265
|
statusBar={statusBar}
|
|
266
|
+
walkthrough={oauthWalkthrough(props)}
|
|
229
267
|
form={
|
|
230
268
|
<Stack spacing={2}>
|
|
231
269
|
{selector}
|
|
@@ -254,8 +292,14 @@ function OAuthPanel({
|
|
|
254
292
|
prepareConnect,
|
|
255
293
|
verification,
|
|
256
294
|
statusBar,
|
|
295
|
+
walkthrough,
|
|
257
296
|
form,
|
|
258
|
-
}: ActivePanelProps & { statusBar: ReactNode; form: ReactNode }) {
|
|
297
|
+
}: ActivePanelProps & { statusBar: ReactNode; walkthrough: ReactNode; form: ReactNode }) {
|
|
298
|
+
// The connection probe, for a store whose connection is a grant: the form's
|
|
299
|
+
// own probe runs off Salvar, which an OAuth store never presses (FUT-691).
|
|
300
|
+
const probe = (
|
|
301
|
+
<ConnectionProbe descriptor={descriptor} config={config} client={client} reload={reload} />
|
|
302
|
+
);
|
|
259
303
|
if (!prepareConnect) {
|
|
260
304
|
return (
|
|
261
305
|
<ProviderCard header={statusBar}>
|
|
@@ -264,6 +308,7 @@ function OAuthPanel({
|
|
|
264
308
|
instalação. Você ainda pode conectar informando as credenciais manualmente.
|
|
265
309
|
</Alert>
|
|
266
310
|
{form}
|
|
311
|
+
{probe}
|
|
267
312
|
{verification}
|
|
268
313
|
</ProviderCard>
|
|
269
314
|
);
|
|
@@ -285,6 +330,15 @@ function OAuthPanel({
|
|
|
285
330
|
manualmente" to find it.
|
|
286
331
|
*/}
|
|
287
332
|
{verification}
|
|
333
|
+
{/*
|
|
334
|
+
The provider's walkthrough, OUTSIDE the disclosure for the same reason
|
|
335
|
+
as the activation step above it: it used to live inside the credential
|
|
336
|
+
form, which on this branch is folded into the manual fallback — so the
|
|
337
|
+
guide (and the stepper answering "where am I") was buried behind a
|
|
338
|
+
label the connect card says there is no reason to open (FUT-691).
|
|
339
|
+
*/}
|
|
340
|
+
{walkthrough}
|
|
341
|
+
{probe}
|
|
288
342
|
{descriptor.credentialSchema.length > 0 ? (
|
|
289
343
|
<Accordion disableGutters data-testid="payments-manual-fallback">
|
|
290
344
|
<AccordionSummary expandIcon={<span aria-hidden>▾</span>}>
|
|
@@ -92,9 +92,10 @@ function anyCredentialStored(
|
|
|
92
92
|
*
|
|
93
93
|
* Two qualifications, both learned the hard way:
|
|
94
94
|
*
|
|
95
|
-
* - Only providers that CAN charge are held to having charged.
|
|
96
|
-
*
|
|
97
|
-
*
|
|
95
|
+
* - Only providers that CAN charge are held to having charged. Every shipped
|
|
96
|
+
* adapter now declares activationCharge (Stripe's tokenizer and InfinitePay's
|
|
97
|
+
* hosted link included — FUT-689/FUT-698), so today this spares only an
|
|
98
|
+
* adapter that genuinely cannot obtain proof.
|
|
98
99
|
* - Turning OFF is never blocked. An owner must be able to pull a provider out
|
|
99
100
|
* of rotation at once, and rows enabled before this rule existed would
|
|
100
101
|
* otherwise be stuck on with no way down.
|