@visns-studio/visns-components 5.5.5 → 5.5.7

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/README.md CHANGED
@@ -111,6 +111,18 @@ The main component that handles authentication and routing.
111
111
  routeConfig={routeConfig} // Route configuration
112
112
  themeType="primary" // Theme type: 'primary', 'secondary', etc.
113
113
  enforce2FA={false} // Whether to enforce 2FA (default: false)
114
+ clientPortalConfig={{
115
+ component: <ClientPortalComponent />, // Main component for client portal
116
+ urls: {
117
+ login: '/clientPortal/login',
118
+ verify: '/clientPortal/verify',
119
+ logout: '/clientPortal/logout',
120
+ profile: '/clientPortal/profile',
121
+ },
122
+ keys: {
123
+ name: ['firstname', 'surname'], // Fields to use for client name display
124
+ },
125
+ }}
114
126
  />
115
127
  ```
116
128
 
@@ -394,6 +406,126 @@ The QrCode component fetches QR code data from the specified URL and displays it
394
406
 
395
407
  ## Two-Factor Authentication (2FA)
396
408
 
409
+ ## Client Portal
410
+
411
+ The GenericAuth component includes a client portal feature that provides a separate authentication flow and interface for clients.
412
+
413
+ ### Client Portal Configuration
414
+
415
+ ```jsx
416
+ <GenericAuth
417
+ // ... other props
418
+ clientPortalConfig={{
419
+ component: <ClientPortalComponent />, // Main component for client portal
420
+ urls: {
421
+ login: '/clientPortal/login', // API endpoint for client login
422
+ verify: '/clientPortal/verify', // API endpoint for OTP verification
423
+ logout: '/clientPortal/logout', // API endpoint for client logout
424
+ profile: '/clientPortal/profile', // API endpoint for client profile
425
+ },
426
+ routes: {
427
+ // Optional additional routes
428
+ profile: ClientProfileComponent,
429
+ documents: ClientDocumentsComponent,
430
+ },
431
+ keys: {
432
+ name: ['firstname', 'surname'], // Fields to use for client name display
433
+ },
434
+ }}
435
+ />
436
+ ```
437
+
438
+ #### Client Name Display Configuration
439
+
440
+ The `keys.name` property in the `clientPortalConfig` allows you to specify which fields from the client profile should be used to display the client's name in the portal header.
441
+
442
+ - If `keys.name` is an array of strings, the values from those fields will be concatenated with spaces in between.
443
+ - If `keys.name` is a single string, the value from that field will be used directly.
444
+ - If the specified fields are empty or not found, it will fall back to `clientProfile.name` or `clientProfile.email`.
445
+
446
+ Examples:
447
+
448
+ ```jsx
449
+ // Using multiple fields (firstname and surname)
450
+ keys: {
451
+ name: ['firstname', 'surname'];
452
+ }
453
+ // Result: "John Smith"
454
+
455
+ // Using a single field
456
+ keys: {
457
+ name: 'fullname';
458
+ }
459
+ // Result: "John Smith"
460
+ ```
461
+
462
+ ### Passing Props to Client Portal Components
463
+
464
+ To pass props to your client portal component, use a function component approach:
465
+
466
+ ```jsx
467
+ <GenericAuth
468
+ // ... other props
469
+ clientPortalConfig={{
470
+ component: (props) => (
471
+ <ClientPortalComponent {...props} customProp="value" />
472
+ ),
473
+ // ... other config
474
+ }}
475
+ />
476
+ ```
477
+
478
+ The function will receive these props:
479
+
480
+ - `clientProfile`: The full client profile object
481
+ - `setClientProfile`: Function to update the client profile
482
+ - `clientId`: The client ID extracted from clientProfile.id
483
+
484
+ You can then access these props in your component:
485
+
486
+ ```jsx
487
+ const ClientPortalComponent = ({
488
+ clientProfile,
489
+ setClientProfile,
490
+ clientId,
491
+ customProp, // Your custom prop
492
+ }) => {
493
+ return (
494
+ <div>
495
+ <h1>
496
+ Welcome, {clientProfile.firstname} {clientProfile.surname}
497
+ </h1>
498
+ <p>Client ID: {clientId}</p>
499
+ <p>Custom prop: {customProp}</p>
500
+ {/* Your component content */}
501
+ </div>
502
+ );
503
+ };
504
+ ```
505
+
506
+ ### Client Authentication Flow
507
+
508
+ 1. Client enters email address on the client login page
509
+ 2. System sends a one-time password (OTP) to the client's email
510
+ 3. Client enters the OTP on the verification page
511
+ 4. If the OTP is valid, the client is authenticated and redirected to the client portal
512
+
513
+ ### Client Portal Routes
514
+
515
+ The client portal uses these routes:
516
+
517
+ - `/client/login` - Client login page
518
+ - `/client/verify` - OTP verification page
519
+ - `/client/portal` - Main client portal (protected, requires authentication)
520
+ - `/client/portal/profile` - Client profile page (if configured in routes)
521
+ - `/client/portal/documents` - Client documents page (if configured in routes)
522
+
523
+ ### Route Grouping
524
+
525
+ Routes in the client portal are grouped by prefix for better organization. All client-related routes are grouped under the `/client` prefix, which makes the routing structure cleaner and more maintainable.
526
+
527
+ This grouping is handled internally by the GenericAuth component, so you don't need to include the `/client` prefix in your route configurations.
528
+
397
529
  ### How 2FA Works
398
530
 
399
531
  1. User enters email and password on the login screen
package/package.json CHANGED
@@ -82,7 +82,7 @@
82
82
  "react-dom": "^17.0.0 || ^18.0.0"
83
83
  },
84
84
  "name": "@visns-studio/visns-components",
85
- "version": "5.5.5",
85
+ "version": "5.5.7",
86
86
  "description": "Various packages to assist in the development of our Custom Applications.",
87
87
  "main": "src/index.js",
88
88
  "files": [
@@ -0,0 +1,126 @@
1
+ import '../../styles/global.css';
2
+
3
+ import React, { useState } from 'react';
4
+ import { useLocation, useNavigate } from 'react-router-dom';
5
+ import { toast } from 'react-toastify';
6
+ import { Person } from 'akar-icons';
7
+
8
+ import CustomFetch from '../Fetch';
9
+
10
+ import styles from './styles/ClientAuth.module.scss';
11
+
12
+ const ClientLogin = ({ logo, setSystemAuth, urls = {} }) => {
13
+ const location = useLocation();
14
+ const navigate = useNavigate();
15
+ const [isLoading, setIsLoading] = useState(false);
16
+ const [email, setEmail] = useState('');
17
+ const [inputError, setInputError] = useState(false);
18
+
19
+ const handleChange = (e) => {
20
+ setEmail(e.target.value);
21
+ setInputError(false);
22
+ };
23
+
24
+ const handleSubmit = async (e) => {
25
+ e.preventDefault();
26
+
27
+ if (!email.trim()) {
28
+ setInputError(true);
29
+ toast.error('Please enter your email address');
30
+ return;
31
+ }
32
+
33
+ setIsLoading(true);
34
+ toast.info('Sending verification code...', { autoClose: 2000 });
35
+
36
+ try {
37
+ const loginUrl = urls.login;
38
+ const result = await CustomFetch(loginUrl, 'POST', {
39
+ email: email,
40
+ location:
41
+ location.state && location.state.previousUrl
42
+ ? location.state.previousUrl
43
+ : '',
44
+ });
45
+
46
+ setIsLoading(false);
47
+
48
+ if (result.data && result.data.success) {
49
+ // Redirect to OTP verification page with email and uuid
50
+ const verifyUrl = '/client/verify';
51
+ navigate(verifyUrl, {
52
+ state: {
53
+ email: email,
54
+ uuid: result.data.uuid,
55
+ previousUrl: location.state?.previousUrl || '',
56
+ // In development, we can pass the OTP for testing
57
+ ...(process.env.NODE_ENV === 'development' && {
58
+ otp: result.data.otp,
59
+ }),
60
+ },
61
+ });
62
+ toast.success(
63
+ result.data.message ||
64
+ 'Verification code sent to your email'
65
+ );
66
+ } else {
67
+ setSystemAuth(false);
68
+ setInputError(true);
69
+ toast.error(
70
+ (result.data && result.data.message) ||
71
+ 'Could not find an account with this email address.'
72
+ );
73
+ }
74
+ } catch (error) {
75
+ setIsLoading(false);
76
+ setInputError(true);
77
+ toast.error(String(error));
78
+ }
79
+ };
80
+
81
+ return (
82
+ <div className={styles.container}>
83
+ <div className={styles.loginBox}>
84
+ <img src={logo} alt="Client Portal" className={styles.logo} />
85
+
86
+ <h1 className={styles.title}>Client Login</h1>
87
+ <p className={styles.subtitle}>
88
+ Enter your email address to receive a verification code
89
+ </p>
90
+
91
+ <form onSubmit={handleSubmit}>
92
+ <div className={styles.formGroup}>
93
+ <label htmlFor="email" className={styles.inputLabel}>
94
+ Email
95
+ </label>
96
+ <input
97
+ id="email"
98
+ type="email"
99
+ name="email"
100
+ value={email}
101
+ onChange={handleChange}
102
+ className={`${styles.input} ${
103
+ inputError ? styles.error : ''
104
+ }`}
105
+ placeholder="Enter your email address"
106
+ autoComplete="email"
107
+ />
108
+ <span className={styles.inputIcon}>
109
+ <Person strokeWidth={2} size={18} />
110
+ </span>
111
+ </div>
112
+
113
+ <button
114
+ className={styles.button}
115
+ type="submit"
116
+ disabled={isLoading}
117
+ >
118
+ {isLoading ? 'Sending...' : 'Continue'}
119
+ </button>
120
+ </form>
121
+ </div>
122
+ </div>
123
+ );
124
+ };
125
+
126
+ export default ClientLogin;
@@ -0,0 +1,257 @@
1
+ import '../../styles/global.css';
2
+
3
+ import React, { useState, useEffect } from 'react';
4
+ import { useNavigate, useLocation } from 'react-router-dom';
5
+ import { toast } from 'react-toastify';
6
+ import { LockOff, ArrowLeft } from 'akar-icons';
7
+
8
+ import CustomFetch from '../Fetch';
9
+
10
+ import styles from './styles/ClientAuth.module.scss';
11
+
12
+ const ClientOTPVerify = ({
13
+ logo,
14
+ setSystemAuth,
15
+ setUserProfile,
16
+ urls = {},
17
+ }) => {
18
+ const navigate = useNavigate();
19
+ const location = useLocation();
20
+ const [isLoading, setIsLoading] = useState(false);
21
+
22
+ // Get data from location state
23
+ const email = location.state?.email || null;
24
+ const uuid = location.state?.uuid || null;
25
+ const previousUrl = location.state?.previousUrl || '';
26
+ const devOtp = location.state?.otp || null; // For development testing
27
+
28
+ // If no email or uuid, redirect to client login
29
+ useEffect(() => {
30
+ if (!email || !uuid) {
31
+ const loginUrl = '/client/login';
32
+ navigate(loginUrl);
33
+ }
34
+ }, [email, uuid, navigate]);
35
+
36
+ // Auto-fill OTP in development mode
37
+ useEffect(() => {
38
+ if (process.env.NODE_ENV === 'development' && devOtp) {
39
+ setVerificationCode(devOtp);
40
+ }
41
+ }, [devOtp]);
42
+
43
+ const [verificationCode, setVerificationCode] = useState('');
44
+ const [codeClass, setCodeClass] = useState('');
45
+ const [resendDisabled, setResendDisabled] = useState(false);
46
+ const [countdown, setCountdown] = useState(0);
47
+
48
+ useEffect(() => {
49
+ let timer;
50
+ if (countdown > 0) {
51
+ timer = setTimeout(() => setCountdown(countdown - 1), 1000);
52
+ } else if (countdown === 0) {
53
+ setResendDisabled(false);
54
+ }
55
+ return () => clearTimeout(timer);
56
+ }, [countdown]);
57
+
58
+ const handleChange = (e) => {
59
+ if (e) {
60
+ const value = e.target.value;
61
+ // Only allow digits and limit to 6 characters
62
+ if (/^\d*$/.test(value) && value.length <= 6) {
63
+ setVerificationCode(value);
64
+ setCodeClass('');
65
+ }
66
+ }
67
+ };
68
+
69
+ const handleResendCode = async () => {
70
+ if (resendDisabled) return;
71
+
72
+ setResendDisabled(true);
73
+ setCountdown(60); // 60 seconds cooldown
74
+
75
+ toast.info('Resending verification code...', { autoClose: 2000 });
76
+
77
+ try {
78
+ const loginUrl = urls.login;
79
+ const result = await CustomFetch(loginUrl, 'POST', {
80
+ email: email,
81
+ resend: true,
82
+ });
83
+
84
+ if (result.data && result.data.success) {
85
+ toast.success(
86
+ result.data.message ||
87
+ 'Verification code resent to your email'
88
+ );
89
+
90
+ // Update UUID with the new one
91
+ if (result.data.uuid) {
92
+ navigate('/client/verify', {
93
+ state: {
94
+ email: email,
95
+ uuid: result.data.uuid,
96
+ previousUrl: previousUrl,
97
+ ...(process.env.NODE_ENV === 'development' && {
98
+ otp: result.data.otp,
99
+ }),
100
+ },
101
+ replace: true, // Replace current entry in history
102
+ });
103
+ }
104
+ } else {
105
+ toast.error(
106
+ (result.data && result.data.message) ||
107
+ 'Could not resend verification code. Please try again later.'
108
+ );
109
+ setResendDisabled(false);
110
+ setCountdown(0);
111
+ }
112
+ } catch (error) {
113
+ toast.error(String(error));
114
+ setResendDisabled(false);
115
+ setCountdown(0);
116
+ }
117
+ };
118
+
119
+ const handleSubmit = async (e) => {
120
+ e.preventDefault();
121
+
122
+ if (!verificationCode.trim() || verificationCode.length !== 6) {
123
+ setCodeClass('inputError');
124
+ toast.error('Please enter a valid 6-digit code');
125
+ return;
126
+ }
127
+
128
+ setIsLoading(true);
129
+ toast.info('Verifying code...', { autoClose: 2000 });
130
+
131
+ try {
132
+ const verifyUrl = urls.verify;
133
+ const result = await CustomFetch(verifyUrl, 'POST', {
134
+ email: email,
135
+ uuid: uuid,
136
+ otp: verificationCode,
137
+ previous_url: previousUrl,
138
+ });
139
+
140
+ setIsLoading(false);
141
+
142
+ // Check if the result contains an error message
143
+ if (!result.data || !result.data.success) {
144
+ setCodeClass('inputError');
145
+ toast.error(
146
+ (result.data && result.data.message) ||
147
+ 'Invalid verification code'
148
+ );
149
+ return;
150
+ }
151
+
152
+ // Successful verification
153
+ setUserProfile(result.data.user);
154
+ toast.success(result.data.message || 'Login successful!');
155
+
156
+ // Set authentication state first
157
+ setSystemAuth(true);
158
+
159
+ if (result.data.redirect_url && result.data.redirect_url !== '') {
160
+ window.location.href = result.data.redirect_url;
161
+ } else {
162
+ // Use replace: true to avoid back button issues
163
+ navigate('/client/portal', { replace: true });
164
+ }
165
+ } catch (error) {
166
+ setIsLoading(false);
167
+ setCodeClass('inputError');
168
+ toast.error(String(error));
169
+ }
170
+ };
171
+
172
+ return (
173
+ <div className={styles.container}>
174
+ <div className={styles.loginBox}>
175
+ <img src={logo} alt="Client Portal" className={styles.logo} />
176
+
177
+ <h1 className={styles.title}>Verification Code</h1>
178
+ <p className={styles.subtitle}>
179
+ Please enter the 6-digit code sent to your email address:{' '}
180
+ <span className={styles.emailHighlight}>{email}</span>
181
+ </p>
182
+
183
+ <form onSubmit={handleSubmit}>
184
+ <div className={styles.formGroup}>
185
+ <label
186
+ htmlFor="verificationCode"
187
+ className={styles.inputLabel}
188
+ >
189
+ Verification Code
190
+ </label>
191
+ <input
192
+ id="verificationCode"
193
+ type="text"
194
+ name="verificationCode"
195
+ value={verificationCode}
196
+ onChange={handleChange}
197
+ className={`${styles.input} ${styles.otpInput} ${
198
+ codeClass === 'inputError' ? styles.error : ''
199
+ }`}
200
+ placeholder="Enter 6-digit code"
201
+ autoComplete="one-time-code"
202
+ inputMode="numeric"
203
+ maxLength="6"
204
+ />
205
+ <span className={styles.inputIcon}>
206
+ <LockOff strokeWidth={2} size={18} />
207
+ </span>
208
+ </div>
209
+
210
+ <button
211
+ className={styles.button}
212
+ type="submit"
213
+ disabled={isLoading}
214
+ >
215
+ {isLoading ? 'Verifying...' : 'Verify'}
216
+ </button>
217
+
218
+ <div className={styles.linkText}>
219
+ Didn't receive the code?{' '}
220
+ <a
221
+ href="#"
222
+ onClick={(e) => {
223
+ e.preventDefault();
224
+ handleResendCode();
225
+ }}
226
+ className={
227
+ resendDisabled
228
+ ? styles.disabled
229
+ : styles.resendLink
230
+ }
231
+ >
232
+ {resendDisabled
233
+ ? `Resend in ${countdown}s`
234
+ : 'Resend Code'}
235
+ </a>
236
+ </div>
237
+
238
+ <div className={styles.linkText}>
239
+ <a
240
+ href="#"
241
+ onClick={(e) => {
242
+ e.preventDefault();
243
+ navigate('/client/login');
244
+ }}
245
+ className={styles.backLink}
246
+ >
247
+ <ArrowLeft strokeWidth={2} size={16} /> Back to
248
+ Login
249
+ </a>
250
+ </div>
251
+ </form>
252
+ </div>
253
+ </div>
254
+ );
255
+ };
256
+
257
+ export default ClientOTPVerify;
@@ -0,0 +1,167 @@
1
+ // Client Auth styles
2
+ .container {
3
+ min-height: 100vh;
4
+ display: flex;
5
+ align-items: center;
6
+ justify-content: center;
7
+ background-color: #f5f5f5;
8
+ }
9
+
10
+ .loginBox {
11
+ width: 100%;
12
+ max-width: 500px;
13
+ padding: 2.5rem;
14
+ margin: 0 auto;
15
+ background-color: white;
16
+ border-radius: 8px;
17
+ box-shadow: 0 4px 12px rgba(0, 0, 0, 0.05);
18
+ }
19
+
20
+ .logo {
21
+ display: block;
22
+ width: 100%;
23
+ max-width: 240px;
24
+ height: auto;
25
+ margin: 0 auto 2rem;
26
+ }
27
+
28
+ .title {
29
+ font-size: 1.75rem;
30
+ font-weight: 600;
31
+ color: #333;
32
+ margin-bottom: 0.5rem;
33
+ text-align: center;
34
+ }
35
+
36
+ .subtitle {
37
+ font-size: 1rem;
38
+ color: #666;
39
+ margin-bottom: 2rem;
40
+ text-align: center;
41
+ line-height: 1.5;
42
+ }
43
+
44
+ .formGroup {
45
+ margin-bottom: 1.5rem;
46
+ position: relative;
47
+ }
48
+
49
+ .inputLabel {
50
+ display: block;
51
+ font-size: 0.9rem;
52
+ font-weight: 500;
53
+ color: #555;
54
+ margin-bottom: 0.5rem;
55
+ }
56
+
57
+ .input {
58
+ width: 100%;
59
+ padding: 0.75rem 1rem;
60
+ font-size: 1rem;
61
+ border: 1px solid #ddd;
62
+ border-radius: 4px;
63
+ background-color: white;
64
+ transition: border-color 0.2s ease;
65
+
66
+ &:focus {
67
+ outline: none;
68
+ border-color: var(--primary-color, #4a6741);
69
+ box-shadow: 0 0 0 2px rgba(74, 103, 65, 0.1);
70
+ }
71
+
72
+ &.error {
73
+ border-color: #e53935;
74
+ }
75
+ }
76
+
77
+ .inputIcon {
78
+ position: absolute;
79
+ right: 1rem;
80
+ top: 2.4rem;
81
+ color: #999;
82
+ }
83
+
84
+ .button {
85
+ width: 100%;
86
+ padding: 0.9rem 1.5rem;
87
+ font-size: 1rem;
88
+ font-weight: 500;
89
+ color: white;
90
+ background-color: var(--primary-color, #0f4229);
91
+ border: none;
92
+ border-radius: 4px;
93
+ cursor: pointer;
94
+ transition: background-color 0.2s ease;
95
+
96
+ &:hover {
97
+ background-color: var(--secondary-color, #0a2e1c);
98
+ }
99
+
100
+ &:disabled {
101
+ background-color: #ccc;
102
+ cursor: not-allowed;
103
+ }
104
+ }
105
+
106
+ .linkText {
107
+ display: block;
108
+ text-align: center;
109
+ margin-top: 1.5rem;
110
+ font-size: 0.9rem;
111
+ color: #666;
112
+
113
+ a {
114
+ color: var(--primary-color, #0f4229);
115
+ text-decoration: none;
116
+ font-weight: 500;
117
+
118
+ &:hover {
119
+ text-decoration: underline;
120
+ }
121
+ }
122
+ }
123
+
124
+ // OTP verification specific styles
125
+ .otpInput {
126
+ letter-spacing: 0.25rem;
127
+ font-size: 1.2rem;
128
+ text-align: center;
129
+ }
130
+
131
+ .resendLink {
132
+ color: var(--primary-color, #0f4229);
133
+ text-decoration: none;
134
+ font-weight: 500;
135
+ cursor: pointer;
136
+
137
+ &:hover {
138
+ text-decoration: underline;
139
+ }
140
+
141
+ &.disabled {
142
+ color: #999;
143
+ cursor: default;
144
+ text-decoration: none;
145
+ }
146
+ }
147
+
148
+ .emailHighlight {
149
+ font-weight: 600;
150
+ }
151
+
152
+ .backLink {
153
+ display: inline-flex;
154
+ align-items: center;
155
+ margin-top: 1rem;
156
+ color: #666;
157
+ text-decoration: none;
158
+ font-size: 0.9rem;
159
+
160
+ svg {
161
+ margin-right: 0.5rem;
162
+ }
163
+
164
+ &:hover {
165
+ color: var(--primary-color, #0f4229);
166
+ }
167
+ }