@12-apps/payments-frontend 2.0.0 → 3.0.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.
@@ -13,22 +13,16 @@ import type {
13
13
  import type { PaymentsSettingsClient } from '../client';
14
14
 
15
15
  import { ConfirmCredentialSave, type PendingSave } from './ConfirmCredentialSave';
16
+ import { CredentialFields } from './CredentialFieldStack';
16
17
  import { isConnected } from './connection-state';
17
- import { CredentialField, DoneRow } from './CredentialFields';
18
+ import { DoneRow } from './CredentialFields';
18
19
  import {
19
- allRequiredStored,
20
+ credentialsComplete,
20
21
  fieldsWellFormed,
21
22
  needsConfirmation,
22
- saveLabel,
23
23
  summaryOf,
24
24
  } from './credential-rules';
25
- import {
26
- FormActions,
27
- ProbeAlert,
28
- ProbeChecklist,
29
- ReverifyWarning,
30
- type VerifyProbe,
31
- } from './CredentialFormAlerts';
25
+ import { ProbeAlert, ProbeChecklist, type VerifyProbe } from './CredentialFormAlerts';
32
26
 
33
27
  /**
34
28
  * The `authMode: 'credentials'` half of the settings page — a provider whose
@@ -131,6 +125,8 @@ interface ProviderFormProps {
131
125
  * imply work has been done.
132
126
  */
133
127
  stored: boolean;
128
+ /** Which connection path these steps describe — see `ActivePanelProps`. */
129
+ path: 'oauth' | 'credentials';
134
130
  }) => ReactNode;
135
131
  }
136
132
 
@@ -187,6 +183,19 @@ function useRetireOnEnvironmentChange(
187
183
  }, [environment, onEditingChange, setProbe, setValues]);
188
184
  }
189
185
 
186
+ /**
187
+ * The form's memory and its two writes.
188
+ *
189
+ * `nothingEdited` guards the save because saving an untouched form is not a
190
+ * free no-op: `saveCredentials` resets the connection to UNVERIFIED, drops the
191
+ * proof and switches the provider OFF. `complete` answers whether there is
192
+ * enough on record for the probe to be worth running — see
193
+ * `credentialsComplete`, which skips the advanced fields an ordinary store must
194
+ * leave empty.
195
+ */
196
+ /** Everything `CredentialFields` needs, without importing the hook itself. */
197
+ export type CredentialFormState = ReturnType<typeof useCredentialForm>;
198
+
190
199
  function useCredentialForm(props: ProviderFormProps) {
191
200
  const {
192
201
  descriptor,
@@ -228,7 +237,13 @@ function useCredentialForm(props: ProviderFormProps) {
228
237
  // no credential to probe, and asking anyway makes the adapter answer
229
238
  // "Handle não configurado" — a store that has entered nothing has not
230
239
  // failed at anything; it is simply NÃO VERIFICADO.
231
- return allRequiredStored(descriptor, next, environment) ? probeNow() : undefined;
240
+ //
241
+ // The same holds one step further along: a set with fields still empty
242
+ // is not a connection yet, and probing it reports the provider's
243
+ // rejection of an incomplete request as a verdict on what the owner
244
+ // typed. Read from `next` — the server's answer — because blank fields
245
+ // PRESERVE what is stored, so the form does not know what is on record.
246
+ return credentialsComplete(descriptor, next, environment, {}) ? probeNow() : undefined;
232
247
  });
233
248
 
234
249
  useRetireOnEnvironmentChange(environment, onEditingChange, setProbe, setValues);
@@ -240,10 +255,9 @@ function useCredentialForm(props: ProviderFormProps) {
240
255
  busy,
241
256
  error,
242
257
  masked: config?.environments[environment] ?? {},
243
- // Saving an untouched form is not a free no-op: `saveCredentials` resets
244
- // the connection to UNVERIFIED, drops the proof and switches it OFF.
245
258
  nothingEdited: Object.keys(values).length === 0,
246
259
  valid: fieldsWellFormed(descriptor, values),
260
+ complete: credentialsComplete(descriptor, config, environment, values),
247
261
  summary: summaryOf(descriptor, config, environment),
248
262
  edit: (spec: string, value: string) => {
249
263
  setValues((v) => ({ ...v, [spec]: value }));
@@ -298,44 +312,11 @@ function collapsedSummary(
298
312
  }
299
313
 
300
314
 
301
- /**
302
- * The live inputs for the step still owed, plus the one button that commits
303
- * them and, on a store that has already proved it can receive, the warning
304
- * that saving will undo that.
305
- *
306
- * Its own component so `ProviderForm` stays about WHICH of the two shapes is on
307
- * screen (the collapsed row, or this) rather than about what each contains.
308
- */
309
- function CredentialFields({
310
- descriptor,
311
- form,
312
- proven,
313
- }: {
314
- descriptor: ProviderDescriptor;
315
- form: ReturnType<typeof useCredentialForm>;
316
- /** A real charge has landed through this connection — see `ReverifyWarning`. */
317
- proven: boolean;
318
- }) {
319
- return (
320
- <Stack spacing={2}>
321
- {proven ? <ReverifyWarning /> : null}
322
- {descriptor.credentialSchema.map((spec) => (
323
- <CredentialField
324
- key={spec.key}
325
- spec={spec}
326
- state={form.masked[spec.key]}
327
- value={form.values[spec.key]}
328
- onChange={(value) => form.edit(spec.key, value)}
329
- />
330
- ))}
331
- <FormActions
332
- busy={form.busy}
333
- label={saveLabel(descriptor)}
334
- disabled={form.nothingEdited || !form.valid}
335
- onSave={form.requestSave}
336
- />
337
- </Stack>
338
- );
315
+
316
+ /** Verdict keys that name no field, so they have no box to be shown at. */
317
+ function unkeyed(descriptor: ProviderDescriptor): (key: string) => boolean {
318
+ const fields = new Set(descriptor.credentialSchema.map((spec) => spec.key));
319
+ return (key: string) => !fields.has(key);
339
320
  }
340
321
 
341
322
  export function ProviderForm(props: ProviderFormProps) {
@@ -369,6 +350,10 @@ export function ProviderForm(props: ProviderFormProps) {
369
350
  sectionFooter: fields,
370
351
  editing: form.editing && !summary,
371
352
  stored: storedHere(descriptor, form.masked),
353
+ // This branch IS the credentials path: it renders only when the
354
+ // provider has no working connect button, so pasted keys are the
355
+ // only way in.
356
+ path: 'credentials',
372
357
  })
373
358
  : (rows ?? fields)}
374
359
 
@@ -376,11 +361,10 @@ export function ProviderForm(props: ProviderFormProps) {
376
361
  {form.probe ? (
377
362
  <ProbeAlert probe={form.probe} busy={form.busy !== null} onRetry={form.verify} />
378
363
  ) : 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 themincluding 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}
364
+ {/* Only what could NOT be tied to a box. Every verdict carrying a field
365
+ key is rendered at that field now; this keeps the leftovers, which is
366
+ where the UNCHECKED rows live the half a green result would deny. */}
367
+ {form.probe ? <ProbeChecklist probe={form.probe} only={unkeyed(descriptor)} /> : null}
384
368
 
385
369
  <ConfirmCredentialSave
386
370
  pending={form.pending}
@@ -1,15 +1,6 @@
1
1
  'use client';
2
2
 
3
- import {
4
- Accordion,
5
- AccordionDetails,
6
- AccordionSummary,
7
- Alert,
8
- Box,
9
- Paper,
10
- Stack,
11
- Typography,
12
- } from '@mui/material';
3
+ import { Box, Paper, Stack } from '@mui/material';
13
4
  import { useCallback, useState, type ReactNode } from 'react';
14
5
 
15
6
  import type {
@@ -19,9 +10,9 @@ import type {
19
10
  } from '@12-apps/payments-backend';
20
11
 
21
12
  import type { PaymentsSettingsClient } from '../client';
22
- import { ConnectionProbe } from './ConnectionProbe';
13
+ import { CARD_SX } from './panel-tokens';
14
+ import { OAuthPanel } from './OAuthPanel';
23
15
  import { EnvironmentNotice, EnvironmentSelector } from './EnvironmentTabs';
24
- import { ProviderConnection } from './ProviderConnection';
25
16
  import { ProviderForm } from './ProviderCredentialForm';
26
17
  import { ProviderStatusBar } from './ProviderStatusBar';
27
18
 
@@ -72,6 +63,15 @@ export interface ActivePanelProps {
72
63
  editing: boolean;
73
64
  /** The environment on screen holds its credentials — see `renderGuide`. */
74
65
  stored: boolean;
66
+ /**
67
+ * Which connection path the walkthrough is describing.
68
+ *
69
+ * The steps are not the same for both: step 1 under authorization says to
70
+ * press a button that is not rendered while the credential form is open,
71
+ * and it opens by saying no key needs copying — to an owner looking at the
72
+ * boxes for those keys. The panel owns the disclosure, so it owns this.
73
+ */
74
+ path: 'oauth' | 'credentials';
75
75
  }) => ReactNode;
76
76
  /** The host's activation step (see `renderVerification`), already resolved. */
77
77
  verification?: ReactNode;
@@ -138,7 +138,7 @@ function EnableBar({ descriptor, config, client, onChanged, reload }: ActivePane
138
138
  * container, so the header and the body carry the padding and the band between
139
139
  * them carries none.
140
140
  */
141
- function ProviderCard({
141
+ export function ProviderCard({
142
142
  header,
143
143
  band,
144
144
  children,
@@ -147,13 +147,16 @@ function ProviderCard({
147
147
  band?: ReactNode;
148
148
  children: ReactNode;
149
149
  }) {
150
+ // The body carries NO padding of its own: each block inside it (a step panel,
151
+ // a done row, the disclosure) sets its own 20px inset, because the
152
+ // environment band between them has to reach both edges and a padded parent
153
+ // makes that impossible. `CARD_SX` is the prototype's hairline verbatim —
154
+ // `Paper variant="outlined"` would paint the theme's divider instead.
150
155
  return (
151
- <Paper variant="outlined">
152
- <Box sx={{ px: 3, pt: 3 }}>{header}</Box>
156
+ <Paper elevation={0} sx={CARD_SX}>
157
+ <Box sx={{ px: '20px', pt: '18px', pb: '6px' }}>{header}</Box>
153
158
  {band}
154
- <Box sx={{ p: 3 }}>
155
- <Stack spacing={2}>{children}</Stack>
156
- </Box>
159
+ <Box>{children}</Box>
157
160
  </Paper>
158
161
  );
159
162
  }
@@ -188,7 +191,7 @@ function oauthWithConnect(props: ActivePanelProps): boolean {
188
191
  * which is the one the connect card and the server-computed stage describe
189
192
  * (the tabs inside the disclosure govern the form, not this).
190
193
  */
191
- function oauthWalkthrough(props: ActivePanelProps): ReactNode {
194
+ function oauthWalkthrough(props: ActivePanelProps, path: 'oauth' | 'credentials'): ReactNode {
192
195
  const { descriptor, config, guide } = props;
193
196
  if (!guide || !oauthWithConnect(props)) return null;
194
197
  return guide({
@@ -196,25 +199,41 @@ function oauthWalkthrough(props: ActivePanelProps): ReactNode {
196
199
  sectionFooter: null,
197
200
  editing: false,
198
201
  stored: requiredStored(descriptor, config, config?.environment ?? 'SANDBOX'),
202
+ path,
199
203
  });
200
204
  }
201
205
 
206
+ /**
207
+ * One provider's screen, assembled.
208
+ *
209
+ * Three pieces of state live HERE rather than in the form, and each for a
210
+ * reason the form cannot see:
211
+ *
212
+ * - `environment` frames everything below it, so the panel puts the switch at
213
+ * the top instead of halfway down the form's own column.
214
+ * - `editing` decides more than the form: reopening step 1 must also take step
215
+ * 3 off the screen. A card offering to charge under a heading about typing a
216
+ * credential is two steps at once, and the one that costs money is the one
217
+ * nobody asked for.
218
+ * - `storedHere` answers for the environment ON SCREEN. The activation step
219
+ * charges through those credentials, and shown on a tab holding none it
220
+ * offers to charge an account that is not there — on the screen whose whole
221
+ * subject is which account receives the money. `config.environment` is the
222
+ * ACTIVE one and cannot answer it; the fields can.
223
+ *
224
+ * On the connect branch the walkthrough leaves the form (see `guide`) and the
225
+ * form stands alone inside the manual disclosure, so `walkthrough` is passed as
226
+ * a FUNCTION: which steps to show depends on the disclosure, whose state lives
227
+ * in `OAuthPanel`.
228
+ */
202
229
  export function ActivePanel(props: ActivePanelProps) {
203
230
  const { descriptor, config, client, onChanged, reload, guide, verification } = props;
204
231
  const statusBar = <EnableBar {...props} />;
205
- // The environment frames everything below it, so the PANEL owns the choice
206
- // and puts the tabs at the top — not the form, halfway down its own column.
207
232
  const [environment, setEnvironment] = useState<PaymentEnvironment>(
208
233
  config?.environment ?? 'SANDBOX',
209
234
  );
210
- // Lifted out of the form because it decides more than the form: reopening
211
- // step 1 must also take step 3 off the screen. A card offering to charge
212
- // R$ 1,01 under a heading about typing your InfiniteTag is two steps at once,
213
- // and the one that costs money is the one you did not ask for.
214
235
  const [editing, setEditing] = useState(false);
215
236
 
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
237
  const oauthConnect = oauthWithConnect(props);
219
238
 
220
239
  const credentials = (
@@ -233,13 +252,12 @@ export function ActivePanel(props: ActivePanelProps) {
233
252
  );
234
253
 
235
254
  const selector = <EnvironmentSelector environment={environment} onChange={setEnvironment} />;
236
- const band = <EnvironmentNotice environment={environment} active={config?.environment ?? null} />;
255
+ // Full-bleed across the card, and the same notice inset for the manual
256
+ // disclosure, where it no longer spans anything. See `EnvironmentNotice`.
257
+ const active = config?.environment ?? null;
258
+ const band = <EnvironmentNotice environment={environment} active={active} />;
259
+ const notice = <EnvironmentNotice environment={environment} active={active} band={false} />;
237
260
 
238
- // The activation step charges through the credentials of the environment it
239
- // is under. Shown on a tab that stores none, it offers to charge an account
240
- // that is not there — on the screen whose entire subject is which account
241
- // receives the store's money. `config.environment` is the ACTIVE one, so it
242
- // cannot answer this; the fields on screen can.
243
261
  const storedHere = requiredStored(descriptor, config, environment);
244
262
 
245
263
  if (descriptor.authMode !== 'oauth') {
@@ -263,11 +281,12 @@ export function ActivePanel(props: ActivePanelProps) {
263
281
  <OAuthPanel
264
282
  {...props}
265
283
  statusBar={statusBar}
266
- walkthrough={oauthWalkthrough(props)}
284
+ walkthrough={(path: 'oauth' | 'credentials') => oauthWalkthrough(props, path)}
267
285
  form={
268
286
  <Stack spacing={2}>
269
287
  {selector}
270
- {band}
288
+ {notice}
289
+ {oauthWalkthrough(props, 'credentials')}
271
290
  {credentials}
272
291
  </Stack>
273
292
  }
@@ -275,80 +294,4 @@ export function ActivePanel(props: ActivePanelProps) {
275
294
  );
276
295
  }
277
296
 
278
- /**
279
- * The OAuth path: a connect button as the happy path, with the credential form
280
- * kept behind a disclosure.
281
- *
282
- * That fallback is not decoration — stores connected before Connect existed
283
- * still hold a pasted token, and a deployment with no registered provider
284
- * application has no working connect button at all. Hiding the form outright
285
- * would strand both.
286
- */
287
- function OAuthPanel({
288
- descriptor,
289
- config,
290
- client,
291
- reload,
292
- prepareConnect,
293
- verification,
294
- statusBar,
295
- walkthrough,
296
- form,
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
- );
303
- if (!prepareConnect) {
304
- return (
305
- <ProviderCard header={statusBar}>
306
- <Alert severity="info">
307
- Este provedor conecta por autorização, mas o botão de conexão não está disponível nesta
308
- instalação. Você ainda pode conectar informando as credenciais manualmente.
309
- </Alert>
310
- {form}
311
- {probe}
312
- {verification}
313
- </ProviderCard>
314
- );
315
- }
316
297
 
317
- return (
318
- <ProviderCard header={statusBar}>
319
- <ProviderConnection
320
- descriptor={descriptor}
321
- config={config}
322
- client={client}
323
- prepareConnect={prepareConnect}
324
- onChanged={reload}
325
- />
326
- {/*
327
- Directly under the connect card, ABOVE the manual disclosure: this is
328
- the step that actually turns the store on, and an owner who just
329
- authorized has no reason to open "prefiro informar as credenciais
330
- manualmente" to find it.
331
- */}
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}
342
- {descriptor.credentialSchema.length > 0 ? (
343
- <Accordion disableGutters data-testid="payments-manual-fallback">
344
- <AccordionSummary expandIcon={<span aria-hidden>▾</span>}>
345
- <Typography variant="body2" color="text.secondary">
346
- Prefiro informar as credenciais manualmente
347
- </Typography>
348
- </AccordionSummary>
349
- <AccordionDetails>{form}</AccordionDetails>
350
- </Accordion>
351
- ) : null}
352
- </ProviderCard>
353
- );
354
- }