@striae-org/striae 5.3.0 → 5.3.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 (27) hide show
  1. package/app/components/actions/case-export/core-export.ts +3 -0
  2. package/app/components/actions/case-export/download-handlers.ts +1 -1
  3. package/app/components/actions/case-import/confirmation-import.ts +62 -22
  4. package/app/components/actions/case-import/confirmation-package.ts +68 -1
  5. package/app/components/actions/case-import/index.ts +1 -1
  6. package/app/components/actions/case-import/orchestrator.ts +78 -53
  7. package/app/components/actions/case-import/zip-processing.ts +157 -407
  8. package/app/components/navbar/case-modals/export-case-modal.module.css +27 -0
  9. package/app/components/navbar/case-modals/export-case-modal.tsx +132 -0
  10. package/app/components/navbar/case-modals/export-confirmations-modal.module.css +24 -0
  11. package/app/components/navbar/case-modals/export-confirmations-modal.tsx +108 -0
  12. package/app/components/sidebar/case-import/components/CasePreviewSection.tsx +1 -9
  13. package/app/components/sidebar/case-import/components/ConfirmationPreviewSection.tsx +36 -5
  14. package/app/components/sidebar/case-import/hooks/useFilePreview.ts +5 -9
  15. package/app/components/sidebar/case-import/index.ts +1 -4
  16. package/app/routes/auth/login.tsx +22 -103
  17. package/app/routes/striae/striae.tsx +77 -13
  18. package/app/types/case.ts +1 -0
  19. package/app/types/export.ts +1 -0
  20. package/app/types/import.ts +10 -0
  21. package/package.json +1 -1
  22. package/workers/audit-worker/wrangler.jsonc.example +1 -1
  23. package/workers/data-worker/wrangler.jsonc.example +1 -1
  24. package/workers/image-worker/wrangler.jsonc.example +1 -1
  25. package/workers/pdf-worker/wrangler.jsonc.example +1 -1
  26. package/workers/user-worker/wrangler.jsonc.example +1 -1
  27. package/wrangler.toml.example +1 -1
@@ -0,0 +1,27 @@
1
+ .modal {
2
+ width: min(480px, calc(100vw - 2rem));
3
+ }
4
+
5
+ .description {
6
+ margin: 0 0 0.9rem;
7
+ color: #4b5563;
8
+ font-size: 0.86rem;
9
+ line-height: 1.5;
10
+ }
11
+
12
+ .confirmButton {
13
+ background: #1f6feb;
14
+ color: #ffffff;
15
+ border-color: #1560d4;
16
+ }
17
+
18
+ .confirmButton:not(:disabled):hover {
19
+ background: #1560d4;
20
+ }
21
+
22
+ .emailError {
23
+ margin: 0.45rem 0 0;
24
+ color: #b91c1c;
25
+ font-size: 0.83rem;
26
+ line-height: 1.4;
27
+ }
@@ -0,0 +1,132 @@
1
+ import { useEffect, useRef, useState } from 'react';
2
+ import { useOverlayDismiss } from '~/hooks/useOverlayDismiss';
3
+ import sharedStyles from './case-modal-shared.module.css';
4
+ import styles from './export-case-modal.module.css';
5
+
6
+ interface ExportCaseModalProps {
7
+ isOpen: boolean;
8
+ caseNumber: string;
9
+ currentUserEmail?: string;
10
+ isSubmitting?: boolean;
11
+ onClose: () => void;
12
+ onSubmit: (designatedReviewerEmail: string | undefined) => Promise<void>;
13
+ }
14
+
15
+ export const ExportCaseModal = ({
16
+ isOpen,
17
+ caseNumber,
18
+ currentUserEmail,
19
+ isSubmitting = false,
20
+ onClose,
21
+ onSubmit,
22
+ }: ExportCaseModalProps) => {
23
+ const [email, setEmail] = useState<string>('');
24
+
25
+ const isSelfEmail =
26
+ email.trim().length > 0 &&
27
+ !!currentUserEmail &&
28
+ email.trim().toLowerCase() === currentUserEmail.toLowerCase();
29
+ const inputRef = useRef<HTMLInputElement>(null);
30
+
31
+ const handleClose = () => {
32
+ setEmail('');
33
+ onClose();
34
+ };
35
+
36
+ const isSubmitDisabled = isSubmitting || isSelfEmail;
37
+
38
+ const {
39
+ requestClose,
40
+ overlayProps,
41
+ getCloseButtonProps,
42
+ } = useOverlayDismiss({
43
+ isOpen,
44
+ onClose: handleClose,
45
+ canDismiss: !isSubmitting,
46
+ });
47
+
48
+ useEffect(() => {
49
+ if (!isOpen) {
50
+ return;
51
+ }
52
+
53
+ const focusId = window.requestAnimationFrame(() => {
54
+ inputRef.current?.focus();
55
+ });
56
+
57
+ return () => {
58
+ window.cancelAnimationFrame(focusId);
59
+ };
60
+ }, [isOpen]);
61
+
62
+ if (!isOpen) return null;
63
+
64
+ const handleSubmit = async () => {
65
+ const trimmed = email.trim() || undefined;
66
+ await onSubmit(trimmed);
67
+ setEmail('');
68
+ };
69
+
70
+ return (
71
+ <div
72
+ className={sharedStyles.overlay}
73
+ aria-label="Close export case dialog"
74
+ {...overlayProps}
75
+ >
76
+ <div
77
+ className={`${sharedStyles.modal} ${styles.modal}`}
78
+ role="dialog"
79
+ aria-modal="true"
80
+ aria-label="Export Case"
81
+ >
82
+ <button {...getCloseButtonProps({ ariaLabel: 'Close export case dialog' })}>
83
+ ×
84
+ </button>
85
+ <h3 className={sharedStyles.title}>Export Case</h3>
86
+ <p className={sharedStyles.subtitle}>Case: {caseNumber}</p>
87
+ <p className={styles.description}>
88
+ You may designate a specific email address for review approval. Only the user
89
+ with the supplied email address will be able to open your case for review in
90
+ Striae. (Optional)
91
+ </p>
92
+ <input
93
+ ref={inputRef}
94
+ type="email"
95
+ value={email}
96
+ onChange={(event) => setEmail(event.target.value)}
97
+ className={sharedStyles.input}
98
+ placeholder="Reviewer email address (optional)"
99
+ disabled={isSubmitting}
100
+ onKeyDown={(event) => {
101
+ if (event.key === 'Enter' && !isSubmitDisabled) {
102
+ void handleSubmit();
103
+ }
104
+ }}
105
+ />
106
+ {isSelfEmail && (
107
+ <p className={styles.emailError}>
108
+ You cannot designate yourself as the reviewer. The recipient must be a different Striae user.
109
+ </p>
110
+ )}
111
+ <div className={sharedStyles.actions}>
112
+ <button
113
+ type="button"
114
+ className={sharedStyles.cancelButton}
115
+ onClick={requestClose}
116
+ disabled={isSubmitting}
117
+ >
118
+ Cancel
119
+ </button>
120
+ <button
121
+ type="button"
122
+ className={`${sharedStyles.confirmButton} ${styles.confirmButton}`}
123
+ onClick={() => void handleSubmit()}
124
+ disabled={isSubmitDisabled}
125
+ >
126
+ {isSubmitting ? 'Exporting...' : 'Export Case'}
127
+ </button>
128
+ </div>
129
+ </div>
130
+ </div>
131
+ );
132
+ };
@@ -0,0 +1,24 @@
1
+ .modal {
2
+ width: min(400px, calc(100vw - 2rem));
3
+ }
4
+
5
+ .warningPanel {
6
+ margin-bottom: 0.8rem;
7
+ }
8
+
9
+ .description {
10
+ margin: 0 0 0.3rem;
11
+ color: #4b5563;
12
+ font-size: 0.86rem;
13
+ line-height: 1.5;
14
+ }
15
+
16
+ .confirmButton {
17
+ background: #1f6feb;
18
+ color: #ffffff;
19
+ border-color: #1560d4;
20
+ }
21
+
22
+ .confirmButton:not(:disabled):hover {
23
+ background: #1560d4;
24
+ }
@@ -0,0 +1,108 @@
1
+ import { useEffect, useRef } from 'react';
2
+ import { useOverlayDismiss } from '~/hooks/useOverlayDismiss';
3
+ import sharedStyles from './case-modal-shared.module.css';
4
+ import styles from './export-confirmations-modal.module.css';
5
+
6
+ interface ExportConfirmationsModalProps {
7
+ isOpen: boolean;
8
+ caseNumber: string;
9
+ confirmedCount: number;
10
+ unconfirmedCount: number;
11
+ isSubmitting?: boolean;
12
+ onClose: () => void;
13
+ onConfirm: () => void;
14
+ }
15
+
16
+ export const ExportConfirmationsModal = ({
17
+ isOpen,
18
+ caseNumber,
19
+ confirmedCount,
20
+ unconfirmedCount,
21
+ isSubmitting = false,
22
+ onClose,
23
+ onConfirm,
24
+ }: ExportConfirmationsModalProps) => {
25
+ const confirmButtonRef = useRef<HTMLButtonElement>(null);
26
+
27
+ const {
28
+ requestClose,
29
+ overlayProps,
30
+ getCloseButtonProps,
31
+ } = useOverlayDismiss({
32
+ isOpen,
33
+ onClose,
34
+ canDismiss: !isSubmitting,
35
+ });
36
+
37
+ useEffect(() => {
38
+ if (!isOpen) return;
39
+
40
+ const focusId = window.requestAnimationFrame(() => {
41
+ confirmButtonRef.current?.focus();
42
+ });
43
+
44
+ return () => {
45
+ window.cancelAnimationFrame(focusId);
46
+ };
47
+ }, [isOpen]);
48
+
49
+ if (!isOpen) return null;
50
+
51
+ const confirmationLabel = confirmedCount === 1 ? '1 confirmation' : `${confirmedCount} confirmations`;
52
+
53
+ return (
54
+ <div
55
+ className={sharedStyles.overlay}
56
+ aria-label="Close export confirmations dialog"
57
+ {...overlayProps}
58
+ >
59
+ <div
60
+ className={`${sharedStyles.modal} ${styles.modal}`}
61
+ role="dialog"
62
+ aria-modal="true"
63
+ aria-label="Export Confirmations"
64
+ >
65
+ <button {...getCloseButtonProps({ ariaLabel: 'Close export confirmations dialog' })}>
66
+ ×
67
+ </button>
68
+ <h3 className={sharedStyles.title}>Export Confirmations</h3>
69
+ <p className={sharedStyles.subtitle}>Case: {caseNumber}</p>
70
+ {unconfirmedCount > 0 && (
71
+ <div className={`${sharedStyles.warningPanel} ${styles.warningPanel}`}>
72
+ <p>
73
+ <strong>
74
+ {unconfirmedCount} image{unconfirmedCount !== 1 ? 's' : ''}{' '}
75
+ {unconfirmedCount !== 1 ? 'are' : 'is'} unconfirmed.
76
+ </strong>
77
+ </p>
78
+ <p>Only confirmed images will be included in this export.</p>
79
+ </div>
80
+ )}
81
+ <p className={styles.description}>
82
+ {confirmedCount === 0
83
+ ? 'No confirmed images found for this case.'
84
+ : `${confirmationLabel} will be exported.`}
85
+ </p>
86
+ <div className={sharedStyles.actions}>
87
+ <button
88
+ type="button"
89
+ className={sharedStyles.cancelButton}
90
+ onClick={requestClose}
91
+ disabled={isSubmitting}
92
+ >
93
+ Cancel
94
+ </button>
95
+ <button
96
+ ref={confirmButtonRef}
97
+ type="button"
98
+ className={`${sharedStyles.confirmButton} ${styles.confirmButton}`}
99
+ onClick={onConfirm}
100
+ disabled={isSubmitting || confirmedCount === 0}
101
+ >
102
+ {isSubmitting ? 'Exporting...' : 'Export Confirmations'}
103
+ </button>
104
+ </div>
105
+ </div>
106
+ </div>
107
+ );
108
+ };
@@ -43,17 +43,10 @@ export const CasePreviewSection = ({
43
43
 
44
44
  if (!casePreview) return null;
45
45
 
46
- const isEncrypted = casePreview.caseNumber === 'ENCRYPTED';
47
-
48
46
  return (
49
47
  <div className={styles.previewSection}>
50
48
  <h3 className={styles.previewTitle}>Case Import Preview</h3>
51
- {isEncrypted ? (
52
- <p className={styles.previewMessage}>
53
- Encrypted package detected. Case details could not be read from the package.
54
- </p>
55
- ) : (
56
- <div className={styles.previewMeta}>
49
+ <div className={styles.previewMeta}>
57
50
  <div className={styles.previewMetaRow}>
58
51
  <span className={styles.previewMetaLabel}>Case</span>
59
52
  <span className={styles.previewMetaValue}>{casePreview.caseNumber}</span>
@@ -89,7 +82,6 @@ export const CasePreviewSection = ({
89
82
  </div>
90
83
  )}
91
84
  </div>
92
- )}
93
85
  {casePreview.archived && (
94
86
  <div className={styles.archivedImportNote}>
95
87
  {ARCHIVED_SELF_IMPORT_NOTE}
@@ -1,9 +1,15 @@
1
+ import { type ConfirmationImportPreview } from '~/types';
1
2
  import styles from '../case-import.module.css';
2
3
 
3
- export type ConfirmationPreview = Record<string, never>;
4
+ function formatDate(isoDate: string | undefined): string {
5
+ if (!isoDate) return 'Unknown';
6
+ const date = new Date(isoDate);
7
+ if (Number.isNaN(date.getTime())) return isoDate;
8
+ return date.toLocaleDateString(undefined, { year: 'numeric', month: 'short', day: 'numeric' });
9
+ }
4
10
 
5
11
  interface ConfirmationPreviewSectionProps {
6
- confirmationPreview: ConfirmationPreview | null;
12
+ confirmationPreview: ConfirmationImportPreview | null;
7
13
  isLoadingPreview: boolean;
8
14
  }
9
15
 
@@ -23,9 +29,34 @@ export const ConfirmationPreviewSection = ({ confirmationPreview, isLoadingPrevi
23
29
  return (
24
30
  <div className={styles.previewSection}>
25
31
  <h3 className={styles.previewTitle}>Confirmation Import Preview</h3>
26
- <p className={styles.previewMessage}>
27
- Encrypted confirmation package detected.
28
- </p>
32
+ <div className={styles.previewMeta}>
33
+ <div className={styles.previewMetaRow}>
34
+ <span className={styles.previewMetaLabel}>Case</span>
35
+ <span className={styles.previewMetaValue}>{confirmationPreview.caseNumber}</span>
36
+ </div>
37
+ {(confirmationPreview.exportedByName || confirmationPreview.exportedBy) && (
38
+ <div className={styles.previewMetaRow}>
39
+ <span className={styles.previewMetaLabel}>Exported by</span>
40
+ <span className={styles.previewMetaValue}>
41
+ {confirmationPreview.exportedByName || confirmationPreview.exportedBy}
42
+ </span>
43
+ </div>
44
+ )}
45
+ {confirmationPreview.exportedByCompany && (
46
+ <div className={styles.previewMetaRow}>
47
+ <span className={styles.previewMetaLabel}>Organization</span>
48
+ <span className={styles.previewMetaValue}>{confirmationPreview.exportedByCompany}</span>
49
+ </div>
50
+ )}
51
+ <div className={styles.previewMetaRow}>
52
+ <span className={styles.previewMetaLabel}>Exported</span>
53
+ <span className={styles.previewMetaValue}>{formatDate(confirmationPreview.exportDate)}</span>
54
+ </div>
55
+ <div className={styles.previewMetaRow}>
56
+ <span className={styles.previewMetaLabel}>Confirmations</span>
57
+ <span className={styles.previewMetaValue}>{confirmationPreview.totalConfirmations}</span>
58
+ </div>
59
+ </div>
29
60
  </div>
30
61
  );
31
62
  };
@@ -1,12 +1,11 @@
1
1
  import { useState, useCallback } from 'react';
2
2
  import type { User } from 'firebase/auth';
3
- import { previewCaseImport, extractConfirmationImportPackage } from '~/components/actions/case-review';
4
- import { type CaseImportPreview } from '~/types';
5
- import { type ConfirmationPreview } from '../components/ConfirmationPreviewSection';
3
+ import { previewCaseImport, previewConfirmationImport } from '~/components/actions/case-review';
4
+ import { type CaseImportPreview, type ConfirmationImportPreview } from '~/types';
6
5
 
7
6
  interface UseFilePreviewReturn {
8
7
  casePreview: CaseImportPreview | null;
9
- confirmationPreview: ConfirmationPreview | null;
8
+ confirmationPreview: ConfirmationImportPreview | null;
10
9
  loadCasePreview: (file: File) => Promise<void>;
11
10
  loadConfirmationPreview: (file: File) => Promise<void>;
12
11
  clearPreviews: () => void;
@@ -22,7 +21,7 @@ export const useFilePreview = (
22
21
  clearImportData: () => void
23
22
  ): UseFilePreviewReturn => {
24
23
  const [casePreview, setCasePreview] = useState<CaseImportPreview | null>(null);
25
- const [confirmationPreview, setConfirmationPreview] = useState<ConfirmationPreview | null>(null);
24
+ const [confirmationPreview, setConfirmationPreview] = useState<ConfirmationImportPreview | null>(null);
26
25
 
27
26
  const loadCasePreview = useCallback(async (file: File) => {
28
27
  if (!user) {
@@ -51,10 +50,7 @@ export const useFilePreview = (
51
50
 
52
51
  setIsLoadingPreview(true);
53
52
  try {
54
- await extractConfirmationImportPackage(file);
55
-
56
- const preview: ConfirmationPreview = {};
57
-
53
+ const preview = await previewConfirmationImport(file, user);
58
54
  setConfirmationPreview(preview);
59
55
  } catch (error) {
60
56
  console.error('Error loading confirmation preview:', error);
@@ -12,7 +12,4 @@ export { useFilePreview } from './hooks/useFilePreview';
12
12
  export { useImportExecution } from './hooks/useImportExecution';
13
13
 
14
14
  // Utils
15
- export * from './utils/file-validation';
16
-
17
- // Types
18
- export type { ConfirmationPreview } from './components/ConfirmationPreviewSection';
15
+ export * from './utils/file-validation';
@@ -1,8 +1,8 @@
1
1
  import { useState, useEffect, useRef } from 'react';
2
- import { Link, useSearchParams, type MetaFunction } from 'react-router';
2
+ import { Link, useSearchParams } from 'react-router';
3
3
  import { auth } from '~/services/firebase';
4
4
  import {
5
- signInWithEmailAndPassword,
5
+ signInWithEmailAndPassword,
6
6
  createUserWithEmailAndPassword,
7
7
  onAuthStateChanged,
8
8
  sendEmailVerification,
@@ -28,95 +28,7 @@ import { generateUniqueId } from '~/utils/common';
28
28
  import { evaluatePasswordPolicy, buildActionCodeSettings, userHasMFA } from '~/utils/auth';
29
29
  import type { UserData } from '~/types';
30
30
 
31
- const APP_CANONICAL_ORIGIN = 'https://striae.app';
32
- const SOCIAL_IMAGE_PATH = '/social-image.png';
33
- const SOCIAL_IMAGE_ALT = 'Striae forensic annotation and comparison workspace';
34
- const LOGIN_PATH_ALIASES = new Set(['/auth', '/auth/', '/auth/login', '/auth/login/']);
35
-
36
- type AuthMetaContent = {
37
- title: string;
38
- description: string;
39
- robots: string;
40
- };
41
-
42
- const getCanonicalPath = (pathname: string): string => {
43
- if (!pathname || LOGIN_PATH_ALIASES.has(pathname)) {
44
- return '/';
45
- }
46
-
47
- return pathname.startsWith('/') ? pathname : `/${pathname}`;
48
- };
49
-
50
- const getAuthMetaContent = (mode: string | null, hasActionCode: boolean): AuthMetaContent => {
51
- if (!mode && !hasActionCode) {
52
- return {
53
- title: 'Striae: A Firearms Examiner\'s Comparison Companion',
54
- description: 'Sign in to Striae to access your comparison annotation workspace, case files, and review tools.',
55
- robots: 'index,follow,max-image-preview:large,max-snippet:-1,max-video-preview:-1',
56
- };
57
- }
58
-
59
- if (mode === 'resetPassword') {
60
- return {
61
- title: 'Striae | Reset Your Password',
62
- description: 'Use this secure page to reset your Striae account password and restore access to your workspace.',
63
- robots: 'noindex,nofollow,noarchive',
64
- };
65
- }
66
-
67
- if (mode === 'verifyEmail') {
68
- return {
69
- title: 'Striae | Verify Your Email Address',
70
- description: 'Confirm your email address to complete Striae account activation and continue securely.',
71
- robots: 'noindex,nofollow,noarchive',
72
- };
73
- }
74
-
75
- if (mode === 'recoverEmail') {
76
- return {
77
- title: 'Striae | Recover Email Access',
78
- description: 'Complete your Striae account email recovery steps securely.',
79
- robots: 'noindex,nofollow,noarchive',
80
- };
81
- }
82
-
83
- return {
84
- title: 'Striae | Account Action',
85
- description: 'Complete your Striae account action securely.',
86
- robots: 'noindex,nofollow,noarchive',
87
- };
88
- };
89
-
90
- export const meta: MetaFunction = ({ location }) => {
91
- const searchParams = new URLSearchParams(location.search);
92
- const mode = searchParams.get('mode');
93
- const hasActionCode = Boolean(searchParams.get('oobCode'));
94
-
95
- const canonicalPath = getCanonicalPath(location.pathname);
96
- const canonicalHref = `${APP_CANONICAL_ORIGIN}${canonicalPath}`;
97
- const socialImageHref = `${APP_CANONICAL_ORIGIN}${SOCIAL_IMAGE_PATH}`;
98
- const { title, description, robots } = getAuthMetaContent(mode, hasActionCode);
99
-
100
- return [
101
- { title },
102
- { name: 'description', content: description },
103
- { name: 'robots', content: robots },
104
- { property: 'og:site_name', content: 'Striae' },
105
- { property: 'og:type', content: 'website' },
106
- { property: 'og:url', content: canonicalHref },
107
- { property: 'og:title', content: title },
108
- { property: 'og:description', content: description },
109
- { property: 'og:image', content: socialImageHref },
110
- { property: 'og:image:secure_url', content: socialImageHref },
111
- { property: 'og:image:alt', content: SOCIAL_IMAGE_ALT },
112
- { name: 'twitter:card', content: 'summary_large_image' },
113
- { name: 'twitter:title', content: title },
114
- { name: 'twitter:description', content: description },
115
- { name: 'twitter:image', content: socialImageHref },
116
- { name: 'twitter:image:alt', content: SOCIAL_IMAGE_ALT },
117
- { tagName: 'link', rel: 'canonical', href: canonicalHref },
118
- ];
119
- };
31
+ const DEMO_COMPANY_NAME = 'STRIAE DEMO';
120
32
 
121
33
  const SUPPORTED_EMAIL_ACTION_MODES = new Set(['resetPassword', 'verifyEmail', 'recoverEmail']);
122
34
 
@@ -157,7 +69,8 @@ export const Login = () => {
157
69
  const [isClient, setIsClient] = useState(false);
158
70
  const [firstName, setFirstName] = useState('');
159
71
  const [lastName, setLastName] = useState('');
160
- const [company, setCompany] = useState('');
72
+ const [company, setCompany] = useState(DEMO_COMPANY_NAME);
73
+ const [badgeId, setBadgeId] = useState('');
161
74
  const [confirmPasswordValue, setConfirmPasswordValue] = useState('');
162
75
 
163
76
  // MFA state
@@ -251,7 +164,6 @@ export const Login = () => {
251
164
  }
252
165
 
253
166
  // Check if user exists in the USER_DB
254
- let hasBadgeId = true;
255
167
  setIsCheckingUser(true);
256
168
  try {
257
169
  const userData = await checkUserExists(currentUser);
@@ -262,8 +174,6 @@ export const Login = () => {
262
174
  setError('This account does not exist or has been deleted');
263
175
  return;
264
176
  }
265
-
266
- hasBadgeId = Boolean(userData.badgeId?.trim());
267
177
  } catch (error) {
268
178
  setIsCheckingUser(false);
269
179
  handleSignOut();
@@ -282,13 +192,8 @@ export const Login = () => {
282
192
  setShowMfaEnrollment(false);
283
193
 
284
194
  if (shouldShowWelcomeToastRef.current) {
285
- if (hasBadgeId) {
286
- setWelcomeToastType('success');
287
- setWelcomeToastMessage(`Welcome to Striae, ${getUserFirstName(currentUser)}!`);
288
- } else {
289
- setWelcomeToastType('warning');
290
- setWelcomeToastMessage('Your badge or ID number is not set. You can set one in Manage Profile.');
291
- }
195
+ setWelcomeToastType('success');
196
+ setWelcomeToastMessage(`Welcome to Striae, ${getUserFirstName(currentUser)}!`);
292
197
  setIsWelcomeToastVisible(true);
293
198
  shouldShowWelcomeToastRef.current = false;
294
199
  }
@@ -373,6 +278,7 @@ export const Login = () => {
373
278
  const formFirstName = firstName;
374
279
  const formLastName = lastName;
375
280
  const formCompany = company;
281
+ const formBadgeId = badgeId;
376
282
 
377
283
  try {
378
284
  if (!isLogin) {
@@ -411,7 +317,8 @@ export const Login = () => {
411
317
  formFirstName,
412
318
  formLastName,
413
319
  companyName || '',
414
- true
320
+ true,
321
+ formBadgeId.trim()
415
322
  );
416
323
 
417
324
  // Log user registration audit event
@@ -691,6 +598,17 @@ export const Login = () => {
691
598
  disabled={isLoading}
692
599
  value={company}
693
600
  onChange={(e) => setCompany(e.target.value)}
601
+ />
602
+ <input
603
+ type="text"
604
+ name="badgeId"
605
+ required
606
+ placeholder="Badge/ID # (required)"
607
+ autoComplete="off"
608
+ className={styles.input}
609
+ disabled={isLoading}
610
+ value={badgeId}
611
+ onChange={(e) => setBadgeId(e.target.value)}
694
612
  />
695
613
  {passwordStrength && (
696
614
  <div className={styles.passwordStrength}>
@@ -740,6 +658,7 @@ export const Login = () => {
740
658
  setFirstName('');
741
659
  setLastName('');
742
660
  setCompany('');
661
+ setBadgeId('');
743
662
  setConfirmPasswordValue('');
744
663
  }}
745
664
  className={styles.toggleButton}