@striae-org/striae 5.3.1 → 5.4.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/.env.example +3 -0
  2. package/app/components/actions/generate-pdf.ts +22 -0
  3. package/app/components/auth/auth.module.css +531 -0
  4. package/app/components/auth/mfa-enrollment.tsx +132 -79
  5. package/app/components/auth/mfa-totp-enrollment.tsx +231 -0
  6. package/app/components/auth/mfa-verification.tsx +155 -33
  7. package/app/components/{sidebar/cases/cases-modal.tsx → navbar/case-modals/all-cases-modal.tsx} +4 -4
  8. package/app/components/navbar/case-modals/archive-case-modal.tsx +9 -10
  9. package/app/components/navbar/case-modals/case-modal-shared.module.css +88 -0
  10. package/app/components/navbar/case-modals/delete-case-modal.tsx +9 -10
  11. package/app/components/navbar/case-modals/export-case-modal.tsx +9 -10
  12. package/app/components/navbar/case-modals/export-confirmations-modal.tsx +9 -10
  13. package/app/components/navbar/case-modals/open-case-modal.tsx +4 -4
  14. package/app/components/navbar/case-modals/rename-case-modal.tsx +9 -10
  15. package/app/components/navbar/navbar.tsx +1 -1
  16. package/app/components/sidebar/files/delete-files-modal.tsx +3 -3
  17. package/app/components/sidebar/files/files-modal.module.css +29 -0
  18. package/app/components/sidebar/notes/{class-details-fields.tsx → class-details/class-details-fields.tsx} +1 -1
  19. package/app/components/sidebar/notes/{class-details-modal.tsx → class-details/class-details-modal.tsx} +1 -1
  20. package/app/components/sidebar/notes/{class-details-sections.tsx → class-details/class-details-sections.tsx} +1 -1
  21. package/app/components/sidebar/notes/notes-editor-form.tsx +2 -2
  22. package/app/components/sidebar/notes/notes-editor-modal.tsx +6 -6
  23. package/app/components/sidebar/notes/notes.module.css +52 -0
  24. package/app/components/toolbar/toolbar-color-selector.tsx +8 -8
  25. package/app/components/toolbar/toolbar.module.css +181 -2
  26. package/app/components/user/delete-account.tsx +7 -7
  27. package/app/components/user/inactivity-warning.tsx +6 -6
  28. package/app/components/user/manage-profile.tsx +18 -1
  29. package/app/components/user/mfa-enrolled-factors.tsx +117 -0
  30. package/app/components/user/mfa-phone-update.tsx +8 -4
  31. package/app/components/user/mfa-totp-section.tsx +446 -0
  32. package/app/components/user/user.module.css +665 -0
  33. package/app/routes/striae/striae.tsx +1 -1
  34. package/app/services/audit/audit.service.ts +1 -1
  35. package/app/services/audit/builders/audit-event-builders-user-security.ts +4 -2
  36. package/app/services/firebase/errors.ts +2 -0
  37. package/app/utils/auth/mfa.ts +35 -1
  38. package/functions/api/image/[[path]].ts +19 -3
  39. package/package.json +16 -21
  40. package/scripts/deploy-all.sh +166 -0
  41. package/scripts/deploy-config/modules/env-utils.sh +322 -0
  42. package/scripts/deploy-config/modules/keys.sh +404 -0
  43. package/scripts/deploy-config/modules/prompt.sh +375 -0
  44. package/scripts/deploy-config/modules/scaffolding.sh +310 -0
  45. package/scripts/deploy-config/modules/validation.sh +354 -0
  46. package/scripts/deploy-config.sh +236 -0
  47. package/scripts/deploy-pages-secrets.sh +231 -0
  48. package/scripts/deploy-pages.sh +34 -0
  49. package/scripts/deploy-primershear-emails.sh +167 -0
  50. package/scripts/deploy-worker-secrets.sh +385 -0
  51. package/scripts/dev.cjs +23 -0
  52. package/scripts/enable-totp-mfa.mjs +57 -0
  53. package/scripts/install-workers.sh +87 -0
  54. package/scripts/run-eslint.cjs +43 -0
  55. package/scripts/update-compatibility-dates.cjs +124 -0
  56. package/scripts/update-markdown-versions.cjs +43 -0
  57. package/workers/audit-worker/package.json +1 -1
  58. package/workers/audit-worker/wrangler.jsonc.example +1 -1
  59. package/workers/data-worker/package.json +1 -1
  60. package/workers/data-worker/wrangler.jsonc.example +1 -1
  61. package/workers/image-worker/package.json +1 -1
  62. package/workers/image-worker/src/image-worker.example.ts +36 -2
  63. package/workers/image-worker/wrangler.jsonc.example +1 -1
  64. package/workers/pdf-worker/package.json +1 -1
  65. package/workers/pdf-worker/wrangler.jsonc.example +1 -1
  66. package/workers/user-worker/package.json +1 -1
  67. package/workers/user-worker/wrangler.jsonc.example +1 -1
  68. package/wrangler.toml.example +1 -1
  69. package/app/components/auth/mfa-enrollment.module.css +0 -276
  70. package/app/components/auth/mfa-verification.module.css +0 -259
  71. package/app/components/navbar/case-modals/archive-case-modal.module.css +0 -34
  72. package/app/components/navbar/case-modals/delete-case-modal.module.css +0 -9
  73. package/app/components/navbar/case-modals/export-case-modal.module.css +0 -27
  74. package/app/components/navbar/case-modals/export-confirmations-modal.module.css +0 -24
  75. package/app/components/navbar/case-modals/open-case-modal.module.css +0 -82
  76. package/app/components/navbar/case-modals/rename-case-modal.module.css +0 -9
  77. package/app/components/sidebar/files/delete-files-modal.module.css +0 -26
  78. package/app/components/sidebar/notes/notes-editor-modal.module.css +0 -49
  79. package/app/components/toolbar/toolbar-color-selector.module.css +0 -171
  80. package/app/components/user/delete-account.module.css +0 -277
  81. package/app/components/user/inactivity-warning.module.css +0 -148
  82. package/app/components/user/manage-profile.module.css +0 -192
  83. package/app/routes/auth/login.module.css +0 -523
  84. package/app/routes/auth/login.tsx +0 -705
  85. /package/app/components/{sidebar → navbar}/case-import/case-import.module.css +0 -0
  86. /package/app/components/{sidebar → navbar}/case-import/case-import.tsx +0 -0
  87. /package/app/components/{sidebar → navbar}/case-import/components/CasePreviewSection.tsx +0 -0
  88. /package/app/components/{sidebar → navbar}/case-import/components/ConfirmationDialog.tsx +0 -0
  89. /package/app/components/{sidebar → navbar}/case-import/components/ConfirmationPreviewSection.tsx +0 -0
  90. /package/app/components/{sidebar → navbar}/case-import/components/ExistingCaseSection.tsx +0 -0
  91. /package/app/components/{sidebar → navbar}/case-import/components/FileSelector.tsx +0 -0
  92. /package/app/components/{sidebar → navbar}/case-import/components/ProgressSection.tsx +0 -0
  93. /package/app/components/{sidebar → navbar}/case-import/hooks/useFilePreview.ts +0 -0
  94. /package/app/components/{sidebar → navbar}/case-import/hooks/useImportExecution.ts +0 -0
  95. /package/app/components/{sidebar → navbar}/case-import/hooks/useImportState.ts +0 -0
  96. /package/app/components/{sidebar → navbar}/case-import/index.ts +0 -0
  97. /package/app/components/{sidebar → navbar}/case-import/utils/file-validation.ts +0 -0
  98. /package/app/components/{sidebar/cases/cases-modal.module.css → navbar/case-modals/all-cases-modal.module.css} +0 -0
  99. /package/app/components/sidebar/notes/{class-details-shared.ts → class-details/class-details-shared.ts} +0 -0
  100. /package/app/components/sidebar/notes/{use-class-details-state.ts → class-details/use-class-details-state.ts} +0 -0
@@ -11,7 +11,8 @@ import {
11
11
  import { handleAuthError, getValidationError } from '~/services/firebase/errors';
12
12
  import { SignOut } from '~/components/actions/signout';
13
13
  import { auditService } from '~/services/audit';
14
- import styles from './mfa-enrollment.module.css';
14
+ import { MfaTotpEnrollment } from './mfa-totp-enrollment';
15
+ import styles from './auth.module.css';
15
16
 
16
17
  interface MFAEnrollmentProps {
17
18
  user: User;
@@ -28,6 +29,7 @@ export const MFAEnrollment: React.FC<MFAEnrollmentProps> = ({
28
29
  onSkip,
29
30
  mandatory = true
30
31
  }) => {
32
+ const [enrollmentMethod, setEnrollmentMethod] = useState<'choice' | 'sms' | 'totp'>('choice');
31
33
  const [phoneNumber, setPhoneNumber] = useState('');
32
34
  const [verificationCode, setVerificationCode] = useState('');
33
35
  const [isLoading, setIsLoading] = useState(false);
@@ -273,12 +275,12 @@ export const MFAEnrollment: React.FC<MFAEnrollmentProps> = ({
273
275
 
274
276
  return (
275
277
  <div className={styles.overlay}>
276
- <div className={styles.modal}>
278
+ <div className={styles.enrollmentModal}>
277
279
  <div className={styles.header}>
278
280
  <h2>Security Setup Required</h2>
279
281
  <p>
280
282
  {mandatory
281
- ? 'Two-factor authentication is required for all accounts. Please set up SMS verification to continue.'
283
+ ? 'Two-factor authentication is required for all accounts. Please set up a second factor to continue.'
282
284
  : 'Enhance your account security with two-factor authentication.'
283
285
  }
284
286
  </p>
@@ -290,91 +292,142 @@ export const MFAEnrollment: React.FC<MFAEnrollmentProps> = ({
290
292
  {errorMessage}
291
293
  </div>
292
294
  )}
293
-
294
- {!codeSent ? (
295
- <div className={styles.phoneStep}>
296
- <h3>Step 1: Enter Your Mobile Number</h3>
297
- <input
298
- type="tel"
299
- value={phoneNumber}
300
- onChange={(e) => {
301
- setPhoneNumber(e.target.value);
302
- if (errorMessage) setErrorMessage(''); // Clear error on input
303
- }}
304
- placeholder="ex. +15551234567"
305
- className={styles.input}
306
- disabled={isLoading}
307
- />
308
- <p className={styles.note}>
309
- We&apos;ll send a verification code to this number.
310
- </p>
295
+
296
+ {enrollmentMethod === 'choice' && (
297
+ <div className={styles.methodChoice}>
298
+ <h3>Choose a Verification Method</h3>
299
+ <button
300
+ type="button"
301
+ onClick={() => setEnrollmentMethod('sms')}
302
+ className={styles.methodButton}
303
+ >
304
+ <span className={styles.methodButtonTitle}>Text Message (SMS)</span>
305
+ <span className={styles.methodButtonDesc}>Receive a code by text message</span>
306
+ </button>
311
307
  <button
312
- onClick={sendVerificationCode}
313
- disabled={isLoading || !phoneNumber.trim()}
314
- className={styles.primaryButton}
308
+ type="button"
309
+ onClick={() => setEnrollmentMethod('totp')}
310
+ className={styles.methodButton}
315
311
  >
316
- {isLoading ? 'Sending...' : 'Send Verification Code'}
312
+ <span className={styles.methodButtonTitle}>Authenticator App</span>
313
+ <span className={styles.methodButtonDesc}>Use Google Authenticator, Authy, or similar</span>
317
314
  </button>
318
315
  </div>
319
- ) : (
320
- <div className={styles.codeStep}>
321
- <h3>Step 2: Enter Verification Code</h3>
322
- <p className={styles.note}>
323
- Enter the 6-digit code sent to {phoneNumber}
324
- </p>
325
- <input
326
- type="text"
327
- value={verificationCode}
328
- onChange={(e) => {
329
- setVerificationCode(e.target.value.replace(/\D/g, ''));
330
- if (errorMessage) setErrorMessage(''); // Clear error on input
331
- }}
332
- placeholder="123456"
333
- maxLength={6}
334
- className={styles.input}
335
- disabled={isLoading}
336
- />
337
-
338
- <div className={styles.buttonGroup}>
339
- <button
340
- onClick={enrollMFA}
341
- disabled={isLoading || verificationCode.length !== 6}
342
- className={styles.primaryButton}
343
- >
344
- {isLoading ? 'Verifying...' : 'Complete Setup'}
345
- </button>
346
-
347
- <button
348
- onClick={() => {
349
- setCodeSent(false);
350
- setVerificationCode('');
351
- setErrorMessage(''); // Clear errors when changing phone number
352
- }}
353
- disabled={isLoading}
354
- className={styles.secondaryButton}
355
- >
356
- Change Phone Number
357
- </button>
358
-
359
- {resendTimer === 0 ? (
360
- <button
361
- onClick={sendVerificationCode}
316
+ )}
317
+
318
+ {enrollmentMethod === 'totp' && (
319
+ <MfaTotpEnrollment
320
+ user={user}
321
+ onSuccess={onSuccess}
322
+ onError={onError}
323
+ onBack={() => {
324
+ setEnrollmentMethod('choice');
325
+ setErrorMessage('');
326
+ }}
327
+ />
328
+ )}
329
+
330
+ {enrollmentMethod === 'sms' && (
331
+ <>
332
+ {!codeSent ? (
333
+ <div className={styles.phoneStep}>
334
+ <h3>Step 1: Enter Your Mobile Number</h3>
335
+ <input
336
+ type="tel"
337
+ value={phoneNumber}
338
+ onChange={(e) => {
339
+ setPhoneNumber(e.target.value);
340
+ if (errorMessage) setErrorMessage(''); // Clear error on input
341
+ }}
342
+ placeholder="ex. +15551234567"
343
+ className={styles.enrollmentInput}
362
344
  disabled={isLoading}
363
- className={styles.secondaryButton}
364
- >
365
- Resend Code
366
- </button>
367
- ) : (
368
- <p className={styles.resendTimer}>
369
- Resend code in {resendTimer}s
345
+ />
346
+ <p className={styles.note}>
347
+ We&apos;ll send a verification code to this number.
370
348
  </p>
371
- )}
372
- </div>
373
- </div>
349
+ <div className={styles.buttonGroup}>
350
+ <button
351
+ onClick={sendVerificationCode}
352
+ disabled={isLoading || !phoneNumber.trim()}
353
+ className={styles.primaryButton}
354
+ >
355
+ {isLoading ? 'Sending...' : 'Send Verification Code'}
356
+ </button>
357
+ <button
358
+ type="button"
359
+ onClick={() => {
360
+ setEnrollmentMethod('choice');
361
+ setErrorMessage('');
362
+ }}
363
+ disabled={isLoading}
364
+ className={styles.enrollmentSecondaryButton}
365
+ >
366
+ Back
367
+ </button>
368
+ </div>
369
+ </div>
370
+ ) : (
371
+ <div className={styles.codeStep}>
372
+ <h3>Step 2: Enter Verification Code</h3>
373
+ <p className={styles.note}>
374
+ Enter the 6-digit code sent to {phoneNumber}
375
+ </p>
376
+ <input
377
+ type="text"
378
+ value={verificationCode}
379
+ onChange={(e) => {
380
+ setVerificationCode(e.target.value.replace(/\D/g, ''));
381
+ if (errorMessage) setErrorMessage(''); // Clear error on input
382
+ }}
383
+ placeholder="123456"
384
+ maxLength={6}
385
+ className={styles.enrollmentInput}
386
+ disabled={isLoading}
387
+ />
388
+
389
+ <div className={styles.buttonGroup}>
390
+ <button
391
+ onClick={enrollMFA}
392
+ disabled={isLoading || verificationCode.length !== 6}
393
+ className={styles.primaryButton}
394
+ >
395
+ {isLoading ? 'Verifying...' : 'Complete Setup'}
396
+ </button>
397
+
398
+ <button
399
+ onClick={() => {
400
+ setCodeSent(false);
401
+ setVerificationCode('');
402
+ setErrorMessage(''); // Clear errors when changing phone number
403
+ }}
404
+ disabled={isLoading}
405
+ className={styles.enrollmentSecondaryButton}
406
+ >
407
+ Change Phone Number
408
+ </button>
409
+
410
+ {resendTimer === 0 ? (
411
+ <button
412
+ onClick={sendVerificationCode}
413
+ disabled={isLoading}
414
+ className={styles.enrollmentSecondaryButton}
415
+ >
416
+ Resend Code
417
+ </button>
418
+ ) : (
419
+ <p className={styles.resendTimer}>
420
+ Resend code in {resendTimer}s
421
+ </p>
422
+ )}
423
+ </div>
424
+ </div>
425
+ )}
426
+ </>
374
427
  )}
375
428
  </div>
376
429
 
377
- {!mandatory && (
430
+ {!mandatory && enrollmentMethod === 'choice' && (
378
431
  <div className={styles.footer}>
379
432
  <button
380
433
  onClick={handleSkip}
@@ -0,0 +1,231 @@
1
+ /* eslint-disable react/prop-types */
2
+ import { useState, useEffect, useRef } from 'react';
3
+ import { multiFactor, TotpMultiFactorGenerator, type TotpSecret, type User } from 'firebase/auth';
4
+ import { toDataURL as qrToDataURL } from 'qrcode';
5
+ import { handleAuthError, getValidationError } from '~/services/firebase/errors';
6
+ import { auditService } from '~/services/audit';
7
+ import styles from './auth.module.css';
8
+
9
+ interface MfaTotpEnrollmentProps {
10
+ user: User;
11
+ onSuccess: () => void;
12
+ onError: (error: string) => void;
13
+ onBack?: () => void;
14
+ }
15
+
16
+ export const MfaTotpEnrollment: React.FC<MfaTotpEnrollmentProps> = ({
17
+ user,
18
+ onSuccess,
19
+ onError,
20
+ onBack,
21
+ }) => {
22
+ const [totpSecret, setTotpSecret] = useState<TotpSecret | null>(null);
23
+ const [qrCodeDataUrl, setQrCodeDataUrl] = useState('');
24
+ const [verificationCode, setVerificationCode] = useState('');
25
+ const [isLoading, setIsLoading] = useState(false);
26
+ const [isFetchingSecret, setIsFetchingSecret] = useState(true);
27
+ const [errorMessage, setErrorMessage] = useState('');
28
+ const [showSecretKey, setShowSecretKey] = useState(false);
29
+ // Persists the secret across re-renders to avoid regenerating it
30
+ const secretRef = useRef<TotpSecret | null>(null);
31
+
32
+ useEffect(() => {
33
+ if (secretRef.current) {
34
+ setTotpSecret(secretRef.current);
35
+ setIsFetchingSecret(false);
36
+ return;
37
+ }
38
+
39
+ const generateSecret = async () => {
40
+ setIsFetchingSecret(true);
41
+ setErrorMessage('');
42
+ try {
43
+ const session = await multiFactor(user).getSession();
44
+ const secret = await TotpMultiFactorGenerator.generateSecret(session);
45
+ secretRef.current = secret;
46
+ setTotpSecret(secret);
47
+
48
+ const qrUrl = secret.generateQrCodeUrl(user.email ?? 'user', 'Striae');
49
+ const dataUrl = await qrToDataURL(qrUrl, { width: 200, margin: 1 });
50
+ setQrCodeDataUrl(dataUrl);
51
+ } catch (err) {
52
+ const { message } = handleAuthError(err);
53
+ setErrorMessage(message);
54
+ onError(message);
55
+ } finally {
56
+ setIsFetchingSecret(false);
57
+ }
58
+ };
59
+
60
+ void generateSecret();
61
+ }, [user, onError]);
62
+
63
+ const handleVerify = async () => {
64
+ if (!totpSecret) {
65
+ const error = getValidationError('MFA_TOTP_SETUP_ERROR');
66
+ setErrorMessage(error);
67
+ onError(error);
68
+ return;
69
+ }
70
+
71
+ if (verificationCode.length !== 6) {
72
+ const error = getValidationError('MFA_CODE_REQUIRED');
73
+ setErrorMessage(error);
74
+ onError(error);
75
+ return;
76
+ }
77
+
78
+ setIsLoading(true);
79
+ setErrorMessage('');
80
+
81
+ try {
82
+ const assertion = TotpMultiFactorGenerator.assertionForEnrollment(
83
+ totpSecret,
84
+ verificationCode
85
+ );
86
+ await multiFactor(user).enroll(assertion, 'Authenticator App');
87
+
88
+ try {
89
+ await auditService.logMfaEnrollment(
90
+ user,
91
+ undefined,
92
+ 'totp',
93
+ 'success',
94
+ 1,
95
+ undefined,
96
+ navigator.userAgent
97
+ );
98
+ } catch (auditErr) {
99
+ console.error('Failed to log TOTP enrollment audit:', auditErr);
100
+ }
101
+
102
+ onSuccess();
103
+ } catch (err) {
104
+ const authError = err as { code?: string; message?: string };
105
+ let errorMsg = '';
106
+
107
+ if (authError.code === 'auth/invalid-verification-code') {
108
+ errorMsg = getValidationError('MFA_INVALID_CODE');
109
+ } else if (authError.code === 'auth/code-expired') {
110
+ errorMsg = getValidationError('MFA_CODE_EXPIRED');
111
+ } else {
112
+ errorMsg = handleAuthError(authError).message;
113
+ }
114
+
115
+ setErrorMessage(errorMsg);
116
+ onError(errorMsg);
117
+
118
+ try {
119
+ await auditService.logSecurityViolation(
120
+ user,
121
+ 'unauthorized-access',
122
+ authError.code === 'auth/invalid-verification-code' ? 'high' : 'medium',
123
+ `Failed TOTP enrollment: ${authError.code} - ${errorMsg}`,
124
+ 'mfa-totp-enrollment-endpoint',
125
+ true
126
+ );
127
+ } catch (auditErr) {
128
+ console.error('Failed to log TOTP enrollment security violation audit:', auditErr);
129
+ }
130
+ } finally {
131
+ setIsLoading(false);
132
+ }
133
+ };
134
+
135
+ if (isFetchingSecret) {
136
+ return (
137
+ <div className={styles.totpStep}>
138
+ <p className={styles.note}>Setting up authenticator&hellip;</p>
139
+ </div>
140
+ );
141
+ }
142
+
143
+ return (
144
+ <div className={styles.totpStep}>
145
+ <h3>Set Up Authenticator App</h3>
146
+
147
+ {errorMessage && (
148
+ <div className={styles.errorMessage}>{errorMessage}</div>
149
+ )}
150
+
151
+ {qrCodeDataUrl && (
152
+ <div className={styles.qrCodeContainer}>
153
+ <p className={styles.note}>
154
+ Scan this QR code with your authenticator app (Google Authenticator, Authy, etc.).
155
+ </p>
156
+ <img
157
+ src={qrCodeDataUrl}
158
+ alt="TOTP QR Code — scan with your authenticator app"
159
+ className={styles.qrCodeImage}
160
+ width={200}
161
+ height={200}
162
+ />
163
+ </div>
164
+ )}
165
+
166
+ <button
167
+ type="button"
168
+ onClick={() => setShowSecretKey((v) => !v)}
169
+ className={styles.enrollmentSecondaryButton}
170
+ style={{ marginBottom: '1rem' }}
171
+ >
172
+ {showSecretKey ? 'Hide' : 'Show'} setup key
173
+ </button>
174
+
175
+ {showSecretKey && totpSecret && (
176
+ <div className={styles.secretKeyDisplay}>
177
+ <p className={styles.note}>
178
+ If you cannot scan the QR code, enter this key manually in your authenticator app:
179
+ </p>
180
+ <code className={styles.secretKey}>{totpSecret.secretKey}</code>
181
+ </div>
182
+ )}
183
+
184
+ <p className={styles.note}>
185
+ After scanning, enter the 6-digit code from your authenticator app below.
186
+ </p>
187
+ <input
188
+ type="text"
189
+ inputMode="numeric"
190
+ value={verificationCode}
191
+ onChange={(e) => {
192
+ setVerificationCode(e.target.value.replace(/\D/g, ''));
193
+ if (errorMessage) setErrorMessage('');
194
+ }}
195
+ onKeyDown={(e) => {
196
+ if (e.key === 'Enter' && verificationCode.length === 6) {
197
+ e.preventDefault();
198
+ void handleVerify();
199
+ }
200
+ }}
201
+ placeholder="123456"
202
+ maxLength={6}
203
+ className={styles.enrollmentInput}
204
+ disabled={isLoading}
205
+ autoComplete="one-time-code"
206
+ />
207
+
208
+ <div className={styles.buttonGroup}>
209
+ <button
210
+ type="button"
211
+ onClick={handleVerify}
212
+ disabled={isLoading || verificationCode.length !== 6}
213
+ className={styles.primaryButton}
214
+ >
215
+ {isLoading ? 'Verifying…' : 'Complete Setup'}
216
+ </button>
217
+
218
+ {onBack && (
219
+ <button
220
+ type="button"
221
+ onClick={onBack}
222
+ disabled={isLoading}
223
+ className={styles.enrollmentSecondaryButton}
224
+ >
225
+ Back
226
+ </button>
227
+ )}
228
+ </div>
229
+ </div>
230
+ );
231
+ };