@truworth/twc-auth 3.0.0 → 3.0.2
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/build/src/constants/password-requirements.js +7 -0
- package/build/src/index.js +7 -0
- package/build/src/screens/PartnerSSO/PartnerLogin/components/PartnerLoginWebComponent/index.js +77 -0
- package/build/src/screens/PartnerSSO/PartnerLogin/hooks/internal/usePartnerLogin.js +50 -0
- package/build/src/screens/PartnerSSO/PartnerLogin/index.js +2 -0
- package/build/src/screens/PartnerSSO/PartnerLogin/types.js +1 -0
- package/build/src/screens/PartnerSSO/PartnerRegistration/components/PartnerRegistrationWebComponent/index.js +203 -0
- package/build/src/screens/PartnerSSO/PartnerRegistration/hooks/internal/usePartnerRegistration.js +90 -0
- package/build/src/screens/PartnerSSO/PartnerRegistration/index.js +2 -0
- package/build/src/screens/PartnerSSO/PartnerRegistration/types.js +1 -0
- package/build/src/screens/PartnerSSO/index.js +2 -0
- package/build/src/screens/Profile/components/EditMobileNumber/index.js +31 -0
- package/build/src/screens/Profile/components/EditMobileNumber/index.native.js +8 -0
- package/build/src/screens/Profile/components/EditPassword/index.js +29 -0
- package/build/src/screens/Profile/components/EditPassword/index.native.js +8 -0
- package/build/src/screens/Profile/components/EditProfile/index.js +89 -0
- package/build/src/screens/Profile/components/EditProfile/index.native.js +8 -0
- package/build/src/screens/Profile/components/OtpVerificationModal/index.js +61 -0
- package/build/src/screens/Profile/components/OtpVerificationModal/index.native.js +7 -0
- package/build/src/screens/Profile/components/ProfileWebComponent/index.js +80 -0
- package/build/src/screens/Profile/components/ProfileWebComponent/index.native.js +9 -0
- package/build/src/screens/Profile/hooks/internal/useProfile.js +185 -0
- package/build/src/screens/Profile/index.js +6 -0
- package/build/src/screens/Profile/index.native.js +6 -0
- package/build/src/screens/Profile/types.js +1 -0
- package/build/types/constants/password-requirements.d.ts +4 -0
- package/build/types/index.d.ts +4 -0
- package/build/types/screens/PartnerSSO/PartnerLogin/components/PartnerLoginWebComponent/index.d.ts +25 -0
- package/build/types/screens/PartnerSSO/PartnerLogin/hooks/internal/usePartnerLogin.d.ts +8 -0
- package/build/types/screens/PartnerSSO/PartnerLogin/index.d.ts +2 -0
- package/build/types/screens/PartnerSSO/PartnerLogin/types.d.ts +35 -0
- package/build/types/screens/PartnerSSO/PartnerRegistration/components/PartnerRegistrationWebComponent/index.d.ts +24 -0
- package/build/types/screens/PartnerSSO/PartnerRegistration/hooks/internal/usePartnerRegistration.d.ts +8 -0
- package/build/types/screens/PartnerSSO/PartnerRegistration/index.d.ts +2 -0
- package/build/types/screens/PartnerSSO/PartnerRegistration/types.d.ts +63 -0
- package/build/types/screens/PartnerSSO/index.d.ts +2 -0
- package/build/types/screens/Profile/components/EditMobileNumber/index.d.ts +6 -0
- package/build/types/screens/Profile/components/EditMobileNumber/index.native.d.ts +3 -0
- package/build/types/screens/Profile/components/EditPassword/index.d.ts +6 -0
- package/build/types/screens/Profile/components/EditPassword/index.native.d.ts +3 -0
- package/build/types/screens/Profile/components/EditProfile/index.d.ts +6 -0
- package/build/types/screens/Profile/components/EditProfile/index.native.d.ts +3 -0
- package/build/types/screens/Profile/components/OtpVerificationModal/index.d.ts +4 -0
- package/build/types/screens/Profile/components/OtpVerificationModal/index.native.d.ts +3 -0
- package/build/types/screens/Profile/components/ProfileWebComponent/index.d.ts +4 -0
- package/build/types/screens/Profile/components/ProfileWebComponent/index.native.d.ts +3 -0
- package/build/types/screens/Profile/hooks/internal/useProfile.d.ts +26 -0
- package/build/types/screens/Profile/index.d.ts +7 -0
- package/build/types/screens/Profile/index.native.d.ts +7 -0
- package/build/types/screens/Profile/types.d.ts +53 -0
- package/package.json +4 -1
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
export const strongPasswordRequirements = [
|
|
2
|
+
{ regex: /.{6,}/, text: "At least 6 characters" },
|
|
3
|
+
{ regex: /[0-9]/, text: "At least 1 number" },
|
|
4
|
+
{ regex: /[a-z]/, text: "At least 1 lowercase letter" },
|
|
5
|
+
{ regex: /[A-Z]/, text: "At least 1 uppercase letter" },
|
|
6
|
+
{ regex: /[!@#$%^&*()_+]/, text: "At least 1 special character" }
|
|
7
|
+
];
|
package/build/src/index.js
CHANGED
|
@@ -14,3 +14,10 @@ export * from './navigator';
|
|
|
14
14
|
export * from './screens/Login/components/LoginWebComponent/index';
|
|
15
15
|
export * from './screens/SignUp/components/SignUpWebComponent/index';
|
|
16
16
|
export * from './screens/SSOLogin/SSOCallback/components/SSOCallbackComponent/index';
|
|
17
|
+
// export partner SSO components
|
|
18
|
+
export * from './screens/PartnerSSO/PartnerLogin/components/PartnerLoginWebComponent';
|
|
19
|
+
export * from './screens/PartnerSSO/PartnerRegistration/components/PartnerRegistrationWebComponent';
|
|
20
|
+
// export profile components
|
|
21
|
+
export * from './screens/Profile';
|
|
22
|
+
// export constants
|
|
23
|
+
export { strongPasswordRequirements } from './constants/password-requirements';
|
package/build/src/screens/PartnerSSO/PartnerLogin/components/PartnerLoginWebComponent/index.js
ADDED
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import React, { Suspense, lazy, useEffect, useState } from 'react';
|
|
3
|
+
import { Flex, Typography } from '@truworth/twc-web-design';
|
|
4
|
+
import { usePartnerLogin } from '../../hooks/internal/usePartnerLogin';
|
|
5
|
+
import { useNavigator } from '../../../../../hooks/useNavigator';
|
|
6
|
+
import redirectAnimation from '../../../../../../assets/animation/redirect-home.json';
|
|
7
|
+
const Lottie = lazy(() => import('react-lottie'));
|
|
8
|
+
/**
|
|
9
|
+
* PartnerLoginWebComponent
|
|
10
|
+
*
|
|
11
|
+
* Handles partner SSO authentication flow for web applications.
|
|
12
|
+
* Extracts token and partner from URL query params, authenticates with backend,
|
|
13
|
+
* and redirects to the specified path on success.
|
|
14
|
+
*
|
|
15
|
+
* @example
|
|
16
|
+
* ```tsx
|
|
17
|
+
* // In your app's partner login route (react-router - pass partner from useParams)
|
|
18
|
+
* const { partner } = useParams();
|
|
19
|
+
* <PartnerLoginWebComponent partner={partner} />
|
|
20
|
+
*
|
|
21
|
+
* // In Next.js (partner comes from query params automatically)
|
|
22
|
+
* <PartnerLoginWebComponent />
|
|
23
|
+
* ```
|
|
24
|
+
*/
|
|
25
|
+
const PartnerLoginWebComponent = ({ partner: partnerProp }) => {
|
|
26
|
+
const navigator = useNavigator();
|
|
27
|
+
const [partner, setPartner] = useState(partnerProp || '');
|
|
28
|
+
const [token, setToken] = useState('');
|
|
29
|
+
const [path, setPath] = useState(undefined);
|
|
30
|
+
const [isInitialized, setIsInitialized] = useState(false);
|
|
31
|
+
// Clear localStorage only ONCE on initial mount
|
|
32
|
+
useEffect(() => {
|
|
33
|
+
if (typeof window !== 'undefined') {
|
|
34
|
+
try {
|
|
35
|
+
localStorage.clear();
|
|
36
|
+
}
|
|
37
|
+
catch (e) {
|
|
38
|
+
// Ignore localStorage errors
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
}, []);
|
|
42
|
+
// Extract query params
|
|
43
|
+
useEffect(() => {
|
|
44
|
+
if (isInitialized)
|
|
45
|
+
return;
|
|
46
|
+
const query = navigator.query;
|
|
47
|
+
// Use prop if provided, otherwise try query params
|
|
48
|
+
const partnerValue = partnerProp || (typeof query.partner === 'string' ? query.partner : '');
|
|
49
|
+
const tokenValue = typeof query.token === 'string' ? query.token : '';
|
|
50
|
+
const pathValue = typeof query.path === 'string' ? query.path : undefined;
|
|
51
|
+
if (partnerValue && tokenValue) {
|
|
52
|
+
setPartner(partnerValue);
|
|
53
|
+
setToken(tokenValue);
|
|
54
|
+
setPath(pathValue);
|
|
55
|
+
setIsInitialized(true);
|
|
56
|
+
}
|
|
57
|
+
}, [navigator.query, partnerProp, isInitialized]);
|
|
58
|
+
const { isLoading, error } = usePartnerLogin({ partner, token, path });
|
|
59
|
+
// If there's an error, redirect back after a delay
|
|
60
|
+
useEffect(() => {
|
|
61
|
+
if (error) {
|
|
62
|
+
setTimeout(() => {
|
|
63
|
+
navigator.back();
|
|
64
|
+
}, 2000);
|
|
65
|
+
}
|
|
66
|
+
}, [error, navigator]);
|
|
67
|
+
return (_jsx(Flex, { justify: 'center', align: 'center', className: 'h-[100vh]', children: _jsxs("div", { className: 'text-center', children: [_jsx(Suspense, { fallback: _jsx("div", { className: "h-[230px] w-[250px]" }), children: _jsx(Lottie
|
|
68
|
+
//@ts-ignore
|
|
69
|
+
, {
|
|
70
|
+
//@ts-ignore
|
|
71
|
+
options: {
|
|
72
|
+
animationData: redirectAnimation,
|
|
73
|
+
loop: true,
|
|
74
|
+
autoplay: true,
|
|
75
|
+
}, height: 230, width: 250 }) }), error ? (_jsxs(_Fragment, { children: [_jsx(Typography, { size: 'h6', type: 'heading', color: 'error', children: error }), _jsx(Typography, { size: 'small', type: 'body', color: 'gray-400', className: 'mt-2', children: "Redirecting back..." })] })) : (_jsx(_Fragment, { children: _jsx(Typography, { size: 'h6', type: 'heading', color: 'gray-400', children: "Authenticating and Redirecting ..." }) }))] }) }));
|
|
76
|
+
};
|
|
77
|
+
export { PartnerLoginWebComponent };
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import { useCallback, useEffect, useState } from 'react';
|
|
2
|
+
import { axiosClient } from '../../../../../api/axiosClient';
|
|
3
|
+
import { showMessage } from '../../../../../helpers/show-message';
|
|
4
|
+
import { useAuthPackageContext } from '../../../../../hooks/internal/useAuthPackageContext';
|
|
5
|
+
/**
|
|
6
|
+
* @internal
|
|
7
|
+
* Hook for managing Partner SSO Login authentication.
|
|
8
|
+
* This hook is not exposed to package consumers.
|
|
9
|
+
*/
|
|
10
|
+
const usePartnerLogin = ({ partner, token, path }) => {
|
|
11
|
+
const [isLoading, setIsLoading] = useState(false);
|
|
12
|
+
const [error, setError] = useState(null);
|
|
13
|
+
const [hasAttempted, setHasAttempted] = useState(false);
|
|
14
|
+
const { onLogin, onTokenChange } = useAuthPackageContext();
|
|
15
|
+
useEffect(() => {
|
|
16
|
+
// Only attempt authentication once when we have partner and token
|
|
17
|
+
if (hasAttempted || !partner || !token) {
|
|
18
|
+
return;
|
|
19
|
+
}
|
|
20
|
+
const authenticate = async () => {
|
|
21
|
+
setHasAttempted(true);
|
|
22
|
+
setIsLoading(true);
|
|
23
|
+
setError(null);
|
|
24
|
+
try {
|
|
25
|
+
const response = await axiosClient({
|
|
26
|
+
url: `/auth/partners/${partner}/authenticate`,
|
|
27
|
+
method: 'POST',
|
|
28
|
+
data: { token },
|
|
29
|
+
});
|
|
30
|
+
const { token: authToken, member } = response.data;
|
|
31
|
+
// Update auth context
|
|
32
|
+
onTokenChange(authToken);
|
|
33
|
+
// Call onLogin callback - this will store token and redirect
|
|
34
|
+
await onLogin({
|
|
35
|
+
token: authToken,
|
|
36
|
+
member,
|
|
37
|
+
});
|
|
38
|
+
}
|
|
39
|
+
catch (err) {
|
|
40
|
+
const errorMessage = err?.response?.data?.errors?.[0]?.message || 'Authentication failed';
|
|
41
|
+
setError(errorMessage);
|
|
42
|
+
showMessage({ message: errorMessage });
|
|
43
|
+
setIsLoading(false);
|
|
44
|
+
}
|
|
45
|
+
};
|
|
46
|
+
authenticate();
|
|
47
|
+
}, [partner, token, path, hasAttempted, onLogin, onTokenChange]);
|
|
48
|
+
return { isLoading, error };
|
|
49
|
+
};
|
|
50
|
+
export { usePartnerLogin };
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,203 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import React, { useEffect, useState } from 'react';
|
|
3
|
+
import { Button, Col, CustomCheckbox, DatePicker, Flex, Form, RadioGroup, Row, TextInput, Typography, useForm, validationPatterns, } from '@truworth/twc-web-design';
|
|
4
|
+
import { CalendarDays } from 'lucide-react';
|
|
5
|
+
import ReCAPTCHA from 'react-google-recaptcha';
|
|
6
|
+
import dayjs from 'dayjs';
|
|
7
|
+
import moment from 'moment';
|
|
8
|
+
import { ScreenLayout } from '../../../../../components/ScreenLayout';
|
|
9
|
+
import { CountryCodeDropdown } from '../../../../CountryCode/components/CountryCodeDropdown';
|
|
10
|
+
import { usePartnerRegistration } from '../../hooks/internal/usePartnerRegistration';
|
|
11
|
+
import { useNavigator } from '../../../../../hooks/useNavigator';
|
|
12
|
+
import { useAuthPackageContext } from '../../../../../hooks/internal/useAuthPackageContext';
|
|
13
|
+
import { showMessage } from '../../../../../helpers/show-message';
|
|
14
|
+
import { CDN_IMAGES_URL } from '../../../../../constants/cdn-url';
|
|
15
|
+
const isProduction = process.env.NODE_ENV === 'production';
|
|
16
|
+
const EMAIL_REGEX = /^(([^<>()[\]\\.,;:\s@"]+(\.[^<>()[\]\\.,;:\s@"]+)*)|(".+"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/;
|
|
17
|
+
/**
|
|
18
|
+
* PartnerRegistrationWebComponent
|
|
19
|
+
*
|
|
20
|
+
* Handles partner SSO registration flow for web applications.
|
|
21
|
+
* Pre-fills form with data from JWT token and collects missing information.
|
|
22
|
+
*
|
|
23
|
+
* @example
|
|
24
|
+
* ```tsx
|
|
25
|
+
* // In your app's partner registration route (react-router - pass partner from useParams)
|
|
26
|
+
* const { partner } = useParams();
|
|
27
|
+
* <PartnerRegistrationWebComponent partner={partner} />
|
|
28
|
+
*
|
|
29
|
+
* // In Next.js (partner comes from query params automatically)
|
|
30
|
+
* <PartnerRegistrationWebComponent />
|
|
31
|
+
* ```
|
|
32
|
+
*/
|
|
33
|
+
const PartnerRegistrationWebComponent = ({ partner: partnerProp }) => {
|
|
34
|
+
const navigator = useNavigator();
|
|
35
|
+
const { appConfig } = useAuthPackageContext();
|
|
36
|
+
// Query params state
|
|
37
|
+
const [partner, setPartner] = useState(partnerProp || '');
|
|
38
|
+
const [token, setToken] = useState('');
|
|
39
|
+
const [path, setPath] = useState(undefined);
|
|
40
|
+
// Form state
|
|
41
|
+
const [selectedCountry, setSelectedCountry] = useState({
|
|
42
|
+
countryCode: 'in',
|
|
43
|
+
phoneCode: '91',
|
|
44
|
+
name: 'INDIA',
|
|
45
|
+
});
|
|
46
|
+
const [isHuman, setIsHuman] = useState(!isProduction);
|
|
47
|
+
const [termsAndConditions, setTermsAndConditions] = useState(false);
|
|
48
|
+
const [emailExists, setEmailExists] = useState(false);
|
|
49
|
+
// Extract query params on mount
|
|
50
|
+
useEffect(() => {
|
|
51
|
+
const query = navigator.query;
|
|
52
|
+
// Use prop if provided, otherwise try query params
|
|
53
|
+
if (!partnerProp && typeof query.partner === 'string') {
|
|
54
|
+
setPartner(query.partner);
|
|
55
|
+
}
|
|
56
|
+
if (typeof query.token === 'string') {
|
|
57
|
+
setToken(query.token);
|
|
58
|
+
}
|
|
59
|
+
if (typeof query.path === 'string') {
|
|
60
|
+
setPath(query.path);
|
|
61
|
+
}
|
|
62
|
+
// Clear localStorage on partner registration (clean slate)
|
|
63
|
+
if (typeof window !== 'undefined') {
|
|
64
|
+
try {
|
|
65
|
+
localStorage.clear();
|
|
66
|
+
}
|
|
67
|
+
catch (e) {
|
|
68
|
+
console.warn('Failed to clear localStorage:', e);
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
}, [navigator.query, partnerProp]);
|
|
72
|
+
// Update partner if prop changes
|
|
73
|
+
useEffect(() => {
|
|
74
|
+
if (partnerProp) {
|
|
75
|
+
setPartner(partnerProp);
|
|
76
|
+
}
|
|
77
|
+
}, [partnerProp]);
|
|
78
|
+
const { isLoading, tokenData, register, checkEmailExists } = usePartnerRegistration({
|
|
79
|
+
partner,
|
|
80
|
+
token,
|
|
81
|
+
path,
|
|
82
|
+
});
|
|
83
|
+
const form = useForm({
|
|
84
|
+
liveValidation: true,
|
|
85
|
+
defaultValues: {
|
|
86
|
+
firstName: '',
|
|
87
|
+
lastName: '',
|
|
88
|
+
dateOfBirth: undefined,
|
|
89
|
+
email: '',
|
|
90
|
+
phone: '',
|
|
91
|
+
gender: '',
|
|
92
|
+
},
|
|
93
|
+
});
|
|
94
|
+
const values = form.watch();
|
|
95
|
+
// Pre-fill form with token data
|
|
96
|
+
useEffect(() => {
|
|
97
|
+
if (tokenData) {
|
|
98
|
+
form.reset({
|
|
99
|
+
firstName: tokenData.firstName?.replace(/\s/g, '') || '',
|
|
100
|
+
lastName: tokenData.lastName?.replace(/\s/g, '') || '',
|
|
101
|
+
email: tokenData.email || '',
|
|
102
|
+
gender: tokenData.gender || '',
|
|
103
|
+
dateOfBirth: tokenData.dob ? moment(tokenData.dob, 'YYYY-MM-DD').toDate() : undefined,
|
|
104
|
+
phone: '',
|
|
105
|
+
});
|
|
106
|
+
}
|
|
107
|
+
}, [tokenData, form]);
|
|
108
|
+
const handleCaptcha = (value) => {
|
|
109
|
+
setIsHuman(Boolean(value));
|
|
110
|
+
};
|
|
111
|
+
const handleEmailBlur = async () => {
|
|
112
|
+
if (values.email && EMAIL_REGEX.test(values.email)) {
|
|
113
|
+
const exists = await checkEmailExists(values.email);
|
|
114
|
+
setEmailExists(exists);
|
|
115
|
+
if (exists) {
|
|
116
|
+
showMessage({ message: 'An account already exists with this email ID' });
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
};
|
|
120
|
+
const disabledDate = (current) => {
|
|
121
|
+
const currentDate = dayjs(current);
|
|
122
|
+
if (!currentDate.isValid()) {
|
|
123
|
+
return false;
|
|
124
|
+
}
|
|
125
|
+
const thirteenYearsAgo = dayjs().subtract(13, 'years');
|
|
126
|
+
const oldestAllowedDate = dayjs('1900-01-01');
|
|
127
|
+
return currentDate.isAfter(thirteenYearsAgo) || currentDate.isBefore(oldestAllowedDate);
|
|
128
|
+
};
|
|
129
|
+
const handleSubmit = async () => {
|
|
130
|
+
if (!values.gender) {
|
|
131
|
+
showMessage({ message: 'Please select your gender' });
|
|
132
|
+
return;
|
|
133
|
+
}
|
|
134
|
+
const formData = {
|
|
135
|
+
email: values.email.toLowerCase(),
|
|
136
|
+
countryCode: selectedCountry.phoneCode,
|
|
137
|
+
mobile: values.phone,
|
|
138
|
+
firstName: values.firstName,
|
|
139
|
+
lastName: values.lastName,
|
|
140
|
+
gender: values.gender,
|
|
141
|
+
dateOfBirth: values.dateOfBirth
|
|
142
|
+
? moment(values.dateOfBirth).format('YYYY-MM-DD')
|
|
143
|
+
: '',
|
|
144
|
+
};
|
|
145
|
+
await register(formData);
|
|
146
|
+
};
|
|
147
|
+
const isFormValid = values.firstName &&
|
|
148
|
+
values.firstName.length >= 3 &&
|
|
149
|
+
values.lastName &&
|
|
150
|
+
values.lastName.length >= 3 &&
|
|
151
|
+
values.gender &&
|
|
152
|
+
values.dateOfBirth &&
|
|
153
|
+
values.email &&
|
|
154
|
+
EMAIL_REGEX.test(values.email) &&
|
|
155
|
+
!emailExists &&
|
|
156
|
+
values.phone &&
|
|
157
|
+
(selectedCountry.phoneCode === '91' ? values.phone.length === 10 : values.phone.length >= 6) &&
|
|
158
|
+
termsAndConditions &&
|
|
159
|
+
isHuman;
|
|
160
|
+
const appName = appConfig?.appName || 'The Wellness Corner';
|
|
161
|
+
const termsAndConditionsUrl = appConfig?.termsAndConditionsUrl || '/privacy-policy';
|
|
162
|
+
return (_jsxs(Flex, { direction: "column", className: "min-h-screen", children: [_jsx(Flex, { justify: "center", align: "center", className: "py-6 border-b", children: _jsx(Typography, { type: "heading", size: "h4", color: "primary", children: appName }) }), _jsxs(Flex, { direction: "row", className: "flex-1", children: [_jsxs(Flex, { direction: "column", justify: "center", className: "hidden lg:flex lg:w-1/2 bg-primary-50 p-12", children: [_jsxs(Typography, { type: "heading", size: "h2", className: "mb-4", children: ["Welcome To", _jsx("br", {}), appName] }), _jsx(Typography, { type: "body", size: "large", color: "gray-600", children: "We just need a few details to get you started. It will only take less than a minute. Please note we only need these details to communicate with you when you avail any of our services." })] }), _jsx(Flex, { direction: "column", className: "w-full lg:w-1/2 p-6 lg:p-12 overflow-y-auto", children: _jsx(ScreenLayout, { title: "Complete Your Registration", subTitle: "Tell us more about you so we can set up your account.", buttonProps: {
|
|
163
|
+
loading: isLoading,
|
|
164
|
+
label: 'Register',
|
|
165
|
+
onClick: handleSubmit,
|
|
166
|
+
disabled: !isFormValid,
|
|
167
|
+
}, children: _jsxs(Form, { form: form, className: "w-full", children: [_jsx(Typography, { type: "heading", size: "h6", className: "mb-4", children: "Account Details" }), _jsx(Row, { gutter: 18, className: "py-0 my-0", children: _jsxs(Col, { xs: 24, children: [_jsx(Form.Item, { label: "Email Address", rules: [
|
|
168
|
+
{ required: true, message: 'Please enter your email' },
|
|
169
|
+
{ pattern: EMAIL_REGEX, message: 'Invalid email address' },
|
|
170
|
+
], ...form.register('email', {
|
|
171
|
+
onBlur: handleEmailBlur,
|
|
172
|
+
}), children: _jsx(TextInput, { type: "email", placeholder: "Enter your email address", className: emailExists ? 'border-red-500' : '' }) }), emailExists && (_jsx(Typography, { type: "utility", size: "small", color: "red-600", className: "-mt-4 mb-4", children: "An account with this email ID already exists" }))] }) }), _jsx(Row, { gutter: 18, className: "py-0 my-0", children: _jsx(Col, { xs: 24, children: _jsx(Form.Item, { label: "Mobile Number", rules: [{ required: true, message: 'Please enter your mobile number' }], ...form.register('phone'), children: _jsx(Flex, { className: "border border-gray-400 rounded-md", children: _jsx(TextInput, { type: "tel", placeholder: "Mobile number", maxLength: selectedCountry.phoneCode === '91' ? 10 : 14, className: "py-4 border-0 md:text-base", addonBefore: _jsx(CountryCodeDropdown, { selectedCountry: selectedCountry, handleSelect: (country) => {
|
|
173
|
+
setSelectedCountry(country);
|
|
174
|
+
form.setValue('phone', '');
|
|
175
|
+
} }) }) }) }) }) }), _jsx(Typography, { type: "heading", size: "h6", className: "mb-4 mt-6", children: "Personal Details" }), _jsxs(Row, { gutter: 18, className: "py-0 my-0", children: [_jsx(Col, { xs: 24, md: 12, children: _jsx(Form.Item, { label: "First Name", rules: [
|
|
176
|
+
{ required: true, message: 'Please enter your first name' },
|
|
177
|
+
{ pattern: validationPatterns.nameOnly, message: 'Name can only contain letters' },
|
|
178
|
+
{
|
|
179
|
+
validator: async (_, value) => {
|
|
180
|
+
if (value && value.length < 3) {
|
|
181
|
+
throw new Error('Name must be at least 3 characters');
|
|
182
|
+
}
|
|
183
|
+
},
|
|
184
|
+
},
|
|
185
|
+
], normalize: (value) => (value ?? '').replace(/[^a-zA-Z]/g, ''), ...form.register('firstName'), children: _jsx(TextInput, { placeholder: "Enter your first name" }) }) }), _jsx(Col, { xs: 24, md: 12, children: _jsx(Form.Item, { label: "Last Name", rules: [
|
|
186
|
+
{ required: true, message: 'Please enter your last name' },
|
|
187
|
+
{ pattern: validationPatterns.nameOnly, message: 'Name can only contain letters' },
|
|
188
|
+
{
|
|
189
|
+
validator: async (_, value) => {
|
|
190
|
+
if (value && value.length < 3) {
|
|
191
|
+
throw new Error('Name must be at least 3 characters');
|
|
192
|
+
}
|
|
193
|
+
},
|
|
194
|
+
},
|
|
195
|
+
], normalize: (value) => (value ?? '').replace(/[^a-zA-Z]/g, ''), ...form.register('lastName'), children: _jsx(TextInput, { placeholder: "Enter your last name" }) }) })] }), _jsxs(Row, { gutter: 18, className: "py-0 mt-4 relative", children: [_jsx(Col, { xs: 18, md: 19, children: _jsx(Typography, { type: "body", size: "large", className: "text-gray-700", children: "Your biological sex at the time of birth" }) }), _jsxs(Col, { xs: 6, md: 5, className: "flex justify-end group", children: [_jsx(Typography, { type: "body", size: "large", className: "text-primary cursor-pointer whitespace-nowrap", children: "Know More" }), _jsxs(Flex, { className: "hidden group-hover:block w-full absolute left-2 top-6 rounded-md p-4 z-10 text-gray-950 bg-white shadow-[0_0_10px_0_rgba(0,0,0,0.3)]", children: [_jsxs(Flex, { className: "p-0 mb-3 gap-3", children: [_jsx("img", { src: `${CDN_IMAGES_URL}/registration/gender-diversity.svg`, alt: "gender diversity", width: 44, height: 44, className: "h-auto" }), _jsxs(Typography, { type: "body", size: "large", className: "text-gray-800", children: [appName, " respects Gender Diversity & Inclusion!"] })] }), _jsxs(Typography, { type: "body", size: "small", className: "text-gray-500", children: [appName, " aims to provide you with a personalized wellness experience. This requires us to incorporate certain algorithms based on protocols provided by established health & medical institutions around the world. These protocols are based on your biological sex at birth."] })] })] })] }), _jsx(Row, { className: "py-0 mb-5", children: _jsx(Col, { children: _jsx(Form.Item, { className: "mt-4", rules: [{ required: true, message: 'Please select your biological sex' }], ...form.register('gender'), children: _jsx(RadioGroup, { orientation: "horizontal", options: [
|
|
196
|
+
{ label: 'Male', value: 'M' },
|
|
197
|
+
{ label: 'Female', value: 'F' },
|
|
198
|
+
] }) }) }) }), _jsx(Row, { gutter: 18, className: "py-0 my-5", children: _jsx(Col, { xs: 24, children: _jsx(Form.Item, { label: "Date of Birth", rules: [{ required: true, message: 'Please select your date of birth' }], ...form.register('dateOfBirth'), children: _jsx(DatePicker, { required: true, size: "middle", format: "dd/MM/yyyy", placeholder: "DD/MM/YYYY",
|
|
199
|
+
/* @ts-ignore */
|
|
200
|
+
disabledDate: disabledDate, iconPosition: "right", defaultPickerValue: dayjs('1990-01-01').toDate(), suffixIcon: _jsx(CalendarDays, { className: "-mt-2" }) }) }) }) }), isProduction &&
|
|
201
|
+
(process.env.NEXT_PUBLIC_RECAPTCHA_SITE_KEY ? (_jsx(Row, { gutter: 10, children: _jsx(Col, { span: 24, children: _jsx(ReCAPTCHA, { className: "g-recaptcha mt-4", sitekey: process.env.NEXT_PUBLIC_RECAPTCHA_SITE_KEY, onChange: handleCaptcha, onExpired: () => setIsHuman(false), onErrored: () => setIsHuman(false), size: "normal" }) }) })) : (_jsx(Typography, { type: "utility", size: "small", color: "red-600", children: "reCAPTCHA misconfigured. Set NEXT_PUBLIC_RECAPTCHA_SITE_KEY." }))), _jsx(Row, { className: "py-0 my-5", children: _jsx(Col, { children: _jsx(CustomCheckbox, { checked: termsAndConditions, onClick: () => setTermsAndConditions((prev) => !prev), className: "mt-4", label: _jsxs(Typography, { type: "body", size: "medium", children: ["I accept the", _jsx("a", { target: "_blank", rel: "noopener noreferrer", href: termsAndConditionsUrl, className: "px-1 text-primary", children: "Terms & Conditions" }), "listed on ", appName] }) }) }) })] }) }) })] })] }));
|
|
202
|
+
};
|
|
203
|
+
export { PartnerRegistrationWebComponent };
|
package/build/src/screens/PartnerSSO/PartnerRegistration/hooks/internal/usePartnerRegistration.js
ADDED
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
import { useCallback, useEffect, useState } from 'react';
|
|
2
|
+
import { jwtDecode } from 'jwt-decode';
|
|
3
|
+
import { axiosClient } from '../../../../../api/axiosClient';
|
|
4
|
+
import { showMessage } from '../../../../../helpers/show-message';
|
|
5
|
+
import { useAuthPackageContext } from '../../../../../hooks/internal/useAuthPackageContext';
|
|
6
|
+
/**
|
|
7
|
+
* @internal
|
|
8
|
+
* Hook for managing Partner SSO Registration.
|
|
9
|
+
* This hook is not exposed to package consumers.
|
|
10
|
+
*/
|
|
11
|
+
const usePartnerRegistration = ({ partner, token, path, }) => {
|
|
12
|
+
const [isLoading, setIsLoading] = useState(false);
|
|
13
|
+
const [error, setError] = useState(null);
|
|
14
|
+
const [tokenData, setTokenData] = useState(null);
|
|
15
|
+
const { onLogin, onTokenChange } = useAuthPackageContext();
|
|
16
|
+
// Decode JWT token to pre-fill form fields
|
|
17
|
+
useEffect(() => {
|
|
18
|
+
if (token) {
|
|
19
|
+
try {
|
|
20
|
+
const decoded = jwtDecode(token);
|
|
21
|
+
setTokenData(decoded);
|
|
22
|
+
}
|
|
23
|
+
catch (err) {
|
|
24
|
+
console.warn('Failed to decode partner token:', err);
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
}, [token]);
|
|
28
|
+
const checkEmailExists = useCallback(async (email) => {
|
|
29
|
+
try {
|
|
30
|
+
await axiosClient({
|
|
31
|
+
url: '/auth/email-exists',
|
|
32
|
+
method: 'POST',
|
|
33
|
+
data: { email },
|
|
34
|
+
});
|
|
35
|
+
// If request succeeds, email exists
|
|
36
|
+
return true;
|
|
37
|
+
}
|
|
38
|
+
catch {
|
|
39
|
+
// If request fails (404), email doesn't exist
|
|
40
|
+
return false;
|
|
41
|
+
}
|
|
42
|
+
}, []);
|
|
43
|
+
const register = useCallback(async (formData) => {
|
|
44
|
+
if (!partner || !token) {
|
|
45
|
+
showMessage({ message: 'Missing partner or token information' });
|
|
46
|
+
return;
|
|
47
|
+
}
|
|
48
|
+
setIsLoading(true);
|
|
49
|
+
setError(null);
|
|
50
|
+
try {
|
|
51
|
+
const response = await axiosClient({
|
|
52
|
+
url: `/auth/partners/${partner}/registration`,
|
|
53
|
+
method: 'POST',
|
|
54
|
+
data: {
|
|
55
|
+
...formData,
|
|
56
|
+
token,
|
|
57
|
+
},
|
|
58
|
+
});
|
|
59
|
+
const { token: authToken, member } = response.data;
|
|
60
|
+
const redirectPath = path || '/dashboard';
|
|
61
|
+
// Update auth context
|
|
62
|
+
onTokenChange(authToken);
|
|
63
|
+
// Call onLogin callback
|
|
64
|
+
await onLogin({
|
|
65
|
+
token: authToken,
|
|
66
|
+
member,
|
|
67
|
+
});
|
|
68
|
+
// Redirect to the specified path
|
|
69
|
+
if (typeof window !== 'undefined') {
|
|
70
|
+
window.location.assign(redirectPath);
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
catch (err) {
|
|
74
|
+
const errorMessage = err?.response?.data?.errors?.[0]?.message || 'Registration failed';
|
|
75
|
+
setError(errorMessage);
|
|
76
|
+
showMessage({ message: errorMessage });
|
|
77
|
+
}
|
|
78
|
+
finally {
|
|
79
|
+
setIsLoading(false);
|
|
80
|
+
}
|
|
81
|
+
}, [partner, token, path, onLogin, onTokenChange]);
|
|
82
|
+
return {
|
|
83
|
+
isLoading,
|
|
84
|
+
error,
|
|
85
|
+
tokenData,
|
|
86
|
+
register,
|
|
87
|
+
checkEmailExists,
|
|
88
|
+
};
|
|
89
|
+
};
|
|
90
|
+
export { usePartnerRegistration };
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
2
|
+
import React, { useState } from 'react';
|
|
3
|
+
import { Button, TextInput, Typography } from '@truworth/twc-web-design';
|
|
4
|
+
import { Phone, Shield } from 'lucide-react';
|
|
5
|
+
import { useProfile } from '../../hooks/internal/useProfile';
|
|
6
|
+
import { OtpVerificationModal } from '../OtpVerificationModal';
|
|
7
|
+
const EditMobileNumber = ({ onSuccess }) => {
|
|
8
|
+
const { loading, profileData, updateMobileNumber } = useProfile();
|
|
9
|
+
const [showOtpModal, setShowOtpModal] = useState(false);
|
|
10
|
+
const [newPhone, setNewPhone] = useState('');
|
|
11
|
+
const handleSubmit = (e) => {
|
|
12
|
+
e.preventDefault();
|
|
13
|
+
const countryCode = profileData?.countryCode || '91';
|
|
14
|
+
updateMobileNumber({ countryCode, mobileNo: newPhone }, () => setShowOtpModal(true));
|
|
15
|
+
};
|
|
16
|
+
const handlePhoneChange = (e) => {
|
|
17
|
+
const value = e.target.value.replace(/\s+|[^\d]/g, '').trim().slice(0, 10);
|
|
18
|
+
setNewPhone(value);
|
|
19
|
+
};
|
|
20
|
+
const handleOtpSuccess = () => {
|
|
21
|
+
setShowOtpModal(false);
|
|
22
|
+
setNewPhone('');
|
|
23
|
+
onSuccess?.();
|
|
24
|
+
};
|
|
25
|
+
const currentPhone = profileData?.phone || '';
|
|
26
|
+
const maskedCurrentPhone = currentPhone
|
|
27
|
+
? `+91 ${currentPhone.slice(0, 2)}******${currentPhone.slice(-2)}`
|
|
28
|
+
: 'Not set';
|
|
29
|
+
return (_jsxs(_Fragment, { children: [_jsxs("form", { onSubmit: handleSubmit, children: [_jsxs("div", { className: "mb-8 pb-8 border-b border-gray-100", children: [_jsx(Typography, { type: "utility", size: "large", className: "text-gray-700 font-medium mb-4", children: "Current Mobile Number" }), _jsxs("div", { className: "flex items-center gap-4 p-4 bg-gray-50 rounded-xl border border-gray-100", children: [_jsx("div", { className: "w-12 h-12 rounded-xl bg-primary/10 flex items-center justify-center", children: _jsx(Phone, { className: "w-6 h-6 text-primary" }) }), _jsxs("div", { children: [_jsx(Typography, { type: "body", size: "large", className: "text-gray-900 font-medium", children: maskedCurrentPhone }), _jsx(Typography, { type: "body", size: "small", className: "text-gray-500", children: "Verified and active" })] }), _jsxs("div", { className: "ml-auto flex items-center gap-1 text-green-600", children: [_jsx(Shield, { className: "w-4 h-4" }), _jsx("span", { className: "text-sm font-medium", children: "Verified" })] })] })] }), _jsxs("div", { className: "mb-8", children: [_jsx(Typography, { type: "utility", size: "large", className: "text-gray-700 font-medium mb-4", children: "Update Mobile Number" }), _jsx(Typography, { type: "body", size: "medium", className: "text-gray-500 mb-4", children: "Enter your new mobile number. We'll send an OTP to verify it." }), _jsxs("div", { className: "flex gap-4", children: [_jsxs("div", { className: "w-24", children: [_jsx("label", { className: "block text-sm font-medium text-gray-700 mb-2", children: "Country" }), _jsx(TextInput, { disabled: true, value: "+91", className: "bg-gray-50 rounded-xl h-12 text-center font-medium" })] }), _jsxs("div", { className: "flex-1", children: [_jsx("label", { className: "block text-sm font-medium text-gray-700 mb-2", children: "New Mobile Number" }), _jsx(TextInput, { value: newPhone, onChange: handlePhoneChange, placeholder: "Enter 10-digit mobile number", className: "rounded-xl h-12", maxLength: 10 })] })] }), newPhone.length > 0 && newPhone.length < 10 && (_jsx(Typography, { type: "body", size: "small", className: "text-amber-600 mt-2", children: "Please enter a valid 10-digit mobile number" }))] }), _jsx("div", { className: "mb-8 p-4 bg-blue-50 rounded-xl border border-blue-100", children: _jsxs("div", { className: "flex items-start gap-3", children: [_jsx("div", { className: "w-8 h-8 rounded-lg bg-blue-100 flex items-center justify-center flex-shrink-0", children: _jsx(Shield, { className: "w-4 h-4 text-blue-600" }) }), _jsxs("div", { children: [_jsx(Typography, { type: "body", size: "medium", className: "text-blue-900 font-medium mb-1", children: "Verification Required" }), _jsx(Typography, { type: "body", size: "small", className: "text-blue-700", children: "For your security, we'll send a one-time password (OTP) to your new mobile number to verify the change." })] })] }) }), _jsx("div", { className: "flex justify-end pt-4 border-t border-gray-100", children: _jsx(Button, { type: "submit", loading: loading, disabled: newPhone.length !== 10, className: "px-8", size: "large", children: "Verify & Update" }) })] }), _jsx(OtpVerificationModal, { open: showOtpModal, onOpenChange: setShowOtpModal, phone: newPhone, onSuccess: handleOtpSuccess })] }));
|
|
30
|
+
};
|
|
31
|
+
export { EditMobileNumber };
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
import React from 'react';
|
|
3
|
+
import { View, Text } from 'react-native';
|
|
4
|
+
// Native implementation placeholder
|
|
5
|
+
const EditMobileNumber = () => {
|
|
6
|
+
return (_jsx(View, { children: _jsx(Text, { children: "Edit Mobile Number - Native Implementation" }) }));
|
|
7
|
+
};
|
|
8
|
+
export { EditMobileNumber };
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import React, { useState } from 'react';
|
|
3
|
+
import { Button, Col, Form, Row, PasswordInput, Typography, Flex, useForm } from '@truworth/twc-web-design';
|
|
4
|
+
import { Shield, AlertCircle } from 'lucide-react';
|
|
5
|
+
import { useProfile } from '../../hooks/internal/useProfile';
|
|
6
|
+
import { strongPasswordRequirements } from '../../../../constants/password-requirements';
|
|
7
|
+
const EditPassword = ({ onSuccess }) => {
|
|
8
|
+
const { loading, changePassword } = useProfile();
|
|
9
|
+
const [currentValidate, setCurrentValidate] = useState(false);
|
|
10
|
+
const [strongPassword, setStrongPassword] = useState(false);
|
|
11
|
+
const editPasswordForm = useForm();
|
|
12
|
+
const handleSubmit = ({ currentPassword, password, newPassword }) => {
|
|
13
|
+
setCurrentValidate(false);
|
|
14
|
+
changePassword({ currentPassword, newPassword }, () => {
|
|
15
|
+
editPasswordForm.reset();
|
|
16
|
+
setCurrentValidate(false);
|
|
17
|
+
onSuccess?.();
|
|
18
|
+
}, () => {
|
|
19
|
+
setCurrentValidate(true);
|
|
20
|
+
});
|
|
21
|
+
};
|
|
22
|
+
const currentPassword = editPasswordForm.watch('currentPassword');
|
|
23
|
+
const password = editPasswordForm.watch('password');
|
|
24
|
+
const newPassword = editPasswordForm.watch('newPassword');
|
|
25
|
+
const passwordsMatch = password && newPassword && password === newPassword;
|
|
26
|
+
const disabled = currentPassword && strongPassword && passwordsMatch;
|
|
27
|
+
return (_jsxs(Form, { form: editPasswordForm, onSubmit: handleSubmit, children: [_jsxs("div", { className: "mb-8 pb-8 border-b border-gray-100", children: [_jsx(Typography, { type: "utility", size: "large", className: "text-gray-700 font-medium mb-4", children: "Current Password" }), _jsx(Typography, { type: "body", size: "medium", className: "text-gray-500 mb-4", children: "Enter your current password to verify it's you." }), _jsx(Row, { children: _jsx(Col, { md: 16, xs: 24, children: _jsx(Form.Item, { name: "currentPassword", normalize: (value) => value.replace(/\s/g, ''), children: _jsx(PasswordInput, { showStrengthIndicator: false, placeholder: "Enter your current password", minLength: 3, className: "rounded-xl" }) }) }) }), currentValidate && (_jsxs(Flex, { align: "center", className: "gap-2 p-3 mt-2 bg-red-50 rounded-xl border border-red-100", children: [_jsx(AlertCircle, { size: 16, className: "text-red-600 flex-shrink-0" }), _jsx(Typography, { type: "body", size: "small", className: "text-red-700", children: "The password you entered doesn't match your current password." })] }))] }), _jsxs("div", { className: "mb-8 pb-8 border-b border-gray-100", children: [_jsx(Typography, { type: "utility", size: "large", className: "text-gray-700 font-medium mb-4", children: "Create New Password" }), _jsx(Typography, { type: "body", size: "medium", className: "text-gray-500 mb-4", children: "Choose a strong password that you don't use for other accounts." }), _jsxs(Row, { gutter: 20, children: [_jsx(Col, { md: 12, xs: 24, children: _jsx(Form.Item, { name: "password", label: "New Password", normalize: (value) => value.replace(/\s/g, ''), children: _jsx(PasswordInput, { requirements: strongPasswordRequirements, showStrengthIndicator: true, placeholder: "Create a new password", minLength: 3, className: "rounded-xl", onAllRequirementsMet: (value) => setStrongPassword(value) }) }) }), _jsxs(Col, { md: 12, xs: 24, children: [_jsx(Form.Item, { name: "newPassword", label: "Confirm New Password", normalize: (value) => value.replace(/\s/g, ''), children: _jsx(PasswordInput, { minLength: 3, placeholder: "Re-enter your new password", showStrengthIndicator: false, className: "rounded-xl" }) }), password && newPassword && !passwordsMatch && (_jsx(Typography, { type: "body", size: "small", className: "text-red-600 mt-1", children: "Passwords don't match" })), passwordsMatch && (_jsx(Typography, { type: "body", size: "small", className: "text-green-600 mt-1", children: "\u2713 Passwords match" }))] })] })] }), _jsx("div", { className: "mb-8 p-4 bg-primary/5 rounded-xl border border-primary/10", children: _jsxs(Flex, { align: "start", className: "gap-3", children: [_jsx("div", { className: "w-8 h-8 rounded-lg bg-primary/10 flex items-center justify-center flex-shrink-0", children: _jsx(Shield, { className: "w-4 h-4 text-primary" }) }), _jsxs("div", { children: [_jsx(Typography, { type: "body", size: "medium", className: "text-gray-900 font-medium mb-1", children: "Password Security Tips" }), _jsxs("ul", { className: "text-sm text-gray-600 space-y-1", children: [_jsx("li", { children: "\u2022 Use a mix of letters, numbers, and symbols" }), _jsx("li", { children: "\u2022 Avoid using personal information" }), _jsx("li", { children: "\u2022 Don't reuse passwords from other accounts" })] })] })] }) }), _jsx(Flex, { justify: "end", className: "pt-4 border-t border-gray-100", children: _jsx(Button, { loading: loading, type: "submit", disabled: !disabled, className: "px-8", size: "large", children: "Update Password" }) })] }));
|
|
28
|
+
};
|
|
29
|
+
export { EditPassword };
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
import React from 'react';
|
|
3
|
+
import { View, Text } from 'react-native';
|
|
4
|
+
// Native implementation placeholder
|
|
5
|
+
const EditPassword = () => {
|
|
6
|
+
return (_jsx(View, { children: _jsx(Text, { children: "Edit Password - Native Implementation" }) }));
|
|
7
|
+
};
|
|
8
|
+
export { EditPassword };
|