@visns-studio/visns-components 5.4.7 → 5.4.8

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.
@@ -1,7 +1,7 @@
1
- // src/components/Profile.jsx
1
+ // src/components/crm/auth/Profile.jsx
2
2
  import '../../styles/global.css';
3
3
 
4
- import React, { useState, useEffect, useRef } from 'react';
4
+ import React, { useState, useEffect, useRef, useCallback, memo } from 'react';
5
5
  import PasswordStrengthBar from 'react-password-strength-bar';
6
6
  import { toast } from 'react-toastify';
7
7
  import SignaturePad from 'react-signature-pad-wrapper';
@@ -9,8 +9,59 @@ import CustomFetch from '../Fetch';
9
9
  import TableFilter from '../TableFilter';
10
10
  import styles from './styles/Profile.module.scss';
11
11
 
12
+ // Form field component to reduce repetition
13
+ const FormField = memo(
14
+ ({
15
+ type = 'text',
16
+ name,
17
+ value,
18
+ onChange,
19
+ label,
20
+ readOnly = false,
21
+ error = '',
22
+ children,
23
+ }) => {
24
+ const isPrefocused = type === 'password';
25
+
26
+ return (
27
+ <div className={styles.formItem}>
28
+ <label className={styles.fi__label}>
29
+ {!isPrefocused && (
30
+ <input
31
+ type={type}
32
+ name={name}
33
+ onChange={onChange}
34
+ className={`${styles.input} ${error}`}
35
+ value={value}
36
+ readOnly={readOnly}
37
+ />
38
+ )}
39
+ <span
40
+ className={`${styles.fi__span} ${
41
+ isPrefocused ? styles.prefocus : ''
42
+ }`}
43
+ >
44
+ {label}
45
+ </span>
46
+ {isPrefocused && (
47
+ <input
48
+ type={type}
49
+ name={name}
50
+ onChange={onChange}
51
+ value={value}
52
+ className={`${styles.input} ${error}`}
53
+ />
54
+ )}
55
+ {children}
56
+ </label>
57
+ </div>
58
+ );
59
+ }
60
+ );
61
+
12
62
  const Profile = ({ userProfile, setUserProfile }) => {
13
63
  const signatureRef = useRef(null);
64
+ const [isLoading, setIsLoading] = useState(false);
14
65
  const [profile, setProfile] = useState({
15
66
  name: '',
16
67
  email: '',
@@ -25,6 +76,16 @@ const Profile = ({ userProfile, setUserProfile }) => {
25
76
  password_confirmation: '',
26
77
  });
27
78
 
79
+ // 2FA related states
80
+ const [twoFactorEnabled, setTwoFactorEnabled] = useState(false);
81
+ const [twoFactorData, setTwoFactorData] = useState({
82
+ qrCodeUrl: '',
83
+ secretKey: '',
84
+ recoveryCodes: [],
85
+ });
86
+ const [verificationCode, setVerificationCode] = useState('');
87
+ const [showTwoFactorSetup, setShowTwoFactorSetup] = useState(false);
88
+
28
89
  const [filters, setFilters] = useState([
29
90
  {
30
91
  id: 'overview',
@@ -32,222 +93,846 @@ const Profile = ({ userProfile, setUserProfile }) => {
32
93
  label: 'Overview',
33
94
  class: 'subactive',
34
95
  },
96
+ {
97
+ id: '2fa',
98
+ show: false,
99
+ label: 'Two-Factor Authentication',
100
+ class: '',
101
+ },
35
102
  ]);
36
103
 
37
- const handleClearSignature = (e) => {
104
+ const handleClearSignature = useCallback((e) => {
38
105
  if (e) {
39
106
  e.preventDefault();
40
107
  signatureRef.current.clear();
41
108
  }
42
- };
109
+ }, []);
43
110
 
44
- const handleChangeProfile = (e) => {
45
- const { name, value } = e.target;
46
- setProfile((prevProfile) => ({
47
- ...prevProfile,
48
- [name]: value,
49
- }));
50
- };
111
+ const handleChangeProfile = useCallback(
112
+ (e) => {
113
+ const { name, value } = e.target;
114
+ setProfile((prevProfile) => ({
115
+ ...prevProfile,
116
+ [name]: value,
117
+ }));
51
118
 
52
- const handleSubmitProfile = async (e) => {
53
- try {
54
- if (e) {
55
- e.preventDefault();
119
+ // Clear error state when user starts typing
120
+ if (inputClasses[name]) {
121
+ setInputClasses((prev) => ({
122
+ ...prev,
123
+ [name]: '',
124
+ }));
56
125
  }
126
+ },
127
+ [inputClasses]
128
+ );
57
129
 
58
- // Validate required fields
59
- if (profile.name.trim() === '' || profile.email.trim() === '') {
60
- setInputClasses({
61
- name: profile.name.trim() === '' ? styles.inputError : '',
62
- email: profile.email.trim() === '' ? styles.inputError : '',
63
- });
130
+ // Validate form fields
131
+ const validateForm = useCallback(() => {
132
+ let isValid = true;
133
+ let newInputClasses = { ...inputClasses };
64
134
 
65
- toast.error('Please fill in all the required fields.');
66
- return;
135
+ // Validate required fields
136
+ if (profile.name.trim() === '') {
137
+ newInputClasses.name = styles.inputError;
138
+ isValid = false;
139
+ }
140
+
141
+ if (profile.email.trim() === '') {
142
+ newInputClasses.email = styles.inputError;
143
+ isValid = false;
144
+ }
145
+
146
+ // Validate password and confirmation
147
+ if (
148
+ profile.password &&
149
+ profile.password !== profile.password_confirmation
150
+ ) {
151
+ newInputClasses.password = styles.inputError;
152
+ newInputClasses.password_confirmation = styles.inputError;
153
+ isValid = false;
154
+ toast.error('Passwords do not match.');
155
+ return { isValid, newInputClasses };
156
+ }
157
+
158
+ if (!isValid) {
159
+ toast.error('Please fill in all the required fields.');
160
+ }
161
+
162
+ setInputClasses(newInputClasses);
163
+ return { isValid, newInputClasses };
164
+ }, [profile, inputClasses]);
165
+
166
+ const handleSubmitProfile = useCallback(
167
+ async (e) => {
168
+ try {
169
+ if (e) {
170
+ e.preventDefault();
171
+ }
172
+
173
+ const { isValid } = validateForm();
174
+ if (!isValid) return;
175
+
176
+ setIsLoading(true);
177
+ const toastId = toast.loading('Updating profile...');
178
+
179
+ // Create a new payload object
180
+ let payload = {
181
+ name: profile.name.trim(),
182
+ email: profile.email.trim(),
183
+ };
184
+
185
+ // Conditionally add password to payload if it has a value
186
+ if (profile.password.trim() !== '') {
187
+ payload.password = profile.password;
188
+ payload.password_confirmation =
189
+ profile.password_confirmation;
190
+ }
191
+
192
+ // Check if signature pad has content before adding to payload
193
+ if (!signatureRef.current.isEmpty()) {
194
+ payload.signature = signatureRef.current.toDataURL();
195
+ } else if (userProfile.signature) {
196
+ // Keep existing signature if no new one is provided
197
+ payload.signature = userProfile.signature;
198
+ }
199
+
200
+ const res = await CustomFetch(
201
+ `/ajax/users/${userProfile.id}`,
202
+ 'PUT',
203
+ payload
204
+ );
205
+
206
+ // Extract the updated user data from the response
207
+ const updatedUserData = res.data.user
208
+ ? res.data.user
209
+ : res.data;
210
+
211
+ // Make sure we have a valid name from the response
212
+ const updatedName = updatedUserData.name || userProfile.name;
213
+
214
+ // Update the parent component's userProfile state
215
+ setUserProfile((prevState) => ({
216
+ ...prevState,
217
+ name: updatedName,
218
+ signature: updatedUserData.signature || prevState.signature,
219
+ }));
220
+
221
+ // Update local profile state with the updated data and reset password fields
222
+ setProfile((prev) => ({
223
+ ...prev,
224
+ name: updatedName, // Keep the updated name
225
+ email: prev.email, // Keep the email
226
+ password: '',
227
+ password_confirmation: '',
228
+ }));
229
+
230
+ toast.update(toastId, {
231
+ render: 'Your profile has been successfully updated.',
232
+ type: 'success',
233
+ isLoading: false,
234
+ autoClose: 5000,
235
+ closeButton: true,
236
+ });
237
+ } catch (err) {
238
+ console.error('Error updating profile:', err);
239
+ toast.error('Failed to update profile. Please try again.');
240
+ } finally {
241
+ setIsLoading(false);
67
242
  }
243
+ },
244
+ [
245
+ profile,
246
+ userProfile.id,
247
+ userProfile.name,
248
+ userProfile.signature,
249
+ validateForm,
250
+ setUserProfile,
251
+ ]
252
+ );
68
253
 
69
- // Validate password and confirmation
70
- if (
71
- profile.password &&
72
- profile.password !== profile.password_confirmation
73
- ) {
74
- setInputClasses({
75
- ...inputClasses,
76
- password: styles.inputError,
77
- password_confirmation: styles.inputError,
254
+ // Function to fetch 2FA setup data (QR code and secret key)
255
+ const fetchTwoFactorSetupData = useCallback(async () => {
256
+ try {
257
+ setIsLoading(true);
258
+ const toastId = toast.loading('Fetching 2FA setup data...');
259
+
260
+ const res = await CustomFetch(
261
+ `/ajax/user/two-factor-auth/enable`,
262
+ 'POST',
263
+ {}
264
+ );
265
+
266
+ if (res.data) {
267
+ // Update state based on the Laravel response structure
268
+ setTwoFactorData({
269
+ qrCodeUrl: res.data.qr_code || '',
270
+ secretKey: res.data.secret_key || '', // Using the secret_key from the response
271
+ recoveryCodes: res.data.recovery_codes || [],
78
272
  });
79
273
 
80
- toast.error('Passwords do not match.');
274
+ // Update 2FA status based on response
275
+ setTwoFactorEnabled(res.data.two_factor_enabled || false);
276
+ // Only show setup if not confirmed yet
277
+ if (!res.data.two_factor_confirmed) {
278
+ setShowTwoFactorSetup(true);
279
+ }
280
+
281
+ toast.update(toastId, {
282
+ render: 'Two-factor authentication setup data fetched successfully',
283
+ type: 'success',
284
+ isLoading: false,
285
+ autoClose: 3000,
286
+ closeButton: true,
287
+ });
288
+ }
289
+ } catch (err) {
290
+ console.error('Error fetching 2FA setup data:', err);
291
+ toast.error('Failed to fetch 2FA setup data. Please try again.');
292
+ } finally {
293
+ setIsLoading(false);
294
+ }
295
+ }, []);
296
+
297
+ // Function to enable 2FA with verification code
298
+ const enableTwoFactor = useCallback(async () => {
299
+ try {
300
+ if (!verificationCode.trim()) {
301
+ toast.error('Please enter the verification code');
81
302
  return;
82
303
  }
83
304
 
84
- // Create a new payload object
85
- let payload = {
86
- name: profile.name,
87
- email: profile.email,
88
- };
305
+ setIsLoading(true);
306
+ const toastId = toast.loading(
307
+ 'Enabling two-factor authentication...'
308
+ );
309
+
310
+ const res = await CustomFetch(
311
+ `/ajax/user/two-factor-auth/confirm`,
312
+ 'POST',
313
+ {
314
+ code: verificationCode.trim(),
315
+ }
316
+ );
317
+
318
+ if (res.data && res.data.two_factor_confirmed) {
319
+ setTwoFactorEnabled(res.data.two_factor_enabled || true);
320
+ setShowTwoFactorSetup(false);
321
+ setVerificationCode('');
89
322
 
90
- // Conditionally add password to payload if it has a value
91
- if (profile.password.trim() !== '') {
92
- payload.password = profile.password;
93
- payload.password_confirmation = profile.password_confirmation;
323
+ toast.update(toastId, {
324
+ render: 'Two-factor authentication enabled successfully',
325
+ type: 'success',
326
+ isLoading: false,
327
+ autoClose: 3000,
328
+ closeButton: true,
329
+ });
330
+ } else {
331
+ toast.update(toastId, {
332
+ render:
333
+ res.data.message ||
334
+ 'Failed to confirm two-factor authentication. Please try again.',
335
+ type: 'error',
336
+ isLoading: false,
337
+ autoClose: 3000,
338
+ closeButton: true,
339
+ });
94
340
  }
341
+ } catch (err) {
342
+ console.error('Error enabling 2FA:', err);
343
+ toast.error(
344
+ 'Failed to enable two-factor authentication. Please try again.'
345
+ );
346
+ } finally {
347
+ setIsLoading(false);
348
+ }
349
+ }, [verificationCode]);
95
350
 
96
- payload.signature = signatureRef.current.toDataURL();
351
+ // Function to disable 2FA
352
+ const disableTwoFactor = useCallback(async () => {
353
+ try {
354
+ if (
355
+ !window.confirm(
356
+ 'Are you sure you want to disable two-factor authentication? This will make your account less secure.'
357
+ )
358
+ ) {
359
+ return;
360
+ }
361
+
362
+ setIsLoading(true);
363
+ const toastId = toast.loading(
364
+ 'Disabling two-factor authentication...'
365
+ );
97
366
 
98
367
  const res = await CustomFetch(
99
- `/ajax/users/${userProfile.id}`,
100
- 'PUT',
101
- payload
368
+ `/ajax/user/two-factor-auth/disable`,
369
+ 'POST',
370
+ {}
102
371
  );
103
372
 
104
- const updatedUserData = res.data.user ? res.data.user : res.data;
373
+ if (res.data) {
374
+ setTwoFactorEnabled(res.data.two_factor_enabled || false);
105
375
 
106
- setUserProfile((prevState) => ({
107
- ...prevState,
108
- name: updatedUserData.name,
109
- signature: updatedUserData.signature,
110
- }));
111
- toast.success('Your profile has been successfully updated.');
376
+ toast.update(toastId, {
377
+ render: 'Two-factor authentication disabled successfully',
378
+ type: 'success',
379
+ isLoading: false,
380
+ autoClose: 3000,
381
+ closeButton: true,
382
+ });
383
+ }
112
384
  } catch (err) {
113
- console.log(err);
385
+ console.error('Error disabling 2FA:', err);
386
+ toast.error(
387
+ 'Failed to disable two-factor authentication. Please try again.'
388
+ );
389
+ } finally {
390
+ setIsLoading(false);
114
391
  }
115
- };
116
-
117
- const renderContent = (filter) => {
118
- if (filter.show && filter.id === 'overview') {
119
- return (
120
- <div className={styles.gridtxt} key={filter.id}>
121
- <div className={styles.gridtxt__header}>
122
- <span>Overview</span>
123
- </div>
124
- <div className={styles.formcontainer}>
125
- <form
126
- className={styles.modalForm}
127
- onSubmit={handleSubmitProfile}
128
- >
129
- <div className={styles.formItem}>
130
- <label className={styles.fi__label}>
131
- <input
132
- type="text"
133
- name="name"
134
- onChange={handleChangeProfile}
135
- className={`${styles.input} ${inputClasses.name}`}
136
- value={profile.name}
137
- />
138
- <span className={styles.fi__span}>
139
- Name *
140
- </span>
141
- </label>
142
- </div>
143
- <div className={styles.formItem}>
144
- <label className={styles.fi__label}>
145
- <input
146
- type="text"
147
- name="email"
148
- readOnly
149
- className={`${styles.input} ${inputClasses.email}`}
150
- value={profile.email}
151
- />
152
- <span className={styles.fi__span}>
153
- Email *
154
- </span>
155
- </label>
156
- </div>
157
- <div className={styles.formItem}>
158
- <label className={styles.fi__label}>
159
- <span
160
- className={`${styles.fi__span} ${styles.prefocus}`}
161
- >
162
- Password
163
- </span>
164
- <input
165
- type="password"
166
- name="password"
167
- onChange={handleChangeProfile}
168
- value={profile.password}
169
- className={`${styles.input} ${inputClasses.password}`}
170
- />
392
+ }, []);
393
+
394
+ // Function to copy text to clipboard
395
+ const copyToClipboard = useCallback((text) => {
396
+ navigator.clipboard
397
+ .writeText(text)
398
+ .then(() => toast.success('Copied to clipboard'))
399
+ .catch(() => toast.error('Failed to copy to clipboard'));
400
+ }, []);
401
+
402
+ // Handle verification code input change
403
+ const handleVerificationCodeChange = useCallback((e) => {
404
+ const value = e.target.value;
405
+ // Only allow digits and limit to 6 characters
406
+ if (/^\d*$/.test(value) && value.length <= 6) {
407
+ setVerificationCode(value);
408
+ }
409
+ }, []);
410
+
411
+ // Memoized content renderer to prevent unnecessary re-renders
412
+ const renderContent = useCallback(
413
+ (filter) => {
414
+ if (filter.show && filter.id === 'overview') {
415
+ return (
416
+ <div className={styles.gridtxt} key={filter.id}>
417
+ <div className={styles.gridtxt__header}>
418
+ <span>Overview</span>
419
+ </div>
420
+ <div className={styles.formcontainer}>
421
+ <form
422
+ className={styles.modalForm}
423
+ onSubmit={handleSubmitProfile}
424
+ >
425
+ <FormField
426
+ name="name"
427
+ value={profile.name}
428
+ onChange={handleChangeProfile}
429
+ label="Name *"
430
+ error={inputClasses.name}
431
+ />
432
+
433
+ <FormField
434
+ name="email"
435
+ value={profile.email}
436
+ readOnly={true}
437
+ label="Email *"
438
+ error={inputClasses.email}
439
+ />
440
+
441
+ <FormField
442
+ type="password"
443
+ name="password"
444
+ value={profile.password}
445
+ onChange={handleChangeProfile}
446
+ label="Password"
447
+ error={inputClasses.password}
448
+ >
171
449
  <PasswordStrengthBar
172
450
  password={profile.password}
173
451
  />
174
- </label>
175
- </div>
176
- <div className={styles.formItem}>
177
- <label className={styles.fi__label}>
178
- <span
179
- className={`${styles.fi__span} ${styles.prefocus}`}
180
- >
181
- Password Confirmation
182
- </span>
183
- <input
184
- type="password"
185
- name="password_confirmation"
186
- onChange={handleChangeProfile}
187
- value={profile.password_confirmation}
188
- className={`${styles.input} ${inputClasses.password_confirmation}`}
189
- />
190
- </label>
191
- </div>
192
- <div className={styles.formItem}>
193
- <label className={styles.fi__label}>
194
- <SignaturePad
195
- ref={signatureRef}
196
- height={200}
197
- options={{
198
- penColor: 'black',
199
- }}
200
- redrawOnResize
201
- canvasProps={{
202
- style: {
203
- border: '2px solid #ccc',
204
- borderRadius: '10px',
205
- padding: '10px',
206
- },
207
- className: styles.sigcanvas,
208
- }}
209
- />
210
- <span className={styles.fi__span}>
211
- Signature
212
- </span>
213
- </label>
214
- </div>
215
- </form>
216
- </div>
452
+ </FormField>
217
453
 
218
- <div className={styles.polActions}>
219
- <button
220
- className={styles.btn}
221
- onClick={handleSubmitProfile}
222
- >
223
- Save
224
- </button>
225
- <button
226
- className={styles.btn}
227
- onClick={handleClearSignature}
454
+ <FormField
455
+ type="password"
456
+ name="password_confirmation"
457
+ value={profile.password_confirmation}
458
+ onChange={handleChangeProfile}
459
+ label="Password Confirmation"
460
+ error={inputClasses.password_confirmation}
461
+ />
462
+
463
+ <div className={styles.formItem}>
464
+ <label className={styles.fi__label}>
465
+ <span
466
+ className={`${styles.fi__span} ${styles.prefocus}`}
467
+ >
468
+ Signature
469
+ </span>
470
+ <div
471
+ className={
472
+ styles.signaturePadWrapper
473
+ }
474
+ >
475
+ <SignaturePad
476
+ ref={signatureRef}
477
+ height={200}
478
+ options={{
479
+ penColor: 'black',
480
+ backgroundColor:
481
+ 'rgba(255, 255, 255, 0.9)',
482
+ }}
483
+ redrawOnResize
484
+ canvasProps={{
485
+ style: {
486
+ border: '2px solid #ccc',
487
+ borderRadius: '10px',
488
+ padding: '10px',
489
+ width: '100%',
490
+ },
491
+ className: styles.sigcanvas,
492
+ }}
493
+ />
494
+ <small
495
+ style={{
496
+ display: 'block',
497
+ marginTop: '5px',
498
+ color: '#666',
499
+ }}
500
+ >
501
+ Draw your signature above
502
+ </small>
503
+ </div>
504
+ </label>
505
+ </div>
506
+ </form>
507
+ </div>
508
+
509
+ <div className={styles.polActions}>
510
+ <button
511
+ className={`${styles.btn} ${
512
+ isLoading ? styles.loading : ''
513
+ }`}
514
+ onClick={handleSubmitProfile}
515
+ disabled={isLoading}
516
+ aria-busy={isLoading}
517
+ >
518
+ {isLoading ? 'Saving...' : 'Save'}
519
+ </button>
520
+ <button
521
+ className={styles.btn}
522
+ onClick={handleClearSignature}
523
+ disabled={isLoading}
524
+ type="button"
525
+ >
526
+ Clear Signature
527
+ </button>
528
+ </div>
529
+ </div>
530
+ );
531
+ } else if (filter.show && filter.id === '2fa') {
532
+ return (
533
+ <div className={styles.gridtxt} key={filter.id}>
534
+ <div className={styles.gridtxt__header}>
535
+ <span>Two-Factor Authentication</span>
536
+ </div>
537
+ <div
538
+ className={styles.formcontainer}
539
+ style={{ maxWidth: '100%' }}
228
540
  >
229
- Clear
230
- </button>
541
+ <div className={styles.twoFactorSection}>
542
+ <div className={styles.twoFactorStatus}>
543
+ <h3>
544
+ Status:{' '}
545
+ {twoFactorEnabled ? (
546
+ <span
547
+ className={styles.statusEnabled}
548
+ >
549
+ Enabled
550
+ </span>
551
+ ) : (
552
+ <span
553
+ className={
554
+ styles.statusDisabled
555
+ }
556
+ >
557
+ Disabled
558
+ </span>
559
+ )}
560
+ </h3>
561
+ <p>
562
+ Two-factor authentication adds an extra
563
+ layer of security to your account. When
564
+ enabled, you'll need to provide both
565
+ your password and a verification code
566
+ from your authenticator app when signing
567
+ in.
568
+ </p>
569
+
570
+ {!twoFactorEnabled &&
571
+ !showTwoFactorSetup && (
572
+ <button
573
+ className={`${styles.btn} ${styles.btnPrimary}`}
574
+ onClick={
575
+ fetchTwoFactorSetupData
576
+ }
577
+ disabled={isLoading}
578
+ >
579
+ {isLoading
580
+ ? 'Loading...'
581
+ : 'Set up two-factor authentication'}
582
+ </button>
583
+ )}
584
+
585
+ {twoFactorEnabled &&
586
+ userProfile.two_factor_confirmed_at && (
587
+ <div>
588
+ <p
589
+ className={
590
+ styles.confirmedMessage
591
+ }
592
+ >
593
+ Two-factor authentication is
594
+ set up and confirmed. Your
595
+ account is now more secure.
596
+ </p>
597
+ <button
598
+ className={`${styles.btn} ${styles.btnDanger}`}
599
+ onClick={disableTwoFactor}
600
+ disabled={isLoading}
601
+ >
602
+ {isLoading
603
+ ? 'Disabling...'
604
+ : 'Disable two-factor authentication'}
605
+ </button>
606
+ </div>
607
+ )}
608
+
609
+ {twoFactorEnabled &&
610
+ !userProfile.two_factor_confirmed_at && (
611
+ <button
612
+ className={`${styles.btn} ${styles.btnDanger}`}
613
+ onClick={disableTwoFactor}
614
+ disabled={isLoading}
615
+ >
616
+ {isLoading
617
+ ? 'Disabling...'
618
+ : 'Disable two-factor authentication'}
619
+ </button>
620
+ )}
621
+ </div>
622
+
623
+ {showTwoFactorSetup && (
624
+ <div className={styles.twoFactorSetup}>
625
+ <h3>
626
+ Set up two-factor authentication
627
+ </h3>
628
+ <ol className={styles.setupSteps}>
629
+ <li>
630
+ <p>
631
+ Install an authenticator app
632
+ on your device (if you don't
633
+ already have one):
634
+ </p>
635
+ <ul>
636
+ <li>
637
+ <a
638
+ href="https://support.google.com/accounts/answer/1066447"
639
+ target="_blank"
640
+ rel="noopener noreferrer"
641
+ >
642
+ Google Authenticator
643
+ </a>
644
+ </li>
645
+ <li>
646
+ <a
647
+ href="https://authy.com/download/"
648
+ target="_blank"
649
+ rel="noopener noreferrer"
650
+ >
651
+ Authy
652
+ </a>
653
+ </li>
654
+ <li>
655
+ <a
656
+ href="https://www.microsoft.com/en-us/security/mobile-authenticator-app"
657
+ target="_blank"
658
+ rel="noopener noreferrer"
659
+ >
660
+ Microsoft
661
+ Authenticator
662
+ </a>
663
+ </li>
664
+ <li>
665
+ Or any other
666
+ TOTP-compatible
667
+ authenticator app
668
+ </li>
669
+ </ul>
670
+ </li>
671
+ <li>
672
+ <p>
673
+ Scan this QR code with your
674
+ authenticator app:
675
+ </p>
676
+ <div
677
+ className={
678
+ styles.qrCodeContainer
679
+ }
680
+ >
681
+ {twoFactorData.qrCodeUrl ? (
682
+ <div
683
+ dangerouslySetInnerHTML={{
684
+ __html: twoFactorData.qrCodeUrl,
685
+ }}
686
+ className={
687
+ styles.qrCodeSvg
688
+ }
689
+ />
690
+ ) : (
691
+ <div
692
+ className={
693
+ styles.qrPlaceholder
694
+ }
695
+ >
696
+ QR Code loading...
697
+ </div>
698
+ )}
699
+ </div>
700
+ </li>
701
+ <li>
702
+ <p>
703
+ Or manually enter this
704
+ secret key in your
705
+ authenticator app:
706
+ </p>
707
+ <div
708
+ className={
709
+ styles.secretKeyContainer
710
+ }
711
+ >
712
+ <code
713
+ className={
714
+ styles.secretKey
715
+ }
716
+ >
717
+ {
718
+ twoFactorData.secretKey
719
+ }
720
+ </code>
721
+ <button
722
+ className={
723
+ styles.copyButton
724
+ }
725
+ onClick={() =>
726
+ copyToClipboard(
727
+ twoFactorData.secretKey
728
+ )
729
+ }
730
+ type="button"
731
+ >
732
+ Copy
733
+ </button>
734
+ </div>
735
+ </li>
736
+ <li>
737
+ <p>
738
+ Enter the 6-digit code from
739
+ your app:
740
+ </p>
741
+ <div
742
+ className={
743
+ styles.verificationCodeContainer
744
+ }
745
+ >
746
+ <input
747
+ type="text"
748
+ className={
749
+ styles.verificationCodeInput
750
+ }
751
+ value={verificationCode}
752
+ onChange={
753
+ handleVerificationCodeChange
754
+ }
755
+ placeholder="000000"
756
+ maxLength={6}
757
+ />
758
+ <button
759
+ className={`${styles.btn} ${styles.btnPrimary}`}
760
+ onClick={
761
+ enableTwoFactor
762
+ }
763
+ disabled={
764
+ isLoading ||
765
+ verificationCode.length !==
766
+ 6
767
+ }
768
+ >
769
+ {isLoading
770
+ ? 'Verifying...'
771
+ : 'Verify'}
772
+ </button>
773
+ </div>
774
+ </li>
775
+ <li>
776
+ <p>
777
+ Save these recovery codes in
778
+ a secure place. If you lose
779
+ access to your authenticator
780
+ app, you can use these codes
781
+ to regain access to your
782
+ account:
783
+ </p>
784
+ <div
785
+ className={
786
+ styles.recoveryCodes
787
+ }
788
+ >
789
+ {twoFactorData.recoveryCodes &&
790
+ twoFactorData.recoveryCodes
791
+ .length > 0 ? (
792
+ <>
793
+ <div
794
+ className={
795
+ styles.recoveryCodesContainer
796
+ }
797
+ >
798
+ {twoFactorData.recoveryCodes.map(
799
+ (
800
+ code,
801
+ index
802
+ ) => (
803
+ <div
804
+ key={
805
+ index
806
+ }
807
+ className={
808
+ styles.recoveryCode
809
+ }
810
+ >
811
+ <code>
812
+ {
813
+ code
814
+ }
815
+ </code>
816
+ </div>
817
+ )
818
+ )}
819
+ </div>
820
+ <button
821
+ className={
822
+ styles.copyButton
823
+ }
824
+ onClick={() =>
825
+ copyToClipboard(
826
+ twoFactorData.recoveryCodes.join(
827
+ '\n'
828
+ )
829
+ )
830
+ }
831
+ type="button"
832
+ >
833
+ Copy All Codes
834
+ </button>
835
+ </>
836
+ ) : (
837
+ <div
838
+ className={
839
+ styles.noRecoveryCodes
840
+ }
841
+ >
842
+ No recovery codes
843
+ available
844
+ </div>
845
+ )}
846
+ </div>
847
+ <p
848
+ className={
849
+ styles.recoveryCodesWarning
850
+ }
851
+ >
852
+ <strong>Warning:</strong>{' '}
853
+ These codes will only be
854
+ shown once. Store them
855
+ securely as they allow
856
+ access to your account if
857
+ you lose your authenticator
858
+ device.
859
+ </p>
860
+ </li>
861
+ </ol>
862
+ <div className={styles.setupActions}>
863
+ <button
864
+ className={`${styles.btn} ${styles.btnSecondary}`}
865
+ onClick={() =>
866
+ setShowTwoFactorSetup(false)
867
+ }
868
+ type="button"
869
+ disabled={isLoading}
870
+ >
871
+ Cancel
872
+ </button>
873
+ </div>
874
+ </div>
875
+ )}
876
+ </div>
877
+ </div>
231
878
  </div>
232
- </div>
233
- );
234
- }
235
- return null;
236
- };
879
+ );
880
+ }
881
+ return null;
882
+ },
883
+ [
884
+ profile,
885
+ inputClasses,
886
+ handleChangeProfile,
887
+ handleSubmitProfile,
888
+ handleClearSignature,
889
+ isLoading,
890
+ twoFactorEnabled,
891
+ showTwoFactorSetup,
892
+ twoFactorData,
893
+ verificationCode,
894
+ fetchTwoFactorSetupData,
895
+ enableTwoFactor,
896
+ disableTwoFactor,
897
+ copyToClipboard,
898
+ handleVerificationCodeChange,
899
+ ]
900
+ );
237
901
 
238
902
  useEffect(() => {
903
+ // Reset form when userProfile changes
239
904
  setProfile((prevState) => ({
240
905
  ...prevState,
241
- name: userProfile.name,
242
- email: userProfile.email,
906
+ name: userProfile.name || '',
907
+ email: userProfile.email || '',
243
908
  password: '',
244
909
  password_confirmation: '',
245
910
  }));
246
911
 
247
- if (userProfile.signature) {
248
- signatureRef.current.fromDataURL(userProfile.signature);
912
+ // Reset input classes
913
+ setInputClasses({
914
+ name: '',
915
+ email: '',
916
+ password: '',
917
+ password_confirmation: '',
918
+ });
919
+
920
+ // Load signature if available
921
+ if (userProfile.signature && signatureRef.current) {
922
+ try {
923
+ signatureRef.current.fromDataURL(userProfile.signature);
924
+ } catch (error) {
925
+ console.error('Error loading signature:', error);
926
+ }
927
+ }
928
+
929
+ // Check if 2FA is enabled for the user from userProfile
930
+ if (userProfile.two_factor_confirmed_at) {
931
+ setTwoFactorEnabled(true);
932
+ } else {
933
+ setTwoFactorEnabled(false);
249
934
  }
250
- }, [userProfile]);
935
+ }, [userProfile.name, userProfile.email, userProfile.signature]);
251
936
 
252
937
  return (
253
938
  <div>
@@ -279,4 +964,4 @@ const Profile = ({ userProfile, setUserProfile }) => {
279
964
  );
280
965
  };
281
966
 
282
- export default Profile;
967
+ export default memo(Profile);