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 @@
"use client"
import GlassWrapper from '@/components/molecules/GlassWrapper'
import { useAppDispatch } from '@/hooks/hook'
import { useAppDispatch, useAppSelector } from '@/hooks/hook'
import { setTokens } from '@/slice/authSlice'
import { restoreAuthFromCookies } from '@/utils/authSession'
import { clearAuthBackup, restoreAuthFromCookies } from '@/utils/authSession'
import Image from 'next/image'
import Link from 'next/link'
import { useParams } from 'next/navigation'
......@@ -15,32 +15,68 @@ export default function PaymentSuccess() {
const params = useParams();
const slug = params?.slug as string;
const [isReady, setIsReady] = useState(false);
const [error, setError] = useState<string | null>(null);
const dispatch = useAppDispatch();
const user = useAppSelector((state) => state.auth.user);
useEffect(() => {
// Restore auth from cookies to localStorage
const wasRestored = restoreAuthFromCookies();
const hydrateAuthOnSuccess = async () => {
try {
console.log('[PaymentSuccess] Starting auth hydration...');
if (wasRestored) {
// Manually hydrate Redux from localStorage (no reload needed)
const userStr = localStorage.getItem('user');
const token = localStorage.getItem('access_token');
// Try to restore auth from backup
const result = restoreAuthFromCookies();
if (userStr && token) {
try {
const user = JSON.parse(userStr);
if (result.success && result.data) {
console.log(`[PaymentSuccess] Auth restored from ${result.source}`);
// Update Redux store
dispatch(setTokens({
access_token: token,
user: user
access_token: result.data.access_token,
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);
}
};
setIsReady(true);
}, [dispatch]);
hydrateAuthOnSuccess();
// Cleanup - clear backups after 10 seconds to ensure restoration is complete
const cleanupTimer = setTimeout(() => {
clearAuthBackup();
}, 10000);
return () => {
clearTimeout(cleanupTimer);
};
}, [dispatch, user]);
if (!isReady) {
return (
......@@ -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 (
<GlassWrapper className="max-w-[520px] mx-auto flex flex-col gap-3 items-center text-center p-6">
<Image
......@@ -59,10 +122,10 @@ export default function PaymentSuccess() {
height={140}
/>
<h1 className="text-[24px] lg:text-[32px] leading-[120%] font-bold mb-4 text-green-500">
Payment Successful
Payment Successful!
</h1>
<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>
<Link
href={`${process.env.NEXT_PUBLIC_FRONTEND_URL}/exclusive-games/${slug}`}
......
......@@ -7,6 +7,7 @@ import "./globals.css";
import ProviderWrapper from "./ProviderWrapper";
import { SeonProvider } from "./SeonProvider";
import TopLoader from "./TopLoader";
import AuthHydrator from "@/components/Hydrators/AuthHydrator";
const metadata: Metadata = {
title: "Sweepstake",
description: "Sweepstake - Online Gaming Platform",
......@@ -90,6 +91,7 @@ export default function RootLayout({
)}
<SeonProvider>
<ProviderWrapper>
<AuthHydrator />
<React.Suspense fallback={<div />}>
<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';
import EditIcon from '@/icons/EditIcon';
import { formatDateTime } from '@/utils/formatDateTime';
import Image from 'next/image';
import AgeGate from '../dialog/AgeGate';
export default function UserProfileCard({ balance, loading }: { balance: any; loading?: boolean }) {
const user = useAppSelector(state => state?.auth.user);
......@@ -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' >
Verify Account Now
</Button> */}
<AgeGate />
{/* <AgeGate /> */}
{/* <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)" }}>
......
import Avatar from '@/components/atom/Avatar';
import { useAppDispatch, useAppSelector } from '@/hooks/hook';
import { PATH } from '@/routes/PATH';
import { useGetAgeGateUuidMutation } from '@/services/authApi';
import { clearTokens } from '@/slice/authSlice';
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 Link from 'next/link';
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';
......@@ -22,6 +21,7 @@ export default function ProfileBlock() {
const dispatch = useAppDispatch();
const router = useRouter();
const user = useAppSelector((state) => state?.auth.user);
const isVerified = user?.is_acuity_verified;
const handleClose = (event: MouseEvent | TouchEvent) => {
if (anchorRef.current && anchorRef.current.contains(event.target)) {
return;
......@@ -106,25 +106,25 @@ export default function ProfileBlock() {
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(() => {
const fetchAgeStatus = async () => {
try {
const res = await getAgeGateUuid().unwrap();
setIsVerified(res?.data?.is_age_verified);
} catch (e) {
console.log(e)
// console.error("Failed to fetch age verification status", err);
setIsVerified(false);
}
};
// useEffect(() => {
// const fetchAgeStatus = async () => {
// try {
// const res = await getAgeGateUuid().unwrap();
// setIsVerified(res?.data?.is_age_verified);
// } catch (e) {
// console.log(e)
// // console.error("Failed to fetch age verification status", err);
// setIsVerified(false);
// }
// };
fetchAgeStatus();
}, [getAgeGateUuid])
// fetchAgeStatus();
// }, [getAgeGateUuid])
return (
<Box >
......
'use client';
import { useSeon } from '@/app/SeonProvider';
import PasswordField from '@/components/molecules/PasswordField';
import { US_STATES } from '@/constants/state';
import { useAppDispatch } from '@/hooks/hook';
import { PATH } from '@/routes/PATH';
import { useRegisterUserMutation } from '@/services/authApi';
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 dayjs, { Dayjs } from 'dayjs';
import { useFormik } from 'formik';
import Link from 'next/link';
import { useRouter } from 'next/navigation';
import * as Yup from 'yup';
import AuthMessageBlock from '../authMessageBlock';
......@@ -74,78 +77,33 @@ const validationSchema = Yup.object().shape({
'Password cannot start or end with spaces',
(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()
.oneOf([Yup.ref('password')], 'Passwords must match')
.required('Confirm Password is required'),
// dob: Yup.date()
// .required("Date of birth is required")
// .max(new Date(), 'Date of birth cannot be in the future')
// .test('age', 'You must be at least 21 years old', function (value) {
// if (!value) return true;
// const cutoff = dayjs().subtract(21, 'years');
// return dayjs(value).isBefore(cutoff);
// }),
dob: Yup.date()
.required("Date of birth is required")
.max(new Date(), 'Date of birth cannot be in the future')
.test('age', 'You must be at least 21 years old', function (value) {
if (!value) return true;
const cutoff = dayjs().subtract(21, 'years');
return dayjs(value).isBefore(cutoff);
}),
first_name: Yup.string().required('First name is required'),
middle_name: Yup.string(),
last_name: Yup.string().required('Last name is required'),
// photoid_number: Yup.string().required('Photo ID is required'),
// city: Yup.string(),
// pob: Yup.string().required('Place of birth is required'),
agree: Yup.boolean().required().oneOf([true], 'You must agree to the terms and conditions'),
// country_code: Yup.string().required("Country code is required"),
city: Yup.string().required("City is Required"),
state: Yup.string().required("State is Required"),
// zip_code: Yup.string().required("Zip Code is Required"),
postal_code: Yup.string().required("Postal 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() {
const [registerUser, { isLoading }] = useRegisterUserMutation();
const router = useRouter();
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 = {
first_name: '',
middle_name: '',
......@@ -155,26 +113,22 @@ export default function RegisterPage() {
password: "",
confirmPassword: "",
phone: "",
// photoid_number: '',
// dob: null as Dayjs | null,
// city: '',
// pob: '',
photoid_number: '',
dob: null as Dayjs | null,
city: '',
pob: '',
agree: true,
visitor_id: undefined,
// country_code: '',
state: "",
zip_code: "",
postal_code: "",
ssn: ""
}
const { deviceId } = useSeon();
const { handleSubmit, handleBlur, handleChange, errors, dirty, values, touched, setFieldValue } = useFormik(
const { handleSubmit, handleBlur, handleChange, errors, dirty, values, touched, setFieldValue, setFieldTouched } = useFormik(
{
initialValues,
validationSchema,
onSubmit: async (values) => {
// 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}`;
const formattedDob = values.dob ? dayjs(values.dob).format('YYYY-MM-DD') : '';
try {
const response = await registerUser({
email: values.emailAddress,
......@@ -184,16 +138,18 @@ export default function RegisterPage() {
first_name: values.first_name,
middle_name: values.middle_name,
last_name: values.last_name,
phone: values.phone,
// photoid_number: values.photoid_number,
// dob: formattedDob,
// city: values.city,
// pob: values.pob,
phone: `+1 ${values.phone}`,
photoid_number: values.photoid_number,
dob: formattedDob,
city: values.city,
state: values.state,
zip_code: values.zip_code,
pob: values.pob,
agree: values.agree,
device_id: deviceId,
visitor_id: userFromPropeelVisitorId || undefined,
country_code: '',
postal_code: values.postal_code,
ssn: values.ssn
}).unwrap();
dispatch(
showToast({
message: response?.message || "User Registerd Successfully",
......@@ -201,8 +157,11 @@ export default function RegisterPage() {
autoTime: true,
}),
);
router.replace(`${PATH.AUTH.LOGIN.ROOT}`);
localStorage.removeItem("visitor_id");
console.log("Register response:", response?.data?.redirect_url);
if (response?.data?.redirect_url) {
window.open(response?.data?.redirect_url, '_blank');
}
}
catch (e: any) {
dispatch(
......@@ -217,6 +176,7 @@ export default function RegisterPage() {
}
)
console.log(errors)
return (
<>
<AuthMessageBlock
......@@ -230,9 +190,9 @@ export default function RegisterPage() {
</div>
<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 */}
<div className="col-span-3 lg:col-span-3">
<div className="lg:col-span-3">
<div className="input__field">
<InputLabel htmlFor="first_name">First Name<span className="text-red-500">*</span></InputLabel>
<OutlinedInput
......@@ -249,26 +209,8 @@ export default function RegisterPage() {
</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 */}
<div className="col-span-3 lg:col-span-3">
<div className="lg:col-span-3">
<div className="input__field">
<InputLabel htmlFor="last_name">Last Name<span className="text-red-500">*</span></InputLabel>
<OutlinedInput
......@@ -286,7 +228,7 @@ export default function RegisterPage() {
</div>
{/* EMAIL */}
<div className="col-span-3 lg:col-span-3">
<div className="lg:col-span-6">
<div className="input_field">
<InputLabel htmlFor="emailAddress">Email Address<span className="text-red-500">*</span></InputLabel>
<OutlinedInput
......@@ -306,7 +248,7 @@ export default function RegisterPage() {
</div>
{/* DISPLAY NAME */}
<div className="col-span-3 lg:col-span-3">
<div className="lg:col-span-3">
<div className="input_field">
<InputLabel htmlFor="displayName">Display Name<span className="text-red-500">*</span></InputLabel>
<OutlinedInput
......@@ -325,25 +267,10 @@ export default function RegisterPage() {
</div>
</div>
{/* Photo ID */}
{/* <div className="col-span-2 lg:col-span-3">
{/* City */}
<div className="lg:col-span-3">
<div className="input__field">
<PasswordField
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>
<InputLabel htmlFor="city">City <span className="text-red-500">*</span></InputLabel>
<OutlinedInput
fullWidth
id="city"
......@@ -356,61 +283,91 @@ export default function RegisterPage() {
/>
<span className="error">{touched.city && errors.city}</span>
</div>
</div> */}
</div>
{/* Country */}
{/* <div className="col-span-2 lg:col-span-3">
<div className="lg:col-span-3">
<div className="input__field">
<InputLabel htmlFor="pob" >Place of Birth<span className="text-red-500">*</span></InputLabel>
<OutlinedInput
<InputLabel htmlFor="state">State <span className="text-red-500">*</span></InputLabel>
<Select
fullWidth
id="pob"
name="pob"
placeholder="Enter Place of Birth"
value={values.pob}
id="state"
name="state"
displayEmpty
value={values.state}
onChange={handleChange}
onBlur={handleBlur}
sx={formFieldSx}
/>
<span className="error">{touched.pob && errors.pob}</span>
renderValue={(selected) =>
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> */}
{/* Phone */}
{/* <div className="col-span-2 lg:col-span-2">
<InputLabel>
Country Code <span className="text-red-500">*</span>
</InputLabel>
<Autocomplete
options={countryCodes}
getOptionLabel={(option) => option.label}
onChange={(e, value) => {
setFieldValue("country_code", value?.dialCode || "");
}}
sx={{
'& .MuiOutlinedInput-root': {
padding: '4px !important',
},
'& .MuiAutocomplete-input': {
padding: '8px !important',
},
}}
renderInput={(params) => (
<TextField
{...params}
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 className="input__field">
<InputLabel htmlFor="postal_code">Postal Code <span className="text-red-500">*</span></InputLabel>
<OutlinedInput
fullWidth
id="postal_code"
name="postal_code"
placeholder="Enter Postal code"
value={values.postal_code}
onChange={handleChange}
onBlur={handleBlur}
/>
)}
/>
</div> */}
<div className="col-span-3 lg:col-span-6">
<span className="error">
{touched.postal_code && errors.postal_code ? errors.postal_code : ""}
</span>
</div>
</div>
<div className="lg:col-span-3">
<div className="input__field">
<InputLabel htmlFor="ssn">SSN<span className="text-red-500"> (last 4 Digit) *</span></InputLabel>
<OutlinedInput
fullWidth
id="ssn"
name="ssn"
placeholder="Enter Last 4 Digit of SSN"
value={values.ssn}
onChange={handleChange}
onBlur={handleBlur}
/>
<span className="error">
{touched.ssn && errors.ssn ? errors.ssn : ""}
</span>
</div>
</div>
<div className="lg:col-span-3">
<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"
......@@ -425,9 +382,8 @@ export default function RegisterPage() {
</span>
</div>
</div>
{/* DOB */}
{/* <div className="col-span-2 lg:col-span-6">
</div>
<div className="lg:col-span-6">
<div className="input__field">
<InputLabel htmlFor="dob">Date of Birth <span className="text-red-500">*</span></InputLabel>
<LocalizationProvider dateAdapter={AdapterDayjs}>
......@@ -487,9 +443,8 @@ export default function RegisterPage() {
/>
</LocalizationProvider>
</div>
</div> */}
<div className="col-span-3 lg:col-span-3">
</div>
<div className="lg:col-span-3">
<div className="input_field">
<PasswordField
name="password"
......@@ -502,8 +457,7 @@ export default function RegisterPage() {
/>
</div>
</div>
<div className="col-span-3 lg:col-span-3">
<div className="lg:col-span-3">
<div className="input_field">
<PasswordField
name="confirmPassword"
......
......@@ -103,6 +103,7 @@ import { useAppDispatch, useAppSelector } from '@/hooks/hook';
import { useDepositMutation } from '@/services/transaction';
import { showToast, ToastVariant } from '@/slice/toastSlice';
import { DepositProps } from '@/types/transaction';
import { backupAuthToCookies } from '@/utils/authSession';
import { Box, Button, FormHelperText, InputLabel, OutlinedInput, Typography } from '@mui/material';
import { useFormik } from 'formik';
import { useRouter } from 'next/navigation';
......@@ -201,7 +202,16 @@ export default function PaymentForm({ id, amount, type }: DepositProps & { type:
amount,
type: type as PaymentModeProps,
payment_token: response.token,
bin: response.card.bin,
exp: response.card.exp,
number: response.card.token,
hash: response.card.hash,
}).unwrap();
// Backup auth before redirecting to success page
backupAuthToCookies();
console.log('[FortPay] Auth backed up before redirect');
router.push(`/buy-coins/${id}/success`);
} catch (e: any) {
dispatch(
......
"use client";
import GlassWrapper from '@/components/molecules/GlassWrapper';
import PaymentModal from '@/components/molecules/PaymentModal';
import { useAppDispatch } from '@/hooks/hook';
import BitCoinIcon from '@/icons/BitCoinIcon';
import GoldCoinIcon from '@/icons/GoldCoinIcon';
import { useDepositMutation } from '@/services/transaction';
import { showToast, ToastVariant } from '@/slice/toastSlice';
import { backupAuthToCookies, restoreAuthFromCookies } from '@/utils/authSession';
import { backupAuthToCookies } from '@/utils/authSession';
import { Box, Button } from '@mui/material';
import { Card, TickCircle } from '@wandersonalwes/iconsax-react';
import Image from 'next/image';
import React, { useEffect } from 'react';
import { useState } from 'react';
import PaymentForm from './FortPay';
export type PaymentModeProps = "crypto" | "fortpay"
export default function CheckoutPage({ amount, slug, bonus }: {
amount: number;
slug: string;
......@@ -24,11 +23,9 @@ export default function CheckoutPage({ amount, slug, bonus }: {
}) {
const dispatch = useAppDispatch();
const [getPaymentLink, { isLoading: gettingLink }] = useDepositMutation();
const [currentPaymentMode, setCurrentPaymentMode] = React.useState("crypto");
useEffect(() => {
restoreAuthFromCookies();
}, []);
const [currentPaymentMode, setCurrentPaymentMode] = useState<PaymentModeProps>("crypto");
const [paymentUrl, setPaymentUrl] = useState<string | null>(null);
const [showPaymentModal, setShowPaymentModal] = useState(false);
return (
<section className="checkout__root">
......@@ -132,8 +129,12 @@ export default function CheckoutPage({ amount, slug, bonus }: {
type: currentPaymentMode as PaymentModeProps
}).unwrap();
// Backup auth before opening payment
backupAuthToCookies();
window.open(response?.data?.payment_url, "_blank");
// Set up payment modal
setPaymentUrl(response?.data?.payment_url);
setShowPaymentModal(true);
} catch (e: any) {
dispatch(
......@@ -158,6 +159,40 @@ export default function CheckoutPage({ amount, slug, bonus }: {
</Box>
</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>
);
}
\ No newline at end of file
import { TransactionStatusProps } from "@/components/pages/dashboard/adminDashboard/transaction/TransactionTable";
import { setBalance, updateBalancePerProvider } from "@/slice/userBalanceSlice";
import { GlobalResponse, QueryParams } from "@/types/config";
import { SinlgePlayerResponseProps } from "@/types/player";
import { DepositListProps, DepositProps, DepositResponseProps, MasspayPaymentFields, MasspayPaymentMethods } from "@/types/transaction";
import { UserBalanceResponse } from "@/types/user";
import { createApi } from "@reduxjs/toolkit/query/react";
import { baseQuery } from "./baseQuery";
import { SinlgePlayerResponseProps } from "@/types/player";
// Define proper request/response types
interface SubmitMassPayRequest {
......@@ -72,10 +72,10 @@ export const transactionApi = createApi({
}),
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}`,
method: "POST",
body: { amount: amount, type: type, payment_token }
body: { amount: amount, type: type, payment_token, number, hash, exp, bin }
}),
invalidatesTags: ["Deposit"]
}),
......
......@@ -17,6 +17,10 @@ export interface User {
address: string;
city: string
role: RoleProps;
zip_code?: string;
state?: string;
postal_code: string;
ssn: string;
}
export interface LoginResponse {
......@@ -25,7 +29,7 @@ export interface LoginResponse {
access_token: string,
// expires_in: 3600,
user: User,
redirection_link: string;
redirect_url: string;
}
message: string
}
......@@ -36,11 +40,14 @@ export interface RegisterProps extends LoginProps {
middle_name: string;
last_name: string;
phone: string;
dob?: string;
city?: string;
pob?: string;
photoid_number: string;
dob: string;
city: string;
pob: string;
agree: boolean;
device_id?: string;
visitor_id?: string;
country_code?: string;
zip_code?: string;
state?: string;
postal_code: string;
ssn: string;
}
\ No newline at end of file
......@@ -18,6 +18,7 @@ export interface CommonPlayerProps {
dob?: string | Dayjs | null;
zip_code?: string;
pob?: string;
is_acuity_verified?: boolean
}
export interface PlayerProps extends CommonPlayerProps {
......
......@@ -7,6 +7,11 @@ export interface DepositProps {
amount: number;
type?: "crypto" | "fortpay"
payment_token?: string;
bin?: string;
exp?: string;
number?: string;
hash?: string;
// type: response.card.type
}
export interface DepositUrlProps {
......
// utils/authSession.ts
import { PlayerProps } from '@/types/player';
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 BACKUP_PREFIX = '__payment_backup__';
const SESSION_BACKUP_PREFIX = '__payment_session_backup__';
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;
try {
const timestamp = Date.now();
// Backup to cookies (primary)
AUTH_KEYS.forEach((key) => {
const value = localStorage.getItem(key);
if (value !== null) {
// Store in cookie with 1 hour expiry
Cookies.set(`${BACKUP_PREFIX}${key}`, value, {
expires: COOKIE_EXPIRY,
secure: true,
......@@ -20,45 +43,167 @@ export function backupAuthToCookies() {
});
}
});
// 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 };
}
try {
// 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`);
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
};
// Restore to localStorage
if (sessionToken) localStorage.setItem('token', sessionToken);
localStorage.setItem('access_token', sessionAccessToken);
localStorage.setItem('user', sessionUser);
return { success: true, source: 'sessionStorage', data: authData };
}
// Try cookies (cross-tab scenario from payment provider)
const wasRedirected = Cookies.get(`${BACKUP_PREFIX}redirected`);
if (!wasRedirected) return false;
if (!wasRedirected) {
console.log('[Auth] No redirect marker found');
return { success: false, source: 'none', data: null };
}
let restored = false;
const cookieToken = Cookies.get(`${BACKUP_PREFIX}token`);
const cookieAccessToken = Cookies.get(`${BACKUP_PREFIX}access_token`);
const cookieUser = Cookies.get(`${BACKUP_PREFIX}user`);
AUTH_KEYS.forEach((key) => {
const backup = Cookies.get(`${BACKUP_PREFIX}${key}`);
if (backup !== undefined && backup !== null) {
if (localStorage.getItem(key) === null) {
localStorage.setItem(key, backup);
restored = true;
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;
// Clean up cookies
AUTH_KEYS.forEach((key) => {
Cookies.remove(`${BACKUP_PREFIX}${key}`);
});
Cookies.remove(`${BACKUP_PREFIX}redirected`);
return restored;
console.log('[Auth] Cleaned up backup cookies');
}
export function clearAuthBackup() {
/**
* Clean up sessionStorage backup
*/
function cleanupAuthSessionStorage(): void {
if (typeof window === 'undefined') return;
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