@12-apps/payments-frontend 2.0.0 → 2.1.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 +2 -2
- package/src/client.ts +31 -1
- package/src/components/ConnectionCard.tsx +234 -0
- package/src/components/CredentialFieldStack.tsx +55 -0
- package/src/components/CredentialFields.tsx +131 -19
- package/src/components/CredentialFormAlerts.tsx +37 -15
- package/src/components/EnvironmentTabs.tsx +72 -23
- package/src/components/OAuthPanel.tsx +166 -0
- package/src/components/PaymentProviderSettings.tsx +23 -3
- package/src/components/ProviderConnection.tsx +119 -77
- package/src/components/ProviderCredentialForm.tsx +40 -56
- package/src/components/ProviderPanel.tsx +54 -111
- package/src/components/ProviderSetupGuide.tsx +174 -54
- package/src/components/ProviderStatusBar.tsx +104 -24
- package/src/components/credential-rules.ts +48 -12
- package/src/components/panel-tokens.ts +166 -0
- package/src/index.ts +3 -3
|
@@ -1,9 +1,11 @@
|
|
|
1
1
|
'use client';
|
|
2
2
|
|
|
3
|
-
import { Alert, Tab, Tabs } from '@mui/material';
|
|
3
|
+
import { Alert, Box, Tab, Tabs, Typography } from '@mui/material';
|
|
4
4
|
|
|
5
5
|
import type { PaymentEnvironment } from '@12-apps/payments-backend';
|
|
6
6
|
|
|
7
|
+
import { T } from './panel-tokens';
|
|
8
|
+
|
|
7
9
|
/** One environment's human name — the tabs and the probe result share it. */
|
|
8
10
|
export const ENVIRONMENT_LABELS: Record<PaymentEnvironment, string> = {
|
|
9
11
|
SANDBOX: 'Sandbox',
|
|
@@ -14,7 +16,22 @@ export const ENVIRONMENT_LABELS: Record<PaymentEnvironment, string> = {
|
|
|
14
16
|
* Sentence case, because these are place names rather than shouted commands —
|
|
15
17
|
* MUI upper-cases tab labels by default, which turns "Produção" into signage.
|
|
16
18
|
*/
|
|
17
|
-
const TAB_SX = {
|
|
19
|
+
const TAB_SX = {
|
|
20
|
+
textTransform: 'none',
|
|
21
|
+
minHeight: 0,
|
|
22
|
+
minWidth: 0,
|
|
23
|
+
borderRadius: '6px',
|
|
24
|
+
px: '14px',
|
|
25
|
+
py: '6px',
|
|
26
|
+
fontSize: '12.5px',
|
|
27
|
+
fontWeight: 600,
|
|
28
|
+
color: T.ink3,
|
|
29
|
+
'&.Mui-selected': {
|
|
30
|
+
background: T.bg,
|
|
31
|
+
color: T.ink,
|
|
32
|
+
boxShadow: '0 1px 3px rgba(0,0,0,.09)',
|
|
33
|
+
},
|
|
34
|
+
} as const;
|
|
18
35
|
|
|
19
36
|
/**
|
|
20
37
|
* The two environments as tabs, not a select: there are exactly two, and which
|
|
@@ -30,26 +47,45 @@ export function EnvironmentSelector({
|
|
|
30
47
|
environment: PaymentEnvironment;
|
|
31
48
|
onChange: (next: PaymentEnvironment) => void;
|
|
32
49
|
}) {
|
|
50
|
+
// A segmented control, not underlined tabs. Two options that CHANGE WHAT
|
|
51
|
+
// EVERY FIELD BELOW MEANS should look like a switch being thrown, and the
|
|
52
|
+
// label above says which switch — "Ambiente" alone reads as a page section.
|
|
33
53
|
return (
|
|
34
|
-
<
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
<
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
54
|
+
<Box>
|
|
55
|
+
<Typography
|
|
56
|
+
sx={{ display: 'block', fontSize: '12px', fontWeight: 650, color: T.ink2, mb: '5px' }}
|
|
57
|
+
>
|
|
58
|
+
Ambiente desta conexão
|
|
59
|
+
</Typography>
|
|
60
|
+
<Tabs
|
|
61
|
+
value={environment}
|
|
62
|
+
onChange={(_, next: PaymentEnvironment) => onChange(next)}
|
|
63
|
+
aria-label="Ambiente desta conexão"
|
|
64
|
+
data-testid="payments-environment-tabs"
|
|
65
|
+
slotProps={{ indicator: { sx: { display: 'none' } } }}
|
|
66
|
+
sx={{
|
|
67
|
+
minHeight: 0,
|
|
68
|
+
width: 'fit-content',
|
|
69
|
+
background: T.bg2,
|
|
70
|
+
border: `1px solid ${T.line}`,
|
|
71
|
+
borderRadius: '9px',
|
|
72
|
+
p: '4px',
|
|
73
|
+
}}
|
|
74
|
+
>
|
|
75
|
+
<Tab
|
|
76
|
+
label="Sandbox"
|
|
77
|
+
value="SANDBOX"
|
|
78
|
+
data-testid="payments-environment-SANDBOX"
|
|
79
|
+
sx={TAB_SX}
|
|
80
|
+
/>
|
|
81
|
+
<Tab
|
|
82
|
+
label="Produção"
|
|
83
|
+
value="PRODUCTION"
|
|
84
|
+
data-testid="payments-environment-PRODUCTION"
|
|
85
|
+
sx={TAB_SX}
|
|
86
|
+
/>
|
|
87
|
+
</Tabs>
|
|
88
|
+
</Box>
|
|
53
89
|
);
|
|
54
90
|
}
|
|
55
91
|
|
|
@@ -72,10 +108,23 @@ export function EnvironmentSelector({
|
|
|
72
108
|
export function EnvironmentNotice({
|
|
73
109
|
environment,
|
|
74
110
|
active,
|
|
111
|
+
band = true,
|
|
75
112
|
}: {
|
|
76
113
|
environment: PaymentEnvironment;
|
|
77
114
|
/** The environment this store's real checkout uses, when it has one. */
|
|
78
115
|
active: PaymentEnvironment | null;
|
|
116
|
+
/**
|
|
117
|
+
* Rendered as the card's full-bleed strip (the default), or as an ordinary
|
|
118
|
+
* inset alert.
|
|
119
|
+
*
|
|
120
|
+
* The band geometry — square corners, and `px: 3` matching the card's own
|
|
121
|
+
* padding so the text lines up with the content above and below — only reads
|
|
122
|
+
* correctly when the strip actually spans the card. Stacked INSIDE the manual
|
|
123
|
+
* disclosure, where it is already inset by the accordion, the same values
|
|
124
|
+
* indent the text a second time and square off a box that is visibly not
|
|
125
|
+
* touching either edge.
|
|
126
|
+
*/
|
|
127
|
+
band?: boolean;
|
|
79
128
|
}) {
|
|
80
129
|
const elsewhere = active !== null && active !== environment ? ENVIRONMENT_LABELS[active] : null;
|
|
81
130
|
const production = environment === 'PRODUCTION';
|
|
@@ -83,9 +132,9 @@ export function EnvironmentNotice({
|
|
|
83
132
|
<Alert
|
|
84
133
|
severity={production ? 'warning' : 'info'}
|
|
85
134
|
variant="standard"
|
|
86
|
-
square
|
|
135
|
+
square={band}
|
|
87
136
|
data-testid={`payments-environment-notice-${environment}`}
|
|
88
|
-
sx={{ borderRadius: 0, py: 0.5, px: 3 }}
|
|
137
|
+
sx={band ? { borderRadius: 0, py: 0.5, px: 3 } : { py: 0.5 }}
|
|
89
138
|
>
|
|
90
139
|
<strong>
|
|
91
140
|
{production ? 'Produção — dinheiro real.' : 'Sandbox — ambiente de teste.'}
|
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import { Alert, Box, Button } from '@mui/material';
|
|
4
|
+
import { useState, type ReactNode } from 'react';
|
|
5
|
+
|
|
6
|
+
import { ConnectionProbe } from './ConnectionProbe';
|
|
7
|
+
import { LINKISH_SX } from './panel-tokens';
|
|
8
|
+
import { ProviderConnection } from './ProviderConnection';
|
|
9
|
+
import type { ActivePanelProps } from './ProviderPanel';
|
|
10
|
+
import { ProviderCard } from './ProviderPanel';
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* The OAuth branch of a provider's screen, and the switch between its two
|
|
14
|
+
* connection paths.
|
|
15
|
+
*
|
|
16
|
+
* Split from `ProviderPanel` — which decides WHICH branch a provider gets —
|
|
17
|
+
* because this one is about what the connect branch looks like once chosen, and
|
|
18
|
+
* because the two together no longer fit the size gate.
|
|
19
|
+
*/
|
|
20
|
+
/**
|
|
21
|
+
* The collapsed row that moves the owner between the two connection paths.
|
|
22
|
+
*
|
|
23
|
+
* Both directions get one, and they are the same control: the row on screen
|
|
24
|
+
* always names the path you are NOT on. Only the manual leg carries content —
|
|
25
|
+
* the OAuth leg's "content" is the connect card, which belongs above the
|
|
26
|
+
* walkthrough rather than nested inside a disclosure — so `children` is
|
|
27
|
+
* optional and an empty one renders as a bare, clickable row.
|
|
28
|
+
*/
|
|
29
|
+
function PathSwitch({
|
|
30
|
+
label,
|
|
31
|
+
expanded,
|
|
32
|
+
onChange,
|
|
33
|
+
testId,
|
|
34
|
+
children,
|
|
35
|
+
}: {
|
|
36
|
+
label: string;
|
|
37
|
+
expanded: boolean;
|
|
38
|
+
onChange: (expanded: boolean) => void;
|
|
39
|
+
testId: string;
|
|
40
|
+
children?: ReactNode;
|
|
41
|
+
}) {
|
|
42
|
+
// Open, this is not a disclosure any more — it IS the path, and dressing a
|
|
43
|
+
// whole block (its own stepper, environment switch, fields and action bar) in
|
|
44
|
+
// a collapsible chrome adds a frame around a frame. So the content renders
|
|
45
|
+
// bare and only the CLOSED state is a control: one quiet line offering the
|
|
46
|
+
// other way in, which is all a path the owner is not on needs to be.
|
|
47
|
+
if (expanded) {
|
|
48
|
+
return (
|
|
49
|
+
<Box data-testid={testId} data-open="true">
|
|
50
|
+
{children}
|
|
51
|
+
</Box>
|
|
52
|
+
);
|
|
53
|
+
}
|
|
54
|
+
return (
|
|
55
|
+
<Box data-testid={testId} sx={{ px: '20px', pb: '20px' }}>
|
|
56
|
+
<Button onClick={() => onChange(true)} sx={{ ...LINKISH_SX, fontSize: '12.5px' }}>
|
|
57
|
+
{label}
|
|
58
|
+
</Button>
|
|
59
|
+
</Box>
|
|
60
|
+
);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* The OAuth path: a connect button as the happy path, with the credential form
|
|
65
|
+
* kept behind a disclosure.
|
|
66
|
+
*
|
|
67
|
+
* ## One path on screen at a time
|
|
68
|
+
*
|
|
69
|
+
* Opening the manual disclosure is the owner SAYING which path they are on.
|
|
70
|
+
* While the connect card stayed above the open form the screen showed both at
|
|
71
|
+
* once — a card explaining that no key needs copying, directly over four boxes
|
|
72
|
+
* asking for keys, with the grant's own controls in between belonging to
|
|
73
|
+
* neither. Every control on that card acts on the OAuth grant; a store
|
|
74
|
+
* connecting by hand has no grant for them to act on.
|
|
75
|
+
*
|
|
76
|
+
* ## Order inside the card
|
|
77
|
+
*
|
|
78
|
+
* The activation step sits directly under the connect card and ABOVE the
|
|
79
|
+
* disclosure: it is what actually turns the store on, and an owner who just
|
|
80
|
+
* authorized has no reason to open a fallback to find it.
|
|
81
|
+
*
|
|
82
|
+
* The walkthrough renders OUTSIDE the disclosure for the same reason — it used
|
|
83
|
+
* to live inside the credential form, which on this branch is folded into the
|
|
84
|
+
* fallback, so the guide and the stepper answering "where am I" were buried
|
|
85
|
+
* behind a label the connect card says there is no reason to open (FUT-691).
|
|
86
|
+
* Its step LIST depends on the path: under authorization step 1 says to press
|
|
87
|
+
* the connect button above, and with pasted keys that button is not rendered.
|
|
88
|
+
* An adapter shipping a `credentialsPath` variant gets its own steps; one that
|
|
89
|
+
* does not keeps the single guide on both paths, unchanged.
|
|
90
|
+
*
|
|
91
|
+
* That fallback is not decoration — stores connected before Connect existed
|
|
92
|
+
* still hold a pasted token, and a deployment with no registered provider
|
|
93
|
+
* application has no working connect button at all. Hiding the form outright
|
|
94
|
+
* would strand both.
|
|
95
|
+
*/
|
|
96
|
+
export function OAuthPanel({
|
|
97
|
+
descriptor,
|
|
98
|
+
config,
|
|
99
|
+
client,
|
|
100
|
+
reload,
|
|
101
|
+
prepareConnect,
|
|
102
|
+
verification,
|
|
103
|
+
statusBar,
|
|
104
|
+
walkthrough,
|
|
105
|
+
form,
|
|
106
|
+
}: ActivePanelProps & {
|
|
107
|
+
statusBar: ReactNode;
|
|
108
|
+
walkthrough: (path: 'oauth' | 'credentials') => ReactNode;
|
|
109
|
+
form: ReactNode;
|
|
110
|
+
}) {
|
|
111
|
+
// Which path the owner is on. The disclosure is no longer just a container —
|
|
112
|
+
// opening it CHOOSES the credentials path, so the panel has to know.
|
|
113
|
+
const [manual, setManual] = useState(false);
|
|
114
|
+
// The connection probe, for a store whose connection is a grant: the form's
|
|
115
|
+
// own probe runs off Salvar, which an OAuth store never presses (FUT-691).
|
|
116
|
+
const probe = (
|
|
117
|
+
<ConnectionProbe descriptor={descriptor} config={config} client={client} reload={reload} />
|
|
118
|
+
);
|
|
119
|
+
if (!prepareConnect) {
|
|
120
|
+
return (
|
|
121
|
+
<ProviderCard header={statusBar}>
|
|
122
|
+
<Alert severity="info">
|
|
123
|
+
Este provedor conecta por autorização, mas o botão de conexão não está disponível nesta
|
|
124
|
+
instalação. Você ainda pode conectar informando as credenciais manualmente.
|
|
125
|
+
</Alert>
|
|
126
|
+
{form}
|
|
127
|
+
{probe}
|
|
128
|
+
{verification}
|
|
129
|
+
</ProviderCard>
|
|
130
|
+
);
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
return (
|
|
134
|
+
<ProviderCard header={statusBar}>
|
|
135
|
+
{manual ? (
|
|
136
|
+
<PathSwitch
|
|
137
|
+
label="Prefiro conectar por autorização"
|
|
138
|
+
expanded={false}
|
|
139
|
+
onChange={() => setManual(false)}
|
|
140
|
+
testId="payments-oauth-fallback"
|
|
141
|
+
/>
|
|
142
|
+
) : (
|
|
143
|
+
<ProviderConnection
|
|
144
|
+
descriptor={descriptor}
|
|
145
|
+
config={config}
|
|
146
|
+
client={client}
|
|
147
|
+
prepareConnect={prepareConnect}
|
|
148
|
+
onChanged={reload}
|
|
149
|
+
/>
|
|
150
|
+
)}
|
|
151
|
+
{verification}
|
|
152
|
+
{manual ? null : walkthrough('oauth')}
|
|
153
|
+
{probe}
|
|
154
|
+
{descriptor.credentialSchema.length > 0 ? (
|
|
155
|
+
<PathSwitch
|
|
156
|
+
label="Prefiro informar as credenciais manualmente"
|
|
157
|
+
expanded={manual}
|
|
158
|
+
onChange={setManual}
|
|
159
|
+
testId="payments-manual-fallback"
|
|
160
|
+
>
|
|
161
|
+
{form}
|
|
162
|
+
</PathSwitch>
|
|
163
|
+
) : null}
|
|
164
|
+
</ProviderCard>
|
|
165
|
+
);
|
|
166
|
+
}
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
import { Alert, Box, Button, CircularProgress } from '@mui/material';
|
|
4
4
|
import { useCallback, type ReactNode } from 'react';
|
|
5
5
|
|
|
6
|
-
import type { MaskedProviderConfig } from '@12-apps/payments-backend';
|
|
6
|
+
import type { MaskedProviderConfig, ProviderSetupGuide } from '@12-apps/payments-backend';
|
|
7
7
|
|
|
8
8
|
import type { PaymentsSettingsClient } from '../client';
|
|
9
9
|
import { canAttemptCharge } from './connection-state';
|
|
@@ -195,6 +195,23 @@ function verificationFor(io: VerificationInputs): ReactNode {
|
|
|
195
195
|
});
|
|
196
196
|
}
|
|
197
197
|
|
|
198
|
+
/**
|
|
199
|
+
* The walkthrough for the connection path the panel currently has open.
|
|
200
|
+
*
|
|
201
|
+
* A provider that accepts both a grant and pasted keys ships one guide with a
|
|
202
|
+
* `credentialsPath` variant; everything else ships one guide, and both paths
|
|
203
|
+
* get it. Only the SECTIONS are swapped — the variant mirrors the base's stage
|
|
204
|
+
* count and confirmable index, which is what lets `blocked`/`hidden` above stay
|
|
205
|
+
* computed from the base guide alone.
|
|
206
|
+
*/
|
|
207
|
+
function guideForPath(
|
|
208
|
+
guide: ProviderSetupGuide | null,
|
|
209
|
+
path: 'oauth' | 'credentials',
|
|
210
|
+
): ProviderSetupGuide | null {
|
|
211
|
+
if (!guide || path === 'oauth') return guide;
|
|
212
|
+
return guide.credentialsPath ? { ...guide.credentialsPath } : guide;
|
|
213
|
+
}
|
|
214
|
+
|
|
198
215
|
interface ProviderScreenProps extends ActivePanelProps {
|
|
199
216
|
onBack: () => void;
|
|
200
217
|
}
|
|
@@ -279,9 +296,12 @@ export function PaymentProviderSettings({
|
|
|
279
296
|
onVerified: () => void reload(),
|
|
280
297
|
onSetupIncomplete: ack.withdraw,
|
|
281
298
|
})}
|
|
282
|
-
guide={(slots) => (
|
|
299
|
+
guide={({ path, ...slots }) => (
|
|
283
300
|
<SetupGuideSection
|
|
284
|
-
|
|
301
|
+
// Which walkthrough, per the path the panel has open. Only the
|
|
302
|
+
// SECTIONS differ; `blocked`/`hidden` above stay computed from the
|
|
303
|
+
// base guide, which `credentialsPath` is required to mirror.
|
|
304
|
+
guide={guideForPath(guide, path)}
|
|
285
305
|
confirmed={ack.confirmed}
|
|
286
306
|
onConfirm={ack.confirm}
|
|
287
307
|
onReopen={ack.withdraw}
|
|
@@ -2,14 +2,10 @@
|
|
|
2
2
|
|
|
3
3
|
import {
|
|
4
4
|
Alert,
|
|
5
|
+
Box,
|
|
5
6
|
Button,
|
|
6
7
|
Chip,
|
|
7
8
|
CircularProgress,
|
|
8
|
-
Dialog,
|
|
9
|
-
DialogActions,
|
|
10
|
-
DialogContent,
|
|
11
|
-
DialogContentText,
|
|
12
|
-
DialogTitle,
|
|
13
9
|
Stack,
|
|
14
10
|
Typography,
|
|
15
11
|
} from '@mui/material';
|
|
@@ -23,7 +19,9 @@ import type {
|
|
|
23
19
|
} from '@12-apps/payments-backend';
|
|
24
20
|
|
|
25
21
|
import type { PaymentsSettingsClient } from '../client';
|
|
22
|
+
import { ConnectionFacts, ConnectSteps, DisconnectDialog } from './ConnectionCard';
|
|
26
23
|
import { expiryProximity, isConnected } from './connection-state';
|
|
24
|
+
import { BAR_MSG_SX, BAR_SX, BTN_PRIMARY_SX, BTN_QUIET_DANGER_SX, T } from './panel-tokens';
|
|
27
25
|
|
|
28
26
|
/**
|
|
29
27
|
* The `authMode: 'oauth'` half of the settings page: a provider whose
|
|
@@ -81,6 +79,47 @@ function ExpiryNote(props: { expiresAt: string }) {
|
|
|
81
79
|
);
|
|
82
80
|
}
|
|
83
81
|
|
|
82
|
+
/**
|
|
83
|
+
* A failed connect, in the owner's terms.
|
|
84
|
+
*
|
|
85
|
+
* The one case worth naming is the deployment that has registered no OAuth
|
|
86
|
+
* application for this provider. It surfaced as
|
|
87
|
+
* `{"error":"CredentialsError","message":"No platform OAuth application
|
|
88
|
+
* credentials configured for stripe/SANDBOX"}` in a red box under the connect
|
|
89
|
+
* button — an error class, a slash-joined pair of internal identifiers, and not
|
|
90
|
+
* one word about what to do. It is not a failure the owner caused or can fix,
|
|
91
|
+
* and it is not a dead end either: the credentials path works, and it is right
|
|
92
|
+
* there on the same screen. So it reads as a warning that names the way round.
|
|
93
|
+
*
|
|
94
|
+
* Anything else is passed through. The adapter's own sentence beats a generic
|
|
95
|
+
* one, and inventing copy for a failure we have not seen is how a screen ends
|
|
96
|
+
* up confidently misdescribing an outage.
|
|
97
|
+
*/
|
|
98
|
+
function connectFailure(
|
|
99
|
+
message: string,
|
|
100
|
+
displayName: string,
|
|
101
|
+
): { severity: 'error' | 'warning'; text: string } {
|
|
102
|
+
if (/no platform oauth application credentials/i.test(message)) {
|
|
103
|
+
return {
|
|
104
|
+
severity: 'warning',
|
|
105
|
+
text:
|
|
106
|
+
`A conexão automática com ${displayName} não está disponível nesta instalação — ` +
|
|
107
|
+
'o aplicativo de autorização não foi cadastrado. Para conectar agora, abra ' +
|
|
108
|
+
'“Prefiro informar as credenciais manualmente” abaixo e cole as suas próprias chaves.',
|
|
109
|
+
};
|
|
110
|
+
}
|
|
111
|
+
return { severity: 'error', text: message };
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
function ConnectError({ message, displayName }: { message: string; displayName: string }) {
|
|
115
|
+
const { severity, text } = connectFailure(message, displayName);
|
|
116
|
+
return (
|
|
117
|
+
<Alert severity={severity} data-testid="payments-connect-failure">
|
|
118
|
+
{text}
|
|
119
|
+
</Alert>
|
|
120
|
+
);
|
|
121
|
+
}
|
|
122
|
+
|
|
84
123
|
function connectLabel(displayName: string, connected: boolean, busy: string | null) {
|
|
85
124
|
if (busy === 'connect') return <CircularProgress size={18} />;
|
|
86
125
|
return connected ? 'Reconectar' : `Conectar com ${displayName}`;
|
|
@@ -134,6 +173,8 @@ function ConnectedAccountDetails(props: { account: ConnectedOAuthAccount }) {
|
|
|
134
173
|
);
|
|
135
174
|
}
|
|
136
175
|
|
|
176
|
+
|
|
177
|
+
|
|
137
178
|
/** Header + explanatory copy + any warning banner for the connection. */
|
|
138
179
|
function ConnectionSummary(props: {
|
|
139
180
|
displayName: string;
|
|
@@ -153,15 +194,11 @@ function ConnectionSummary(props: {
|
|
|
153
194
|
once connected, since the provider sealed it into the grant.
|
|
154
195
|
*/}
|
|
155
196
|
{props.connected ? (
|
|
156
|
-
<
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
label={props.environment === 'PRODUCTION' ? 'Produção' : 'Sandbox (testes)'}
|
|
162
|
-
color={props.environment === 'PRODUCTION' ? 'default' : 'warning'}
|
|
163
|
-
/>
|
|
164
|
-
</Stack>
|
|
197
|
+
<ConnectionFacts
|
|
198
|
+
environment={props.environment}
|
|
199
|
+
account={props.connectedAccount}
|
|
200
|
+
displayName={props.displayName}
|
|
201
|
+
/>
|
|
165
202
|
) : null}
|
|
166
203
|
{/*
|
|
167
204
|
The connected sentence names NO platform. It used to open with one
|
|
@@ -176,11 +213,12 @@ function ConnectionSummary(props: {
|
|
|
176
213
|
grant, not whose logo is on the page. The subject is the connection
|
|
177
214
|
itself, which is true for every host and needs no new configuration.
|
|
178
215
|
*/}
|
|
179
|
-
<Typography
|
|
216
|
+
<Typography sx={{ fontSize: '13px', color: T.ink2, lineHeight: 1.6 }}>
|
|
180
217
|
{props.connected
|
|
181
218
|
? 'Sua conta está conectada. As cobranças são criadas em seu nome — nenhuma chave precisa ser copiada.'
|
|
182
219
|
: `Conecte sua conta ${props.displayName} autorizando o acesso no site do provedor. Nenhuma chave precisa ser copiada.`}
|
|
183
220
|
</Typography>
|
|
221
|
+
{props.connected ? null : <ConnectSteps displayName={props.displayName} />}
|
|
184
222
|
{props.connected && props.connectedAccount ? (
|
|
185
223
|
<ConnectedAccountDetails account={props.connectedAccount} />
|
|
186
224
|
) : null}
|
|
@@ -194,47 +232,16 @@ function ConnectionSummary(props: {
|
|
|
194
232
|
);
|
|
195
233
|
}
|
|
196
234
|
|
|
235
|
+
|
|
236
|
+
|
|
197
237
|
/**
|
|
198
|
-
*
|
|
199
|
-
*
|
|
200
|
-
*
|
|
201
|
-
*
|
|
238
|
+
* The connect card's action bar.
|
|
239
|
+
*
|
|
240
|
+
* Removing the connection is stated QUIETLY beside the step's own button rather
|
|
241
|
+
* than as a second filled control: an owner reaches it deliberately or not at
|
|
242
|
+
* all, and a red button of equal weight on every visit is an invitation to
|
|
243
|
+
* misclick the one action here that cannot be undone from this screen.
|
|
202
244
|
*/
|
|
203
|
-
function DisconnectDialog(props: {
|
|
204
|
-
open: boolean;
|
|
205
|
-
displayName: string;
|
|
206
|
-
busy: boolean;
|
|
207
|
-
onCancel: () => void;
|
|
208
|
-
onConfirm: () => void;
|
|
209
|
-
}) {
|
|
210
|
-
return (
|
|
211
|
-
<Dialog open={props.open} onClose={props.onCancel} data-testid="payments-disconnect-confirm">
|
|
212
|
-
<DialogTitle>Desconectar {props.displayName}?</DialogTitle>
|
|
213
|
-
<DialogContent>
|
|
214
|
-
<DialogContentText>
|
|
215
|
-
A autorização será revogada no {props.displayName} e sua loja deixa de conseguir cobrar
|
|
216
|
-
imediatamente. Para voltar a receber, será necessário conectar a conta novamente
|
|
217
|
-
autorizando o acesso no site do provedor.
|
|
218
|
-
</DialogContentText>
|
|
219
|
-
</DialogContent>
|
|
220
|
-
<DialogActions>
|
|
221
|
-
<Button onClick={props.onCancel} disabled={props.busy}>
|
|
222
|
-
Cancelar
|
|
223
|
-
</Button>
|
|
224
|
-
<Button
|
|
225
|
-
color="error"
|
|
226
|
-
variant="contained"
|
|
227
|
-
onClick={props.onConfirm}
|
|
228
|
-
disabled={props.busy}
|
|
229
|
-
data-testid="payments-disconnect-confirm-action"
|
|
230
|
-
>
|
|
231
|
-
{props.busy ? <CircularProgress size={18} /> : 'Desconectar'}
|
|
232
|
-
</Button>
|
|
233
|
-
</DialogActions>
|
|
234
|
-
</Dialog>
|
|
235
|
-
);
|
|
236
|
-
}
|
|
237
|
-
|
|
238
245
|
function ConnectionActions(props: {
|
|
239
246
|
displayName: string;
|
|
240
247
|
connected: boolean;
|
|
@@ -244,16 +251,32 @@ function ConnectionActions(props: {
|
|
|
244
251
|
}) {
|
|
245
252
|
const { displayName, connected, busy, onConnect, onDisconnect } = props;
|
|
246
253
|
return (
|
|
247
|
-
<
|
|
248
|
-
<
|
|
249
|
-
{
|
|
250
|
-
|
|
254
|
+
<Box sx={BAR_SX} data-testid="payments-connection-bar">
|
|
255
|
+
<Typography sx={BAR_MSG_SX}>
|
|
256
|
+
{connected
|
|
257
|
+
? `Conta ${displayName} conectada. Revogue quando quiser, aqui ou no painel do provedor.`
|
|
258
|
+
: 'Você sai para o provedor e volta para cá — leva menos de um minuto.'}
|
|
259
|
+
</Typography>
|
|
251
260
|
{connected ? (
|
|
252
|
-
<Button
|
|
253
|
-
{busy
|
|
261
|
+
<Button
|
|
262
|
+
disabled={busy !== null}
|
|
263
|
+
onClick={onDisconnect}
|
|
264
|
+
sx={BTN_QUIET_DANGER_SX}
|
|
265
|
+
data-testid="payments-disconnect"
|
|
266
|
+
>
|
|
267
|
+
{busy === 'disconnect' ? <CircularProgress size={18} /> : 'Remover conexão'}
|
|
254
268
|
</Button>
|
|
255
269
|
) : null}
|
|
256
|
-
|
|
270
|
+
<Button
|
|
271
|
+
variant="contained"
|
|
272
|
+
disableElevation
|
|
273
|
+
disabled={busy !== null}
|
|
274
|
+
onClick={onConnect}
|
|
275
|
+
sx={BTN_PRIMARY_SX}
|
|
276
|
+
>
|
|
277
|
+
{connectLabel(displayName, connected, busy)}
|
|
278
|
+
</Button>
|
|
279
|
+
</Box>
|
|
257
280
|
);
|
|
258
281
|
}
|
|
259
282
|
|
|
@@ -278,17 +301,37 @@ function useConnectionAction(onChanged: () => void) {
|
|
|
278
301
|
return { busy, error, run };
|
|
279
302
|
}
|
|
280
303
|
|
|
304
|
+
/**
|
|
305
|
+
* The connection as this card reads it, with every default applied once.
|
|
306
|
+
*
|
|
307
|
+
* `environment` follows whatever the provider is already configured for and
|
|
308
|
+
* falls back to SANDBOX, so a live grant is never the accident — changing it is
|
|
309
|
+
* an ADVANCED action that lives with the manual credentials, not on the
|
|
310
|
+
* one-button connect card.
|
|
311
|
+
*/
|
|
312
|
+
function readConnection(config: MaskedProviderConfig | null) {
|
|
313
|
+
return {
|
|
314
|
+
environment: (config?.environment ?? 'SANDBOX') as PaymentEnvironment,
|
|
315
|
+
status: config?.status ?? 'UNVERIFIED',
|
|
316
|
+
connected: isConnected(config),
|
|
317
|
+
expiresAt: config?.expiresAt ?? null,
|
|
318
|
+
account: config?.connectedAccount ?? null,
|
|
319
|
+
receiving: config?.enabled === true,
|
|
320
|
+
};
|
|
321
|
+
}
|
|
322
|
+
|
|
281
323
|
export function ProviderConnection(props: ProviderConnectionProps) {
|
|
282
324
|
const { descriptor, config, client, prepareConnect, onChanged } = props;
|
|
283
325
|
const { busy, error, run } = useConnectionAction(onChanged);
|
|
284
326
|
const [confirmingDisconnect, setConfirmingDisconnect] = useState(false);
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
327
|
+
const { environment, status, connected, expiresAt, account, receiving } = readConnection(config);
|
|
328
|
+
|
|
329
|
+
/** Run a dialog action and shut the dialog once it has actually landed. */
|
|
330
|
+
const close = (kind: string, action: () => Promise<unknown>) =>
|
|
331
|
+
void run(kind, async () => {
|
|
332
|
+
await action();
|
|
333
|
+
setConfirmingDisconnect(false);
|
|
334
|
+
});
|
|
292
335
|
|
|
293
336
|
const connect = () =>
|
|
294
337
|
void run('connect', async () => {
|
|
@@ -307,12 +350,12 @@ export function ProviderConnection(props: ProviderConnectionProps) {
|
|
|
307
350
|
displayName={descriptor.displayName}
|
|
308
351
|
status={status}
|
|
309
352
|
connected={connected}
|
|
310
|
-
expiresAt={
|
|
353
|
+
expiresAt={expiresAt}
|
|
311
354
|
environment={environment}
|
|
312
|
-
connectedAccount={
|
|
355
|
+
connectedAccount={account}
|
|
313
356
|
/>
|
|
314
357
|
|
|
315
|
-
{error ? <
|
|
358
|
+
{error ? <ConnectError message={error} displayName={descriptor.displayName} /> : null}
|
|
316
359
|
|
|
317
360
|
<ConnectionActions
|
|
318
361
|
displayName={descriptor.displayName}
|
|
@@ -326,13 +369,12 @@ export function ProviderConnection(props: ProviderConnectionProps) {
|
|
|
326
369
|
open={confirmingDisconnect}
|
|
327
370
|
displayName={descriptor.displayName}
|
|
328
371
|
busy={busy === 'disconnect'}
|
|
372
|
+
// Live on this provider: removing it stops checkout, and pausing is a
|
|
373
|
+
// real alternative rather than a lesser version of the same thing.
|
|
374
|
+
receiving={receiving}
|
|
375
|
+
onPauseInstead={() => close('pause', () => client.setEnabled(descriptor.name, false))}
|
|
329
376
|
onCancel={() => setConfirmingDisconnect(false)}
|
|
330
|
-
onConfirm={() =>
|
|
331
|
-
void run('disconnect', async () => {
|
|
332
|
-
await client.disconnectOAuth(descriptor.name);
|
|
333
|
-
setConfirmingDisconnect(false);
|
|
334
|
-
})
|
|
335
|
-
}
|
|
377
|
+
onConfirm={() => close('disconnect', () => client.disconnectOAuth(descriptor.name))}
|
|
336
378
|
/>
|
|
337
379
|
</Stack>
|
|
338
380
|
);
|