@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.
Files changed (100) hide show
  1. package/README.md +1658 -18
  2. package/package.json +10 -4
  3. package/src/components/DataGrid.jsx +619 -158
  4. package/src/components/Fetch.jsx +80 -3
  5. package/src/components/Form.jsx +9 -0
  6. package/src/components/Navigation.jsx +109 -50
  7. package/src/components/Notification.jsx +279 -9
  8. package/src/components/TableFilter.jsx +14 -1
  9. package/src/components/auth/AuthBrandPanel.jsx +43 -0
  10. package/src/components/auth/AuthLoading.jsx +78 -0
  11. package/src/components/auth/AuthShell.jsx +59 -0
  12. package/src/components/auth/ClientAuth.jsx +161 -0
  13. package/src/components/auth/ClientAuthFrame.jsx +59 -0
  14. package/src/components/auth/ClientLogin.jsx +266 -55
  15. package/src/components/auth/ClientOTPVerify.jsx +587 -115
  16. package/src/components/auth/ImpersonateGate.jsx +254 -0
  17. package/src/components/auth/Login.jsx +134 -41
  18. package/src/components/auth/LogoutScreen.jsx +112 -0
  19. package/src/components/auth/Reset.jsx +134 -77
  20. package/src/components/auth/TwoFactorAuth.jsx +475 -297
  21. package/src/components/auth/Verify.jsx +237 -126
  22. package/src/components/auth/authEndpoints.js +105 -0
  23. package/src/components/auth/authFont.js +23 -0
  24. package/src/components/auth/authHelpers.js +465 -0
  25. package/src/components/auth/clientAuthProtocols.js +240 -0
  26. package/src/components/auth/useOptionalRouter.js +49 -0
  27. package/src/components/callQueue/CallQueuePop.jsx +1502 -0
  28. package/src/components/callQueue/CallQueueSettings.jsx +508 -0
  29. package/src/components/callQueue/callQueueHelpers.js +280 -0
  30. package/src/components/callQueue/callQueueSettingsHelpers.js +75 -0
  31. package/src/components/columns/AutoGrowCell.jsx +141 -0
  32. package/src/components/columns/ColumnRenderers.jsx +53 -7
  33. package/src/components/generic/GenericAuth.jsx +163 -96
  34. package/src/components/generic/GenericDetail.jsx +215 -90
  35. package/src/components/generic/GenericIndex.jsx +20 -47
  36. package/src/components/generic/GenericMain.jsx +5 -0
  37. package/src/components/generic/GroupedReportRenderer.jsx +1 -5
  38. package/src/components/generic/StandardModal.jsx +17 -5
  39. package/src/components/generic/reportSemanticSteps/SemanticEntityStep.jsx +1 -6
  40. package/src/components/generic/reportSemanticSteps/SemanticFieldsStep.jsx +4 -11
  41. package/src/components/generic/reportSemanticSteps/SemanticFiltersStep.jsx +9 -27
  42. package/src/components/generic/reportSemanticSteps/SemanticGroupingStep.jsx +5 -13
  43. package/src/components/generic/reportSemanticSteps/SemanticParameterPrompt.jsx +5 -13
  44. package/src/components/generic/reportSemanticSteps/SemanticPreviewStep.jsx +14 -31
  45. package/src/components/generic/reportSemanticSteps/SemanticRelationsStep.jsx +3 -9
  46. package/src/components/generic/reportSemanticSteps/SemanticValueInput.jsx +2 -15
  47. package/src/components/navActive.js +60 -0
  48. package/src/components/notify/desktopNotifications.js +256 -0
  49. package/src/components/sketch/SketchField.jsx +12 -2
  50. package/src/components/sms/SmsComposeModal.jsx +495 -0
  51. package/src/components/sms/SmsInbox.jsx +596 -0
  52. package/src/components/sms/SmsInboxBadge.jsx +468 -0
  53. package/src/components/sms/SmsLineSettings.jsx +854 -0
  54. package/src/components/sms/SmsThreadPanel.jsx +1184 -0
  55. package/src/components/sms/smsEndpoints.js +56 -0
  56. package/src/components/sms/smsHelpers.js +1075 -0
  57. package/src/components/sms/smsLiveState.js +132 -0
  58. package/src/components/sms/useSmsLive.js +307 -0
  59. package/src/components/styles/CallQueuePop.module.scss +646 -0
  60. package/src/components/styles/CallQueueSettings.module.scss +460 -0
  61. package/src/components/styles/ClientAuth.module.scss +229 -0
  62. package/src/components/styles/DataGrid.module.scss +10 -0
  63. package/src/components/styles/GenericDetail.module.scss +153 -26
  64. package/src/components/styles/GenericIndex.module.scss +13 -0
  65. package/src/components/styles/GenericMain.module.scss +19 -2
  66. package/src/components/styles/ImpersonateGate.module.scss +85 -0
  67. package/src/components/styles/Login.module.scss +13 -122
  68. package/src/components/styles/LogoutScreen.module.scss +89 -0
  69. package/src/components/styles/Navigation.module.scss +509 -223
  70. package/src/components/styles/Notification.module.scss +103 -0
  71. package/src/components/styles/Reset.module.scss +52 -186
  72. package/src/components/styles/Sms.module.scss +2382 -0
  73. package/src/components/styles/TableFilter.module.scss +118 -98
  74. package/src/components/styles/TwoFactorAuth.module.scss +115 -176
  75. package/src/components/styles/Vault.module.scss +1983 -0
  76. package/src/components/styles/Verify.module.scss +57 -180
  77. package/src/components/styles/_authBrandPanel.scss +163 -0
  78. package/src/components/styles/_authShell.scss +369 -0
  79. package/src/components/styles/global-datagrid.css +29 -0
  80. package/src/components/utils/buildEnv.js +67 -0
  81. package/src/components/utils/displayValue.js +94 -0
  82. package/src/components/utils/useDensity.js +345 -9
  83. package/src/components/vault/OtpChip.jsx +208 -0
  84. package/src/components/vault/PasswordGenerator.jsx +145 -0
  85. package/src/components/vault/QrScanner.jsx +326 -0
  86. package/src/components/vault/VaultAccessLog.jsx +225 -0
  87. package/src/components/vault/VaultConfirmPanel.jsx +113 -0
  88. package/src/components/vault/VaultEntryForm.jsx +740 -0
  89. package/src/components/vault/VaultManager.jsx +1000 -0
  90. package/src/components/vault/VaultQuickSearch.jsx +681 -0
  91. package/src/components/vault/qrDecode.js +257 -0
  92. package/src/components/vault/useDebouncedValue.js +22 -0
  93. package/src/components/vault/useVaultReveal.js +160 -0
  94. package/src/components/vault/vaultClipboard.js +33 -0
  95. package/src/components/vault/vaultEndpoints.js +40 -0
  96. package/src/components/vault/vaultFit.js +116 -0
  97. package/src/components/vault/vaultHelpers.js +547 -0
  98. package/src/components/vault/vaultNavigation.jsx +58 -0
  99. package/src/components/vault/vaultOtp.js +105 -0
  100. 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
- const ClientLogin = ({ logo, setSystemAuth, urls = {} }) => {
13
- const location = useLocation();
14
- const navigate = useNavigate();
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
- const [email, setEmail] = useState('');
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('Please enter your email address or mobile number');
192
+ toast.error(copy.required);
30
193
  return;
31
194
  }
32
195
 
33
196
  setIsLoading(true);
34
- toast.info('Sending verification code...', { autoClose: 2000 });
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 loginUrl = urls.login;
38
- const result = await CustomFetch(loginUrl, 'POST', {
39
- email: email,
40
- location:
41
- location.state && location.state.previousUrl
42
- ? location.state.previousUrl
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
- setIsLoading(false);
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
- if (result.data && result.data.success) {
49
- // Redirect to OTP verification page with email and uuid
50
- const verifyUrl = '/client/verify';
51
- navigate(verifyUrl, {
52
- state: {
53
- email: email,
54
- uuid: result.data.uuid,
55
- previousUrl: location.state?.previousUrl || '',
56
- // In development, we can pass the OTP for testing
57
- ...(process.env.NODE_ENV === 'development' && {
58
- otp: result.data.otp,
59
- }),
60
- },
61
- });
62
- toast.success(
63
- result.data.message ||
64
- 'Verification code sent'
65
- );
66
- } else {
67
- setSystemAuth(false);
68
- setInputError(true);
69
- toast.error(
70
- (result.data && result.data.message) ||
71
- 'Could not find an account with this email address or mobile number.'
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
- setInputError(true);
77
- toast.error(String(error));
281
+ fail(error);
78
282
  }
79
283
  };
80
284
 
81
285
  return (
82
- <div className={styles.container}>
83
- <div className={styles.loginBox}>
84
- <img src={logo} alt="Client Portal" className={styles.logo} />
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}>Client Login</h1>
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
- Email or Mobile
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="Enter your email address or mobile number"
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 ? 'Sending...' : 'Continue'}
330
+ {isLoading ? labels.submitting : labels.submit}
119
331
  </button>
120
332
  </form>
121
- </div>
122
- </div>
333
+ </ClientAuthFrame>
123
334
  );
124
335
  };
125
336