goobs-frontend 0.9.13 → 0.9.14

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "goobs-frontend",
3
- "version": "0.9.13",
3
+ "version": "0.9.14",
4
4
  "type": "module",
5
5
  "description": "A comprehensive React-based libary that extends the functionality of Material-UI",
6
6
  "license": "MIT",
@@ -5,7 +5,7 @@ import { within, userEvent, expect, fireEvent } from '@storybook/test'
5
5
  import ConfirmationCodeInputs from './index'
6
6
 
7
7
  // Helper function to set input values directly without relying on userEvent.clear()
8
- const setInputValue = (input: HTMLInputElement, value: string) => {
8
+ const setInputValue = (input: HTMLInputElement, value: string): void => {
9
9
  // Use fireEvent directly which is more reliable in test environments
10
10
  fireEvent.change(input, { target: { value } })
11
11
 
@@ -14,7 +14,8 @@ const setInputValue = (input: HTMLInputElement, value: string) => {
14
14
  }
15
15
 
16
16
  // Helper function to wait for a specific time
17
- const sleep = (ms: number) => new Promise(resolve => setTimeout(resolve, ms))
17
+ const sleep = (ms: number): Promise<void> =>
18
+ new Promise(resolve => setTimeout(resolve, ms))
18
19
 
19
20
  /**
20
21
  * Configure Storybook metadata
@@ -35,7 +36,8 @@ const meta: Meta<typeof ConfirmationCodeInputs> = {
35
36
  onVerify: { action: 'onVerify clicked' },
36
37
  onSendResend: { action: 'onSendResend clicked' },
37
38
  },
38
- }
39
+ } as const
40
+
39
41
  export default meta
40
42
 
41
43
  type Story = StoryObj<typeof ConfirmationCodeInputs>
@@ -53,6 +55,7 @@ export const Basic: Story = {
53
55
  isValid={false}
54
56
  aria-label="Basic Confirmation Code"
55
57
  showActionButtons={false}
58
+ onDisableVerification={() => {}}
56
59
  />
57
60
  ),
58
61
  play: ({ canvasElement }) => {
@@ -85,6 +88,7 @@ export const PrefilledValue: Story = {
85
88
  value="1234"
86
89
  aria-label="Prefilled Confirmation Code"
87
90
  showActionButtons={false}
91
+ onDisableVerification={() => {}}
88
92
  />
89
93
  ),
90
94
  play: ({ canvasElement }) => {
@@ -120,6 +124,7 @@ export const ValidCode: Story = {
120
124
  isValid={true}
121
125
  aria-label="Valid Confirmation Code"
122
126
  showActionButtons={false}
127
+ onDisableVerification={() => {}}
123
128
  />
124
129
  ),
125
130
  play: ({ canvasElement }) => {
@@ -158,6 +163,7 @@ export const ManualTyping: Story = {
158
163
  value=""
159
164
  aria-label="Manual Code Entry"
160
165
  showActionButtons={false}
166
+ onDisableVerification={() => {}}
161
167
  />
162
168
  ),
163
169
  play: async ({ canvasElement, step }) => {
@@ -168,7 +174,7 @@ export const ManualTyping: Story = {
168
174
  // Get each input element
169
175
  const element = canvas.getByTestId(`code-input-${i + 1}`)
170
176
  // Convert to HTMLInputElement for proper typing
171
- return element as unknown as HTMLInputElement
177
+ return element as HTMLInputElement
172
178
  })
173
179
 
174
180
  // Step 1: Verify initial state
@@ -213,6 +219,7 @@ export const ArrowAndBackspace: Story = {
213
219
  value=""
214
220
  aria-label="Arrow Navigation Code"
215
221
  showActionButtons={false}
222
+ onDisableVerification={() => {}}
216
223
  />
217
224
  ),
218
225
  play: async ({ canvasElement, step }) => {
@@ -223,7 +230,7 @@ export const ArrowAndBackspace: Story = {
223
230
  // Get each input element
224
231
  const element = canvas.getByTestId(`code-input-${i + 1}`)
225
232
  // Convert to HTMLInputElement for proper typing
226
- return element as unknown as HTMLInputElement
233
+ return element as HTMLInputElement
227
234
  })
228
235
 
229
236
  // Use individual variables for clarity
@@ -281,6 +288,7 @@ export const WithSendCodeButton: Story = {
281
288
  codeSent: false,
282
289
  onVerify: () => console.log('Verify clicked'),
283
290
  onSendResend: () => console.log('Send Code clicked'),
291
+ onDisableVerification: () => {},
284
292
  },
285
293
  play: async ({ canvasElement }) => {
286
294
  const canvas = within(canvasElement)
@@ -312,6 +320,7 @@ export const WithResendCodeButton: Story = {
312
320
  codeSent: true,
313
321
  onVerify: () => console.log('Verify clicked'),
314
322
  onSendResend: () => console.log('Resend Code clicked'),
323
+ onDisableVerification: () => {},
315
324
  },
316
325
  play: async ({ canvasElement }) => {
317
326
  const canvas = within(canvasElement)
@@ -339,6 +348,7 @@ export const ValidCodeWithButtons: Story = {
339
348
  codeSent: true,
340
349
  onVerify: () => console.log('Verify clicked'),
341
350
  onSendResend: () => console.log('Resend Code clicked'),
351
+ onDisableVerification: () => {},
342
352
  },
343
353
  play: async ({ canvasElement }) => {
344
354
  const canvas = within(canvasElement)
@@ -384,6 +394,7 @@ export const WithCustomButtonProps: Story = {
384
394
  backgroundcolor: 'green',
385
395
  fontvariant: 'merrihelperfooter',
386
396
  },
397
+ onDisableVerification: () => {},
387
398
  },
388
399
  play: async ({ canvasElement }) => {
389
400
  const canvas = within(canvasElement)
@@ -417,6 +428,7 @@ export const ButtonBehaviorTest: Story = {
417
428
  codeSent={false}
418
429
  onVerify={() => console.log('Verify clicked')}
419
430
  onSendResend={() => console.log('Send/Resend Code clicked')}
431
+ onDisableVerification={() => {}}
420
432
  />
421
433
  ),
422
434
  play: async ({ canvasElement, step }) => {
@@ -449,7 +461,7 @@ export const ButtonBehaviorTest: Story = {
449
461
  // Get all inputs by testId
450
462
  const inputs = Array.from({ length: 4 }, (_, i) => {
451
463
  const element = canvas.getByTestId(`code-input-${i + 1}`)
452
- return element as unknown as HTMLInputElement
464
+ return element as HTMLInputElement
453
465
  })
454
466
 
455
467
  // Fill each input individually and force blur/change events
@@ -551,6 +563,7 @@ export const WithCodeSentFalse: Story = {
551
563
  codeSent: false,
552
564
  onVerify: () => console.log('Verify clicked'),
553
565
  onSendResend: () => console.log('Send/Resend Code clicked'),
566
+ onDisableVerification: () => {},
554
567
  },
555
568
  play: ({ canvasElement }) => {
556
569
  // Get buttons with direct DOM queries to be more reliable
@@ -579,6 +592,7 @@ export const WithCodeSentTrue: Story = {
579
592
  codeSent: true, // This is the key difference
580
593
  onVerify: () => console.log('Verify clicked'),
581
594
  onSendResend: () => console.log('Send/Resend Code clicked'),
595
+ onDisableVerification: () => {},
582
596
  },
583
597
  play: ({ canvasElement }) => {
584
598
  // Get buttons with direct DOM queries to be more reliable
@@ -1,14 +1,8 @@
1
1
  'use client'
2
- import React, {
3
- ChangeEvent,
4
- KeyboardEvent,
5
- useState,
6
- useEffect,
7
- useCallback,
8
- FC,
9
- } from 'react'
10
- import { Input, Box } from '@mui/material'
11
- import { red, green } from '../../styles/palette'
2
+ import React, { ChangeEvent, useState, useEffect, FC } from 'react'
3
+ import { Box, Typography } from '@mui/material'
4
+ import { CheckCircleOutline } from '@mui/icons-material'
5
+ import { red, grey } from '../../styles/palette'
12
6
  import CustomButton, { CustomButtonProps } from '../Button'
13
7
 
14
8
  export interface ConfirmationCodeInputsProps {
@@ -30,144 +24,29 @@ export interface ConfirmationCodeInputsProps {
30
24
  /** Callback function for when the Send/Resend button is clicked */
31
25
  onSendResend?: () => void | Promise<void>
32
26
 
27
+ /** Required callback function for when verification is disabled */
28
+ onDisableVerification: () => void | Promise<void>
29
+
33
30
  /** Custom props for the Verify button */
34
31
  verifyButtonProps?: Partial<CustomButtonProps>
35
32
 
36
33
  /** Custom props for the Send/Resend button */
37
34
  sendResendButtonProps?: Partial<CustomButtonProps>
38
35
 
36
+ /** Custom props for the Disable Verification button */
37
+ disableVerificationButtonProps?: Partial<CustomButtonProps>
38
+
39
39
  /** Whether to show action buttons (verify and send/resend) */
40
40
  showActionButtons?: boolean
41
41
 
42
42
  /** Whether to show the Send/Resend button. If false, only the Verify button will be shown. */
43
43
  showSendResendButton?: boolean
44
- }
45
-
46
- interface UseCodeConfirmationProps {
47
- codeLength: number
48
- onChange?: (value: string) => void
49
- inputRefs: React.MutableRefObject<(HTMLInputElement | null)[]>
50
- setInternalValue: React.Dispatch<React.SetStateAction<string>>
51
- }
52
-
53
- const useCodeConfirmation = ({
54
- codeLength,
55
- onChange,
56
- inputRefs,
57
- setInternalValue,
58
- }: UseCodeConfirmationProps) => {
59
- const [code, setCode] = useState<Record<string, string>>(
60
- Object.fromEntries(
61
- Array.from({ length: codeLength }, (_, i) => [`code${i + 1}`, ''])
62
- )
63
- )
64
-
65
- const handleCodeChange = useCallback(
66
- (event: React.ChangeEvent<HTMLInputElement>, index: number) => {
67
- // Only keep digits
68
- const value = event.target.value.replace(/\D/g, '')
69
-
70
- // Handle the case when the input is cleared
71
- if (value === '') {
72
- // When clearing, we just update the code
73
- setCode(prevCode => {
74
- const newCode = {
75
- ...prevCode,
76
- [`code${index + 1}`]: value,
77
- }
78
- // Combine all code pieces
79
- const combinedValue = Object.values(newCode).join('')
80
-
81
- // Update both internal state and external handler
82
- setInternalValue(combinedValue)
83
- onChange?.(combinedValue)
84
- return newCode
85
- })
86
- return
87
- }
88
-
89
- // Handle direct value setting via typing
90
- if (value.length <= 1) {
91
- // Only process if it's a single digit or empty
92
- setCode(prevCode => {
93
- const newCode = {
94
- ...prevCode,
95
- [`code${index + 1}`]: value,
96
- }
97
- // Combine all code pieces
98
- const combinedValue = Object.values(newCode).join('')
99
-
100
- // Update both internal state and external handler
101
- setInternalValue(combinedValue)
102
- onChange?.(combinedValue)
103
- return newCode
104
- })
105
- }
106
- },
107
- [onChange, setInternalValue]
108
- )
109
-
110
- const handleKeyDown = useCallback(
111
- (event: React.KeyboardEvent<HTMLInputElement>, index: number) => {
112
- // Allow only numeric keys, navigation keys, and backspace
113
- const allowedKeys = [
114
- 'Backspace',
115
- 'ArrowLeft',
116
- 'ArrowRight',
117
- 'Tab',
118
- 'Delete',
119
- 'Home',
120
- 'End',
121
- ]
122
-
123
- if (!allowedKeys.includes(event.key) && !/^\d$/.test(event.key)) {
124
- event.preventDefault()
125
- return
126
- }
127
-
128
- // If user pressed backspace on an empty input, move cursor to previous
129
- if (event.key === 'Backspace' && !code[`code${index + 1}`] && index > 0) {
130
- event.preventDefault()
131
- setCode(prevCode => {
132
- const newCode = {
133
- ...prevCode,
134
- [`code${index}`]: '',
135
- }
136
- const combinedValue = Object.values(newCode).join('')
137
44
 
138
- // Update both internal state and external handler
139
- setInternalValue(combinedValue)
140
- onChange?.(combinedValue)
141
- return newCode
142
- })
45
+ /** Custom success message to display */
46
+ successMessage?: string
143
47
 
144
- // Move focus to previous input
145
- setTimeout(() => {
146
- if (inputRefs && inputRefs.current && inputRefs.current[index - 1]) {
147
- inputRefs.current[index - 1]?.focus()
148
- }
149
- }, 0)
150
- } else if (event.key === 'ArrowLeft' && index > 0) {
151
- // Move focus left
152
- event.preventDefault()
153
- if (inputRefs && inputRefs.current && inputRefs.current[index - 1]) {
154
- inputRefs.current[index - 1]?.focus()
155
- }
156
- } else if (event.key === 'ArrowRight' && index < codeLength - 1) {
157
- // Move focus right
158
- event.preventDefault()
159
- if (inputRefs && inputRefs.current && inputRefs.current[index + 1]) {
160
- inputRefs.current[index + 1]?.focus()
161
- }
162
- }
163
- },
164
- [code, codeLength, onChange, inputRefs, setInternalValue]
165
- )
166
-
167
- return {
168
- handleCodeChange,
169
- handleKeyDown,
170
- }
48
+ /** Whether to show the success state UI */
49
+ showSuccessState?: boolean
171
50
  }
172
51
 
173
52
  const ConfirmationCodeInputs: FC<ConfirmationCodeInputsProps> = ({
@@ -181,155 +60,95 @@ const ConfirmationCodeInputs: FC<ConfirmationCodeInputsProps> = ({
181
60
  codeSent = false,
182
61
  onVerify,
183
62
  onSendResend,
63
+ onDisableVerification,
184
64
  verifyButtonProps = {},
185
65
  sendResendButtonProps = {},
66
+ disableVerificationButtonProps = {},
186
67
  showActionButtons = false,
187
68
  showSendResendButton = true,
69
+ successMessage = 'Verification Successful',
70
+ showSuccessState = false,
188
71
  ...props
189
72
  }) => {
190
73
  // Initialize internal state with the value prop
191
74
  const [internalValue, setInternalValue] = useState(value)
192
-
193
- // Create refs for each input
194
- const inputRefs = React.useRef<(HTMLInputElement | null)[]>([])
195
-
196
- // Initialize refs array
197
- React.useEffect(() => {
198
- inputRefs.current = inputRefs.current.slice(0, codeLength)
199
- // Fill with nulls if needed
200
- while (inputRefs.current.length < codeLength) {
201
- inputRefs.current.push(null)
202
- }
203
- }, [codeLength])
75
+ const [isFocused, setIsFocused] = useState(false)
76
+ const [cursorPosition, setCursorPosition] = useState(value.length)
77
+ const inputRef = React.useRef<HTMLInputElement>(null)
204
78
 
205
79
  // Update internal state when value prop changes
206
80
  useEffect(() => {
207
- // Only update if the value actually changed to avoid infinite loops
208
81
  if (internalValue !== value) {
209
82
  setInternalValue(value)
83
+ setCursorPosition(value.length)
210
84
  }
211
85
  }, [value, internalValue])
212
86
 
213
- // Custom onChange handler that updates both internal state and calls the external onChange
214
- const handleChangeWithState = (newValue: string) => {
215
- // Set internal state only if it's changed
87
+ const handleChange = (event: ChangeEvent<HTMLInputElement>) => {
88
+ // Only allow digits and limit to codeLength
89
+ const newValue = event.target.value.replace(/\D/g, '').slice(0, codeLength)
90
+
216
91
  if (internalValue !== newValue) {
217
92
  setInternalValue(newValue)
218
- }
219
-
220
- // Call external onChange if provided
221
- if (onChange && newValue !== value) {
222
- onChange(newValue)
93
+ setCursorPosition(event.target.selectionStart || newValue.length)
94
+ onChange?.(newValue)
223
95
  }
224
96
  }
225
97
 
226
- const { handleCodeChange, handleKeyDown } = useCodeConfirmation({
227
- codeLength,
228
- onChange: handleChangeWithState,
229
- inputRefs,
230
- setInternalValue, // Pass setInternalValue to ensure direct updates
231
- })
232
-
233
- // Special document-level handler for left arrow
234
- useEffect(() => {
235
- const handleGlobalKeyDown = (e: KeyboardEvent): void => {
236
- if (e.key === 'ArrowLeft') {
237
- const activeIndex = inputRefs.current.findIndex(
238
- ref => ref === document.activeElement
239
- )
240
- if (activeIndex > 0) {
241
- e.preventDefault()
242
- inputRefs.current[activeIndex - 1]?.focus()
243
- }
244
- }
245
- }
246
-
247
- document.addEventListener(
248
- 'keydown',
249
- handleGlobalKeyDown as unknown as EventListener
250
- )
251
- return () => {
252
- document.removeEventListener(
253
- 'keydown',
254
- handleGlobalKeyDown as unknown as EventListener
255
- )
98
+ const handleSelect = () => {
99
+ if (inputRef.current) {
100
+ setCursorPosition(inputRef.current.selectionStart || internalValue.length)
256
101
  }
257
- }, [])
102
+ }
258
103
 
259
104
  // Calculate container width based on number of inputs
260
- // Each input is 40px + 4px gap (MUI spacing 1) between them
261
- const inputAreaWidth = codeLength * 40 + (codeLength - 1) * 8 + 44 // Add 44px for the indicator dot and margin
105
+ const inputAreaWidth = codeLength * 40 + (codeLength - 1) * 8 + 44
262
106
 
263
- // Calculate button container width - ensure minimum width for buttons
264
- // Each button should be at least 120px for good UX
107
+ // Calculate button container width
265
108
  const minButtonWidth = 120
266
- // If only showing the verify button, we need less space
267
109
  const buttonContainerWidth = showSendResendButton
268
- ? Math.max(inputAreaWidth, minButtonWidth * 2 + 16) // 16px for gap between buttons
110
+ ? Math.max(inputAreaWidth, minButtonWidth * 2 + 16)
269
111
  : Math.max(inputAreaWidth, minButtonWidth)
270
112
 
271
- // For manual changes (typing digits), also handle auto-focus next field
272
- const handleChange = (
273
- event: ChangeEvent<HTMLInputElement>,
274
- index: number
275
- ) => {
276
- const inputValue = event.target.value.replace(/\D/g, '') // Only keep digits
277
-
278
- // Handle case when user types multiple digits at once
279
- if (inputValue.length > 1) {
280
- // Take only the last character typed
281
- const lastChar = inputValue.charAt(inputValue.length - 1)
282
- event.target.value = lastChar
283
- }
284
-
285
- // Apply the change to state
286
- handleCodeChange(event, index)
113
+ // Split the value into individual digits for display
114
+ const digits = internalValue.padEnd(codeLength, ' ').split('')
287
115
 
288
- // Only move focus if there's a non-empty value and not the last input
289
- if (inputValue && inputValue.length > 0 && index < codeLength - 1) {
290
- // Focus next input
291
- setTimeout(() => {
292
- if (inputRefs.current && inputRefs.current[index + 1]) {
293
- inputRefs.current[index + 1]?.focus()
294
- }
295
- }, 0)
296
- } else if (inputValue === '' && index > 0) {
297
- // If input is cleared and not the first input, try to focus the previous input
298
- setTimeout(() => {
299
- if (inputRefs.current && inputRefs.current[index - 1]) {
300
- inputRefs.current[index - 1]?.focus()
301
- }
302
- }, 0)
303
- }
304
- }
305
-
306
- const handleKeyDownWrapper = (
307
- event: KeyboardEvent<HTMLInputElement>,
308
- index: number
309
- ) => {
310
- // Direct focus handling for left arrow
311
- if (event.key === 'ArrowLeft' && index > 0) {
312
- event.preventDefault()
313
- if (inputRefs.current && inputRefs.current[index - 1]) {
314
- inputRefs.current[index - 1]?.focus()
315
- }
316
- return
317
- }
116
+ // Check if all code fields are filled
117
+ const allFieldsFilled = internalValue.length >= codeLength
318
118
 
319
- // For other keys, use the normal handler
320
- handleKeyDown(event, index)
119
+ // If showing success state, render the success UI
120
+ if (showSuccessState) {
121
+ return (
122
+ <Box
123
+ display="flex"
124
+ flexDirection="column"
125
+ alignItems="center"
126
+ gap={2}
127
+ padding={3}
128
+ width="100%"
129
+ >
130
+ <CheckCircleOutline sx={{ fontSize: 60, color: 'green' }} />
131
+ <Typography variant="h5" align="center">
132
+ {successMessage}
133
+ </Typography>
134
+ <Box sx={{ display: 'flex', gap: 2, width: '100%' }}>
135
+ <CustomButton
136
+ text="Disable Verification"
137
+ fontcolor="white"
138
+ backgroundcolor="black"
139
+ width="100%"
140
+ height="40px"
141
+ variant="outlined"
142
+ {...disableVerificationButtonProps}
143
+ onClick={() => {
144
+ void onDisableVerification()
145
+ }}
146
+ />
147
+ </Box>
148
+ </Box>
149
+ )
321
150
  }
322
151
 
323
- // Safely create an array of string digits from internal state
324
- const digits: string[] = internalValue
325
- ? internalValue.split('')
326
- : Array.from({ length: codeLength }, () => '')
327
-
328
- // Check if all code fields are filled
329
- const allFieldsFilled =
330
- digits.filter(digit => digit !== '' && digit !== undefined).length >=
331
- codeLength
332
-
333
152
  return (
334
153
  <Box
335
154
  display="flex"
@@ -347,47 +166,97 @@ const ConfirmationCodeInputs: FC<ConfirmationCodeInputsProps> = ({
347
166
  position="relative"
348
167
  marginBottom={2}
349
168
  >
350
- <Box display="flex" gap={1}>
351
- {Array.from({ length: codeLength }, (_, index) => (
352
- <Input
353
- key={`code-input-${index}-${digits[index] || ''}`}
354
- name={`code${index + 1}`}
355
- value={digits[index] || ''}
356
- inputRef={(el: HTMLInputElement | null) => {
357
- inputRefs.current[index] = el
358
- }}
359
- inputProps={{
360
- maxLength: 1,
361
- pattern: '[0-9]*',
362
- inputMode: 'numeric',
363
- 'aria-label': `Code Digit ${index + 1}`,
364
- 'aria-required': ariaRequired,
365
- 'aria-invalid': ariaInvalid,
366
- 'data-testid': `code-input-${index + 1}`,
367
- 'data-index': index,
368
- }}
169
+ <Box
170
+ display="flex"
171
+ gap={1}
172
+ sx={{
173
+ position: 'relative',
174
+ width: '100%',
175
+ }}
176
+ >
177
+ {/* Hidden input for actual value */}
178
+ <input
179
+ ref={inputRef}
180
+ type="text"
181
+ inputMode="numeric"
182
+ pattern="[0-9]*"
183
+ value={internalValue}
184
+ onChange={handleChange}
185
+ onFocus={() => setIsFocused(true)}
186
+ onBlur={() => setIsFocused(false)}
187
+ onSelect={handleSelect}
188
+ onKeyUp={handleSelect}
189
+ onMouseUp={handleSelect}
190
+ aria-label={ariaLabel || 'Confirmation Code'}
191
+ aria-required={ariaRequired}
192
+ aria-invalid={ariaInvalid}
193
+ style={{
194
+ position: 'absolute',
195
+ top: 0,
196
+ left: 0,
197
+ width: '100%',
198
+ height: '50px',
199
+ opacity: 0,
200
+ cursor: 'text',
201
+ fontSize: '16px',
202
+ letterSpacing: '39px',
203
+ paddingLeft: '15px',
204
+ zIndex: 1,
205
+ }}
206
+ {...props}
207
+ />
208
+
209
+ {/* Visual segments */}
210
+ {digits.map((digit, index) => (
211
+ <Box
212
+ key={index}
369
213
  sx={{
370
- border: '1px solid',
371
- borderColor: 'black',
214
+ border: '1px solid black',
372
215
  borderRadius: 1,
373
216
  width: 40,
374
217
  height: 50,
375
- input: {
376
- textAlign: 'center',
377
- color: 'black',
218
+ display: 'flex',
219
+ alignItems: 'center',
220
+ justifyContent: 'center',
221
+ color: 'black',
222
+ backgroundColor: 'white',
223
+ fontSize: '16px',
224
+ userSelect: 'none',
225
+ pointerEvents: 'none',
226
+ position: 'relative',
227
+ '&::after':
228
+ isFocused && index === cursorPosition
229
+ ? {
230
+ content: '""',
231
+ position: 'absolute',
232
+ right:
233
+ index === cursorPosition && cursorPosition > 0
234
+ ? '0'
235
+ : 'auto',
236
+ left:
237
+ index === cursorPosition && cursorPosition === 0
238
+ ? '0'
239
+ : 'auto',
240
+ transform: 'none',
241
+ top: '15%',
242
+ height: '70%',
243
+ width: '1px',
244
+ backgroundColor: 'black',
245
+ animation: 'blink 1s step-end infinite',
246
+ }
247
+ : {},
248
+ '@keyframes blink': {
249
+ 'from, to': {
250
+ opacity: 1,
251
+ },
252
+ '50%': {
253
+ opacity: 0,
254
+ },
378
255
  },
379
256
  }}
380
- onChange={(event: ChangeEvent<HTMLInputElement>) =>
381
- handleChange(event, index)
382
- }
383
- onKeyDown={(event: KeyboardEvent<HTMLInputElement>) =>
384
- handleKeyDownWrapper(event, index)
385
- }
386
- onFocus={() => {
387
- inputRefs.current[index]?.focus()
388
- }}
389
- {...props}
390
- />
257
+ >
258
+ {digit.trim()}
259
+ </Box>
391
260
  ))}
392
261
  </Box>
393
262
 
@@ -395,7 +264,7 @@ const ConfirmationCodeInputs: FC<ConfirmationCodeInputsProps> = ({
395
264
  width={20}
396
265
  height={20}
397
266
  borderRadius="50%"
398
- bgcolor={isValid ? green.main : red.main}
267
+ bgcolor={isValid ? grey.main : red.main}
399
268
  position="static"
400
269
  role="status"
401
270
  aria-label={isValid ? 'Code is valid' : 'Code is invalid'}
@@ -411,7 +280,6 @@ const ConfirmationCodeInputs: FC<ConfirmationCodeInputsProps> = ({
411
280
  width={`${buttonContainerWidth}px`}
412
281
  marginTop={1}
413
282
  sx={{
414
- // Center the button container if it's wider than input area
415
283
  marginLeft:
416
284
  buttonContainerWidth > inputAreaWidth
417
285
  ? `${-(buttonContainerWidth - inputAreaWidth) / 2}px`
@@ -433,12 +301,11 @@ const ConfirmationCodeInputs: FC<ConfirmationCodeInputsProps> = ({
433
301
  onClick={() => {
434
302
  if (onSendResend) void onSendResend()
435
303
  }}
436
- // Send/Resend button is always enabled, preserve any custom disableButton setting
437
304
  disableButton={sendResendButtonProps?.disableButton || 'false'}
438
305
  />
439
306
  )}
440
307
  <CustomButton
441
- text="Verify Phone"
308
+ text="Verify"
442
309
  fontcolor="white"
443
310
  backgroundcolor="black"
444
311
  width={
@@ -8,8 +8,8 @@ import Typography from '../Typography'
8
8
  /**
9
9
  * Props for the QRCodeComponent
10
10
  * @typedef {Object} QRCodeProps
11
- * @property {string} username - The username for the MFA setup
12
- * @property {string} appName - The name of the application for MFA
11
+ * @property {string} username - The username/email for the MFA setup
12
+ * @property {string} [appName] - The name of the application for MFA (defaults to "ThothOS")
13
13
  * @property {number} [size] - The size of the QR code in pixels
14
14
  * @property {string} [title] - An optional title to display above the QR code
15
15
  * @property {SxProps} [sx] - Custom styles to apply to the component
@@ -17,7 +17,7 @@ import Typography from '../Typography'
17
17
  */
18
18
  export interface QRCodeProps {
19
19
  username: string
20
- appName: string
20
+ appName?: string
21
21
  size?: number
22
22
  title?: string
23
23
  sx?: SxProps
@@ -30,13 +30,22 @@ export interface QRCodeProps {
30
30
  * @returns {React.ReactElement} The rendered QR code component
31
31
  */
32
32
  const QRCodeComponent: React.FC<QRCodeProps> = React.memo(
33
- ({ username, appName, size = 256, title, sx, onSecretGenerated }) => {
33
+ ({
34
+ username,
35
+ appName = 'ThothOS',
36
+ size = 256,
37
+ title,
38
+ sx,
39
+ onSecretGenerated,
40
+ }) => {
34
41
  // Generate the secret and OTP auth URL
35
42
  const { secret, otpAuth } = useMemo(() => {
36
43
  const generatedSecret = authenticator.generateSecret()
44
+
45
+ // We're using the raw username (likely email) directly instead of "your%20account"
37
46
  const otpAuthUrl = authenticator.keyuri(
38
- encodeURIComponent(username),
39
- encodeURIComponent(appName),
47
+ username, // Use the raw username/email without encoding
48
+ appName, // Now defaulting to "ThothOS"
40
49
  generatedSecret
41
50
  )
42
51
  return { secret: generatedSecret, otpAuth: otpAuthUrl }
@@ -100,6 +109,14 @@ const QRCodeComponent: React.FC<QRCodeProps> = React.memo(
100
109
  size={responsiveSize}
101
110
  style={{ height: 'auto', maxWidth: '100%', width: '100%' }}
102
111
  aria-label={`QR Code for ${title || 'MFA Setup'}`}
112
+ data-testid="mfa-qrcode"
113
+ />
114
+ </Box>
115
+ <Box sx={{ mt: 2, textAlign: 'center' }}>
116
+ <Typography
117
+ text={`${appName}: ${username}`}
118
+ fontvariant="merriparagraph"
119
+ align="center"
103
120
  />
104
121
  </Box>
105
122
  </Paper>
@@ -127,5 +144,17 @@ export function verifyMFAToken(token: string, secret: string): boolean {
127
144
  throw new Error('Invalid secret')
128
145
  }
129
146
 
130
- return authenticator.verify({ token, secret })
147
+ try {
148
+ // Configure authenticator options to match Microsoft Authenticator
149
+ authenticator.options = {
150
+ window: 1, // Allow codes from 1 step before and after
151
+ digits: 6, // Microsoft Authenticator uses 6-digit codes
152
+ step: 30, // 30-second interval for code generation
153
+ }
154
+
155
+ return authenticator.verify({ token, secret })
156
+ } catch (error) {
157
+ console.error('MFA verification error:', error)
158
+ return false
159
+ }
131
160
  }