@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.19.0",
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.22.0",
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 the whole assembly stays inside the manual disclosure. It
61
- * is written for someone pasting credentials by hand, so there it is a wall
62
- * of instructions contradicting the card above it, which says no key needs
63
- * copying. Reachable, just not the first thing read.
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. Stripe and
96
- * InfinitePay have no browser tokenization written, so requiring the proof
97
- * would not make their "Ativo" honest it would make them unactivatable.
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.