@visns-studio/visns-components 6.6.3 → 6.15.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/README.md +1658 -18
- package/package.json +10 -4
- package/src/components/DataGrid.jsx +619 -158
- package/src/components/Fetch.jsx +80 -3
- package/src/components/Form.jsx +9 -0
- package/src/components/Navigation.jsx +109 -50
- package/src/components/Notification.jsx +279 -9
- package/src/components/TableFilter.jsx +14 -1
- package/src/components/auth/AuthBrandPanel.jsx +43 -0
- package/src/components/auth/AuthLoading.jsx +78 -0
- package/src/components/auth/AuthShell.jsx +59 -0
- package/src/components/auth/ClientAuth.jsx +161 -0
- package/src/components/auth/ClientAuthFrame.jsx +59 -0
- package/src/components/auth/ClientLogin.jsx +266 -55
- package/src/components/auth/ClientOTPVerify.jsx +587 -115
- package/src/components/auth/ImpersonateGate.jsx +254 -0
- package/src/components/auth/Login.jsx +134 -41
- package/src/components/auth/LogoutScreen.jsx +112 -0
- package/src/components/auth/Reset.jsx +134 -77
- package/src/components/auth/TwoFactorAuth.jsx +475 -297
- package/src/components/auth/Verify.jsx +237 -126
- package/src/components/auth/authEndpoints.js +105 -0
- package/src/components/auth/authFont.js +23 -0
- package/src/components/auth/authHelpers.js +465 -0
- package/src/components/auth/clientAuthProtocols.js +240 -0
- package/src/components/auth/useOptionalRouter.js +49 -0
- package/src/components/callQueue/CallQueuePop.jsx +1502 -0
- package/src/components/callQueue/CallQueueSettings.jsx +508 -0
- package/src/components/callQueue/callQueueHelpers.js +280 -0
- package/src/components/callQueue/callQueueSettingsHelpers.js +75 -0
- package/src/components/columns/AutoGrowCell.jsx +141 -0
- package/src/components/columns/ColumnRenderers.jsx +53 -7
- package/src/components/generic/GenericAuth.jsx +163 -96
- package/src/components/generic/GenericDetail.jsx +215 -90
- package/src/components/generic/GenericIndex.jsx +20 -47
- package/src/components/generic/GenericMain.jsx +5 -0
- package/src/components/generic/GroupedReportRenderer.jsx +1 -5
- package/src/components/generic/StandardModal.jsx +17 -5
- package/src/components/generic/reportSemanticSteps/SemanticEntityStep.jsx +1 -6
- package/src/components/generic/reportSemanticSteps/SemanticFieldsStep.jsx +4 -11
- package/src/components/generic/reportSemanticSteps/SemanticFiltersStep.jsx +9 -27
- package/src/components/generic/reportSemanticSteps/SemanticGroupingStep.jsx +5 -13
- package/src/components/generic/reportSemanticSteps/SemanticParameterPrompt.jsx +5 -13
- package/src/components/generic/reportSemanticSteps/SemanticPreviewStep.jsx +14 -31
- package/src/components/generic/reportSemanticSteps/SemanticRelationsStep.jsx +3 -9
- package/src/components/generic/reportSemanticSteps/SemanticValueInput.jsx +2 -15
- package/src/components/navActive.js +60 -0
- package/src/components/notify/desktopNotifications.js +256 -0
- package/src/components/sketch/SketchField.jsx +12 -2
- package/src/components/sms/SmsComposeModal.jsx +495 -0
- package/src/components/sms/SmsInbox.jsx +596 -0
- package/src/components/sms/SmsInboxBadge.jsx +468 -0
- package/src/components/sms/SmsLineSettings.jsx +854 -0
- package/src/components/sms/SmsThreadPanel.jsx +1184 -0
- package/src/components/sms/smsEndpoints.js +56 -0
- package/src/components/sms/smsHelpers.js +1075 -0
- package/src/components/sms/smsLiveState.js +132 -0
- package/src/components/sms/useSmsLive.js +307 -0
- package/src/components/styles/CallQueuePop.module.scss +646 -0
- package/src/components/styles/CallQueueSettings.module.scss +460 -0
- package/src/components/styles/ClientAuth.module.scss +229 -0
- package/src/components/styles/DataGrid.module.scss +10 -0
- package/src/components/styles/GenericDetail.module.scss +153 -26
- package/src/components/styles/GenericIndex.module.scss +13 -0
- package/src/components/styles/GenericMain.module.scss +19 -2
- package/src/components/styles/ImpersonateGate.module.scss +85 -0
- package/src/components/styles/Login.module.scss +13 -122
- package/src/components/styles/LogoutScreen.module.scss +89 -0
- package/src/components/styles/Navigation.module.scss +509 -223
- package/src/components/styles/Notification.module.scss +103 -0
- package/src/components/styles/Reset.module.scss +52 -186
- package/src/components/styles/Sms.module.scss +2382 -0
- package/src/components/styles/TableFilter.module.scss +118 -98
- package/src/components/styles/TwoFactorAuth.module.scss +115 -176
- package/src/components/styles/Vault.module.scss +1983 -0
- package/src/components/styles/Verify.module.scss +57 -180
- package/src/components/styles/_authBrandPanel.scss +163 -0
- package/src/components/styles/_authShell.scss +369 -0
- package/src/components/styles/global-datagrid.css +29 -0
- package/src/components/utils/buildEnv.js +67 -0
- package/src/components/utils/displayValue.js +94 -0
- package/src/components/utils/useDensity.js +345 -9
- package/src/components/vault/OtpChip.jsx +208 -0
- package/src/components/vault/PasswordGenerator.jsx +145 -0
- package/src/components/vault/QrScanner.jsx +326 -0
- package/src/components/vault/VaultAccessLog.jsx +225 -0
- package/src/components/vault/VaultConfirmPanel.jsx +113 -0
- package/src/components/vault/VaultEntryForm.jsx +740 -0
- package/src/components/vault/VaultManager.jsx +1000 -0
- package/src/components/vault/VaultQuickSearch.jsx +681 -0
- package/src/components/vault/qrDecode.js +257 -0
- package/src/components/vault/useDebouncedValue.js +22 -0
- package/src/components/vault/useVaultReveal.js +160 -0
- package/src/components/vault/vaultClipboard.js +33 -0
- package/src/components/vault/vaultEndpoints.js +40 -0
- package/src/components/vault/vaultFit.js +116 -0
- package/src/components/vault/vaultHelpers.js +547 -0
- package/src/components/vault/vaultNavigation.jsx +58 -0
- package/src/components/vault/vaultOtp.js +105 -0
- package/src/index.js +233 -1
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
import React, { useState } from 'react';
|
|
2
|
+
|
|
3
|
+
import ClientLogin from './ClientLogin';
|
|
4
|
+
import ClientOTPVerify from './ClientOTPVerify';
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Both client sign-in steps on one page.
|
|
8
|
+
*
|
|
9
|
+
* ClientLogin and ClientOTPVerify were written as two routes with a router
|
|
10
|
+
* navigation between them. That is one shape a sign-in can take; the other is
|
|
11
|
+
* a single page that swaps its own contents, which is what the Next.js portal
|
|
12
|
+
* does and what an App Router page can actually host. This composes the two
|
|
13
|
+
* screens into that shape by holding the challenge in state instead of in
|
|
14
|
+
* router state.
|
|
15
|
+
*
|
|
16
|
+
* It needs no router, so it mounts cleanly under Next.js, and it is the whole
|
|
17
|
+
* of the "zero adapter code" story: a consumer supplies endpoints, a protocol
|
|
18
|
+
* name and an `onAuthenticated`, and writes nothing that knows a request body
|
|
19
|
+
* from a response field.
|
|
20
|
+
*
|
|
21
|
+
* <ClientAuth
|
|
22
|
+
* layout="split"
|
|
23
|
+
* loginBg="/images/office.jpg"
|
|
24
|
+
* branding={{ name: 'Client Portal', tagline: '…' }}
|
|
25
|
+
* protocol="contact"
|
|
26
|
+
* urls={{ login: '/api/auth/request-otp', verify: '/api/auth/login-otp' }}
|
|
27
|
+
* usernameDomain="virtualportal.com.au"
|
|
28
|
+
* showContactHint
|
|
29
|
+
* showDevOtp
|
|
30
|
+
* showMaskedContact
|
|
31
|
+
* showContactField
|
|
32
|
+
* resendCooldown={0}
|
|
33
|
+
* onAuthenticated={(user, token) => {
|
|
34
|
+
* setCookie('portal_token', token, { maxAge: 28800 });
|
|
35
|
+
* setUserData(user);
|
|
36
|
+
* router.push('/portal/dashboard');
|
|
37
|
+
* }}
|
|
38
|
+
* />
|
|
39
|
+
*
|
|
40
|
+
* Props are forwarded to whichever step owns them; `loginProps` and
|
|
41
|
+
* `verifyProps` are escape hatches for anything that has to differ between the
|
|
42
|
+
* two. The shared ones (logo, urls, protocol, …) are passed to both.
|
|
43
|
+
*/
|
|
44
|
+
|
|
45
|
+
const ClientAuth = ({
|
|
46
|
+
/** Shared. */
|
|
47
|
+
logo,
|
|
48
|
+
/**
|
|
49
|
+
* 'card' (default) renders exactly what every consumer rendered before
|
|
50
|
+
* 6.8.3: a box centred in the space the host page gives it. 'split' is the
|
|
51
|
+
* staff sign-in's two-pane language — brand on one side, task on the other
|
|
52
|
+
* — sized to fill its PARENT rather than the viewport, so the consumer
|
|
53
|
+
* keeps its own header and footer and hands this the space between.
|
|
54
|
+
*/
|
|
55
|
+
layout = 'card',
|
|
56
|
+
/** Split layout: the brand panel's photograph. Ignored by 'card'. */
|
|
57
|
+
loginBg,
|
|
58
|
+
/**
|
|
59
|
+
* Split layout: {eyebrow, name, tagline, footer}, rendered in the same
|
|
60
|
+
* type hierarchy as the staff Login's brand panel. Ignored by 'card'.
|
|
61
|
+
*/
|
|
62
|
+
branding,
|
|
63
|
+
urls,
|
|
64
|
+
paths,
|
|
65
|
+
protocol,
|
|
66
|
+
contactField,
|
|
67
|
+
usernameDomain = null,
|
|
68
|
+
showDevOtp = false,
|
|
69
|
+
onError,
|
|
70
|
+
|
|
71
|
+
/** Step one. */
|
|
72
|
+
showContactHint = false,
|
|
73
|
+
contactHints,
|
|
74
|
+
loginCopy,
|
|
75
|
+
loginLabels,
|
|
76
|
+
|
|
77
|
+
/** Step two. */
|
|
78
|
+
resendCooldown = 60,
|
|
79
|
+
showMaskedContact = false,
|
|
80
|
+
showContactField = false,
|
|
81
|
+
minimalResponse = true,
|
|
82
|
+
verifyCopy,
|
|
83
|
+
verifyLabels,
|
|
84
|
+
|
|
85
|
+
/** Called with `(user, token, rawResponse)` on a successful verification. */
|
|
86
|
+
onAuthenticated,
|
|
87
|
+
|
|
88
|
+
/** Notified with the challenge each time a code is sent. */
|
|
89
|
+
onCodeSent,
|
|
90
|
+
|
|
91
|
+
/** Extra props for either step, merged last. */
|
|
92
|
+
loginProps = {},
|
|
93
|
+
verifyProps = {},
|
|
94
|
+
}) => {
|
|
95
|
+
const [challenge, setChallenge] = useState(null);
|
|
96
|
+
// What the user typed, kept across the back link so returning to step one
|
|
97
|
+
// does not blank the field.
|
|
98
|
+
const [contact, setContact] = useState('');
|
|
99
|
+
|
|
100
|
+
if (challenge) {
|
|
101
|
+
return (
|
|
102
|
+
<ClientOTPVerify
|
|
103
|
+
logo={logo}
|
|
104
|
+
layout={layout}
|
|
105
|
+
loginBg={loginBg}
|
|
106
|
+
branding={branding}
|
|
107
|
+
urls={urls}
|
|
108
|
+
paths={paths}
|
|
109
|
+
protocol={protocol}
|
|
110
|
+
contactField={contactField}
|
|
111
|
+
challenge={challenge}
|
|
112
|
+
copy={verifyCopy}
|
|
113
|
+
labels={verifyLabels}
|
|
114
|
+
resendCooldown={resendCooldown}
|
|
115
|
+
showMaskedContact={showMaskedContact}
|
|
116
|
+
showContactField={showContactField}
|
|
117
|
+
showDevOtp={showDevOtp}
|
|
118
|
+
minimalResponse={minimalResponse}
|
|
119
|
+
onAuthenticated={onAuthenticated}
|
|
120
|
+
onError={onError}
|
|
121
|
+
// The back link clears the challenge rather than navigating.
|
|
122
|
+
// Passing it is also what stops the verify screen redirecting
|
|
123
|
+
// when it decides it has nothing to answer.
|
|
124
|
+
onBack={() => setChallenge(null)}
|
|
125
|
+
{...verifyProps}
|
|
126
|
+
/>
|
|
127
|
+
);
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
return (
|
|
131
|
+
<ClientLogin
|
|
132
|
+
logo={logo}
|
|
133
|
+
layout={layout}
|
|
134
|
+
loginBg={loginBg}
|
|
135
|
+
branding={branding}
|
|
136
|
+
urls={urls}
|
|
137
|
+
paths={paths}
|
|
138
|
+
protocol={protocol}
|
|
139
|
+
contactField={contactField}
|
|
140
|
+
usernameDomain={usernameDomain}
|
|
141
|
+
showContactHint={showContactHint}
|
|
142
|
+
contactHints={contactHints}
|
|
143
|
+
showDevOtp={showDevOtp}
|
|
144
|
+
copy={loginCopy}
|
|
145
|
+
labels={loginLabels}
|
|
146
|
+
initialContact={contact}
|
|
147
|
+
onError={onError}
|
|
148
|
+
onChallengeIssued={(issued) => {
|
|
149
|
+
setContact(issued.rawContact || '');
|
|
150
|
+
setChallenge(issued);
|
|
151
|
+
|
|
152
|
+
if (typeof onCodeSent === 'function') {
|
|
153
|
+
onCodeSent(issued);
|
|
154
|
+
}
|
|
155
|
+
}}
|
|
156
|
+
{...loginProps}
|
|
157
|
+
/>
|
|
158
|
+
);
|
|
159
|
+
};
|
|
160
|
+
|
|
161
|
+
export default ClientAuth;
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
|
|
3
|
+
import AuthBrandPanel from './AuthBrandPanel';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* The frame around a client sign-in step.
|
|
7
|
+
*
|
|
8
|
+
* Both steps render the same children — logo, title, subtitle, form — and the
|
|
9
|
+
* only thing `layout` changes is what sits around them. Keeping that choice
|
|
10
|
+
* here means neither ClientLogin nor ClientOTPVerify has to know there are two
|
|
11
|
+
* layouts, and the form markup stays untouched by the design work.
|
|
12
|
+
*
|
|
13
|
+
* `card` (default) — the original: a box centred in whatever vertical space
|
|
14
|
+
* the host page happens to give it. Byte-identical to
|
|
15
|
+
* what every consumer rendered before 6.8.3.
|
|
16
|
+
* `split` — the staff sign-in's language: the brand on one side, the task on
|
|
17
|
+
* the other. A SECTION rather than a screen, sized by its parent —
|
|
18
|
+
* the consumer keeps its own header and footer and hands this the
|
|
19
|
+
* space between, which is why there is no `100vh` in its styles.
|
|
20
|
+
*/
|
|
21
|
+
const ClientAuthFrame = ({
|
|
22
|
+
layout = 'card',
|
|
23
|
+
styles,
|
|
24
|
+
loginBg,
|
|
25
|
+
branding = {},
|
|
26
|
+
children,
|
|
27
|
+
}) => {
|
|
28
|
+
if (layout === 'split') {
|
|
29
|
+
return (
|
|
30
|
+
<section className={styles.split}>
|
|
31
|
+
<AuthBrandPanel
|
|
32
|
+
styles={styles}
|
|
33
|
+
loginBg={loginBg}
|
|
34
|
+
branding={branding}
|
|
35
|
+
/>
|
|
36
|
+
|
|
37
|
+
<div className={styles.splitPane}>
|
|
38
|
+
<div className={styles.splitCard}>
|
|
39
|
+
{children}
|
|
40
|
+
|
|
41
|
+
{branding.footer ? (
|
|
42
|
+
<p className={styles.splitFoot}>
|
|
43
|
+
{branding.footer}
|
|
44
|
+
</p>
|
|
45
|
+
) : null}
|
|
46
|
+
</div>
|
|
47
|
+
</div>
|
|
48
|
+
</section>
|
|
49
|
+
);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
return (
|
|
53
|
+
<div className={styles.container}>
|
|
54
|
+
<div className={styles.loginBox}>{children}</div>
|
|
55
|
+
</div>
|
|
56
|
+
);
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
export default ClientAuthFrame;
|
|
@@ -1,19 +1,146 @@
|
|
|
1
1
|
import '../styles/global.css';
|
|
2
2
|
|
|
3
3
|
import React, { useState } from 'react';
|
|
4
|
-
import { useLocation, useNavigate } from 'react-router-dom';
|
|
5
4
|
import { toast } from 'react-toastify';
|
|
6
5
|
import { User } from 'lucide-react';
|
|
7
6
|
|
|
8
7
|
import CustomFetch from '../Fetch';
|
|
8
|
+
import useOptionalRouter from './useOptionalRouter';
|
|
9
|
+
import ClientAuthFrame from './ClientAuthFrame';
|
|
10
|
+
import { resolveClientPaths } from './authEndpoints';
|
|
11
|
+
import {
|
|
12
|
+
applyUsernameDomain,
|
|
13
|
+
contactHint,
|
|
14
|
+
normaliseAuthError,
|
|
15
|
+
} from './authHelpers';
|
|
16
|
+
import { resolveClientProtocol } from './clientAuthProtocols';
|
|
9
17
|
|
|
10
18
|
import styles from '../styles/ClientAuth.module.scss';
|
|
11
19
|
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
20
|
+
/**
|
|
21
|
+
* Client portal sign-in, step one: identify yourself, receive a code.
|
|
22
|
+
*
|
|
23
|
+
* Three things about this screen are configurable rather than assumed, because
|
|
24
|
+
* each was a blocker for the Next.js portal in 6.7.0:
|
|
25
|
+
*
|
|
26
|
+
* - the WIRE PROTOCOL (`protocol`), because the two backends in the wild
|
|
27
|
+
* disagree about the request body and about how success is signalled. See
|
|
28
|
+
* clientAuthProtocols.js.
|
|
29
|
+
* - the HAND-OFF to step two (`onChallengeIssued`), because a router
|
|
30
|
+
* navigation carrying router state is one way to get there, not the only
|
|
31
|
+
* one. An app rendering both steps on a single page just holds the
|
|
32
|
+
* challenge in its own state.
|
|
33
|
+
* - NAVIGATION (`onNavigate`), because react-router is not a given. The
|
|
34
|
+
* router hooks are consulted only when nothing else was supplied, and only
|
|
35
|
+
* when a Router actually exists.
|
|
36
|
+
*
|
|
37
|
+
* With none of those passed, this renders and behaves exactly as 6.6.3 did.
|
|
38
|
+
*/
|
|
39
|
+
|
|
40
|
+
const DEFAULT_COPY = {
|
|
41
|
+
title: 'Client Login',
|
|
42
|
+
subtitle:
|
|
43
|
+
'Enter your email address or mobile number to receive a verification code',
|
|
44
|
+
fieldLabel: 'Email or Mobile',
|
|
45
|
+
placeholder: 'Enter your email address or mobile number',
|
|
46
|
+
/** Client-side validation. */
|
|
47
|
+
required: 'Please enter your email address or mobile number',
|
|
48
|
+
/** Fallback when the server declines without saying why. */
|
|
49
|
+
notFound:
|
|
50
|
+
'Could not find an account with this email address or mobile number.',
|
|
51
|
+
sending: 'Sending verification code...',
|
|
52
|
+
sent: 'Verification code sent',
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Button labels. These are the 6.6.3 strings, NOT the portal's — the
|
|
57
|
+
* backward-compatibility rule outranks matching the portal's wording, so an app
|
|
58
|
+
* that wants "Send Code" / "Please wait..." asks for them:
|
|
59
|
+
* labels={{ submit: 'Send Code', submitting: 'Please wait...' }}
|
|
60
|
+
*/
|
|
61
|
+
const DEFAULT_LABELS = {
|
|
62
|
+
submit: 'Continue',
|
|
63
|
+
submitting: 'Sending...',
|
|
64
|
+
};
|
|
65
|
+
|
|
66
|
+
const ClientLogin = ({
|
|
67
|
+
logo,
|
|
68
|
+
setSystemAuth,
|
|
69
|
+
/** Endpoint map — `urls.login` is the request this screen makes. */
|
|
70
|
+
urls = {},
|
|
71
|
+
/** Route paths; `/client/*` unless overridden. See authEndpoints.js. */
|
|
72
|
+
paths,
|
|
73
|
+
/**
|
|
74
|
+
* Wire protocol: `'uuid'` (default, unchanged) or `'contact'` for
|
|
75
|
+
* ThroughLife's `/api/auth/*`. An object of overrides is accepted too.
|
|
76
|
+
*/
|
|
77
|
+
protocol,
|
|
78
|
+
/** Merged over DEFAULT_COPY. */
|
|
79
|
+
copy: copyOverrides,
|
|
80
|
+
/** Merged over DEFAULT_LABELS. */
|
|
81
|
+
labels: labelOverrides,
|
|
82
|
+
/** Show the live "Email address detected" hint under the field. */
|
|
83
|
+
showContactHint = false,
|
|
84
|
+
/** Merged over DEFAULT_CONTACT_HINTS in authHelpers.js. */
|
|
85
|
+
contactHints,
|
|
86
|
+
/**
|
|
87
|
+
* Complete a bare username to `name@domain` before sending, the way the
|
|
88
|
+
* portal does (`virtualportal.com.au`). Null leaves the input untouched.
|
|
89
|
+
*/
|
|
90
|
+
usernameDomain = null,
|
|
91
|
+
/**
|
|
92
|
+
* Overrides the protocol's own request-body key for the identifier. Left
|
|
93
|
+
* unset, `uuid` sends `email` and `contact` sends `contact`.
|
|
94
|
+
*/
|
|
95
|
+
contactField,
|
|
96
|
+
/**
|
|
97
|
+
* Carry the server's dev OTP through to the verify screen so it can offer
|
|
98
|
+
* an auto-fill. Only ever honoured outside production.
|
|
99
|
+
*/
|
|
100
|
+
showDevOtp = false,
|
|
101
|
+
/** Initial field value — used by the "keep what I typed" back link. */
|
|
102
|
+
initialContact,
|
|
103
|
+
/**
|
|
104
|
+
* 'card' (default, unchanged) or 'split' — the two-pane brand layout. See
|
|
105
|
+
* ClientAuthFrame; the form itself is identical either way.
|
|
106
|
+
*/
|
|
107
|
+
layout = 'card',
|
|
108
|
+
/** Split layout: the brand panel's photograph. */
|
|
109
|
+
loginBg,
|
|
110
|
+
/** Split layout: {eyebrow, name, tagline, footer}. */
|
|
111
|
+
branding,
|
|
112
|
+
/**
|
|
113
|
+
* Called with the whole challenge once a code has been sent. When present,
|
|
114
|
+
* this screen does NOT navigate: the consumer owns what happens next.
|
|
115
|
+
*
|
|
116
|
+
* The challenge is `{contact, rawContact, challengeId, maskedContact,
|
|
117
|
+
* contactMethod, devOtp, message, previousUrl}` — everything
|
|
118
|
+
* ClientOTPVerify needs, in the shape its `challenge` prop takes.
|
|
119
|
+
*/
|
|
120
|
+
onChallengeIssued,
|
|
121
|
+
/**
|
|
122
|
+
* Navigation, when this screen still owns it. `(path, options) => void`.
|
|
123
|
+
* Falls back to react-router's navigate, then to a plain page load.
|
|
124
|
+
*/
|
|
125
|
+
onNavigate,
|
|
126
|
+
/** Notified with the normalised message when the request is declined. */
|
|
127
|
+
onError,
|
|
128
|
+
}) => {
|
|
129
|
+
const { navigate, location } = useOptionalRouter();
|
|
130
|
+
|
|
131
|
+
const routes = resolveClientPaths(paths);
|
|
132
|
+
const wire = resolveClientProtocol(protocol);
|
|
133
|
+
const copy = { ...DEFAULT_COPY, ...(copyOverrides || {}) };
|
|
134
|
+
const labels = { ...DEFAULT_LABELS, ...(labelOverrides || {}) };
|
|
135
|
+
const field = contactField || wire.contactField;
|
|
136
|
+
const previousUrl = location?.state?.previousUrl || '';
|
|
137
|
+
|
|
15
138
|
const [isLoading, setIsLoading] = useState(false);
|
|
16
|
-
|
|
139
|
+
// Prefilled when the verify screen sent the user back with its "keep what I
|
|
140
|
+
// typed" back link. Nothing sets it otherwise, so this is '' as before.
|
|
141
|
+
const [email, setEmail] = useState(
|
|
142
|
+
initialContact ?? location?.state?.email ?? ''
|
|
143
|
+
);
|
|
17
144
|
const [inputError, setInputError] = useState(false);
|
|
18
145
|
|
|
19
146
|
const handleChange = (e) => {
|
|
@@ -21,77 +148,156 @@ const ClientLogin = ({ logo, setSystemAuth, urls = {} }) => {
|
|
|
21
148
|
setInputError(false);
|
|
22
149
|
};
|
|
23
150
|
|
|
151
|
+
/** Move, by whichever mechanism this app actually has. */
|
|
152
|
+
const go = (path, options) => {
|
|
153
|
+
if (typeof onNavigate === 'function') {
|
|
154
|
+
onNavigate(path, options);
|
|
155
|
+
|
|
156
|
+
return;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
if (navigate) {
|
|
160
|
+
navigate(path, options);
|
|
161
|
+
|
|
162
|
+
return;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
if (typeof window !== 'undefined') {
|
|
166
|
+
window.location.href = path;
|
|
167
|
+
}
|
|
168
|
+
};
|
|
169
|
+
|
|
170
|
+
const fail = (error) => {
|
|
171
|
+
const message = normaliseAuthError(error, copy.notFound);
|
|
172
|
+
|
|
173
|
+
setInputError(true);
|
|
174
|
+
toast.error(message);
|
|
175
|
+
|
|
176
|
+
if (typeof onError === 'function') {
|
|
177
|
+
onError(message);
|
|
178
|
+
}
|
|
179
|
+
};
|
|
180
|
+
|
|
24
181
|
const handleSubmit = async (e) => {
|
|
25
182
|
e.preventDefault();
|
|
26
183
|
|
|
184
|
+
// The button is disabled too; this is the belt to that pair of braces,
|
|
185
|
+
// since a form can also be submitted by Enter in the field.
|
|
186
|
+
if (isLoading) {
|
|
187
|
+
return;
|
|
188
|
+
}
|
|
189
|
+
|
|
27
190
|
if (!email.trim()) {
|
|
28
191
|
setInputError(true);
|
|
29
|
-
toast.error(
|
|
192
|
+
toast.error(copy.required);
|
|
30
193
|
return;
|
|
31
194
|
}
|
|
32
195
|
|
|
33
196
|
setIsLoading(true);
|
|
34
|
-
toast.info(
|
|
197
|
+
toast.info(copy.sending, { autoClose: 2000 });
|
|
198
|
+
|
|
199
|
+
const rawContact = email.trim();
|
|
200
|
+
// A no-op unless `usernameDomain` is set.
|
|
201
|
+
const contact = applyUsernameDomain(rawContact, usernameDomain);
|
|
35
202
|
|
|
36
203
|
try {
|
|
37
|
-
const
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
204
|
+
const result = await CustomFetch(
|
|
205
|
+
urls.login,
|
|
206
|
+
'POST',
|
|
207
|
+
wire.requestBody({
|
|
208
|
+
contact,
|
|
209
|
+
contactField: field,
|
|
210
|
+
previousUrl,
|
|
211
|
+
resend: false,
|
|
212
|
+
})
|
|
213
|
+
);
|
|
45
214
|
|
|
46
|
-
|
|
215
|
+
const body = result.data;
|
|
216
|
+
|
|
217
|
+
// Cleared per-branch: on success this screen hands over to step
|
|
218
|
+
// two, and dropping the button back to "Continue" first left a gap
|
|
219
|
+
// where a second code could be requested.
|
|
220
|
+
if (!wire.isSuccess(body)) {
|
|
221
|
+
setIsLoading(false);
|
|
222
|
+
|
|
223
|
+
if (typeof setSystemAuth === 'function') {
|
|
224
|
+
setSystemAuth(false);
|
|
225
|
+
}
|
|
47
226
|
|
|
48
|
-
|
|
49
|
-
//
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
227
|
+
// `{error: '…'}` bodies are as common here as `{message}` ones,
|
|
228
|
+
// and only one of them used to reach the user.
|
|
229
|
+
fail(body);
|
|
230
|
+
|
|
231
|
+
return;
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
const read = wire.readChallenge(body);
|
|
235
|
+
|
|
236
|
+
// Everything step two needs, in one object. Both forms of the
|
|
237
|
+
// identifier travel: which one the verify call sends is the
|
|
238
|
+
// protocol's decision, not this screen's.
|
|
239
|
+
const challenge = {
|
|
240
|
+
contact,
|
|
241
|
+
rawContact,
|
|
242
|
+
challengeId: read.challengeId,
|
|
243
|
+
maskedContact: read.maskedContact,
|
|
244
|
+
contactMethod: read.contactMethod,
|
|
245
|
+
message: read.message,
|
|
246
|
+
previousUrl,
|
|
247
|
+
// Never in a production build, and never without the opt-in —
|
|
248
|
+
// the panel that renders it is gated a second time.
|
|
249
|
+
devOtp:
|
|
250
|
+
process.env.NODE_ENV !== 'production' &&
|
|
251
|
+
(showDevOtp || process.env.NODE_ENV === 'development')
|
|
252
|
+
? read.devOtp
|
|
253
|
+
: null,
|
|
254
|
+
};
|
|
255
|
+
|
|
256
|
+
toast.success(read.message || copy.sent);
|
|
257
|
+
|
|
258
|
+
if (typeof onChallengeIssued === 'function') {
|
|
259
|
+
onChallengeIssued(challenge);
|
|
260
|
+
|
|
261
|
+
return;
|
|
73
262
|
}
|
|
263
|
+
|
|
264
|
+
// Router state is the 6.6.3 hand-off. Both the old key spellings
|
|
265
|
+
// and the whole challenge are written, so an app already reading
|
|
266
|
+
// `state.uuid` keeps working.
|
|
267
|
+
go(routes.verify, {
|
|
268
|
+
state: {
|
|
269
|
+
email: rawContact,
|
|
270
|
+
contact: challenge.contact,
|
|
271
|
+
uuid: challenge.challengeId,
|
|
272
|
+
challenge,
|
|
273
|
+
maskedContact: challenge.maskedContact,
|
|
274
|
+
contactMethod: challenge.contactMethod,
|
|
275
|
+
previousUrl,
|
|
276
|
+
...(challenge.devOtp ? { otp: challenge.devOtp } : {}),
|
|
277
|
+
},
|
|
278
|
+
});
|
|
74
279
|
} catch (error) {
|
|
75
280
|
setIsLoading(false);
|
|
76
|
-
|
|
77
|
-
toast.error(String(error));
|
|
281
|
+
fail(error);
|
|
78
282
|
}
|
|
79
283
|
};
|
|
80
284
|
|
|
81
285
|
return (
|
|
82
|
-
<
|
|
83
|
-
|
|
84
|
-
|
|
286
|
+
<ClientAuthFrame
|
|
287
|
+
layout={layout}
|
|
288
|
+
styles={styles}
|
|
289
|
+
loginBg={loginBg}
|
|
290
|
+
branding={branding}
|
|
291
|
+
>
|
|
292
|
+
<img src={logo} alt="Client Portal" className={styles.logo} />
|
|
85
293
|
|
|
86
|
-
<h1 className={styles.title}>
|
|
87
|
-
<p className={styles.subtitle}>
|
|
88
|
-
Enter your email address or mobile number to receive a verification code
|
|
89
|
-
</p>
|
|
294
|
+
<h1 className={styles.title}>{copy.title}</h1>
|
|
295
|
+
<p className={styles.subtitle}>{copy.subtitle}</p>
|
|
90
296
|
|
|
91
297
|
<form onSubmit={handleSubmit}>
|
|
92
298
|
<div className={styles.formGroup}>
|
|
93
299
|
<label htmlFor="email" className={styles.inputLabel}>
|
|
94
|
-
|
|
300
|
+
{copy.fieldLabel}
|
|
95
301
|
</label>
|
|
96
302
|
<input
|
|
97
303
|
id="email"
|
|
@@ -102,12 +308,18 @@ const ClientLogin = ({ logo, setSystemAuth, urls = {} }) => {
|
|
|
102
308
|
className={`${styles.input} ${
|
|
103
309
|
inputError ? styles.error : ''
|
|
104
310
|
}`}
|
|
105
|
-
placeholder=
|
|
311
|
+
placeholder={copy.placeholder}
|
|
106
312
|
autoComplete="username"
|
|
313
|
+
disabled={isLoading}
|
|
107
314
|
/>
|
|
108
315
|
<span className={styles.inputIcon}>
|
|
109
316
|
<User strokeWidth={2} size={18} />
|
|
110
317
|
</span>
|
|
318
|
+
{showContactHint ? (
|
|
319
|
+
<p className={styles.inputHint}>
|
|
320
|
+
{contactHint(email, contactHints)}
|
|
321
|
+
</p>
|
|
322
|
+
) : null}
|
|
111
323
|
</div>
|
|
112
324
|
|
|
113
325
|
<button
|
|
@@ -115,11 +327,10 @@ const ClientLogin = ({ logo, setSystemAuth, urls = {} }) => {
|
|
|
115
327
|
type="submit"
|
|
116
328
|
disabled={isLoading}
|
|
117
329
|
>
|
|
118
|
-
{isLoading ?
|
|
330
|
+
{isLoading ? labels.submitting : labels.submit}
|
|
119
331
|
</button>
|
|
120
332
|
</form>
|
|
121
|
-
|
|
122
|
-
</div>
|
|
333
|
+
</ClientAuthFrame>
|
|
123
334
|
);
|
|
124
335
|
};
|
|
125
336
|
|