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
  {
2
2
  "name": "goobs-frontend",
3
- "version": "0.9.14",
3
+ "version": "0.9.16",
4
4
  "type": "module",
5
5
  "description": "A comprehensive React-based libary that extends the functionality of Material-UI",
6
6
  "license": "MIT",
@@ -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
- <Checkbox
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, { ChangeEvent, useState, useEffect, FC } from '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
- ...props
93
+ inputStyle = {},
72
94
  }) => {
73
95
  // Initialize internal state with the value prop
74
96
  const [internalValue, setInternalValue] = useState(value)
75
- const [isFocused, setIsFocused] = useState(false)
76
- const [cursorPosition, setCursorPosition] = useState(value.length)
77
- const inputRef = React.useRef<HTMLInputElement>(null)
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
- 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)
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
- if (internalValue !== newValue) {
92
- setInternalValue(newValue)
93
- setCursorPosition(event.target.selectionStart || newValue.length)
94
- onChange?.(newValue)
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
- const handleSelect = () => {
99
- if (inputRef.current) {
100
- setCursorPosition(inputRef.current.selectionStart || internalValue.length)
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
- // Calculate container width based on number of inputs
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 for display
114
- const digits = internalValue.padEnd(codeLength, ' ').split('')
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
- 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}
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
  /* -------------------------------------------------------------------------- */