@truworth/twc-auth 1.2.12-beta.0 → 1.2.12-beta.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/README.md +16 -14
- package/build/src/components/AdvancedTransitionWrapper/index.js +2 -8
- package/build/src/components/ConfirmationModal/index.js +2 -2
- package/build/src/components/LoadingAnimation/index.js +50 -0
- package/build/src/constants/password-requirements.js +7 -0
- package/build/src/contexts/AuthContext.js +5 -1
- package/build/src/hooks/useNavigator.js +83 -0
- package/build/src/index.js +8 -0
- package/build/src/screens/CountryCode/components/CountryCodeDropdown/index.js +2 -3
- package/build/src/screens/CreatePassword/hooks/internal/useCreatePassword.js +11 -4
- package/build/src/screens/CreatePassword/index.js +6 -30
- package/build/src/screens/EnterMobile/components/ExistingAccountsSheet/index.js +1 -1
- package/build/src/screens/EnterMobile/hooks/internal/useEnterMobile.js +4 -1
- package/build/src/screens/EnterMobile/index.js +10 -10
- package/build/src/screens/EnterMobile/index.native.js +3 -3
- package/build/src/screens/Login/components/LoginWebComponent/index.js +3 -4
- package/build/src/screens/LoginWithMobileOTP/hooks/internal/useLoginWithMobileOTP.js +5 -5
- package/build/src/screens/LoginWithMobileOTP/index.js +2 -2
- package/build/src/screens/LoginWithMobileOTP/index.native.js +2 -2
- package/build/src/screens/PartnerSSO/PartnerLogin/components/PartnerLoginWebComponent/index.js +71 -0
- package/build/src/screens/PartnerSSO/PartnerLogin/components/PartnerLoginWebComponent/index.native.js +6 -0
- package/build/src/screens/PartnerSSO/PartnerLogin/hooks/internal/usePartnerLogin.js +52 -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 +207 -0
- package/build/src/screens/PartnerSSO/PartnerRegistration/components/PartnerRegistrationWebComponent/index.native.js +6 -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/src/screens/SSOLogin/AuthWebView/index.native.js +27 -12
- package/build/src/screens/SSOLogin/AuthenticationMethods/hooks/internal/useSSOAuthenticationMethods.js +3 -2
- package/build/src/screens/SSOLogin/AuthenticationMethods/index.js +1 -0
- package/build/src/screens/SSOLogin/AuthenticationMethods/index.native.js +6 -1
- package/build/src/screens/SSOLogin/SSOCallback/components/SSOCallbackComponent/index.js +31 -28
- package/build/src/screens/SSOLogin/SSOCallback/hooks/internal/useSSOCallback.js +23 -8
- package/build/src/screens/SSOLogin/SSOCallback/index.native.js +2 -2
- package/build/src/screens/SignUp/components/SignUpForm/index.js +17 -17
- package/build/src/screens/SignUp/components/SignUpWebComponent/index.js +7 -6
- package/build/src/screens/UserConsent/index.js +11 -17
- package/build/src/screens/Welcome/SocialAuth/hooks/web/useFacebookAuth.web.js +3 -4
- package/build/src/screens/Welcome/SocialAuth/hooks/web/useGoogleAuth.web.js +3 -4
- package/build/src/screens/Welcome/index.js +1 -1
- package/build/types/components/ConfirmationModal/index.d.ts +1 -1
- package/build/types/components/ConfirmationModal/types.d.ts +3 -0
- package/build/types/components/LoadingAnimation/index.d.ts +6 -0
- package/build/types/constants/password-requirements.d.ts +4 -0
- package/build/types/contexts/AuthContext.d.ts +3 -1
- package/build/types/contexts/type.d.ts +24 -1
- package/build/types/hooks/useNavigator.d.ts +66 -0
- package/build/types/index.d.ts +5 -0
- package/build/types/navigator/index.native.d.ts +5 -2
- package/build/types/screens/CreatePassword/hooks/internal/useCreatePassword.d.ts +12 -1
- package/build/types/screens/EnterMobile/types.d.ts +2 -2
- package/build/types/screens/LoginWithMobileOTP/hooks/internal/useLoginWithMobileOTP.d.ts +2 -2
- package/build/types/screens/LoginWithMobileOTP/index.d.ts +2 -2
- package/build/types/screens/PartnerSSO/PartnerLogin/components/PartnerLoginWebComponent/index.d.ts +25 -0
- package/build/types/screens/PartnerSSO/PartnerLogin/components/PartnerLoginWebComponent/index.native.d.ts +2 -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/components/PartnerRegistrationWebComponent/index.native.d.ts +2 -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/build/types/screens/SSOLogin/AuthenticationMethods/types.d.ts +4 -2
- package/build/types/screens/SSOLogin/SSOCallback/components/SSOCallbackComponent/index.d.ts +5 -1
- package/build/types/screens/SSOLogin/SSOCallback/hooks/internal/useSSOCallback.d.ts +5 -1
- package/build/types/screens/SSOLogin/SSOCallback/types.d.ts +3 -0
- package/get-metro-config.js +19 -26
- package/package.json +5 -6
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import React, { useEffect, useState } from 'react';
|
|
3
|
+
import { Button, DatePicker, TextInput, TextArea, UploadButton, Col, Form, Row, RadioGroup, Flex, Typography, showNotification, useForm } from '@truworth/twc-web-design';
|
|
4
|
+
import { Camera, Info, UserRound } from 'lucide-react';
|
|
5
|
+
import { useProfile } from '../../hooks/internal/useProfile';
|
|
6
|
+
import { MEMBER_IMAGES_URL } from '../../../../constants/cdn-url';
|
|
7
|
+
const EditProfile = ({ onSuccess }) => {
|
|
8
|
+
const { loading, profileData, imageUrl, setImageUrl, updateProfile, uploadProfileImage } = useProfile();
|
|
9
|
+
const [image, setImage] = useState(null);
|
|
10
|
+
const editProfileForm = useForm({
|
|
11
|
+
values: {
|
|
12
|
+
firstName: profileData?.firstName || '',
|
|
13
|
+
lastName: profileData?.lastName || '',
|
|
14
|
+
bio: profileData?.bio || '',
|
|
15
|
+
gender: profileData?.gender || '',
|
|
16
|
+
dateOfBirth: profileData?.dateOfBirth instanceof Date ? profileData.dateOfBirth : undefined
|
|
17
|
+
},
|
|
18
|
+
liveValidation: true
|
|
19
|
+
});
|
|
20
|
+
const handleSubmit = async ({ firstName, lastName, bio }) => {
|
|
21
|
+
const payload = {
|
|
22
|
+
firstName,
|
|
23
|
+
lastName
|
|
24
|
+
};
|
|
25
|
+
if (bio)
|
|
26
|
+
payload.bio = bio;
|
|
27
|
+
if (image) {
|
|
28
|
+
const uploadedFileName = await uploadProfileImage(image);
|
|
29
|
+
if (uploadedFileName) {
|
|
30
|
+
payload.image = uploadedFileName;
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
await updateProfile(payload, () => {
|
|
34
|
+
onSuccess?.();
|
|
35
|
+
localStorage.setItem('profileData', JSON.stringify({
|
|
36
|
+
firstName: payload.firstName,
|
|
37
|
+
lastName: payload.lastName,
|
|
38
|
+
image: payload.image ? `${MEMBER_IMAGES_URL}${payload.image}` : imageUrl
|
|
39
|
+
}));
|
|
40
|
+
});
|
|
41
|
+
};
|
|
42
|
+
const beforeUpload = async (files) => {
|
|
43
|
+
const file = files[0];
|
|
44
|
+
if (!file)
|
|
45
|
+
return false;
|
|
46
|
+
const validTypes = ['image/png', 'image/jpg', 'image/jpeg'];
|
|
47
|
+
const isValidType = validTypes.includes(file.type);
|
|
48
|
+
const isLt5M = file.size / 1024 / 1024 < 5;
|
|
49
|
+
if (!isValidType) {
|
|
50
|
+
showNotification({
|
|
51
|
+
type: 'error',
|
|
52
|
+
description: `${file.name} is not a valid image file`,
|
|
53
|
+
showIcon: false
|
|
54
|
+
});
|
|
55
|
+
return false;
|
|
56
|
+
}
|
|
57
|
+
if (!isLt5M) {
|
|
58
|
+
showNotification({
|
|
59
|
+
type: 'error',
|
|
60
|
+
description: 'Image must be smaller than 5 MB',
|
|
61
|
+
showIcon: false
|
|
62
|
+
});
|
|
63
|
+
return false;
|
|
64
|
+
}
|
|
65
|
+
const reader = new FileReader();
|
|
66
|
+
reader.onload = () => setImageUrl(reader.result);
|
|
67
|
+
reader.readAsDataURL(file);
|
|
68
|
+
setImage(file);
|
|
69
|
+
return false;
|
|
70
|
+
};
|
|
71
|
+
useEffect(() => {
|
|
72
|
+
window.scrollTo({ top: 0 });
|
|
73
|
+
}, []);
|
|
74
|
+
const firstName = editProfileForm.watch('firstName');
|
|
75
|
+
const lastName = editProfileForm.watch('lastName');
|
|
76
|
+
const bio = editProfileForm.watch('bio');
|
|
77
|
+
const isDisabled = firstName?.length > 2 && lastName?.length > 2;
|
|
78
|
+
return (_jsxs(Form, { form: editProfileForm, 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: "Profile Picture" }), _jsxs(Flex, { align: "center", className: "gap-6", children: [_jsxs("div", { className: "relative group", children: [_jsx("div", { className: "w-24 h-24 rounded-2xl bg-gray-100 flex items-center justify-center overflow-hidden border-2 border-gray-200", children: !imageUrl ? (_jsx(UserRound, { size: 40, className: "text-gray-400" })) : (_jsx("img", { src: imageUrl, alt: "Profile", className: "w-full h-full object-cover" })) }), _jsx("div", { className: "absolute inset-0 rounded-2xl bg-black/50 opacity-0 group-hover:opacity-100 transition-opacity flex items-center justify-center", children: _jsx(UploadButton, { files: [], accept: ".png,.jpg,.jpeg", leftIcon: _jsx(Camera, { size: 24, className: "text-white" }), size: "small", buttonStyle: "border-0 p-0 bg-transparent hover:bg-transparent", onUpload: (files) => beforeUpload(files), showAttachments: false }) })] }), _jsxs("div", { children: [_jsx(Typography, { type: "body", size: "medium", className: "text-gray-900 font-medium", children: "Upload a new photo" }), _jsx(Typography, { type: "body", size: "small", className: "text-gray-500 mt-1", children: "JPG, PNG or JPEG. Max size 5MB" }), _jsx(UploadButton, { files: [], accept: ".png,.jpg,.jpeg", label: "Choose File", size: "small", buttonStyle: "mt-3 border-gray-300 text-gray-700 hover:bg-gray-50", onUpload: (files) => beforeUpload(files), showAttachments: false })] })] })] }), _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: "Personal Information" }), _jsxs(Row, { gutter: 20, children: [_jsx(Col, { md: 12, xs: 24, children: _jsx(Form.Item, { label: "First Name", name: "firstName", rules: [{
|
|
79
|
+
required: true,
|
|
80
|
+
message: 'Enter at least 3 letters.'
|
|
81
|
+
}], children: _jsx(TextInput, { placeholder: "Enter first name", className: "rounded-xl h-12" }) }) }), _jsx(Col, { md: 12, xs: 24, children: _jsx(Form.Item, { label: "Last Name", name: "lastName", rules: [{
|
|
82
|
+
required: true,
|
|
83
|
+
message: 'Enter at least 3 letters.'
|
|
84
|
+
}], children: _jsx(TextInput, { placeholder: "Enter last name", className: "rounded-xl h-12" }) }) })] })] }), _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: "Additional Details" }), _jsxs(Row, { gutter: 20, children: [_jsx(Col, { md: 12, xs: 24, children: _jsx(Form.Item, { label: _jsx("span", { className: "text-gray-500", children: "Gender" }), name: "gender", children: _jsx(RadioGroup, { disabled: true, orientation: "horizontal", options: [
|
|
85
|
+
{ label: 'Male', value: 'M' },
|
|
86
|
+
{ label: 'Female', value: 'F' }
|
|
87
|
+
] }) }) }), _jsx(Col, { md: 12, xs: 24, children: _jsx(Form.Item, { label: _jsx("span", { className: "text-gray-500", children: "Date of Birth" }), name: "dateOfBirth", children: _jsx(DatePicker, { disabled: true, format: "dd/MM/yyyy", iconPosition: "right", className: "!bg-gray-50 rounded-xl h-12" }) }) })] }), _jsxs(Flex, { align: "center", className: "gap-2 p-3 bg-amber-50 rounded-xl border border-amber-100", children: [_jsx(Info, { size: 16, className: "text-amber-600 flex-shrink-0" }), _jsx(Typography, { type: "body", size: "small", className: "text-amber-800", children: "Please contact support to update Gender or Date of Birth." })] })] }), _jsxs("div", { className: "mb-8", children: [_jsx(Typography, { type: "utility", size: "large", className: "text-gray-700 font-medium mb-4", children: "About You" }), _jsx(Form.Item, { label: "Bio", name: "bio", children: _jsx(TextArea, { maxCount: 150, placeholder: "Tell us a bit about yourself..." }) })] }), _jsx(Flex, { justify: "end", className: "pt-4 border-t border-gray-100", children: _jsx(Button, { type: "submit", loading: loading, disabled: !isDisabled, className: "px-8", size: "large", children: "Save Changes" }) })] }));
|
|
88
|
+
};
|
|
89
|
+
export { EditProfile };
|
|
@@ -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 EditProfile = () => {
|
|
6
|
+
return (_jsx(View, { children: _jsx(Text, { children: "Edit Profile - Native Implementation" }) }));
|
|
7
|
+
};
|
|
8
|
+
export { EditProfile };
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import React, { useEffect, useState, useCallback } from 'react';
|
|
3
|
+
import { Button, Typography, ResponsiveModal, OTPInput } from '@truworth/twc-web-design';
|
|
4
|
+
import { useProfile } from '../../hooks/internal/useProfile';
|
|
5
|
+
const OtpVerificationModal = ({ open, onOpenChange, phone, onSuccess, successMessage = 'Mobile number updated', title = 'Confirm Mobile Number' }) => {
|
|
6
|
+
const { loading, sendMobileOtp, verifyMobileOtp } = useProfile();
|
|
7
|
+
const [otp, setOtp] = useState('');
|
|
8
|
+
const [otpError, setOtpError] = useState(false);
|
|
9
|
+
const [resendTimer, setResendTimer] = useState(180);
|
|
10
|
+
const [timeLeft, setTimeLeft] = useState('03:00');
|
|
11
|
+
// Timer countdown
|
|
12
|
+
useEffect(() => {
|
|
13
|
+
if (!resendTimer)
|
|
14
|
+
return;
|
|
15
|
+
const intervalId = setInterval(() => {
|
|
16
|
+
const minute = Math.floor(resendTimer / 60);
|
|
17
|
+
const seconds = resendTimer % 60;
|
|
18
|
+
const formattedTime = `${minute < 10 ? '0' : ''}${minute}:${seconds < 10 ? '0' : ''}${seconds}`;
|
|
19
|
+
setTimeLeft(formattedTime);
|
|
20
|
+
setResendTimer(prev => prev - 1);
|
|
21
|
+
}, 1000);
|
|
22
|
+
return () => clearInterval(intervalId);
|
|
23
|
+
}, [resendTimer]);
|
|
24
|
+
// Send OTP when modal opens
|
|
25
|
+
useEffect(() => {
|
|
26
|
+
if (open) {
|
|
27
|
+
sendMobileOtp();
|
|
28
|
+
setResendTimer(180);
|
|
29
|
+
setOtp('');
|
|
30
|
+
setOtpError(false);
|
|
31
|
+
}
|
|
32
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
33
|
+
}, [open]);
|
|
34
|
+
const handleChange = useCallback((code) => {
|
|
35
|
+
setOtp(code);
|
|
36
|
+
setOtpError(false);
|
|
37
|
+
}, []);
|
|
38
|
+
const handleVerify = useCallback(() => {
|
|
39
|
+
setOtpError(false);
|
|
40
|
+
verifyMobileOtp(otp, () => {
|
|
41
|
+
onOpenChange(false);
|
|
42
|
+
onSuccess?.();
|
|
43
|
+
}, () => {
|
|
44
|
+
setOtpError(true);
|
|
45
|
+
});
|
|
46
|
+
}, [otp, verifyMobileOtp, onOpenChange, onSuccess]);
|
|
47
|
+
const handleResend = useCallback(() => {
|
|
48
|
+
setResendTimer(180);
|
|
49
|
+
setOtpError(false);
|
|
50
|
+
setOtp('');
|
|
51
|
+
sendMobileOtp();
|
|
52
|
+
}, [sendMobileOtp]);
|
|
53
|
+
const handleClose = useCallback(() => {
|
|
54
|
+
setOtp('');
|
|
55
|
+
setOtpError(false);
|
|
56
|
+
onOpenChange(false);
|
|
57
|
+
}, [onOpenChange]);
|
|
58
|
+
const maskedPhone = phone ? `XXXXXXX${phone.slice(-3)}` : '';
|
|
59
|
+
return (_jsxs(ResponsiveModal, { size: "sm", title: _jsx(Typography, { type: "heading", size: "h6", children: title }), footer: null, open: open, showCloseButton: false, onOpenChange: handleClose, children: [_jsxs(Typography, { type: "body", size: "large", className: "mb-7 text-gray-400 text-center", children: ["OTP sent to", _jsx("span", { className: "pl-2 text-secondary-dark", children: maskedPhone })] }), _jsx(OTPInput, { className: "!w-full", onChange: handleChange, error: otpError, maxLength: 6, value: otp }), otpError && (_jsx(Typography, { type: "body", size: "medium", className: "text-utility-danger-main mt-2", children: "Please enter the correct OTP." })), resendTimer < 60 ? (_jsxs("div", { className: "flex justify-between items-center my-7", children: [_jsx(Typography, { type: "utility", size: "medium", className: "text-gray-400", children: "Didn't receive OTP?" }), _jsx(Button, { size: "small", variant: "link", onClick: handleResend, children: "Resend Code" })] })) : (_jsxs(Typography, { type: "utility", size: "medium", className: "my-7 text-gray-400", children: ["Resend Code in ", timeLeft] })), _jsx(Button, { isFullWidth: true, onClick: handleVerify, loading: loading, disabled: otp.length < 6, children: "Continue" })] }));
|
|
60
|
+
};
|
|
61
|
+
export { OtpVerificationModal };
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import React, { useState } from 'react';
|
|
3
|
+
import { AvatarWithInitials, ResponsiveModal, Button, Flex, Typography } from '@truworth/twc-web-design';
|
|
4
|
+
import { Lock, Phone, UserRound, Loader2, ChevronRight } from 'lucide-react';
|
|
5
|
+
import { EditProfile } from '../EditProfile';
|
|
6
|
+
import { EditMobileNumber } from '../EditMobileNumber';
|
|
7
|
+
import { EditPassword } from '../EditPassword';
|
|
8
|
+
import { useProfile } from '../../hooks/internal/useProfile';
|
|
9
|
+
import { useAuthContext } from '../../../../hooks/useAuthContext';
|
|
10
|
+
const tabs = [
|
|
11
|
+
{
|
|
12
|
+
key: 'edit-profile',
|
|
13
|
+
label: 'Edit Profile',
|
|
14
|
+
icon: _jsx(UserRound, { className: "w-5 h-5" })
|
|
15
|
+
},
|
|
16
|
+
{
|
|
17
|
+
key: 'mobile-number',
|
|
18
|
+
label: 'Mobile Number',
|
|
19
|
+
icon: _jsx(Phone, { className: "w-5 h-5" })
|
|
20
|
+
},
|
|
21
|
+
{
|
|
22
|
+
key: 'password',
|
|
23
|
+
label: 'Password',
|
|
24
|
+
icon: _jsx(Lock, { className: "w-5 h-5" })
|
|
25
|
+
}
|
|
26
|
+
];
|
|
27
|
+
const ProfileWebComponent = ({ onProfileUpdate, onMobileUpdate, onPasswordUpdate, containerClassName = '' }) => {
|
|
28
|
+
const { isLoadingProfile } = useAuthContext();
|
|
29
|
+
const { profile, profileData } = useProfile();
|
|
30
|
+
const [activeTab, setActiveTab] = useState('edit-profile');
|
|
31
|
+
const [showUnsavedModal, setShowUnsavedModal] = useState(false);
|
|
32
|
+
const [pendingTab, setPendingTab] = useState(null);
|
|
33
|
+
const [hasDirtyForm, setHasDirtyForm] = useState(false);
|
|
34
|
+
// Show loading state while profile is being fetched
|
|
35
|
+
if (isLoadingProfile && !profile) {
|
|
36
|
+
return (_jsx("div", { className: containerClassName, children: _jsx(Flex, { justify: "center", align: "center", className: "py-20", children: _jsx(Loader2, { className: "w-8 h-8 animate-spin text-primary" }) }) }));
|
|
37
|
+
}
|
|
38
|
+
const fullName = `${profile?.firstName || ''} ${profile?.lastName || ''}`.trim();
|
|
39
|
+
const handleTabChange = (key) => {
|
|
40
|
+
if (hasDirtyForm) {
|
|
41
|
+
setShowUnsavedModal(true);
|
|
42
|
+
setPendingTab(key);
|
|
43
|
+
}
|
|
44
|
+
else {
|
|
45
|
+
setActiveTab(key);
|
|
46
|
+
}
|
|
47
|
+
};
|
|
48
|
+
const handleConfirmLeave = () => {
|
|
49
|
+
setHasDirtyForm(false);
|
|
50
|
+
if (pendingTab) {
|
|
51
|
+
setActiveTab(pendingTab);
|
|
52
|
+
setPendingTab(null);
|
|
53
|
+
}
|
|
54
|
+
setShowUnsavedModal(false);
|
|
55
|
+
};
|
|
56
|
+
const renderTabContent = () => {
|
|
57
|
+
switch (activeTab) {
|
|
58
|
+
case 'edit-profile':
|
|
59
|
+
return _jsx(EditProfile, { onSuccess: onProfileUpdate });
|
|
60
|
+
case 'mobile-number':
|
|
61
|
+
return _jsx(EditMobileNumber, { onSuccess: onMobileUpdate });
|
|
62
|
+
case 'password':
|
|
63
|
+
return _jsx(EditPassword, { onSuccess: onPasswordUpdate });
|
|
64
|
+
default:
|
|
65
|
+
return null;
|
|
66
|
+
}
|
|
67
|
+
};
|
|
68
|
+
return (_jsxs("div", { className: `max-w-5xl mx-auto ${containerClassName}`, children: [_jsxs("div", { className: "mb-6", children: [_jsx(Typography, { type: "heading", size: "h4", className: "text-gray-900", children: "Account Settings" }), _jsx(Typography, { type: "body", size: "medium", className: "text-gray-500 mt-1", children: "Manage your profile and preferences" })] }), _jsxs("div", { className: "grid grid-cols-1 lg:grid-cols-12 gap-6", children: [_jsxs("div", { className: "lg:col-span-4 space-y-4", children: [_jsx("div", { className: "bg-white rounded-2xl border border-gray-100 p-5 shadow-sm", children: _jsxs(Flex, { align: "center", className: "gap-4", children: [_jsxs("div", { className: "relative", children: [_jsx(AvatarWithInitials, { size: "large", name: fullName || 'User', url: profileData?.image, className: "w-16 h-16 text-xl" }), _jsx("div", { className: "absolute -bottom-1 -right-1 w-5 h-5 bg-green-500 rounded-full border-2 border-white" })] }), _jsxs("div", { className: "flex-1 min-w-0", children: [_jsx(Typography, { type: "body", size: "large", className: "font-semibold text-gray-900 truncate", children: fullName || 'Welcome' }), _jsx(Typography, { type: "body", size: "small", className: "text-gray-500 truncate", children: profile?.email })] })] }) }), _jsx("div", { className: "bg-white rounded-2xl border border-gray-100 overflow-hidden shadow-sm", children: _jsx("nav", { className: "p-2", children: tabs.map((tab) => (_jsxs("button", { onClick: () => handleTabChange(tab.key), className: `
|
|
69
|
+
w-full flex items-center gap-3 px-4 py-3 rounded-xl text-left transition-all duration-200
|
|
70
|
+
${activeTab === tab.key
|
|
71
|
+
? 'bg-primary/10'
|
|
72
|
+
: 'hover:bg-gray-50'}
|
|
73
|
+
`, children: [_jsx("div", { className: `
|
|
74
|
+
w-9 h-9 rounded-lg flex items-center justify-center transition-colors
|
|
75
|
+
${activeTab === tab.key
|
|
76
|
+
? 'bg-primary text-white'
|
|
77
|
+
: 'bg-gray-100 text-gray-500'}
|
|
78
|
+
`, children: tab.icon }), _jsx("div", { className: "flex-1 min-w-0", children: _jsx(Typography, { type: "body", size: "medium", className: `font-medium ${activeTab === tab.key ? 'text-primary' : 'text-gray-900'}`, children: tab.label }) }), _jsx(ChevronRight, { className: `w-4 h-4 transition-colors ${activeTab === tab.key ? 'text-primary' : 'text-gray-300'}` })] }, tab.key))) }) })] }), _jsx("div", { className: "lg:col-span-8", children: _jsx("div", { className: "bg-white rounded-2xl border border-gray-100 shadow-sm overflow-hidden", children: _jsx("div", { className: "p-6 lg:p-8", children: renderTabContent() }) }) })] }), _jsx(ResponsiveModal, { title: "", size: "sm", open: showUnsavedModal, footer: null, showCloseButton: false, onClose: () => setShowUnsavedModal(false), onOpenChange: () => setShowUnsavedModal(false), children: _jsxs(Flex, { direction: "column", align: "center", className: "py-6", children: [_jsx("div", { className: "w-16 h-16 rounded-full bg-amber-100 flex items-center justify-center mb-4", children: _jsx("svg", { className: "w-8 h-8 text-amber-500", fill: "none", viewBox: "0 0 24 24", stroke: "currentColor", children: _jsx("path", { strokeLinecap: "round", strokeLinejoin: "round", strokeWidth: 2, d: "M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-3L13.732 4c-.77-1.333-2.694-1.333-3.464 0L3.34 16c-.77 1.333.192 3 1.732 3z" }) }) }), _jsx(Typography, { type: "heading", size: "h6", className: "mb-2 text-gray-900", children: "Unsaved Changes" }), _jsx(Typography, { type: "body", size: "medium", className: "text-gray-500 text-center mb-6", children: "You have unsaved changes. Are you sure you want to leave?" }), _jsxs(Flex, { className: "gap-3 w-full", children: [_jsx(Button, { isFullWidth: true, variant: "outline", onClick: () => setShowUnsavedModal(false), children: "Stay" }), _jsx(Button, { isFullWidth: true, onClick: handleConfirmLeave, children: "Leave" })] })] }) })] }));
|
|
79
|
+
};
|
|
80
|
+
export { ProfileWebComponent };
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
// Native implementation - Profile is typically handled differently on mobile
|
|
3
|
+
const ProfileWebComponent = () => {
|
|
4
|
+
if (process.env.NODE_ENV !== 'production') {
|
|
5
|
+
console.warn('[@truworth/twc-auth] ProfileWebComponent is only available for web');
|
|
6
|
+
}
|
|
7
|
+
return null;
|
|
8
|
+
};
|
|
9
|
+
export { ProfileWebComponent };
|
|
@@ -0,0 +1,185 @@
|
|
|
1
|
+
import { useState, useEffect } from 'react';
|
|
2
|
+
import { useAuthContext } from '../../../../hooks/useAuthContext';
|
|
3
|
+
import { useRequest } from '../../../../hooks/useRequest';
|
|
4
|
+
import { showMessage } from '../../../../helpers/show-message';
|
|
5
|
+
import { parseISO } from 'date-fns';
|
|
6
|
+
const useProfile = () => {
|
|
7
|
+
const { profile, getUserProfile } = useAuthContext();
|
|
8
|
+
const { makeRequest } = useRequest();
|
|
9
|
+
const [loading, setLoading] = useState(false);
|
|
10
|
+
const [imageUrl, setImageUrl] = useState('');
|
|
11
|
+
const [profileData, setProfileData] = useState({
|
|
12
|
+
firstName: '',
|
|
13
|
+
lastName: '',
|
|
14
|
+
dateOfBirth: new Date(),
|
|
15
|
+
gender: '',
|
|
16
|
+
phone: '',
|
|
17
|
+
bio: '',
|
|
18
|
+
image: '',
|
|
19
|
+
countryCode: '91'
|
|
20
|
+
});
|
|
21
|
+
useEffect(() => {
|
|
22
|
+
if (profile) {
|
|
23
|
+
const dob = profile.dateOfBirth && typeof profile.dateOfBirth === 'string'
|
|
24
|
+
? parseISO(profile.dateOfBirth)
|
|
25
|
+
: profile.dateOfBirth || new Date();
|
|
26
|
+
setProfileData({
|
|
27
|
+
firstName: profile.firstName || '',
|
|
28
|
+
lastName: profile.lastName || '',
|
|
29
|
+
gender: profile.gender || '',
|
|
30
|
+
dateOfBirth: dob,
|
|
31
|
+
image: profile.image || '',
|
|
32
|
+
phone: profile.phone || '',
|
|
33
|
+
bio: profile.bio || '',
|
|
34
|
+
countryCode: profile.countryCode || '91'
|
|
35
|
+
});
|
|
36
|
+
if (profile.image) {
|
|
37
|
+
setImageUrl(profile.image);
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
}, [profile]);
|
|
41
|
+
const updateProfile = async (data, onSuccess) => {
|
|
42
|
+
setLoading(true);
|
|
43
|
+
try {
|
|
44
|
+
await makeRequest({
|
|
45
|
+
url: '/user/profile',
|
|
46
|
+
method: 'POST',
|
|
47
|
+
body: data,
|
|
48
|
+
onSuccess: () => {
|
|
49
|
+
showMessage({ message: 'Your profile information is updated.' });
|
|
50
|
+
getUserProfile?.();
|
|
51
|
+
onSuccess?.();
|
|
52
|
+
},
|
|
53
|
+
onFailure: () => {
|
|
54
|
+
showMessage({ message: 'Failed to update profile.' });
|
|
55
|
+
}
|
|
56
|
+
});
|
|
57
|
+
}
|
|
58
|
+
finally {
|
|
59
|
+
setLoading(false);
|
|
60
|
+
}
|
|
61
|
+
};
|
|
62
|
+
const uploadProfileImage = async (file) => {
|
|
63
|
+
const fileData = new FormData();
|
|
64
|
+
fileData.append('file', file);
|
|
65
|
+
try {
|
|
66
|
+
const response = await makeRequest({
|
|
67
|
+
url: '/upload/profile-picture',
|
|
68
|
+
method: 'POST',
|
|
69
|
+
body: fileData,
|
|
70
|
+
headers: {
|
|
71
|
+
'Content-Type': 'multipart/form-data'
|
|
72
|
+
}
|
|
73
|
+
});
|
|
74
|
+
return response?.fileName || null;
|
|
75
|
+
}
|
|
76
|
+
catch {
|
|
77
|
+
showMessage({ message: 'Failed to upload image.' });
|
|
78
|
+
return null;
|
|
79
|
+
}
|
|
80
|
+
};
|
|
81
|
+
const updateMobileNumber = async (data, onSuccess, onFailure) => {
|
|
82
|
+
setLoading(true);
|
|
83
|
+
try {
|
|
84
|
+
await makeRequest({
|
|
85
|
+
url: '/user/mobile-number',
|
|
86
|
+
method: 'POST',
|
|
87
|
+
body: data,
|
|
88
|
+
onSuccess: () => {
|
|
89
|
+
onSuccess?.();
|
|
90
|
+
},
|
|
91
|
+
onFailure: () => {
|
|
92
|
+
showMessage({ message: 'Failed to update mobile number.' });
|
|
93
|
+
onFailure?.();
|
|
94
|
+
}
|
|
95
|
+
});
|
|
96
|
+
}
|
|
97
|
+
finally {
|
|
98
|
+
setLoading(false);
|
|
99
|
+
}
|
|
100
|
+
};
|
|
101
|
+
const sendMobileOtp = async (onSuccess, onFailure) => {
|
|
102
|
+
try {
|
|
103
|
+
await makeRequest({
|
|
104
|
+
url: '/auth/mobile/send-otp',
|
|
105
|
+
method: 'POST',
|
|
106
|
+
onSuccess: () => onSuccess?.(),
|
|
107
|
+
onFailure: () => onFailure?.()
|
|
108
|
+
});
|
|
109
|
+
}
|
|
110
|
+
catch {
|
|
111
|
+
onFailure?.();
|
|
112
|
+
}
|
|
113
|
+
};
|
|
114
|
+
const verifyMobileOtp = async (otp, onSuccess, onFailure) => {
|
|
115
|
+
setLoading(true);
|
|
116
|
+
try {
|
|
117
|
+
await makeRequest({
|
|
118
|
+
url: '/auth/mobile/verify-otp',
|
|
119
|
+
method: 'POST',
|
|
120
|
+
body: { otp },
|
|
121
|
+
onSuccess: () => {
|
|
122
|
+
showMessage({ message: 'Mobile number updated successfully.' });
|
|
123
|
+
getUserProfile?.();
|
|
124
|
+
onSuccess?.();
|
|
125
|
+
},
|
|
126
|
+
onFailure: () => {
|
|
127
|
+
onFailure?.();
|
|
128
|
+
}
|
|
129
|
+
});
|
|
130
|
+
}
|
|
131
|
+
finally {
|
|
132
|
+
setLoading(false);
|
|
133
|
+
}
|
|
134
|
+
};
|
|
135
|
+
const changePassword = async (data, onSuccess, onFailure) => {
|
|
136
|
+
setLoading(true);
|
|
137
|
+
try {
|
|
138
|
+
// First, validate current password
|
|
139
|
+
const initResponse = await makeRequest({
|
|
140
|
+
url: '/auth/change-password/initialize-session',
|
|
141
|
+
method: 'POST',
|
|
142
|
+
body: { password: data.currentPassword }
|
|
143
|
+
});
|
|
144
|
+
if (initResponse?.token) {
|
|
145
|
+
// Then change the password
|
|
146
|
+
await makeRequest({
|
|
147
|
+
url: '/auth/change-password',
|
|
148
|
+
method: 'POST',
|
|
149
|
+
body: { password: data.newPassword, token: initResponse.token },
|
|
150
|
+
onSuccess: () => {
|
|
151
|
+
showMessage({ message: 'Your password has been updated.' });
|
|
152
|
+
onSuccess?.();
|
|
153
|
+
},
|
|
154
|
+
onFailure: () => {
|
|
155
|
+
showMessage({ message: 'Failed to change password.' });
|
|
156
|
+
onFailure?.();
|
|
157
|
+
}
|
|
158
|
+
});
|
|
159
|
+
}
|
|
160
|
+
else {
|
|
161
|
+
onFailure?.('Invalid current password');
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
catch {
|
|
165
|
+
onFailure?.('Current password is incorrect');
|
|
166
|
+
}
|
|
167
|
+
finally {
|
|
168
|
+
setLoading(false);
|
|
169
|
+
}
|
|
170
|
+
};
|
|
171
|
+
return {
|
|
172
|
+
loading,
|
|
173
|
+
profile,
|
|
174
|
+
profileData,
|
|
175
|
+
imageUrl,
|
|
176
|
+
setImageUrl,
|
|
177
|
+
updateProfile,
|
|
178
|
+
uploadProfileImage,
|
|
179
|
+
updateMobileNumber,
|
|
180
|
+
sendMobileOtp,
|
|
181
|
+
verifyMobileOtp,
|
|
182
|
+
changePassword
|
|
183
|
+
};
|
|
184
|
+
};
|
|
185
|
+
export { useProfile };
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
export { ProfileWebComponent } from './components/ProfileWebComponent';
|
|
2
|
+
export { EditProfile } from './components/EditProfile';
|
|
3
|
+
export { EditMobileNumber } from './components/EditMobileNumber';
|
|
4
|
+
export { EditPassword } from './components/EditPassword';
|
|
5
|
+
export { OtpVerificationModal } from './components/OtpVerificationModal';
|
|
6
|
+
export { useProfile } from './hooks/internal/useProfile';
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
export { ProfileWebComponent } from './components/ProfileWebComponent/index.native';
|
|
2
|
+
export { EditProfile } from './components/EditProfile/index.native';
|
|
3
|
+
export { EditMobileNumber } from './components/EditMobileNumber/index.native';
|
|
4
|
+
export { EditPassword } from './components/EditPassword/index.native';
|
|
5
|
+
export { OtpVerificationModal } from './components/OtpVerificationModal/index.native';
|
|
6
|
+
export { useProfile } from './hooks/internal/useProfile';
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -3,25 +3,40 @@ import { WebView } from '@truworth/twc-rn-common';
|
|
|
3
3
|
import { Layout } from '@ui-kitten/components';
|
|
4
4
|
import { Text } from 'react-native';
|
|
5
5
|
import parse from 'url-parse';
|
|
6
|
-
import { useState } from 'react';
|
|
6
|
+
import { useState, useCallback } from 'react';
|
|
7
|
+
const SAML_CALLBACK_PATHS = ['/saml', 'thewellnesscorner.com/saml'];
|
|
7
8
|
const SSOAuthWebView = ({ navigation, route }) => {
|
|
8
|
-
const { clientId, authenticationUrl, redirectUri } = route.params;
|
|
9
|
+
const { clientId, authenticationUrl, redirectUri, authMethod } = route.params;
|
|
9
10
|
const [error, setError] = useState(null);
|
|
11
|
+
const handleNavigationStateChange = useCallback((navState) => {
|
|
12
|
+
setError(null);
|
|
13
|
+
const parsedUrl = parse(navState.url, true);
|
|
14
|
+
const currentUrl = parsedUrl.href;
|
|
15
|
+
const authUrl = parse(authenticationUrl, true).href;
|
|
16
|
+
if (currentUrl === authUrl)
|
|
17
|
+
return;
|
|
18
|
+
if (authMethod === 'saml') {
|
|
19
|
+
const isSamlCallback = SAML_CALLBACK_PATHS.some(path => navState.url.includes(path));
|
|
20
|
+
if (isSamlCallback) {
|
|
21
|
+
const { code } = parsedUrl.query;
|
|
22
|
+
if (code) {
|
|
23
|
+
navigation.replace('SSOCallback', { clientId, code: code, authMethod: 'saml' });
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
else {
|
|
28
|
+
if (redirectUri && navState.url.includes(redirectUri)) {
|
|
29
|
+
const { code } = parsedUrl.query;
|
|
30
|
+
navigation.replace('SSOCallback', { clientId, code: code, authMethod: 'oidc' });
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
}, [authenticationUrl, authMethod, clientId, navigation, redirectUri]);
|
|
10
34
|
return (_jsxs(Layout, { style: { flex: 1 }, children: [error && (_jsx(Layout, { style: { padding: 16, backgroundColor: '#fee' }, children: _jsx(Text, { style: { color: 'red' }, children: error }) })), _jsx(WebView, { source: { uri: authenticationUrl }, style: { flex: 1 }, onError: (syntheticEvent) => {
|
|
11
35
|
const { nativeEvent } = syntheticEvent;
|
|
12
36
|
setError(`Failed to load authentication page: ${nativeEvent.description}`);
|
|
13
37
|
}, onHttpError: (syntheticEvent) => {
|
|
14
38
|
const { nativeEvent } = syntheticEvent;
|
|
15
39
|
setError(`Authentication provider error: ${nativeEvent.statusCode}`);
|
|
16
|
-
}, onNavigationStateChange:
|
|
17
|
-
setError(null);
|
|
18
|
-
const currentUrl = parse(navState.url, true).href;
|
|
19
|
-
const authUrl = parse(authenticationUrl, true).href;
|
|
20
|
-
if (currentUrl != authUrl && navState.url.includes(redirectUri)) {
|
|
21
|
-
const query = parse(navState.url, true).query;
|
|
22
|
-
const { code } = query;
|
|
23
|
-
navigation.replace('SSOCallback', { clientId, code });
|
|
24
|
-
}
|
|
25
|
-
} })] }));
|
|
40
|
+
}, onNavigationStateChange: handleNavigationStateChange })] }));
|
|
26
41
|
};
|
|
27
42
|
export default SSOAuthWebView;
|
|
@@ -17,8 +17,9 @@ const useSSOAuthenticationMethods = () => {
|
|
|
17
17
|
}).then((res) => {
|
|
18
18
|
if (!isMountedRef.current)
|
|
19
19
|
return;
|
|
20
|
-
const { authenticationUrl, redirectUri } = res.data;
|
|
21
|
-
|
|
20
|
+
const { authenticationUrl, redirectUri, authMethod } = res.data;
|
|
21
|
+
const resolvedAuthMethod = authMethod === 'saml' ? 'saml' : 'oidc';
|
|
22
|
+
onSSOLoginInitiated({ clientId, authenticationUrl, redirectUri, authMethod: resolvedAuthMethod });
|
|
22
23
|
}).catch((error) => {
|
|
23
24
|
if (!isMountedRef.current)
|
|
24
25
|
return;
|
|
@@ -29,6 +29,7 @@ const SSOAuthenticationMethods = ({ client, onPressBack, handleMobileLogin }) =>
|
|
|
29
29
|
const onSSOLoginInitiated = (result) => {
|
|
30
30
|
try {
|
|
31
31
|
localStorage.setItem('clientId', String(result.clientId));
|
|
32
|
+
localStorage.setItem('authMethod', result.authMethod);
|
|
32
33
|
const u = new URL(result.authenticationUrl);
|
|
33
34
|
window.location.href = u.toString();
|
|
34
35
|
}
|
|
@@ -43,7 +43,12 @@ const SSOAuthenticationMethods = ({ route }) => {
|
|
|
43
43
|
return (_jsx(View, { style: { marginTop: 70, alignSelf: 'center' }, children: _jsx(Logo, {}) }));
|
|
44
44
|
};
|
|
45
45
|
const onSSOLoginInitiated = (result) => {
|
|
46
|
-
navigation.navigate('SSOAuthWebView', {
|
|
46
|
+
navigation.navigate('SSOAuthWebView', {
|
|
47
|
+
clientId: result.clientId,
|
|
48
|
+
authenticationUrl: result.authenticationUrl,
|
|
49
|
+
redirectUri: result.redirectUri,
|
|
50
|
+
authMethod: result.authMethod,
|
|
51
|
+
});
|
|
47
52
|
};
|
|
48
53
|
return (_jsx(ScreenLayout, { title: "", containerStyle: { flex: 1, backgroundColor: primary.white }, children: _jsxs(View, { style: { flex: 1, justifyContent: 'center' }, children: [client.image ?
|
|
49
54
|
_jsx(FastImage, { source: { uri: client.image }, resizeMode: 'contain', style: { width: 180, aspectRatio: aspectRatio, alignSelf: 'center', marginTop: -150 } })
|