@truworth/twc-auth 2.0.0 → 3.0.1
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/components/ConfirmationModal/index.js +2 -2
- package/build/src/constants/password-requirements.js +7 -0
- package/build/src/index.js +4 -0
- package/build/src/screens/EnterMobile/components/ExistingAccountsSheet/index.js +1 -1
- 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/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/constants/password-requirements.d.ts +4 -0
- package/build/types/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 +2 -2
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
2
|
import { IonIcon } from '../../components/IonIcon';
|
|
3
3
|
import { Button, Flex, ResponsiveModal, Typography } from '@truworth/twc-web-design';
|
|
4
|
-
const ConfirmationModal = ({ title, visible, onClose, onProceed, onDismiss, primaryLabel, secondaryLabel, iconName = "information-circle-outline",
|
|
5
|
-
return (_jsx(ResponsiveModal, { title: "", open: visible, onClose: onClose, footer: null, showCloseButton: false, className: 'py-0', children: _jsxs(Flex, { direction: "column", align: "center", justify: "center", children: [_jsxs(Flex, { direction: "column", align: "center", justify: "center", children: [_jsx(IonIcon, { name: iconName,
|
|
4
|
+
const ConfirmationModal = ({ title, visible, onClose, onProceed, onDismiss, primaryLabel, secondaryLabel, iconName = "information-circle-outline", iconClassName = "text-utility-warning-main", description }) => {
|
|
5
|
+
return (_jsx(ResponsiveModal, { title: "", open: visible, onClose: onClose, footer: null, showCloseButton: false, className: 'py-0', children: _jsxs(Flex, { direction: "column", align: "center", justify: "center", children: [_jsxs(Flex, { direction: "column", align: "center", justify: "center", children: [_jsx(IonIcon, { name: iconName, className: `text-[96px] ${iconClassName}` }), title &&
|
|
6
6
|
_jsx(Typography, { type: "heading", size: "h6", className: "mt-2 text-center", children: title }), description &&
|
|
7
7
|
_jsx(Typography, { type: "utility", size: "medium", color: 'gray-600', className: "mt-2 mb-6 text-center", children: description })] }), primaryLabel &&
|
|
8
8
|
_jsx(Button, { onClick: () => onProceed?.(), label: primaryLabel, className: 'w-full my-3' }), secondaryLabel &&
|
|
@@ -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,7 @@ 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 profile components
|
|
18
|
+
export * from './screens/Profile';
|
|
19
|
+
// export constants
|
|
20
|
+
export { strongPasswordRequirements } from './constants/password-requirements';
|
|
@@ -23,7 +23,7 @@ const ExistingAccountsSheet = ({ visible, hide, phone, existingAccounts, onIniti
|
|
|
23
23
|
};
|
|
24
24
|
const displayPhone = phone ? `+91${phone.replace(/^.{8}/, 'XXXXXXXX')}` : '+91**********';
|
|
25
25
|
return (_jsxs(ResponsiveModal, { open: visible, onClose: hide, onOpenChange: hide, maskClosable: false, title: _jsxs(_Fragment, { children: [_jsx(Typography, { type: "heading", size: "h6", children: "Multiple Accounts Found" }), _jsxs(Typography, { type: "utility", size: "medium", color: "gray-600", className: "mt-2 mb-3", children: ["The phone number ", displayPhone, " is linked to the following accounts."] }), _jsx("hr", {})] }), centered: true, showCloseButton: false, className: "px-0", footer: _jsxs(Flex, { direction: "column", className: 'pt-1', children: [_jsx(Flex, { className: "flex-1 border-t border-gray-300 mb-2" }), selectedAccount &&
|
|
26
|
-
_jsxs(Flex, { align: 'center', className: 'bg-utility-warning-bg p-2 rounded-lg gap-1', children: [_jsx(IonIcon, { name: "information-circle-outline",
|
|
26
|
+
_jsxs(Flex, { align: 'center', className: 'bg-utility-warning-bg p-2 rounded-lg gap-1', children: [_jsx(IonIcon, { name: "information-circle-outline", className: "text-utility-warning-main text-2xl" }), _jsxs(Typography, { type: "utility", size: "small", children: [_jsx("span", { className: "text-gray-500 text-[12px]", children: "Once you proceed, this phone number will be removed" }), " from other accounts."] })] }), _jsx(Button, { isFullWidth: true, loading: loading, variant: "primary", label: "Link and Continue", className: 'mt-4 mb-[-16px]', onClick: onContinue, disabled: !selectedAccount }), _jsx(SupportDetails, {})] }), children: [_jsx(Typography, { type: "utility", size: "medium", className: "mb-4", children: "Select account you want to continue with." }), existingAccounts.map((item) => {
|
|
27
27
|
return (_jsx(OptionCard, { item: item, onClick: () => setSelectedAccount(item), selectedItem: selectedAccount }, item.memberId));
|
|
28
28
|
})] }));
|
|
29
29
|
};
|
|
@@ -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 };
|
|
@@ -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 {};
|
|
@@ -23,6 +23,6 @@ const Welcome = ({ onClickEnterEmail }) => {
|
|
|
23
23
|
// Fallback to default logo
|
|
24
24
|
return (_jsx("img", { width: 160, height: 35, className: 'md:mt-0 mt-6 md:h-[40px] h-[30px]', src: `${CDN_IMAGES_URL}/new-twc_logo.svg` }));
|
|
25
25
|
};
|
|
26
|
-
return (_jsx(AdvancedTransitionWrapper, { type: 'slide', duration: 0.3, children: _jsxs(Flex, { justify: 'center', align: "center", direction: 'column', className: 'h-screen w-[80%] mx-auto gap-6', children: [renderLogo(), _jsxs(Flex, { justify: "center", align: "center", direction: "column", className: 'gap-2', children: [_jsx(Typography, { type: 'heading', size: 'h5', className: 'text-center text-
|
|
26
|
+
return (_jsx(AdvancedTransitionWrapper, { type: 'slide', duration: 0.3, children: _jsxs(Flex, { justify: 'center', align: "center", direction: 'column', className: 'h-screen w-[80%] mx-auto gap-6', children: [renderLogo(), _jsxs(Flex, { justify: "center", align: "center", direction: "column", className: 'gap-2', children: [_jsx(Typography, { type: 'heading', size: 'h5', className: 'text-center text-gray-900 font-semibold', children: "Welcome!" }), _jsx(Typography, { type: 'utility', size: 'large', color: "gray-400", className: 'text-center font-semibold', children: "Select an option to proceed" })] }), _jsx(Button, { label: "Enter Your Email", variant: "secondary", onClick: onClickEnterEmail, leftIcon: _jsx(Mail, { size: 20 }), isFullWidth: true })] }) }));
|
|
27
27
|
};
|
|
28
28
|
export default Welcome;
|
|
@@ -1,3 +1,3 @@
|
|
|
1
1
|
import type { ConfirmationModalProps } from './types';
|
|
2
|
-
declare const ConfirmationModal: ({ title, visible, onClose, onProceed, onDismiss, primaryLabel, secondaryLabel, iconName,
|
|
2
|
+
declare const ConfirmationModal: ({ title, visible, onClose, onProceed, onDismiss, primaryLabel, secondaryLabel, iconName, iconClassName, description }: ConfirmationModalProps) => import("react/jsx-runtime").JSX.Element;
|
|
3
3
|
export { ConfirmationModal };
|
|
@@ -4,7 +4,10 @@ type ConfirmationModalProps = {
|
|
|
4
4
|
primaryLabel: string;
|
|
5
5
|
secondaryLabel?: string;
|
|
6
6
|
iconName?: string;
|
|
7
|
+
/** @deprecated Use iconClassName instead for Tailwind classes */
|
|
7
8
|
iconColor?: string;
|
|
9
|
+
/** Tailwind classes for icon styling, e.g. "text-primary" or "text-utility-warning-main" */
|
|
10
|
+
iconClassName?: string;
|
|
8
11
|
onClose: () => void;
|
|
9
12
|
onProceed?: () => void;
|
|
10
13
|
onDismiss?: () => void;
|
package/build/types/index.d.ts
CHANGED
|
@@ -9,3 +9,5 @@ export * from './navigator';
|
|
|
9
9
|
export * from './screens/Login/components/LoginWebComponent/index';
|
|
10
10
|
export * from './screens/SignUp/components/SignUpWebComponent/index';
|
|
11
11
|
export * from './screens/SSOLogin/SSOCallback/components/SSOCallbackComponent/index';
|
|
12
|
+
export * from './screens/Profile';
|
|
13
|
+
export { strongPasswordRequirements } from './constants/password-requirements';
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import type { ProfileData } from '../../types';
|
|
2
|
+
declare const useProfile: () => {
|
|
3
|
+
loading: boolean;
|
|
4
|
+
profile: import("../../../..").MemberProfile | null;
|
|
5
|
+
profileData: ProfileData;
|
|
6
|
+
imageUrl: string;
|
|
7
|
+
setImageUrl: import("react").Dispatch<import("react").SetStateAction<string>>;
|
|
8
|
+
updateProfile: (data: {
|
|
9
|
+
firstName: string;
|
|
10
|
+
lastName: string;
|
|
11
|
+
bio?: string;
|
|
12
|
+
image?: string;
|
|
13
|
+
}, onSuccess?: () => void) => Promise<void>;
|
|
14
|
+
uploadProfileImage: (file: File) => Promise<string | null>;
|
|
15
|
+
updateMobileNumber: (data: {
|
|
16
|
+
countryCode: string;
|
|
17
|
+
mobileNo: string;
|
|
18
|
+
}, onSuccess?: () => void, onFailure?: () => void) => Promise<void>;
|
|
19
|
+
sendMobileOtp: (onSuccess?: () => void, onFailure?: () => void) => Promise<void>;
|
|
20
|
+
verifyMobileOtp: (otp: string, onSuccess?: () => void, onFailure?: () => void) => Promise<void>;
|
|
21
|
+
changePassword: (data: {
|
|
22
|
+
currentPassword: string;
|
|
23
|
+
newPassword: string;
|
|
24
|
+
}, onSuccess?: () => void, onFailure?: (error?: string) => void) => Promise<void>;
|
|
25
|
+
};
|
|
26
|
+
export { useProfile };
|
|
@@ -0,0 +1,7 @@
|
|
|
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';
|
|
7
|
+
export type * from './types';
|
|
@@ -0,0 +1,7 @@
|
|
|
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';
|
|
7
|
+
export type * from './types';
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import type React from 'react';
|
|
2
|
+
interface ProfileData {
|
|
3
|
+
firstName: string;
|
|
4
|
+
lastName: string;
|
|
5
|
+
dateOfBirth: Date | string;
|
|
6
|
+
gender: string;
|
|
7
|
+
bio?: string;
|
|
8
|
+
image?: string;
|
|
9
|
+
phone?: string;
|
|
10
|
+
countryCode?: string;
|
|
11
|
+
}
|
|
12
|
+
interface EditProfileProps {
|
|
13
|
+
profileData: ProfileData;
|
|
14
|
+
imageUrl?: string;
|
|
15
|
+
setImageUrl: React.Dispatch<React.SetStateAction<string>>;
|
|
16
|
+
editProfileForm: any;
|
|
17
|
+
onSuccess?: () => void;
|
|
18
|
+
}
|
|
19
|
+
interface EditMobileNumberProps {
|
|
20
|
+
countryCode?: string;
|
|
21
|
+
editPhoneForm: any;
|
|
22
|
+
onSuccess?: () => void;
|
|
23
|
+
}
|
|
24
|
+
interface EditPasswordProps {
|
|
25
|
+
editPasswordForm: any;
|
|
26
|
+
onSuccess?: () => void;
|
|
27
|
+
}
|
|
28
|
+
interface OtpVerificationModalProps {
|
|
29
|
+
open: boolean;
|
|
30
|
+
onOpenChange: (open: boolean) => void;
|
|
31
|
+
phone: string;
|
|
32
|
+
countryCode?: string;
|
|
33
|
+
onSuccess?: () => void;
|
|
34
|
+
successMessage?: string;
|
|
35
|
+
title?: string | React.ReactNode;
|
|
36
|
+
}
|
|
37
|
+
interface ProfileWebComponentProps {
|
|
38
|
+
/** Optional header component to wrap the profile content */
|
|
39
|
+
HeaderComponent?: React.ComponentType<{
|
|
40
|
+
children: React.ReactNode;
|
|
41
|
+
}>;
|
|
42
|
+
/** Optional footer component */
|
|
43
|
+
FooterComponent?: React.ComponentType;
|
|
44
|
+
/** Callback when profile is updated successfully */
|
|
45
|
+
onProfileUpdate?: () => void;
|
|
46
|
+
/** Callback when mobile number is updated successfully */
|
|
47
|
+
onMobileUpdate?: () => void;
|
|
48
|
+
/** Callback when password is updated successfully */
|
|
49
|
+
onPasswordUpdate?: () => void;
|
|
50
|
+
/** Custom container className */
|
|
51
|
+
containerClassName?: string;
|
|
52
|
+
}
|
|
53
|
+
export type { ProfileData, EditProfileProps, EditMobileNumberProps, EditPasswordProps, OtpVerificationModalProps, ProfileWebComponentProps };
|
package/package.json
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
"access": "public"
|
|
5
5
|
},
|
|
6
6
|
"description": "Truworth Auth Package for React Native and Web",
|
|
7
|
-
"version": "
|
|
7
|
+
"version": "3.0.1",
|
|
8
8
|
"main": "build/src/index.js",
|
|
9
9
|
"types": "build/types/index.d.ts",
|
|
10
10
|
"files": [
|
|
@@ -27,7 +27,7 @@
|
|
|
27
27
|
"@react-navigation/native": "6.1.17",
|
|
28
28
|
"@react-navigation/native-stack": "6.10.0",
|
|
29
29
|
"@truworth/twc-rn-common": "1.1.2",
|
|
30
|
-
"@truworth/twc-web-design": "
|
|
30
|
+
"@truworth/twc-web-design": "2.0.0",
|
|
31
31
|
"@twotalltotems/react-native-otp-input": "1.3.11",
|
|
32
32
|
"@types/crypto-js": "^4.2.2",
|
|
33
33
|
"@types/fbemitter": "^2.0.35",
|