@visns-studio/visns-components 6.6.4 → 6.15.1

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 (98) hide show
  1. package/README.md +1623 -18
  2. package/package.json +10 -4
  3. package/src/components/DataGrid.jsx +181 -38
  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/ColumnRenderers.jsx +53 -7
  32. package/src/components/generic/GenericAuth.jsx +163 -96
  33. package/src/components/generic/GenericDetail.jsx +215 -90
  34. package/src/components/generic/GenericIndex.jsx +20 -47
  35. package/src/components/generic/GenericMain.jsx +5 -0
  36. package/src/components/generic/GroupedReportRenderer.jsx +1 -5
  37. package/src/components/generic/StandardModal.jsx +17 -5
  38. package/src/components/generic/reportSemanticSteps/SemanticEntityStep.jsx +1 -6
  39. package/src/components/generic/reportSemanticSteps/SemanticFieldsStep.jsx +4 -11
  40. package/src/components/generic/reportSemanticSteps/SemanticFiltersStep.jsx +9 -27
  41. package/src/components/generic/reportSemanticSteps/SemanticGroupingStep.jsx +5 -13
  42. package/src/components/generic/reportSemanticSteps/SemanticParameterPrompt.jsx +5 -13
  43. package/src/components/generic/reportSemanticSteps/SemanticPreviewStep.jsx +14 -31
  44. package/src/components/generic/reportSemanticSteps/SemanticRelationsStep.jsx +3 -9
  45. package/src/components/generic/reportSemanticSteps/SemanticValueInput.jsx +2 -15
  46. package/src/components/navActive.js +60 -0
  47. package/src/components/notify/desktopNotifications.js +256 -0
  48. package/src/components/sketch/SketchField.jsx +12 -2
  49. package/src/components/sms/SmsComposeModal.jsx +495 -0
  50. package/src/components/sms/SmsInbox.jsx +596 -0
  51. package/src/components/sms/SmsInboxBadge.jsx +468 -0
  52. package/src/components/sms/SmsLineSettings.jsx +854 -0
  53. package/src/components/sms/SmsThreadPanel.jsx +1184 -0
  54. package/src/components/sms/smsEndpoints.js +56 -0
  55. package/src/components/sms/smsHelpers.js +1075 -0
  56. package/src/components/sms/smsLiveState.js +132 -0
  57. package/src/components/sms/useSmsLive.js +307 -0
  58. package/src/components/styles/CallQueuePop.module.scss +646 -0
  59. package/src/components/styles/CallQueueSettings.module.scss +460 -0
  60. package/src/components/styles/ClientAuth.module.scss +229 -0
  61. package/src/components/styles/GenericDetail.module.scss +153 -26
  62. package/src/components/styles/GenericIndex.module.scss +13 -0
  63. package/src/components/styles/GenericMain.module.scss +19 -2
  64. package/src/components/styles/ImpersonateGate.module.scss +85 -0
  65. package/src/components/styles/Login.module.scss +13 -122
  66. package/src/components/styles/LogoutScreen.module.scss +89 -0
  67. package/src/components/styles/Navigation.module.scss +509 -223
  68. package/src/components/styles/Notification.module.scss +103 -0
  69. package/src/components/styles/Reset.module.scss +52 -186
  70. package/src/components/styles/Sms.module.scss +2382 -0
  71. package/src/components/styles/TableFilter.module.scss +118 -98
  72. package/src/components/styles/TwoFactorAuth.module.scss +115 -176
  73. package/src/components/styles/Vault.module.scss +1983 -0
  74. package/src/components/styles/Verify.module.scss +57 -180
  75. package/src/components/styles/_authBrandPanel.scss +163 -0
  76. package/src/components/styles/_authShell.scss +369 -0
  77. package/src/components/styles/global-datagrid.css +27 -2
  78. package/src/components/utils/buildEnv.js +67 -0
  79. package/src/components/utils/displayValue.js +94 -0
  80. package/src/components/utils/useDensity.js +345 -9
  81. package/src/components/vault/OtpChip.jsx +208 -0
  82. package/src/components/vault/PasswordGenerator.jsx +145 -0
  83. package/src/components/vault/QrScanner.jsx +326 -0
  84. package/src/components/vault/VaultAccessLog.jsx +225 -0
  85. package/src/components/vault/VaultConfirmPanel.jsx +113 -0
  86. package/src/components/vault/VaultEntryForm.jsx +740 -0
  87. package/src/components/vault/VaultManager.jsx +1000 -0
  88. package/src/components/vault/VaultQuickSearch.jsx +681 -0
  89. package/src/components/vault/qrDecode.js +257 -0
  90. package/src/components/vault/useDebouncedValue.js +22 -0
  91. package/src/components/vault/useVaultReveal.js +160 -0
  92. package/src/components/vault/vaultClipboard.js +33 -0
  93. package/src/components/vault/vaultEndpoints.js +40 -0
  94. package/src/components/vault/vaultFit.js +116 -0
  95. package/src/components/vault/vaultHelpers.js +547 -0
  96. package/src/components/vault/vaultNavigation.jsx +58 -0
  97. package/src/components/vault/vaultOtp.js +105 -0
  98. package/src/index.js +233 -1
@@ -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