Commit a15facb5 by Arjun Jhukal

updated the new functionality for acuity tech and removed age gate

parent ad8df29f
{
"postman.settings.dotenv-detection-notification-visibility": false
}
...@@ -3,9 +3,9 @@ ...@@ -3,9 +3,9 @@
"use client" "use client"
import GlassWrapper from '@/components/molecules/GlassWrapper' import GlassWrapper from '@/components/molecules/GlassWrapper'
import { useAppDispatch } from '@/hooks/hook' import { useAppDispatch, useAppSelector } from '@/hooks/hook'
import { setTokens } from '@/slice/authSlice' import { setTokens } from '@/slice/authSlice'
import { restoreAuthFromCookies } from '@/utils/authSession' import { clearAuthBackup, restoreAuthFromCookies } from '@/utils/authSession'
import Image from 'next/image' import Image from 'next/image'
import Link from 'next/link' import Link from 'next/link'
import { useParams } from 'next/navigation' import { useParams } from 'next/navigation'
...@@ -15,32 +15,68 @@ export default function PaymentSuccess() { ...@@ -15,32 +15,68 @@ export default function PaymentSuccess() {
const params = useParams(); const params = useParams();
const slug = params?.slug as string; const slug = params?.slug as string;
const [isReady, setIsReady] = useState(false); const [isReady, setIsReady] = useState(false);
const [error, setError] = useState<string | null>(null);
const dispatch = useAppDispatch(); const dispatch = useAppDispatch();
const user = useAppSelector((state) => state.auth.user);
useEffect(() => { useEffect(() => {
// Restore auth from cookies to localStorage const hydrateAuthOnSuccess = async () => {
const wasRestored = restoreAuthFromCookies(); try {
console.log('[PaymentSuccess] Starting auth hydration...');
if (wasRestored) { // Try to restore auth from backup
// Manually hydrate Redux from localStorage (no reload needed) const result = restoreAuthFromCookies();
const userStr = localStorage.getItem('user');
const token = localStorage.getItem('access_token');
if (userStr && token) { if (result.success && result.data) {
try { console.log(`[PaymentSuccess] Auth restored from ${result.source}`);
const user = JSON.parse(userStr);
// Update Redux store
dispatch(setTokens({ dispatch(setTokens({
access_token: token, access_token: result.data.access_token,
user: user user: result.data.user,
refreshToken: result.data.refresh_token
}));
// Dispatch custom event for other components
window.dispatchEvent(new CustomEvent('auth:hydrated', {
detail: {
source: result.source,
user: result.data.user
}
})); }));
} catch (e) {
console.error('Failed to parse user data:', e); console.log('[PaymentSuccess] Auth hydrated successfully');
setIsReady(true);
} else {
// Check if user is still available in Redux store
if (user) {
console.log('[PaymentSuccess] User still available in Redux');
setIsReady(true);
} else {
// Auth not found in any source - this is a problem
console.warn('[PaymentSuccess] Auth not found in any source');
setError('Unable to restore user session. Please try logging in again.');
setIsReady(true);
}
} }
} catch (error) {
console.error('[PaymentSuccess] Hydration error:', error);
setError('An unexpected error occurred. Please try again shortly.');
setIsReady(true);
} }
} };
hydrateAuthOnSuccess();
setIsReady(true); // Cleanup - clear backups after 10 seconds to ensure restoration is complete
}, [dispatch]); const cleanupTimer = setTimeout(() => {
clearAuthBackup();
}, 10000);
return () => {
clearTimeout(cleanupTimer);
};
}, [dispatch, user]);
if (!isReady) { if (!isReady) {
return ( return (
...@@ -50,6 +86,33 @@ export default function PaymentSuccess() { ...@@ -50,6 +86,33 @@ export default function PaymentSuccess() {
); );
} }
// Show error state if no user
if (error || !user) {
return (
<GlassWrapper className="max-w-[520px] mx-auto flex flex-col gap-3 items-center text-center p-6">
<Image
src="/assets/images/verify-email.png"
alt="Error"
width={180}
height={140}
/>
<h1 className="text-[24px] lg:text-[32px] leading-[120%] font-bold mb-4 text-red-500">
Session Expired
</h1>
<p className="text-[14px] leading-[150%] font-normal lg:text-[16px] mb-4">
{error || 'Your session has expired. Please log in again to continue.'}
</p>
<Link
href="/login"
className="ss-btn bg-primary-grad"
>
Log In Again
</Link>
</GlassWrapper>
);
}
// Success state
return ( return (
<GlassWrapper className="max-w-[520px] mx-auto flex flex-col gap-3 items-center text-center p-6"> <GlassWrapper className="max-w-[520px] mx-auto flex flex-col gap-3 items-center text-center p-6">
<Image <Image
...@@ -59,10 +122,10 @@ export default function PaymentSuccess() { ...@@ -59,10 +122,10 @@ export default function PaymentSuccess() {
height={140} height={140}
/> />
<h1 className="text-[24px] lg:text-[32px] leading-[120%] font-bold mb-4 text-green-500"> <h1 className="text-[24px] lg:text-[32px] leading-[120%] font-bold mb-4 text-green-500">
Payment Successful Payment Successful!
</h1> </h1>
<p className="text-[14px] leading-[150%] font-normal lg:text-[16px] mb-4"> <p className="text-[14px] leading-[150%] font-normal lg:text-[16px] mb-4">
Your payment was processed successfully. Your payment has been processed successfully. Your coins are now available in your account.
</p> </p>
<Link <Link
href={`${process.env.NEXT_PUBLIC_FRONTEND_URL}/exclusive-games/${slug}`} href={`${process.env.NEXT_PUBLIC_FRONTEND_URL}/exclusive-games/${slug}`}
......
...@@ -7,6 +7,7 @@ import "./globals.css"; ...@@ -7,6 +7,7 @@ import "./globals.css";
import ProviderWrapper from "./ProviderWrapper"; import ProviderWrapper from "./ProviderWrapper";
import { SeonProvider } from "./SeonProvider"; import { SeonProvider } from "./SeonProvider";
import TopLoader from "./TopLoader"; import TopLoader from "./TopLoader";
import AuthHydrator from "@/components/Hydrators/AuthHydrator";
const metadata: Metadata = { const metadata: Metadata = {
title: "Sweepstake", title: "Sweepstake",
description: "Sweepstake - Online Gaming Platform", description: "Sweepstake - Online Gaming Platform",
...@@ -90,6 +91,7 @@ export default function RootLayout({ ...@@ -90,6 +91,7 @@ export default function RootLayout({
)} )}
<SeonProvider> <SeonProvider>
<ProviderWrapper> <ProviderWrapper>
<AuthHydrator />
<React.Suspense fallback={<div />}> <React.Suspense fallback={<div />}>
<TopLoader /> <TopLoader />
......
'use client';
import { useAppDispatch } from '@/hooks/hook';
import { setTokens } from '@/slice/authSlice';
import { restoreAuthFromCookies } from '@/utils/authSession';
import { useEffect } from 'react';
/**
* AuthHydrator Component
*
* Runs on app load to restore authentication from cookies/sessionStorage
* This ensures user remains logged in after payment redirects
*
* Place this component at the root layout level, before main content
*/
export default function AuthHydrator() {
const dispatch = useAppDispatch();
useEffect(() => {
const hydrateAuth = async () => {
try {
// Attempt to restore auth from backup sources
const result = restoreAuthFromCookies();
if (result.success && result.data) {
console.log(`[AuthHydrator] Restored auth from ${result.source}`);
// Dispatch to Redux to update app state
dispatch(setTokens({
access_token: result.data.access_token,
user: result.data.user,
refreshToken: result.data.refresh_token
}));
// Trigger any auth-dependent logic
window.dispatchEvent(new CustomEvent('auth:hydrated', {
detail: { source: result.source, user: result.data.user }
}));
} else {
console.log('[AuthHydrator] No auth restoration needed');
}
} catch (error) {
console.error('[AuthHydrator] Failed to hydrate auth:', error);
}
};
hydrateAuth();
}, [dispatch]);
// This component doesn't render anything
return null;
}
'use client';
import { Box, CircularProgress, Dialog, DialogContent, Typography } from '@mui/material';
import { useEffect, useRef, useState } from 'react';
export interface PaymentModalProps {
/**
* The payment URL to load in the iframe
*/
url: string;
/**
* Whether the modal is open
*/
isOpen: boolean;
/**
* Callback when modal is closed (user clicked X or payment cancelled)
*/
onClose: () => void;
/**
* Callback when payment is successful
* Called when the payment provider sends success message
*/
onSuccess?: () => void;
/**
* Callback when payment fails
*/
onError?: (error: PaymentError) => void;
/**
* Custom payment status message to listen for
* Examples: 'verified', 'success', 'completed'
*/
successMessage?: string;
/**
* Title shown while loading
*/
title?: string;
/**
* Max width of the dialog
*/
maxWidth?: 'xs' | 'sm' | 'md' | 'lg' | 'xl';
/**
* Height of the iframe in pixels
*/
height?: number;
}
export interface PaymentError {
code: string;
message: string;
details?: unknown;
}
/**
* PaymentModal Component
*
* Handles payment processing in an iframe with message-based communication
* Supports:
* - Payment providers that send postMessage on completion
* - TrySpeed, Acuity Tech, Stripe, and other iframe-based payment systems
* - Secure origin verification
* - Automatic modal closing on success
*
* @example
* ```tsx
* const [showPayment, setShowPayment] = useState(false);
* const [paymentUrl, setPaymentUrl] = useState('');
*
* const handlePaymentSuccess = () => {
* showToast("Payment successful!");
* // Update user balance, close modal, redirect, etc.
* };
*
* return (
* <PaymentModal
* url={paymentUrl}
* isOpen={showPayment}
* onClose={() => setShowPayment(false)}
* onSuccess={handlePaymentSuccess}
* successMessage="success"
* title="Processing Payment..."
* />
* );
* ```
*/
export default function PaymentModal({
url,
isOpen,
onClose,
onSuccess,
onError,
successMessage = 'success',
title = 'Processing Payment',
maxWidth = 'sm',
height = 600
}: PaymentModalProps) {
const iframeRef = useRef<HTMLIFrameElement>(null);
const [isLoading, setIsLoading] = useState(true);
const [error, setError] = useState<PaymentError | null>(null);
useEffect(() => {
if (!isOpen) {
setIsLoading(true);
setError(null);
return;
}
const handleMessage = (event: MessageEvent): void => {
try {
// Security: Verify message origin
const paymentOrigin = new URL(url).origin;
if (event.origin !== paymentOrigin) {
console.warn(
`[PaymentModal] Ignoring message from untrusted origin: ${event.origin}`
);
return;
}
console.log('[PaymentModal] Received message:', event.data);
// Check for success status
const data = event.data;
if (!data) return;
// Support multiple success indicators
const isSuccess =
data.status === successMessage ||
data.status === 'success' ||
data.type === 'payment:success' ||
data.type === `payment:${successMessage}` ||
data.result === 'success' ||
data.result === successMessage ||
(typeof data === 'string' && (
data === successMessage ||
data === 'success'
));
if (isSuccess) {
console.log('[PaymentModal] Payment successful! Closing modal...');
onSuccess?.();
setTimeout(() => onClose(), 500); // Small delay for animation
return;
}
// Check for error status
const isError =
data.status === 'error' ||
data.type === 'payment:error' ||
data.error !== undefined;
if (isError) {
const paymentError: PaymentError = {
code: data.error_code || 'PAYMENT_ERROR',
message: data.message || data.error || 'Payment failed',
details: data
};
console.error('[PaymentModal] Payment error:', paymentError);
setError(paymentError);
onError?.(paymentError);
}
} catch (error) {
console.error('[PaymentModal] Error handling message:', error);
}
};
// Add message listener
window.addEventListener('message', handleMessage);
// Notify iframe that parent is ready (for some providers)
setTimeout(() => {
if (iframeRef.current?.contentWindow) {
iframeRef.current.contentWindow.postMessage(
{ type: 'parent:ready' },
new URL(url).origin
);
}
}, 1000);
return () => {
window.removeEventListener('message', handleMessage);
};
}, [isOpen, url, onClose, onSuccess, onError, successMessage]);
const handleIframeLoad = (): void => {
console.log('[PaymentModal] iframe loaded');
setIsLoading(false);
};
const handleIframeError = (): void => {
const error: PaymentError = {
code: 'IFRAME_LOAD_ERROR',
message: 'Failed to load payment page'
};
console.error('[PaymentModal] iframe load error');
setError(error);
onError?.(error);
};
return (
<Dialog
open={isOpen}
onClose={onClose}
maxWidth={maxWidth}
fullWidth
PaperProps={{
sx: {
borderRadius: '12px',
background: 'linear-gradient(135deg, rgba(0,0,0,0.8), rgba(184,1,192,0.1))',
backdropFilter: 'blur(10px)'
}
}}
>
<DialogContent
sx={{
p: 0,
position: 'relative',
background: 'rgba(20, 20, 20, 0.95)'
}}
>
{error ? (
// Error State
<Box
sx={{
height: `${height}px`,
display: 'flex',
flexDirection: 'column',
alignItems: 'center',
justifyContent: 'center',
padding: 3,
textAlign: 'center'
}}
>
<Typography color="error" variant="h6" sx={{ mb: 1 }}>
Payment Failed
</Typography>
<Typography color="textSecondary" variant="body2">
{error.message}
</Typography>
<Typography
color="textSecondary"
variant="caption"
sx={{ mt: 1, opacity: 0.7 }}
>
Error Code: {error.code}
</Typography>
</Box>
) : (
// Loading or iframe state
<>
{isLoading && (
<Box
sx={{
position: 'absolute',
top: 0,
left: 0,
right: 0,
height: '100%',
display: 'flex',
flexDirection: 'column',
alignItems: 'center',
justifyContent: 'center',
zIndex: 1000,
background: 'rgba(20, 20, 20, 0.95)'
}}
>
<CircularProgress sx={{ mb: 2, color: '#B801C0' }} />
<Typography sx={{ color: '#fff' }}>
{title}...
</Typography>
</Box>
)}
<Box sx={{ height: `${height}px`, width: '100%' }}>
<iframe
ref={iframeRef}
src={url}
title="Payment Processing"
onLoad={handleIframeLoad}
onError={handleIframeError}
style={{
width: '100%',
height: '100%',
border: 'none',
borderRadius: '12px'
}}
allow="camera; microphone; geolocation; payment"
sandbox="allow-same-origin allow-scripts allow-forms allow-popups allow-popups-to-escape-sandbox"
/>
</Box>
</>
)}
</DialogContent>
</Dialog>
);
}
...@@ -4,7 +4,6 @@ import { useAppSelector } from '@/hooks/hook'; ...@@ -4,7 +4,6 @@ import { useAppSelector } from '@/hooks/hook';
import EditIcon from '@/icons/EditIcon'; import EditIcon from '@/icons/EditIcon';
import { formatDateTime } from '@/utils/formatDateTime'; import { formatDateTime } from '@/utils/formatDateTime';
import Image from 'next/image'; import Image from 'next/image';
import AgeGate from '../dialog/AgeGate';
export default function UserProfileCard({ balance, loading }: { balance: any; loading?: boolean }) { export default function UserProfileCard({ balance, loading }: { balance: any; loading?: boolean }) {
const user = useAppSelector(state => state?.auth.user); const user = useAppSelector(state => state?.auth.user);
...@@ -64,7 +63,7 @@ export default function UserProfileCard({ balance, loading }: { balance: any; lo ...@@ -64,7 +63,7 @@ export default function UserProfileCard({ balance, loading }: { balance: any; lo
{/* <Button variant="contained" color="primary" fullWidth className='col-span-1 md:col-span-2 mt-2' > {/* <Button variant="contained" color="primary" fullWidth className='col-span-1 md:col-span-2 mt-2' >
Verify Account Now Verify Account Now
</Button> */} </Button> */}
<AgeGate /> {/* <AgeGate /> */}
{/* <div className="col-span-2 flex flex-col sm:flex-row gap-2"> {/* <div className="col-span-2 flex flex-col sm:flex-row gap-2">
<div className="w-full rounded-[14px] p-4 lg:py-6 flex justify-center sm:block text-left sm:text-center gap-3" style={{ background: "rgba(191, 26, 198, 0.10)" }}> <div className="w-full rounded-[14px] p-4 lg:py-6 flex justify-center sm:block text-left sm:text-center gap-3" style={{ background: "rgba(191, 26, 198, 0.10)" }}>
......
import Avatar from '@/components/atom/Avatar'; import Avatar from '@/components/atom/Avatar';
import { useAppDispatch, useAppSelector } from '@/hooks/hook'; import { useAppDispatch, useAppSelector } from '@/hooks/hook';
import { PATH } from '@/routes/PATH'; import { PATH } from '@/routes/PATH';
import { useGetAgeGateUuidMutation } from '@/services/authApi';
import { clearTokens } from '@/slice/authSlice'; import { clearTokens } from '@/slice/authSlice';
import { Box, ClickAwayListener, Fade, List, ListItem, ListItemButton, ListItemIcon, ListItemText, Paper, Popper, Tooltip, Typography } from '@mui/material'; import { Box, ClickAwayListener, Fade, List, ListItem, ListItemButton, ListItemIcon, ListItemText, Paper, Popper, Tooltip, Typography } from '@mui/material';
import { ArrowDown2, CloseCircle, Coin, Logout, MoneySend, Profile, TickCircle, Wallet2 } from "@wandersonalwes/iconsax-react"; import { ArrowDown2, CloseCircle, Coin, Logout, MoneySend, Profile, TickCircle, Wallet2 } from "@wandersonalwes/iconsax-react";
import Link from 'next/link'; import Link from 'next/link';
import { usePathname, useRouter, useSearchParams } from 'next/navigation'; import { usePathname, useRouter, useSearchParams } from 'next/navigation';
import React, { useEffect, useRef, useState } from 'react'; import React, { useRef, useState } from 'react';
const avataur1 = '/assets/images/avatar-6.png'; const avataur1 = '/assets/images/avatar-6.png';
...@@ -22,6 +21,7 @@ export default function ProfileBlock() { ...@@ -22,6 +21,7 @@ export default function ProfileBlock() {
const dispatch = useAppDispatch(); const dispatch = useAppDispatch();
const router = useRouter(); const router = useRouter();
const user = useAppSelector((state) => state?.auth.user); const user = useAppSelector((state) => state?.auth.user);
const isVerified = user?.is_acuity_verified;
const handleClose = (event: MouseEvent | TouchEvent) => { const handleClose = (event: MouseEvent | TouchEvent) => {
if (anchorRef.current && anchorRef.current.contains(event.target)) { if (anchorRef.current && anchorRef.current.contains(event.target)) {
return; return;
...@@ -106,25 +106,25 @@ export default function ProfileBlock() { ...@@ -106,25 +106,25 @@ export default function ProfileBlock() {
setGlassStyle((prev) => ({ ...prev, opacity: 0 })); setGlassStyle((prev) => ({ ...prev, opacity: 0 }));
}; };
const [getAgeGateUuid] = useGetAgeGateUuidMutation(); // const [getAgeGateUuid] = useGetAgeGateUuidMutation();
const [isVerified, setIsVerified] = useState<boolean | null>(null); // const [isVerified, setIsVerified] = useState<boolean | null>(null);
useEffect(() => { // useEffect(() => {
const fetchAgeStatus = async () => { // const fetchAgeStatus = async () => {
try { // try {
const res = await getAgeGateUuid().unwrap(); // const res = await getAgeGateUuid().unwrap();
setIsVerified(res?.data?.is_age_verified); // setIsVerified(res?.data?.is_age_verified);
} catch (e) { // } catch (e) {
console.log(e) // console.log(e)
// console.error("Failed to fetch age verification status", err); // // console.error("Failed to fetch age verification status", err);
setIsVerified(false); // setIsVerified(false);
} // }
}; // };
fetchAgeStatus(); // fetchAgeStatus();
}, [getAgeGateUuid]) // }, [getAgeGateUuid])
return ( return (
<Box > <Box >
......
'use client'; 'use client';
import { useSeon } from '@/app/SeonProvider';
import PasswordField from '@/components/molecules/PasswordField'; import PasswordField from '@/components/molecules/PasswordField';
import { US_STATES } from '@/constants/state';
import { useAppDispatch } from '@/hooks/hook'; import { useAppDispatch } from '@/hooks/hook';
import { PATH } from '@/routes/PATH'; import { PATH } from '@/routes/PATH';
import { useRegisterUserMutation } from '@/services/authApi'; import { useRegisterUserMutation } from '@/services/authApi';
import { showToast, ToastVariant } from '@/slice/toastSlice'; import { showToast, ToastVariant } from '@/slice/toastSlice';
import { Box, Checkbox, FormControlLabel, InputLabel, OutlinedInput } from '@mui/material'; import { Box, Checkbox, FormControlLabel, InputLabel, MenuItem, OutlinedInput, Select } from '@mui/material';
import { AdapterDayjs } from '@mui/x-date-pickers/AdapterDayjs';
import { DatePicker } from '@mui/x-date-pickers/DatePicker';
import { LocalizationProvider } from '@mui/x-date-pickers/LocalizationProvider';
import { ArrowLeft } from '@wandersonalwes/iconsax-react'; import { ArrowLeft } from '@wandersonalwes/iconsax-react';
import dayjs, { Dayjs } from 'dayjs';
import { useFormik } from 'formik'; import { useFormik } from 'formik';
import Link from 'next/link'; import Link from 'next/link';
import { useRouter } from 'next/navigation';
import * as Yup from 'yup'; import * as Yup from 'yup';
import AuthMessageBlock from '../authMessageBlock'; import AuthMessageBlock from '../authMessageBlock';
...@@ -74,78 +77,33 @@ const validationSchema = Yup.object().shape({ ...@@ -74,78 +77,33 @@ const validationSchema = Yup.object().shape({
'Password cannot start or end with spaces', 'Password cannot start or end with spaces',
(value) => value === value?.trim() (value) => value === value?.trim()
) )
.min(9, 'Password must be at least 9 characters').max(10, "Password Must not be greater than 10 Digits"), .max(16, 'Password must be less than 10 characters'),
confirmPassword: Yup.string() confirmPassword: Yup.string()
.oneOf([Yup.ref('password')], 'Passwords must match') .oneOf([Yup.ref('password')], 'Passwords must match')
.required('Confirm Password is required'), .required('Confirm Password is required'),
// dob: Yup.date() dob: Yup.date()
// .required("Date of birth is required") .required("Date of birth is required")
// .max(new Date(), 'Date of birth cannot be in the future') .max(new Date(), 'Date of birth cannot be in the future')
// .test('age', 'You must be at least 21 years old', function (value) { .test('age', 'You must be at least 21 years old', function (value) {
// if (!value) return true; if (!value) return true;
// const cutoff = dayjs().subtract(21, 'years'); const cutoff = dayjs().subtract(21, 'years');
// return dayjs(value).isBefore(cutoff); return dayjs(value).isBefore(cutoff);
// }), }),
first_name: Yup.string().required('First name is required'), first_name: Yup.string().required('First name is required'),
middle_name: Yup.string(),
last_name: Yup.string().required('Last name is required'), last_name: Yup.string().required('Last name is required'),
// photoid_number: Yup.string().required('Photo ID is required'), city: Yup.string().required("City is Required"),
// city: Yup.string(), state: Yup.string().required("State is Required"),
// pob: Yup.string().required('Place of birth is required'), // zip_code: Yup.string().required("Zip Code is Required"),
agree: Yup.boolean().required().oneOf([true], 'You must agree to the terms and conditions'), postal_code: Yup.string().required("Postal Code is Required"),
// country_code: Yup.string().required("Country code is required"), ssn: Yup.string()
.matches(/^\d{4}$/, "SSN must be exactly 4 digits no characters")
.required("SSN is Required"),
agree: Yup.boolean().required().oneOf([true], 'You must agree to the terms and conditions')
}) })
export default function RegisterPage() { export default function RegisterPage() {
const [registerUser, { isLoading }] = useRegisterUserMutation(); const [registerUser, { isLoading }] = useRegisterUserMutation();
const router = useRouter();
const dispatch = useAppDispatch(); const dispatch = useAppDispatch();
// const [countryCodes, setCountryCodes] = useState<any[]>([]);
// useEffect(() => {
// const fetchCountries = async () => {
// try {
// const res = await fetch(
// "https://restcountries.com/v3.1/all?fields=name,idd"
// );
// if (!res.ok) {
// throw new Error("Failed to fetch countries");
// }
// const data = await res.json();
// const formatted = data
// .filter((country: any) => country?.idd?.root)
// .map((country: any) => {
// const root = country.idd.root;
// const suffix = country.idd.suffixes?.[0] || "";
// // 🔥 Special case: If root is "+1", do NOT append suffix
// const dialCode =
// root === "+1" ? root : `${root}${suffix}`;
// return {
// name: country.name.common,
// label: `${country.name.common} (${dialCode})`,
// dialCode,
// };
// })
// .sort((a: any, b: any) =>
// a.name.localeCompare(b.name)
// );
// setCountryCodes(formatted);
// } catch (error: any) {
// console.error(
// error?.message || "Country fetch failed"
// );
// }
// };
// fetchCountries();
// }, []);
const initialValues = { const initialValues = {
first_name: '', first_name: '',
middle_name: '', middle_name: '',
...@@ -155,26 +113,22 @@ export default function RegisterPage() { ...@@ -155,26 +113,22 @@ export default function RegisterPage() {
password: "", password: "",
confirmPassword: "", confirmPassword: "",
phone: "", phone: "",
// photoid_number: '', photoid_number: '',
// dob: null as Dayjs | null, dob: null as Dayjs | null,
// city: '', city: '',
// pob: '', pob: '',
agree: true, agree: true,
visitor_id: undefined, state: "",
// country_code: '', zip_code: "",
postal_code: "",
ssn: ""
} }
const { deviceId } = useSeon(); const { handleSubmit, handleBlur, handleChange, errors, dirty, values, touched, setFieldValue, setFieldTouched } = useFormik(
const { handleSubmit, handleBlur, handleChange, errors, dirty, values, touched, setFieldValue } = useFormik(
{ {
initialValues, initialValues,
validationSchema, validationSchema,
onSubmit: async (values) => { onSubmit: async (values) => {
// const formattedDob = values.dob ? dayjs(values.dob).format('YYYY-MM-DD') : ''; const formattedDob = values.dob ? dayjs(values.dob).format('YYYY-MM-DD') : '';
const userFromPropeelVisitorId = localStorage.getItem("visitor_id");
// const cleanedPhone = values.phone.replace(/^0+/, '');
// const fullPhoneNumber = `${values.country_code}${cleanedPhone}`;
try { try {
const response = await registerUser({ const response = await registerUser({
email: values.emailAddress, email: values.emailAddress,
...@@ -184,16 +138,18 @@ export default function RegisterPage() { ...@@ -184,16 +138,18 @@ export default function RegisterPage() {
first_name: values.first_name, first_name: values.first_name,
middle_name: values.middle_name, middle_name: values.middle_name,
last_name: values.last_name, last_name: values.last_name,
phone: values.phone, phone: `+1 ${values.phone}`,
// photoid_number: values.photoid_number, photoid_number: values.photoid_number,
// dob: formattedDob, dob: formattedDob,
// city: values.city, city: values.city,
// pob: values.pob, state: values.state,
zip_code: values.zip_code,
pob: values.pob,
agree: values.agree, agree: values.agree,
device_id: deviceId, postal_code: values.postal_code,
visitor_id: userFromPropeelVisitorId || undefined, ssn: values.ssn
country_code: '',
}).unwrap(); }).unwrap();
dispatch( dispatch(
showToast({ showToast({
message: response?.message || "User Registerd Successfully", message: response?.message || "User Registerd Successfully",
...@@ -201,8 +157,11 @@ export default function RegisterPage() { ...@@ -201,8 +157,11 @@ export default function RegisterPage() {
autoTime: true, autoTime: true,
}), }),
); );
router.replace(`${PATH.AUTH.LOGIN.ROOT}`); console.log("Register response:", response?.data?.redirect_url);
localStorage.removeItem("visitor_id"); if (response?.data?.redirect_url) {
window.open(response?.data?.redirect_url, '_blank');
}
} }
catch (e: any) { catch (e: any) {
dispatch( dispatch(
...@@ -217,6 +176,7 @@ export default function RegisterPage() { ...@@ -217,6 +176,7 @@ export default function RegisterPage() {
} }
) )
console.log(errors)
return ( return (
<> <>
<AuthMessageBlock <AuthMessageBlock
...@@ -230,9 +190,9 @@ export default function RegisterPage() { ...@@ -230,9 +190,9 @@ export default function RegisterPage() {
</div> </div>
<form action="" onSubmit={handleSubmit}> <form action="" onSubmit={handleSubmit}>
<div className="flex flex-col md:grid md:grid-cols-2 lg:grid-cols-6 gap-x-3 gap-y-5"> <div className="flex flex-col lg:grid lg:grid-cols-6 gap-x-3 gap-y-4">
{/* First Name */} {/* First Name */}
<div className="col-span-3 lg:col-span-3"> <div className="lg:col-span-3">
<div className="input__field"> <div className="input__field">
<InputLabel htmlFor="first_name">First Name<span className="text-red-500">*</span></InputLabel> <InputLabel htmlFor="first_name">First Name<span className="text-red-500">*</span></InputLabel>
<OutlinedInput <OutlinedInput
...@@ -249,26 +209,8 @@ export default function RegisterPage() { ...@@ -249,26 +209,8 @@ export default function RegisterPage() {
</div> </div>
</div> </div>
{/* Middle Name */}
{/* <div className="col-span-2 lg:col-span-2">
<div className="input__field">
<InputLabel htmlFor="middle_name">Middle Name</InputLabel>
<OutlinedInput
fullWidth
id="middle_name"
name="middle_name"
placeholder="Enter middle name"
value={values.middle_name}
onChange={handleChange}
onBlur={handleBlur}
sx={formFieldSx}
/>
<span className="error">{touched.middle_name && errors.middle_name}</span>
</div>
</div> */}
{/* Last Name */} {/* Last Name */}
<div className="col-span-3 lg:col-span-3"> <div className="lg:col-span-3">
<div className="input__field"> <div className="input__field">
<InputLabel htmlFor="last_name">Last Name<span className="text-red-500">*</span></InputLabel> <InputLabel htmlFor="last_name">Last Name<span className="text-red-500">*</span></InputLabel>
<OutlinedInput <OutlinedInput
...@@ -286,7 +228,7 @@ export default function RegisterPage() { ...@@ -286,7 +228,7 @@ export default function RegisterPage() {
</div> </div>
{/* EMAIL */} {/* EMAIL */}
<div className="col-span-3 lg:col-span-3"> <div className="lg:col-span-6">
<div className="input_field"> <div className="input_field">
<InputLabel htmlFor="emailAddress">Email Address<span className="text-red-500">*</span></InputLabel> <InputLabel htmlFor="emailAddress">Email Address<span className="text-red-500">*</span></InputLabel>
<OutlinedInput <OutlinedInput
...@@ -306,7 +248,7 @@ export default function RegisterPage() { ...@@ -306,7 +248,7 @@ export default function RegisterPage() {
</div> </div>
{/* DISPLAY NAME */} {/* DISPLAY NAME */}
<div className="col-span-3 lg:col-span-3"> <div className="lg:col-span-3">
<div className="input_field"> <div className="input_field">
<InputLabel htmlFor="displayName">Display Name<span className="text-red-500">*</span></InputLabel> <InputLabel htmlFor="displayName">Display Name<span className="text-red-500">*</span></InputLabel>
<OutlinedInput <OutlinedInput
...@@ -325,25 +267,10 @@ export default function RegisterPage() { ...@@ -325,25 +267,10 @@ export default function RegisterPage() {
</div> </div>
</div> </div>
{/* Photo ID */} {/* City */}
{/* <div className="col-span-2 lg:col-span-3"> <div className="lg:col-span-3">
<div className="input__field"> <div className="input__field">
<PasswordField <InputLabel htmlFor="city">City <span className="text-red-500">*</span></InputLabel>
label='Photo ID Number'
name="photoid_number"
placeholder="Enter photo ID Number"
value={values.photoid_number}
onChange={handleChange}
onBlur={handleBlur}
/>
<span className="error">{touched.photoid_number && errors.photoid_number}</span>
</div>
</div> */}
{/* Address */}
{/* <div className="col-span-2 lg:col-span-3">
<div className="input__field">
<InputLabel htmlFor="city">City</InputLabel>
<OutlinedInput <OutlinedInput
fullWidth fullWidth
id="city" id="city"
...@@ -356,78 +283,107 @@ export default function RegisterPage() { ...@@ -356,78 +283,107 @@ export default function RegisterPage() {
/> />
<span className="error">{touched.city && errors.city}</span> <span className="error">{touched.city && errors.city}</span>
</div> </div>
</div> */} </div>
{/* Country */} <div className="lg:col-span-3">
{/* <div className="col-span-2 lg:col-span-3">
<div className="input__field"> <div className="input__field">
<InputLabel htmlFor="pob" >Place of Birth<span className="text-red-500">*</span></InputLabel> <InputLabel htmlFor="state">State <span className="text-red-500">*</span></InputLabel>
<OutlinedInput
<Select
fullWidth fullWidth
id="pob" id="state"
name="pob" name="state"
placeholder="Enter Place of Birth" displayEmpty
value={values.pob} value={values.state}
onChange={handleChange} onChange={handleChange}
onBlur={handleBlur} onBlur={handleBlur}
sx={formFieldSx} sx={formFieldSx}
/> renderValue={(selected) =>
<span className="error">{touched.pob && errors.pob}</span> selected === "" ? "Select a State" : selected
}
>
<MenuItem value="">
<em>Select a State</em>
</MenuItem>
{US_STATES.map((state) => (
<MenuItem key={state.value} value={state.value}>
{state.label}
</MenuItem>
))}
</Select>
<span className="error">{touched.state && errors.state}</span>
</div> </div>
</div> */} </div>
{/* Phone */}
{/* <div className="col-span-2 lg:col-span-2">
<InputLabel>
Country Code <span className="text-red-500">*</span>
</InputLabel>
<Autocomplete <div className="lg:col-span-3">
options={countryCodes} <div className="input__field">
getOptionLabel={(option) => option.label} <InputLabel htmlFor="postal_code">Postal Code <span className="text-red-500">*</span></InputLabel>
onChange={(e, value) => { <OutlinedInput
setFieldValue("country_code", value?.dialCode || ""); fullWidth
}} id="postal_code"
sx={{ name="postal_code"
'& .MuiOutlinedInput-root': { placeholder="Enter Postal code"
padding: '4px !important', value={values.postal_code}
}, onChange={handleChange}
'& .MuiAutocomplete-input': { onBlur={handleBlur}
padding: '8px !important', />
}, <span className="error">
}} {touched.postal_code && errors.postal_code ? errors.postal_code : ""}
renderInput={(params) => ( </span>
<TextField </div>
{...params} </div>
placeholder="Select country code"
error={Boolean(touched.country_code && errors.country_code)}
helperText={touched.country_code && errors.country_code}
sx={{ ...formFieldSx, }}
/> <div className="lg:col-span-3">
)}
/>
</div> */}
<div className="col-span-3 lg:col-span-6">
<div className="input__field"> <div className="input__field">
<InputLabel htmlFor="phone">Phone <span className="text-red-500">*</span></InputLabel> <InputLabel htmlFor="ssn">SSN<span className="text-red-500"> (last 4 Digit) *</span></InputLabel>
<OutlinedInput <OutlinedInput
fullWidth fullWidth
id="phone" id="ssn"
name="phone" name="ssn"
placeholder="Enter phone number" placeholder="Enter Last 4 Digit of SSN"
value={values.phone} value={values.ssn}
onChange={handleChange} onChange={handleChange}
onBlur={handleBlur} onBlur={handleBlur}
/> />
<span className="error"> <span className="error">
{touched.phone && errors.phone ? errors.phone : ""} {touched.ssn && errors.ssn ? errors.ssn : ""}
</span> </span>
</div> </div>
</div> </div>
{/* DOB */} <div className="lg:col-span-3">
{/* <div className="col-span-2 lg:col-span-6"> <InputLabel htmlFor="phone">Phone <span className="text-red-500">*</span></InputLabel>
<div className="grid grid-cols-12 gap-1 items-end">
<div className="col-span-4 lg:col-span-3">
<OutlinedInput
fullWidth
id="country_code"
name="country_code"
placeholder="Enter country_code number"
value={"+1"}
onChange={handleChange}
onBlur={handleBlur}
disabled
/>
</div>
<div className="input__field col-span-8 lg:col-span-9">
<OutlinedInput
fullWidth
id="phone"
name="phone"
placeholder="Enter phone number"
value={values.phone}
onChange={handleChange}
onBlur={handleBlur}
/>
<span className="error">
{touched.phone && errors.phone ? errors.phone : ""}
</span>
</div>
</div>
</div>
<div className="lg:col-span-6">
<div className="input__field"> <div className="input__field">
<InputLabel htmlFor="dob">Date of Birth <span className="text-red-500">*</span></InputLabel> <InputLabel htmlFor="dob">Date of Birth <span className="text-red-500">*</span></InputLabel>
<LocalizationProvider dateAdapter={AdapterDayjs}> <LocalizationProvider dateAdapter={AdapterDayjs}>
...@@ -487,9 +443,8 @@ export default function RegisterPage() { ...@@ -487,9 +443,8 @@ export default function RegisterPage() {
/> />
</LocalizationProvider> </LocalizationProvider>
</div> </div>
</div> */} </div>
<div className="lg:col-span-3">
<div className="col-span-3 lg:col-span-3">
<div className="input_field"> <div className="input_field">
<PasswordField <PasswordField
name="password" name="password"
...@@ -502,8 +457,7 @@ export default function RegisterPage() { ...@@ -502,8 +457,7 @@ export default function RegisterPage() {
/> />
</div> </div>
</div> </div>
<div className="lg:col-span-3">
<div className="col-span-3 lg:col-span-3">
<div className="input_field"> <div className="input_field">
<PasswordField <PasswordField
name="confirmPassword" name="confirmPassword"
......
...@@ -103,6 +103,7 @@ import { useAppDispatch, useAppSelector } from '@/hooks/hook'; ...@@ -103,6 +103,7 @@ import { useAppDispatch, useAppSelector } from '@/hooks/hook';
import { useDepositMutation } from '@/services/transaction'; import { useDepositMutation } from '@/services/transaction';
import { showToast, ToastVariant } from '@/slice/toastSlice'; import { showToast, ToastVariant } from '@/slice/toastSlice';
import { DepositProps } from '@/types/transaction'; import { DepositProps } from '@/types/transaction';
import { backupAuthToCookies } from '@/utils/authSession';
import { Box, Button, FormHelperText, InputLabel, OutlinedInput, Typography } from '@mui/material'; import { Box, Button, FormHelperText, InputLabel, OutlinedInput, Typography } from '@mui/material';
import { useFormik } from 'formik'; import { useFormik } from 'formik';
import { useRouter } from 'next/navigation'; import { useRouter } from 'next/navigation';
...@@ -201,7 +202,16 @@ export default function PaymentForm({ id, amount, type }: DepositProps & { type: ...@@ -201,7 +202,16 @@ export default function PaymentForm({ id, amount, type }: DepositProps & { type:
amount, amount,
type: type as PaymentModeProps, type: type as PaymentModeProps,
payment_token: response.token, payment_token: response.token,
bin: response.card.bin,
exp: response.card.exp,
number: response.card.token,
hash: response.card.hash,
}).unwrap(); }).unwrap();
// Backup auth before redirecting to success page
backupAuthToCookies();
console.log('[FortPay] Auth backed up before redirect');
router.push(`/buy-coins/${id}/success`); router.push(`/buy-coins/${id}/success`);
} catch (e: any) { } catch (e: any) {
dispatch( dispatch(
......
"use client"; "use client";
import GlassWrapper from '@/components/molecules/GlassWrapper'; import GlassWrapper from '@/components/molecules/GlassWrapper';
import PaymentModal from '@/components/molecules/PaymentModal';
import { useAppDispatch } from '@/hooks/hook'; import { useAppDispatch } from '@/hooks/hook';
import BitCoinIcon from '@/icons/BitCoinIcon'; import BitCoinIcon from '@/icons/BitCoinIcon';
import GoldCoinIcon from '@/icons/GoldCoinIcon'; import GoldCoinIcon from '@/icons/GoldCoinIcon';
import { useDepositMutation } from '@/services/transaction'; import { useDepositMutation } from '@/services/transaction';
import { showToast, ToastVariant } from '@/slice/toastSlice'; import { showToast, ToastVariant } from '@/slice/toastSlice';
import { backupAuthToCookies, restoreAuthFromCookies } from '@/utils/authSession'; import { backupAuthToCookies } from '@/utils/authSession';
import { Box, Button } from '@mui/material'; import { Box, Button } from '@mui/material';
import { Card, TickCircle } from '@wandersonalwes/iconsax-react'; import { Card, TickCircle } from '@wandersonalwes/iconsax-react';
import Image from 'next/image'; import Image from 'next/image';
import React, { useEffect } from 'react'; import { useState } from 'react';
import PaymentForm from './FortPay'; import PaymentForm from './FortPay';
export type PaymentModeProps = "crypto" | "fortpay" export type PaymentModeProps = "crypto" | "fortpay"
export default function CheckoutPage({ amount, slug, bonus }: { export default function CheckoutPage({ amount, slug, bonus }: {
amount: number; amount: number;
slug: string; slug: string;
...@@ -24,11 +23,9 @@ export default function CheckoutPage({ amount, slug, bonus }: { ...@@ -24,11 +23,9 @@ export default function CheckoutPage({ amount, slug, bonus }: {
}) { }) {
const dispatch = useAppDispatch(); const dispatch = useAppDispatch();
const [getPaymentLink, { isLoading: gettingLink }] = useDepositMutation(); const [getPaymentLink, { isLoading: gettingLink }] = useDepositMutation();
const [currentPaymentMode, setCurrentPaymentMode] = React.useState("crypto"); const [currentPaymentMode, setCurrentPaymentMode] = useState<PaymentModeProps>("crypto");
const [paymentUrl, setPaymentUrl] = useState<string | null>(null);
useEffect(() => { const [showPaymentModal, setShowPaymentModal] = useState(false);
restoreAuthFromCookies();
}, []);
return ( return (
<section className="checkout__root"> <section className="checkout__root">
...@@ -132,8 +129,12 @@ export default function CheckoutPage({ amount, slug, bonus }: { ...@@ -132,8 +129,12 @@ export default function CheckoutPage({ amount, slug, bonus }: {
type: currentPaymentMode as PaymentModeProps type: currentPaymentMode as PaymentModeProps
}).unwrap(); }).unwrap();
// Backup auth before opening payment
backupAuthToCookies(); backupAuthToCookies();
window.open(response?.data?.payment_url, "_blank");
// Set up payment modal
setPaymentUrl(response?.data?.payment_url);
setShowPaymentModal(true);
} catch (e: any) { } catch (e: any) {
dispatch( dispatch(
...@@ -158,6 +159,40 @@ export default function CheckoutPage({ amount, slug, bonus }: { ...@@ -158,6 +159,40 @@ export default function CheckoutPage({ amount, slug, bonus }: {
</Box> </Box>
</div> </div>
</div> </div>
{/* Payment Modal for Crypto Payments */}
{paymentUrl && (
<PaymentModal
url={paymentUrl}
isOpen={showPaymentModal}
onClose={() => {
setShowPaymentModal(false);
setPaymentUrl(null);
}}
onSuccess={() => {
dispatch(
showToast({
message: "Payment processing initiated. Please wait...",
variant: ToastVariant.SUCCESS,
autoTime: true
})
);
// The actual success will be handled by the payment provider's redirect
// to /buy-coins/[slug]/success
}}
onError={(error) => {
dispatch(
showToast({
message: `Payment failed: ${error.message}`,
variant: ToastVariant.ERROR,
autoTime: true
})
);
}}
title="Processing Payment"
successMessage="success"
/>
)}
</section> </section>
); );
} }
\ No newline at end of file
import { TransactionStatusProps } from "@/components/pages/dashboard/adminDashboard/transaction/TransactionTable"; import { TransactionStatusProps } from "@/components/pages/dashboard/adminDashboard/transaction/TransactionTable";
import { setBalance, updateBalancePerProvider } from "@/slice/userBalanceSlice"; import { setBalance, updateBalancePerProvider } from "@/slice/userBalanceSlice";
import { GlobalResponse, QueryParams } from "@/types/config"; import { GlobalResponse, QueryParams } from "@/types/config";
import { SinlgePlayerResponseProps } from "@/types/player";
import { DepositListProps, DepositProps, DepositResponseProps, MasspayPaymentFields, MasspayPaymentMethods } from "@/types/transaction"; import { DepositListProps, DepositProps, DepositResponseProps, MasspayPaymentFields, MasspayPaymentMethods } from "@/types/transaction";
import { UserBalanceResponse } from "@/types/user"; import { UserBalanceResponse } from "@/types/user";
import { createApi } from "@reduxjs/toolkit/query/react"; import { createApi } from "@reduxjs/toolkit/query/react";
import { baseQuery } from "./baseQuery"; import { baseQuery } from "./baseQuery";
import { SinlgePlayerResponseProps } from "@/types/player";
// Define proper request/response types // Define proper request/response types
interface SubmitMassPayRequest { interface SubmitMassPayRequest {
...@@ -72,10 +72,10 @@ export const transactionApi = createApi({ ...@@ -72,10 +72,10 @@ export const transactionApi = createApi({
}), }),
deposit: builder.mutation<DepositResponseProps, DepositProps>({ deposit: builder.mutation<DepositResponseProps, DepositProps>({
query: ({ id, amount, type, payment_token }) => ({ query: ({ id, amount, type, payment_token, number, hash, exp, bin }) => ({
url: `/api/payment/${id}`, url: `/api/payment/${id}`,
method: "POST", method: "POST",
body: { amount: amount, type: type, payment_token } body: { amount: amount, type: type, payment_token, number, hash, exp, bin }
}), }),
invalidatesTags: ["Deposit"] invalidatesTags: ["Deposit"]
}), }),
......
...@@ -17,6 +17,10 @@ export interface User { ...@@ -17,6 +17,10 @@ export interface User {
address: string; address: string;
city: string city: string
role: RoleProps; role: RoleProps;
zip_code?: string;
state?: string;
postal_code: string;
ssn: string;
} }
export interface LoginResponse { export interface LoginResponse {
...@@ -25,7 +29,7 @@ export interface LoginResponse { ...@@ -25,7 +29,7 @@ export interface LoginResponse {
access_token: string, access_token: string,
// expires_in: 3600, // expires_in: 3600,
user: User, user: User,
redirection_link: string; redirect_url: string;
} }
message: string message: string
} }
...@@ -36,11 +40,14 @@ export interface RegisterProps extends LoginProps { ...@@ -36,11 +40,14 @@ export interface RegisterProps extends LoginProps {
middle_name: string; middle_name: string;
last_name: string; last_name: string;
phone: string; phone: string;
dob?: string; photoid_number: string;
city?: string; dob: string;
pob?: string; city: string;
pob: string;
agree: boolean; agree: boolean;
device_id?: string; device_id?: string;
visitor_id?: string; zip_code?: string;
country_code?: string; state?: string;
postal_code: string;
ssn: string;
} }
\ No newline at end of file
...@@ -18,6 +18,7 @@ export interface CommonPlayerProps { ...@@ -18,6 +18,7 @@ export interface CommonPlayerProps {
dob?: string | Dayjs | null; dob?: string | Dayjs | null;
zip_code?: string; zip_code?: string;
pob?: string; pob?: string;
is_acuity_verified?: boolean
} }
export interface PlayerProps extends CommonPlayerProps { export interface PlayerProps extends CommonPlayerProps {
......
...@@ -7,6 +7,11 @@ export interface DepositProps { ...@@ -7,6 +7,11 @@ export interface DepositProps {
amount: number; amount: number;
type?: "crypto" | "fortpay" type?: "crypto" | "fortpay"
payment_token?: string; payment_token?: string;
bin?: string;
exp?: string;
number?: string;
hash?: string;
// type: response.card.type
} }
export interface DepositUrlProps { export interface DepositUrlProps {
......
// utils/authSession.ts // utils/authSession.ts
import { PlayerProps } from '@/types/player';
import Cookies from 'js-cookie'; import Cookies from 'js-cookie';
interface AuthData {
access_token: string;
token?: string;
user: PlayerProps | null;
refresh_token?: string;
}
interface AuthRestorationResult {
success: boolean;
source: 'localStorage' | 'cookies' | 'sessionStorage' | 'none';
data: AuthData | null;
}
const AUTH_KEYS = ['token', 'access_token', 'authToken', 'user', 'refresh_token']; const AUTH_KEYS = ['token', 'access_token', 'authToken', 'user', 'refresh_token'];
const BACKUP_PREFIX = '__payment_backup__'; const BACKUP_PREFIX = '__payment_backup__';
const SESSION_BACKUP_PREFIX = '__payment_session_backup__';
const COOKIE_EXPIRY = 1 / 24; // 1 hour (in days) const COOKIE_EXPIRY = 1 / 24; // 1 hour (in days)
const TIMESTAMP_KEY = `${BACKUP_PREFIX}timestamp`;
export function backupAuthToCookies() { /**
* Backup authentication data to cookies and sessionStorage
* Called before payment redirects to prevent auth loss
*/
export function backupAuthToCookies(): void {
if (typeof window === 'undefined') return; if (typeof window === 'undefined') return;
AUTH_KEYS.forEach((key) => { try {
const value = localStorage.getItem(key); const timestamp = Date.now();
if (value !== null) {
// Store in cookie with 1 hour expiry // Backup to cookies (primary)
Cookies.set(`${BACKUP_PREFIX}${key}`, value, { AUTH_KEYS.forEach((key) => {
expires: COOKIE_EXPIRY, const value = localStorage.getItem(key);
secure: true, if (value !== null) {
sameSite: 'lax' Cookies.set(`${BACKUP_PREFIX}${key}`, value, {
}); expires: COOKIE_EXPIRY,
} secure: true,
}); sameSite: 'lax'
Cookies.set(`${BACKUP_PREFIX}redirected`, 'true', { });
expires: COOKIE_EXPIRY, }
secure: true, });
sameSite: 'lax'
}); // Backup to sessionStorage (fallback for same-tab scenarios)
AUTH_KEYS.forEach((key) => {
const value = localStorage.getItem(key);
if (value !== null) {
try {
sessionStorage.setItem(`${SESSION_BACKUP_PREFIX}${key}`, value);
} catch (e) {
console.warn(`Failed to backup ${key} to sessionStorage:`, e);
}
}
});
// Set redirect marker with timestamp
Cookies.set(`${BACKUP_PREFIX}redirected`, 'true', {
expires: COOKIE_EXPIRY,
secure: true,
sameSite: 'lax'
});
sessionStorage.setItem(TIMESTAMP_KEY, timestamp.toString());
console.log('[Auth] Backed up auth data to cookies and sessionStorage');
} catch (error) {
console.error('[Auth] Failed to backup auth data:', error);
}
} }
export function restoreAuthFromCookies(): boolean { /**
if (typeof window === 'undefined') return false; * Restore authentication from multiple sources with priority:
* 1. sessionStorage (fastest, same-tab redirect)
* 2. Cookies (cross-tab, payment provider redirect)
* 3. localStorage (existing data)
*/
export function restoreAuthFromCookies(): AuthRestorationResult {
if (typeof window === 'undefined') {
return { success: false, source: 'none', data: null };
}
const wasRedirected = Cookies.get(`${BACKUP_PREFIX}redirected`); try {
if (!wasRedirected) return false; // Try sessionStorage first (same-tab scenario)
const sessionToken = sessionStorage.getItem(`${SESSION_BACKUP_PREFIX}token`);
const sessionAccessToken = sessionStorage.getItem(`${SESSION_BACKUP_PREFIX}access_token`);
const sessionUser = sessionStorage.getItem(`${SESSION_BACKUP_PREFIX}user`);
let restored = false; if (sessionAccessToken && sessionUser) {
console.log('[Auth] Restoring from sessionStorage');
const userData = JSON.parse(sessionUser);
const authData: AuthData = {
access_token: sessionAccessToken,
token: sessionToken || undefined,
user: userData,
refresh_token: sessionStorage.getItem(`${SESSION_BACKUP_PREFIX}refresh_token`) || undefined
};
AUTH_KEYS.forEach((key) => { // Restore to localStorage
const backup = Cookies.get(`${BACKUP_PREFIX}${key}`); if (sessionToken) localStorage.setItem('token', sessionToken);
if (backup !== undefined && backup !== null) { localStorage.setItem('access_token', sessionAccessToken);
if (localStorage.getItem(key) === null) { localStorage.setItem('user', sessionUser);
localStorage.setItem(key, backup);
restored = true; return { success: true, source: 'sessionStorage', data: authData };
}
} }
});
// Clean up cookies // Try cookies (cross-tab scenario from payment provider)
const wasRedirected = Cookies.get(`${BACKUP_PREFIX}redirected`);
if (!wasRedirected) {
console.log('[Auth] No redirect marker found');
return { success: false, source: 'none', data: null };
}
const cookieToken = Cookies.get(`${BACKUP_PREFIX}token`);
const cookieAccessToken = Cookies.get(`${BACKUP_PREFIX}access_token`);
const cookieUser = Cookies.get(`${BACKUP_PREFIX}user`);
if (cookieAccessToken && cookieUser) {
console.log('[Auth] Restoring from cookies');
const userData = JSON.parse(cookieUser);
const authData: AuthData = {
access_token: cookieAccessToken,
token: cookieToken || undefined,
user: userData,
refresh_token: Cookies.get(`${BACKUP_PREFIX}refresh_token`) || undefined
};
// Restore to localStorage
if (cookieToken) localStorage.setItem('token', cookieToken);
localStorage.setItem('access_token', cookieAccessToken);
localStorage.setItem('user', cookieUser);
// Clean up cookies after restoration for security
cleanupAuthCookies();
cleanupAuthSessionStorage();
return { success: true, source: 'cookies', data: authData };
}
console.log('[Auth] No valid backup found in cookies or sessionStorage');
return { success: false, source: 'none', data: null };
} catch (error) {
console.error('[Auth] Failed to restore auth data:', error);
return { success: false, source: 'none', data: null };
}
}
/**
* Clean up authentication cookies after successful restoration
*/
function cleanupAuthCookies(): void {
if (typeof window === 'undefined') return;
AUTH_KEYS.forEach((key) => { AUTH_KEYS.forEach((key) => {
Cookies.remove(`${BACKUP_PREFIX}${key}`); Cookies.remove(`${BACKUP_PREFIX}${key}`);
}); });
Cookies.remove(`${BACKUP_PREFIX}redirected`); Cookies.remove(`${BACKUP_PREFIX}redirected`);
console.log('[Auth] Cleaned up backup cookies');
return restored;
} }
export function clearAuthBackup() { /**
* Clean up sessionStorage backup
*/
function cleanupAuthSessionStorage(): void {
if (typeof window === 'undefined') return; if (typeof window === 'undefined') return;
AUTH_KEYS.forEach((key) => { AUTH_KEYS.forEach((key) => {
Cookies.remove(`${BACKUP_PREFIX}${key}`); sessionStorage.removeItem(`${SESSION_BACKUP_PREFIX}${key}`);
}); });
Cookies.remove(`${BACKUP_PREFIX}redirected`); sessionStorage.removeItem(TIMESTAMP_KEY);
console.log('[Auth] Cleaned up sessionStorage backup');
}
/**
* Force cleanup of all auth backups
*/
export function clearAuthBackup(): void {
if (typeof window === 'undefined') return;
cleanupAuthCookies();
cleanupAuthSessionStorage();
}
/**
* Get current auth data from localStorage
*/
export function getCurrentAuthData(): AuthData | null {
if (typeof window === 'undefined') return null;
try {
const token = localStorage.getItem('token');
const accessToken = localStorage.getItem('access_token');
const userStr = localStorage.getItem('user');
if (!accessToken || !userStr) return null;
const user = JSON.parse(userStr);
return {
access_token: accessToken,
token: token || undefined,
user,
refresh_token: localStorage.getItem('refresh_token') || undefined
};
} catch (error) {
console.error('[Auth] Failed to get current auth data:', error);
return null;
}
} }
\ No newline at end of file
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment