goobs-frontend 0.9.14 → 0.9.16
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
|
'use client'
|
|
2
2
|
|
|
3
|
-
import { Checkbox } from '@mui/material'
|
|
3
|
+
import { Checkbox, styled } from '@mui/material'
|
|
4
4
|
import React from 'react'
|
|
5
5
|
import * as palette from '../../styles/palette'
|
|
6
6
|
|
|
@@ -12,6 +12,40 @@ export interface CheckboxProps {
|
|
|
12
12
|
disabled?: boolean
|
|
13
13
|
}
|
|
14
14
|
|
|
15
|
+
// Create a styled version of MUI Checkbox with our custom styles
|
|
16
|
+
const StyledCheckbox = styled(Checkbox)(() => ({
|
|
17
|
+
padding: '8px',
|
|
18
|
+
'& .MuiSvgIcon-root': {
|
|
19
|
+
fontSize: '24px',
|
|
20
|
+
},
|
|
21
|
+
// Style for unchecked state
|
|
22
|
+
'&:not(.Mui-checked):not(.Mui-indeterminate):not(.Mui-disabled)': {
|
|
23
|
+
'& .MuiSvgIcon-root': {
|
|
24
|
+
color: 'transparent',
|
|
25
|
+
// Add a visible background to make it obvious it's a checkbox
|
|
26
|
+
backgroundColor: palette.grey.light,
|
|
27
|
+
border: `2px solid ${palette.marine.main}`,
|
|
28
|
+
borderRadius: '3px',
|
|
29
|
+
},
|
|
30
|
+
},
|
|
31
|
+
'&.Mui-checked .MuiSvgIcon-root': {
|
|
32
|
+
color: palette.marine.main,
|
|
33
|
+
border: 'none',
|
|
34
|
+
},
|
|
35
|
+
'&.Mui-indeterminate .MuiSvgIcon-root': {
|
|
36
|
+
color: palette.marine.main,
|
|
37
|
+
border: 'none',
|
|
38
|
+
},
|
|
39
|
+
'&.Mui-disabled .MuiSvgIcon-root': {
|
|
40
|
+
color: palette.grey.main,
|
|
41
|
+
border: `2px solid ${palette.grey.main}`,
|
|
42
|
+
borderRadius: '3px',
|
|
43
|
+
},
|
|
44
|
+
'&:hover': {
|
|
45
|
+
backgroundColor: `${palette.marine.light}33`,
|
|
46
|
+
},
|
|
47
|
+
}))
|
|
48
|
+
|
|
15
49
|
function CustomCheckbox({
|
|
16
50
|
onClick,
|
|
17
51
|
checked,
|
|
@@ -35,28 +69,13 @@ function CustomCheckbox({
|
|
|
35
69
|
}
|
|
36
70
|
|
|
37
71
|
return (
|
|
38
|
-
<
|
|
39
|
-
sx={{
|
|
40
|
-
color: palette.marine.main,
|
|
41
|
-
'&.Mui-checked': {
|
|
42
|
-
color: palette.marine.main,
|
|
43
|
-
},
|
|
44
|
-
'&.Mui-indeterminate': {
|
|
45
|
-
color: palette.marine.main,
|
|
46
|
-
},
|
|
47
|
-
'&.Mui-disabled': {
|
|
48
|
-
color: palette.grey.main,
|
|
49
|
-
},
|
|
50
|
-
'&:hover': {
|
|
51
|
-
backgroundColor: palette.marine.light,
|
|
52
|
-
opacity: 0.1,
|
|
53
|
-
},
|
|
54
|
-
}}
|
|
72
|
+
<StyledCheckbox
|
|
55
73
|
checked={checked}
|
|
56
74
|
indeterminate={indeterminate}
|
|
57
75
|
onClick={handleClick}
|
|
58
76
|
onChange={handleChange}
|
|
59
77
|
disabled={disabled}
|
|
78
|
+
disableRipple
|
|
60
79
|
{...props}
|
|
61
80
|
/>
|
|
62
81
|
)
|
|
@@ -1,8 +1,7 @@
|
|
|
1
1
|
'use client'
|
|
2
|
-
import React, {
|
|
3
|
-
import { Box, Typography } from '@mui/material'
|
|
2
|
+
import React, { useState, useEffect, FC, useRef } from 'react'
|
|
3
|
+
import { Box, Typography, styled } from '@mui/material'
|
|
4
4
|
import { CheckCircleOutline } from '@mui/icons-material'
|
|
5
|
-
import { red, grey } from '../../styles/palette'
|
|
6
5
|
import CustomButton, { CustomButtonProps } from '../Button'
|
|
7
6
|
|
|
8
7
|
export interface ConfirmationCodeInputsProps {
|
|
@@ -47,8 +46,31 @@ export interface ConfirmationCodeInputsProps {
|
|
|
47
46
|
|
|
48
47
|
/** Whether to show the success state UI */
|
|
49
48
|
showSuccessState?: boolean
|
|
49
|
+
|
|
50
|
+
/** Custom styling for the input fields */
|
|
51
|
+
inputStyle?: React.CSSProperties
|
|
50
52
|
}
|
|
51
53
|
|
|
54
|
+
// Custom styled input for verification code digits
|
|
55
|
+
const CodeInput = styled('input')(() => ({
|
|
56
|
+
width: '40px',
|
|
57
|
+
height: '50px',
|
|
58
|
+
padding: '0',
|
|
59
|
+
textAlign: 'center',
|
|
60
|
+
fontSize: '16px',
|
|
61
|
+
fontWeight: 'normal',
|
|
62
|
+
color: 'black',
|
|
63
|
+
backgroundColor: 'white',
|
|
64
|
+
border: '1px solid black',
|
|
65
|
+
borderRadius: '4px',
|
|
66
|
+
outline: 'none',
|
|
67
|
+
// The cursor is visible (not hiding with caretColor)
|
|
68
|
+
'&:focus': {
|
|
69
|
+
borderColor: 'black',
|
|
70
|
+
borderWidth: '2px',
|
|
71
|
+
},
|
|
72
|
+
}))
|
|
73
|
+
|
|
52
74
|
const ConfirmationCodeInputs: FC<ConfirmationCodeInputsProps> = ({
|
|
53
75
|
codeLength = 6,
|
|
54
76
|
isValid,
|
|
@@ -68,40 +90,190 @@ const ConfirmationCodeInputs: FC<ConfirmationCodeInputsProps> = ({
|
|
|
68
90
|
showSendResendButton = true,
|
|
69
91
|
successMessage = 'Verification Successful',
|
|
70
92
|
showSuccessState = false,
|
|
71
|
-
|
|
93
|
+
inputStyle = {},
|
|
72
94
|
}) => {
|
|
73
95
|
// Initialize internal state with the value prop
|
|
74
96
|
const [internalValue, setInternalValue] = useState(value)
|
|
75
|
-
const
|
|
76
|
-
|
|
77
|
-
|
|
97
|
+
const inputRefs = useRef<(HTMLInputElement | null)[]>([])
|
|
98
|
+
|
|
99
|
+
// Initialize refs array
|
|
100
|
+
useEffect(() => {
|
|
101
|
+
inputRefs.current = Array(codeLength).fill(
|
|
102
|
+
null
|
|
103
|
+
) as (HTMLInputElement | null)[]
|
|
104
|
+
}, [codeLength])
|
|
78
105
|
|
|
79
106
|
// Update internal state when value prop changes
|
|
80
107
|
useEffect(() => {
|
|
81
108
|
if (internalValue !== value) {
|
|
82
109
|
setInternalValue(value)
|
|
83
|
-
setCursorPosition(value.length)
|
|
84
110
|
}
|
|
85
111
|
}, [value, internalValue])
|
|
86
112
|
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
const
|
|
113
|
+
// Auto-focus first input field when component mounts
|
|
114
|
+
useEffect(() => {
|
|
115
|
+
const timer = setTimeout(() => {
|
|
116
|
+
if (inputRefs.current[0] && internalValue.length === 0) {
|
|
117
|
+
inputRefs.current[0].focus()
|
|
118
|
+
}
|
|
119
|
+
}, 100)
|
|
120
|
+
return () => clearTimeout(timer)
|
|
121
|
+
}, [internalValue.length])
|
|
90
122
|
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
123
|
+
// Handle input change for a specific digit
|
|
124
|
+
const handleInputChange = (
|
|
125
|
+
e: React.ChangeEvent<HTMLInputElement>,
|
|
126
|
+
index: number
|
|
127
|
+
) => {
|
|
128
|
+
const target = e.target
|
|
129
|
+
const val = target.value
|
|
130
|
+
|
|
131
|
+
// Only accept numbers
|
|
132
|
+
if (!/^\d*$/.test(val)) {
|
|
133
|
+
return
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
// Create a copy of the current value as an array of characters
|
|
137
|
+
let newValueArr = internalValue.padEnd(codeLength, '').split('')
|
|
138
|
+
|
|
139
|
+
// If input has multiple characters (from paste), process them
|
|
140
|
+
if (val.length > 1) {
|
|
141
|
+
const digits = val.split('')
|
|
142
|
+
for (let i = 0; i < digits.length; i++) {
|
|
143
|
+
if (index + i < codeLength) {
|
|
144
|
+
newValueArr[index + i] = digits[i]
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
} else {
|
|
148
|
+
// Only replace the single character at the index
|
|
149
|
+
newValueArr[index] = val.charAt(val.length - 1)
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
// Convert back to string and remove trailing spaces
|
|
153
|
+
const newValue = newValueArr.join('').trimEnd()
|
|
154
|
+
|
|
155
|
+
// Update internal state and call onChange
|
|
156
|
+
setInternalValue(newValue)
|
|
157
|
+
onChange?.(newValue)
|
|
158
|
+
|
|
159
|
+
// Move focus to next input if we have a value and there's a next input
|
|
160
|
+
if (val && index < codeLength - 1) {
|
|
161
|
+
inputRefs.current[index + 1]?.focus()
|
|
95
162
|
}
|
|
96
163
|
}
|
|
97
164
|
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
165
|
+
// Handle key down events for navigation
|
|
166
|
+
const handleKeyDown = (
|
|
167
|
+
e: React.KeyboardEvent<HTMLInputElement>,
|
|
168
|
+
index: number
|
|
169
|
+
) => {
|
|
170
|
+
const target = e.target as HTMLInputElement
|
|
171
|
+
|
|
172
|
+
switch (e.key) {
|
|
173
|
+
case 'Backspace': {
|
|
174
|
+
if (target.value === '') {
|
|
175
|
+
// If current field is empty and not the first field, move to previous field
|
|
176
|
+
if (index > 0) {
|
|
177
|
+
e.preventDefault()
|
|
178
|
+
inputRefs.current[index - 1]?.focus()
|
|
179
|
+
|
|
180
|
+
// Also clear the previous field if needed
|
|
181
|
+
const newValueArr = internalValue.split('')
|
|
182
|
+
newValueArr[index - 1] = ''
|
|
183
|
+
const newValue = newValueArr.join('').trimEnd()
|
|
184
|
+
setInternalValue(newValue)
|
|
185
|
+
onChange?.(newValue)
|
|
186
|
+
}
|
|
187
|
+
} else {
|
|
188
|
+
// Clear current field but don't move
|
|
189
|
+
const newValueArr = internalValue.split('')
|
|
190
|
+
newValueArr[index] = ''
|
|
191
|
+
const newValue = newValueArr.join('').trimEnd()
|
|
192
|
+
setInternalValue(newValue)
|
|
193
|
+
onChange?.(newValue)
|
|
194
|
+
}
|
|
195
|
+
break
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
case 'Delete': {
|
|
199
|
+
// Clear current field
|
|
200
|
+
const newValueArr = internalValue.split('')
|
|
201
|
+
newValueArr[index] = ''
|
|
202
|
+
const newValue = newValueArr.join('').trimEnd()
|
|
203
|
+
setInternalValue(newValue)
|
|
204
|
+
onChange?.(newValue)
|
|
205
|
+
break
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
case 'ArrowLeft':
|
|
209
|
+
// Move to previous input if exists
|
|
210
|
+
if (index > 0) {
|
|
211
|
+
e.preventDefault()
|
|
212
|
+
inputRefs.current[index - 1]?.focus()
|
|
213
|
+
}
|
|
214
|
+
break
|
|
215
|
+
|
|
216
|
+
case 'ArrowRight':
|
|
217
|
+
// Move to next input if exists
|
|
218
|
+
if (index < codeLength - 1) {
|
|
219
|
+
e.preventDefault()
|
|
220
|
+
inputRefs.current[index + 1]?.focus()
|
|
221
|
+
}
|
|
222
|
+
break
|
|
223
|
+
|
|
224
|
+
default: {
|
|
225
|
+
// For number keys, handle them directly
|
|
226
|
+
if (/^\d$/.test(e.key)) {
|
|
227
|
+
e.preventDefault()
|
|
228
|
+
|
|
229
|
+
// Update the value at this index
|
|
230
|
+
const newValueArr = internalValue.padEnd(codeLength, '').split('')
|
|
231
|
+
newValueArr[index] = e.key
|
|
232
|
+
const newValue = newValueArr.join('').trimEnd()
|
|
233
|
+
|
|
234
|
+
setInternalValue(newValue)
|
|
235
|
+
onChange?.(newValue)
|
|
236
|
+
|
|
237
|
+
// Move to next input if there's one
|
|
238
|
+
if (index < codeLength - 1) {
|
|
239
|
+
inputRefs.current[index + 1]?.focus()
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
break
|
|
243
|
+
}
|
|
101
244
|
}
|
|
102
245
|
}
|
|
103
246
|
|
|
104
|
-
//
|
|
247
|
+
// Handle paste event to distribute digits across inputs
|
|
248
|
+
const handlePaste = (
|
|
249
|
+
e: React.ClipboardEvent<HTMLInputElement>,
|
|
250
|
+
index: number
|
|
251
|
+
) => {
|
|
252
|
+
e.preventDefault()
|
|
253
|
+
const pastedData = e.clipboardData.getData('text')
|
|
254
|
+
const digits = pastedData.replace(/\D/g, '').slice(0, codeLength - index)
|
|
255
|
+
|
|
256
|
+
if (digits) {
|
|
257
|
+
// Update the value from the current index onwards
|
|
258
|
+
const newValueArr = internalValue.padEnd(codeLength, '').split('')
|
|
259
|
+
|
|
260
|
+
for (let i = 0; i < digits.length; i++) {
|
|
261
|
+
if (index + i < codeLength) {
|
|
262
|
+
newValueArr[index + i] = digits[i]
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
const newValue = newValueArr.join('').trimEnd()
|
|
267
|
+
setInternalValue(newValue)
|
|
268
|
+
onChange?.(newValue)
|
|
269
|
+
|
|
270
|
+
// Focus the input after the last pasted digit or the last input
|
|
271
|
+
const focusIndex = Math.min(index + digits.length, codeLength - 1)
|
|
272
|
+
inputRefs.current[focusIndex]?.focus()
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
// Calculate container width based on number of inputs and spacing
|
|
105
277
|
const inputAreaWidth = codeLength * 40 + (codeLength - 1) * 8 + 44
|
|
106
278
|
|
|
107
279
|
// Calculate button container width
|
|
@@ -110,8 +282,8 @@ const ConfirmationCodeInputs: FC<ConfirmationCodeInputsProps> = ({
|
|
|
110
282
|
? Math.max(inputAreaWidth, minButtonWidth * 2 + 16)
|
|
111
283
|
: Math.max(inputAreaWidth, minButtonWidth)
|
|
112
284
|
|
|
113
|
-
// Split the value into individual digits
|
|
114
|
-
const digits = internalValue.padEnd(codeLength, '
|
|
285
|
+
// Split the value into individual digits
|
|
286
|
+
const digits = internalValue.padEnd(codeLength, '').split('')
|
|
115
287
|
|
|
116
288
|
// Check if all code fields are filled
|
|
117
289
|
const allFieldsFilled = internalValue.length >= codeLength
|
|
@@ -149,6 +321,17 @@ const ConfirmationCodeInputs: FC<ConfirmationCodeInputsProps> = ({
|
|
|
149
321
|
)
|
|
150
322
|
}
|
|
151
323
|
|
|
324
|
+
const statusIndicator = (
|
|
325
|
+
<Box
|
|
326
|
+
width={20}
|
|
327
|
+
height={20}
|
|
328
|
+
borderRadius="50%"
|
|
329
|
+
bgcolor={isValid ? 'green' : 'red'}
|
|
330
|
+
role="status"
|
|
331
|
+
aria-label={isValid ? 'Code is valid' : 'Code is invalid'}
|
|
332
|
+
/>
|
|
333
|
+
)
|
|
334
|
+
|
|
152
335
|
return (
|
|
153
336
|
<Box
|
|
154
337
|
display="flex"
|
|
@@ -166,111 +349,33 @@ const ConfirmationCodeInputs: FC<ConfirmationCodeInputsProps> = ({
|
|
|
166
349
|
position="relative"
|
|
167
350
|
marginBottom={2}
|
|
168
351
|
>
|
|
169
|
-
<Box
|
|
170
|
-
display="flex"
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
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}
|
|
213
|
-
sx={{
|
|
214
|
-
border: '1px solid black',
|
|
215
|
-
borderRadius: 1,
|
|
216
|
-
width: 40,
|
|
217
|
-
height: 50,
|
|
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
|
-
},
|
|
255
|
-
},
|
|
256
|
-
}}
|
|
257
|
-
>
|
|
258
|
-
{digit.trim()}
|
|
259
|
-
</Box>
|
|
260
|
-
))}
|
|
352
|
+
<Box display="flex" alignItems="center" width="100%">
|
|
353
|
+
<Box display="flex" gap={1} width="100%">
|
|
354
|
+
{Array.from({ length: codeLength }).map((_, index) => (
|
|
355
|
+
<CodeInput
|
|
356
|
+
key={index}
|
|
357
|
+
ref={el => {
|
|
358
|
+
inputRefs.current[index] = el
|
|
359
|
+
}}
|
|
360
|
+
type="text"
|
|
361
|
+
inputMode="numeric"
|
|
362
|
+
pattern="[0-9]*"
|
|
363
|
+
maxLength={1}
|
|
364
|
+
value={digits[index] || ''}
|
|
365
|
+
onChange={e => handleInputChange(e, index)}
|
|
366
|
+
onKeyDown={e => handleKeyDown(e, index)}
|
|
367
|
+
onPaste={e => handlePaste(e, index)}
|
|
368
|
+
aria-label={`${ariaLabel || 'Confirmation Code'} digit ${index + 1}`}
|
|
369
|
+
aria-required={ariaRequired}
|
|
370
|
+
aria-invalid={ariaInvalid}
|
|
371
|
+
style={{
|
|
372
|
+
...inputStyle,
|
|
373
|
+
}}
|
|
374
|
+
/>
|
|
375
|
+
))}
|
|
376
|
+
</Box>
|
|
377
|
+
<Box ml={2}>{statusIndicator}</Box>
|
|
261
378
|
</Box>
|
|
262
|
-
|
|
263
|
-
<Box
|
|
264
|
-
width={20}
|
|
265
|
-
height={20}
|
|
266
|
-
borderRadius="50%"
|
|
267
|
-
bgcolor={isValid ? grey.main : red.main}
|
|
268
|
-
position="static"
|
|
269
|
-
role="status"
|
|
270
|
-
aria-label={isValid ? 'Code is valid' : 'Code is invalid'}
|
|
271
|
-
alignSelf="center"
|
|
272
|
-
marginRight={2}
|
|
273
|
-
/>
|
|
274
379
|
</Box>
|
|
275
380
|
|
|
276
381
|
{showActionButtons && (
|
|
@@ -4,6 +4,11 @@ import { Box, Paper } from '@mui/material'
|
|
|
4
4
|
import { SxProps } from '@mui/system'
|
|
5
5
|
import { authenticator } from 'otplib'
|
|
6
6
|
import Typography from '../Typography'
|
|
7
|
+
import CustomButton, { CustomButtonProps } from '../Button'
|
|
8
|
+
import { CheckCircleOutline } from '@mui/icons-material'
|
|
9
|
+
import ConfirmationCodeInputs, {
|
|
10
|
+
ConfirmationCodeInputsProps,
|
|
11
|
+
} from '../ConfirmationCodeInput'
|
|
7
12
|
|
|
8
13
|
/**
|
|
9
14
|
* Props for the QRCodeComponent
|
|
@@ -14,6 +19,18 @@ import Typography from '../Typography'
|
|
|
14
19
|
* @property {string} [title] - An optional title to display above the QR code
|
|
15
20
|
* @property {SxProps} [sx] - Custom styles to apply to the component
|
|
16
21
|
* @property {(secret: string) => void} [onSecretGenerated] - Callback function to receive the generated secret
|
|
22
|
+
* @property {boolean} [showVerifyButton] - Whether to show the verify button
|
|
23
|
+
* @property {() => void | Promise<void>} [onVerify] - Callback function for when the Verify button is clicked
|
|
24
|
+
* @property {() => void | Promise<void>} [onDisableVerification] - Required callback function for when verification is disabled
|
|
25
|
+
* @property {Partial<CustomButtonProps>} [verifyButtonProps] - Custom props for the Verify button
|
|
26
|
+
* @property {Partial<CustomButtonProps>} [disableVerificationButtonProps] - Custom props for the Disable Verification button
|
|
27
|
+
* @property {boolean} [showSuccessState] - Whether to show the success state UI
|
|
28
|
+
* @property {string} [successMessage] - Custom success message to display
|
|
29
|
+
* @property {boolean} [showConfirmationInput] - Whether to show the confirmation code input
|
|
30
|
+
* @property {string} [confirmationCode] - The current confirmation code value
|
|
31
|
+
* @property {(value: string) => void} [onConfirmationCodeChange] - Callback for when confirmation code changes
|
|
32
|
+
* @property {ConfirmationCodeInputsProps} [confirmationCodeProps] - Custom props for the confirmation code input
|
|
33
|
+
* @property {boolean} [showDisableConfirmation] - Whether to show the disable confirmation state
|
|
17
34
|
*/
|
|
18
35
|
export interface QRCodeProps {
|
|
19
36
|
username: string
|
|
@@ -22,6 +39,18 @@ export interface QRCodeProps {
|
|
|
22
39
|
title?: string
|
|
23
40
|
sx?: SxProps
|
|
24
41
|
onSecretGenerated?: (secret: string) => void
|
|
42
|
+
showVerifyButton?: boolean
|
|
43
|
+
onVerify?: () => void | Promise<void>
|
|
44
|
+
onDisableVerification?: () => void | Promise<void>
|
|
45
|
+
verifyButtonProps?: Partial<CustomButtonProps>
|
|
46
|
+
disableVerificationButtonProps?: Partial<CustomButtonProps>
|
|
47
|
+
showSuccessState?: boolean
|
|
48
|
+
successMessage?: string
|
|
49
|
+
showConfirmationInput?: boolean
|
|
50
|
+
confirmationCode?: string
|
|
51
|
+
onConfirmationCodeChange?: (value: string) => void
|
|
52
|
+
confirmationCodeProps?: Partial<ConfirmationCodeInputsProps>
|
|
53
|
+
showDisableConfirmation?: boolean
|
|
25
54
|
}
|
|
26
55
|
|
|
27
56
|
/**
|
|
@@ -37,6 +66,18 @@ const QRCodeComponent: React.FC<QRCodeProps> = React.memo(
|
|
|
37
66
|
title,
|
|
38
67
|
sx,
|
|
39
68
|
onSecretGenerated,
|
|
69
|
+
showVerifyButton = false,
|
|
70
|
+
onVerify,
|
|
71
|
+
onDisableVerification,
|
|
72
|
+
verifyButtonProps = {},
|
|
73
|
+
disableVerificationButtonProps = {},
|
|
74
|
+
showSuccessState = false,
|
|
75
|
+
successMessage = 'Verification Successful',
|
|
76
|
+
showConfirmationInput = false,
|
|
77
|
+
confirmationCode = '',
|
|
78
|
+
onConfirmationCodeChange,
|
|
79
|
+
confirmationCodeProps = {},
|
|
80
|
+
showDisableConfirmation = false,
|
|
40
81
|
}) => {
|
|
41
82
|
// Generate the secret and OTP auth URL
|
|
42
83
|
const { secret, otpAuth } = useMemo(() => {
|
|
@@ -75,6 +116,128 @@ const QRCodeComponent: React.FC<QRCodeProps> = React.memo(
|
|
|
75
116
|
)
|
|
76
117
|
}
|
|
77
118
|
|
|
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
|
|
132
|
+
text={successMessage}
|
|
133
|
+
fontvariant="merrih5"
|
|
134
|
+
align="center"
|
|
135
|
+
/>
|
|
136
|
+
<Box sx={{ display: 'flex', gap: 2, width: '100%' }}>
|
|
137
|
+
<CustomButton
|
|
138
|
+
text="Disable Verification"
|
|
139
|
+
fontcolor="white"
|
|
140
|
+
backgroundcolor="black"
|
|
141
|
+
width="100%"
|
|
142
|
+
height="40px"
|
|
143
|
+
variant="outlined"
|
|
144
|
+
{...disableVerificationButtonProps}
|
|
145
|
+
onClick={() => {
|
|
146
|
+
if (onDisableVerification) void onDisableVerification()
|
|
147
|
+
}}
|
|
148
|
+
/>
|
|
149
|
+
</Box>
|
|
150
|
+
</Box>
|
|
151
|
+
)
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
// If showing disable confirmation state, render QR with confirmation input
|
|
155
|
+
if (showDisableConfirmation) {
|
|
156
|
+
return (
|
|
157
|
+
<Paper
|
|
158
|
+
elevation={3}
|
|
159
|
+
sx={{
|
|
160
|
+
p: 3,
|
|
161
|
+
display: 'inline-block',
|
|
162
|
+
maxWidth: '100%',
|
|
163
|
+
boxSizing: 'border-box',
|
|
164
|
+
...sx,
|
|
165
|
+
}}
|
|
166
|
+
>
|
|
167
|
+
{title && (
|
|
168
|
+
<Typography
|
|
169
|
+
text={title}
|
|
170
|
+
fontvariant="merrih5"
|
|
171
|
+
align="center"
|
|
172
|
+
gutterBottom
|
|
173
|
+
/>
|
|
174
|
+
)}
|
|
175
|
+
<Box
|
|
176
|
+
sx={{
|
|
177
|
+
display: 'flex',
|
|
178
|
+
justifyContent: 'center',
|
|
179
|
+
alignItems: 'center',
|
|
180
|
+
width: responsiveSize,
|
|
181
|
+
height: responsiveSize,
|
|
182
|
+
margin: 'auto',
|
|
183
|
+
}}
|
|
184
|
+
>
|
|
185
|
+
<QRCode
|
|
186
|
+
value={otpAuth}
|
|
187
|
+
size={responsiveSize}
|
|
188
|
+
style={{ height: 'auto', maxWidth: '100%', width: '100%' }}
|
|
189
|
+
aria-label={`QR Code for ${title || 'MFA Setup'}`}
|
|
190
|
+
data-testid="mfa-qrcode"
|
|
191
|
+
/>
|
|
192
|
+
</Box>
|
|
193
|
+
<Box sx={{ mt: 2, textAlign: 'center' }}>
|
|
194
|
+
<Typography
|
|
195
|
+
text={`${appName}: ${username}`}
|
|
196
|
+
fontvariant="merriparagraph"
|
|
197
|
+
align="center"
|
|
198
|
+
/>
|
|
199
|
+
</Box>
|
|
200
|
+
|
|
201
|
+
<Box
|
|
202
|
+
sx={{
|
|
203
|
+
mt: 3,
|
|
204
|
+
display: 'flex',
|
|
205
|
+
flexDirection: 'column',
|
|
206
|
+
alignItems: 'center',
|
|
207
|
+
gap: 2,
|
|
208
|
+
}}
|
|
209
|
+
>
|
|
210
|
+
<ConfirmationCodeInputs
|
|
211
|
+
isValid={false}
|
|
212
|
+
codeLength={6}
|
|
213
|
+
value={confirmationCode}
|
|
214
|
+
onChange={onConfirmationCodeChange}
|
|
215
|
+
showActionButtons={false}
|
|
216
|
+
onDisableVerification={() => {}}
|
|
217
|
+
{...confirmationCodeProps}
|
|
218
|
+
/>
|
|
219
|
+
|
|
220
|
+
<CustomButton
|
|
221
|
+
text="Verify & Disable"
|
|
222
|
+
fontcolor="white"
|
|
223
|
+
backgroundcolor="black"
|
|
224
|
+
width="100%"
|
|
225
|
+
height="40px"
|
|
226
|
+
{...verifyButtonProps}
|
|
227
|
+
onClick={() => {
|
|
228
|
+
if (onVerify) void onVerify()
|
|
229
|
+
}}
|
|
230
|
+
disableButton={
|
|
231
|
+
verifyButtonProps?.disableButton ||
|
|
232
|
+
(confirmationCode.length < 6 ? 'true' : 'false')
|
|
233
|
+
}
|
|
234
|
+
/>
|
|
235
|
+
</Box>
|
|
236
|
+
</Paper>
|
|
237
|
+
)
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
// Default QR code view with optional confirmation input and buttons
|
|
78
241
|
return (
|
|
79
242
|
<Paper
|
|
80
243
|
elevation={3}
|
|
@@ -119,6 +282,44 @@ const QRCodeComponent: React.FC<QRCodeProps> = React.memo(
|
|
|
119
282
|
align="center"
|
|
120
283
|
/>
|
|
121
284
|
</Box>
|
|
285
|
+
|
|
286
|
+
{showConfirmationInput && (
|
|
287
|
+
<Box sx={{ mt: 3, display: 'flex', justifyContent: 'center' }}>
|
|
288
|
+
<ConfirmationCodeInputs
|
|
289
|
+
isValid={false}
|
|
290
|
+
codeLength={6}
|
|
291
|
+
value={confirmationCode}
|
|
292
|
+
onChange={onConfirmationCodeChange}
|
|
293
|
+
showActionButtons={false}
|
|
294
|
+
onDisableVerification={() => {}}
|
|
295
|
+
{...confirmationCodeProps}
|
|
296
|
+
/>
|
|
297
|
+
</Box>
|
|
298
|
+
)}
|
|
299
|
+
|
|
300
|
+
{showVerifyButton && (
|
|
301
|
+
<Box
|
|
302
|
+
sx={{ mt: 3, display: 'flex', justifyContent: 'center', gap: 2 }}
|
|
303
|
+
>
|
|
304
|
+
<CustomButton
|
|
305
|
+
text="Verify Code"
|
|
306
|
+
fontcolor="white"
|
|
307
|
+
backgroundcolor="black"
|
|
308
|
+
width="100%"
|
|
309
|
+
height="40px"
|
|
310
|
+
{...verifyButtonProps}
|
|
311
|
+
onClick={() => {
|
|
312
|
+
if (onVerify) void onVerify()
|
|
313
|
+
}}
|
|
314
|
+
disableButton={
|
|
315
|
+
verifyButtonProps?.disableButton ||
|
|
316
|
+
(showConfirmationInput && confirmationCode.length < 6
|
|
317
|
+
? 'true'
|
|
318
|
+
: 'false')
|
|
319
|
+
}
|
|
320
|
+
/>
|
|
321
|
+
</Box>
|
|
322
|
+
)}
|
|
122
323
|
</Paper>
|
|
123
324
|
)
|
|
124
325
|
}
|
|
@@ -148,8 +349,8 @@ export function verifyMFAToken(token: string, secret: string): boolean {
|
|
|
148
349
|
// Configure authenticator options to match Microsoft Authenticator
|
|
149
350
|
authenticator.options = {
|
|
150
351
|
window: 1, // Allow codes from 1 step before and after
|
|
151
|
-
digits: 6, // Microsoft Authenticator uses 6-digit codes
|
|
152
352
|
step: 30, // 30-second interval for code generation
|
|
353
|
+
digits: 6, // Microsoft Authenticator uses 6-digit codes
|
|
153
354
|
}
|
|
154
355
|
|
|
155
356
|
return authenticator.verify({ token, secret })
|
|
@@ -229,9 +229,6 @@ const Typography = ({
|
|
|
229
229
|
if (typeof actualVariant === 'string' && actualVariant.length > 0) {
|
|
230
230
|
// First, try to get the variant from the theme
|
|
231
231
|
try {
|
|
232
|
-
// Log the actual variant being used
|
|
233
|
-
console.log('Using variant:', actualVariant)
|
|
234
|
-
|
|
235
232
|
// Check if we're using a custom font variant (e.g., 'merrih2')
|
|
236
233
|
if (/^(arapey|inter|merri)/.test(actualVariant)) {
|
|
237
234
|
// For custom variants, we need to check if they exist in the theme
|
|
@@ -246,7 +243,6 @@ const Typography = ({
|
|
|
246
243
|
const themeVariant = themeTypography[
|
|
247
244
|
actualVariant
|
|
248
245
|
] as TypographyVariantStyle
|
|
249
|
-
console.log('Found theme variant:', themeVariant)
|
|
250
246
|
|
|
251
247
|
if (themeVariant) {
|
|
252
248
|
variantStyle = {
|
|
@@ -259,8 +255,6 @@ const Typography = ({
|
|
|
259
255
|
}
|
|
260
256
|
} else {
|
|
261
257
|
// Custom variant not in theme, fallback to hardcoded styles
|
|
262
|
-
console.log('Custom variant not found in theme, using fallback')
|
|
263
|
-
|
|
264
258
|
const fontFamily = actualVariant.startsWith('arapey')
|
|
265
259
|
? 'arapey'
|
|
266
260
|
: actualVariant.startsWith('inter')
|
package/src/index.ts
CHANGED
|
@@ -45,6 +45,7 @@ import { RawSeverityLevel } from './components/ProjectBoard/types'
|
|
|
45
45
|
// Here is the new horizontal `Tabs` import
|
|
46
46
|
import Tabs, { TabsProps } from './components/Tabs'
|
|
47
47
|
import { Task } from './components/ProjectBoard/types'
|
|
48
|
+
import Checkbox, { CheckboxProps } from './components/Checkbox'
|
|
48
49
|
|
|
49
50
|
// New imports
|
|
50
51
|
import DateField, { DateFieldProps } from './components/Field/Date'
|
|
@@ -172,7 +173,7 @@ export { CompanyAddTaskCustomerDropdown }
|
|
|
172
173
|
export { CompanyAddTaskCustomerProvided }
|
|
173
174
|
export { CustomerAddTask }
|
|
174
175
|
export { NoUserAddTask }
|
|
175
|
-
|
|
176
|
+
export { Checkbox }
|
|
176
177
|
// New named exports
|
|
177
178
|
export { DateField }
|
|
178
179
|
export { Dropdown }
|
|
@@ -195,6 +196,7 @@ export type { SearchableDropdownProps }
|
|
|
195
196
|
export { ShowTask }
|
|
196
197
|
export type { Task }
|
|
197
198
|
export type { RawCustomer }
|
|
199
|
+
export type { CheckboxProps }
|
|
198
200
|
/* -------------------------------------------------------------------------- */
|
|
199
201
|
/* Named Type Exports */
|
|
200
202
|
/* -------------------------------------------------------------------------- */
|