Implement stripe and add success and cancel page

This commit is contained in:
Hemant Vishwakarma
2026-04-22 16:51:15 +05:30
parent 6e20b8a544
commit 205b19ae50
8 changed files with 523 additions and 172 deletions

54
package-lock.json generated
View File

@@ -35,12 +35,15 @@
"@radix-ui/react-toggle-group": "^1.1.2",
"@radix-ui/react-tooltip": "^1.1.8",
"@reduxjs/toolkit": "^2.11.2",
"@stripe/react-stripe-js": "^6.2.0",
"@stripe/stripe-js": "^9.2.0",
"@tailwindcss/postcss": "^4.1.13",
"@tailwindcss/vite": "^4.1.14",
"class-variance-authority": "^0.7.1",
"clsx": "*",
"cmdk": "^1.1.1",
"embla-carousel-react": "^8.6.0",
"i18n-iso-countries": "^7.14.0",
"input-otp": "^1.4.2",
"lucide-react": "^0.487.0",
"motion": "*",
@@ -2237,6 +2240,27 @@
"integrity": "sha512-e7Mew686owMaPJVNNLs55PUvgz371nKgwsc4vxE49zsODpJEnxgxRo2y/OKrqueavXgZNMDVj3DdHFlaSAeU8g==",
"license": "MIT"
},
"node_modules/@stripe/react-stripe-js": {
"version": "6.2.0",
"resolved": "https://registry.npmjs.org/@stripe/react-stripe-js/-/react-stripe-js-6.2.0.tgz",
"integrity": "sha512-GSCErjljZEQv9LaxP30xGOwstcMyyUzb5JyihXwvjOU95yrfhbiPG4K2KkwxYxn+WY0/AyHsRhPPoGRw7urBzg==",
"dependencies": {
"prop-types": "^15.7.2"
},
"peerDependencies": {
"@stripe/stripe-js": ">=9.2.0 <10.0.0",
"react": ">=16.8.0 <20.0.0",
"react-dom": ">=16.8.0 <20.0.0"
}
},
"node_modules/@stripe/stripe-js": {
"version": "9.2.0",
"resolved": "https://registry.npmjs.org/@stripe/stripe-js/-/stripe-js-9.2.0.tgz",
"integrity": "sha512-YSzLC0t6VS9MDdPTynSMqU8IxrItFUjkDORALFT6sSMR/XZ5Vgm3RDp/Gk7z727MC4A9s4MFVel0gF0c7+kdrg==",
"engines": {
"node": ">=12.16"
}
},
"node_modules/@swc/core": {
"version": "1.13.5",
"resolved": "https://registry.npmjs.org/@swc/core/-/core-1.13.5.tgz",
@@ -3073,7 +3097,6 @@
"integrity": "sha512-yCAeZl7a0DxgNVteXFHt9+uyFbqXGy/ShC4BlcHkoE0AfGXYv/BUiplV72DjMYXHDBXFjhvr6DD1NiRVfB4j8g==",
"devOptional": true,
"license": "MIT",
"peer": true,
"dependencies": {
"undici-types": "~6.21.0"
}
@@ -3084,7 +3107,6 @@
"integrity": "sha512-cMoR+FoAf/Jyq6+Df2/Z41jISvGZZ2eTlnsaJRptmZ76Caldwy1odD4xTr/gNV9VLj0AWgg/nmkevIyUfIIq5w==",
"devOptional": true,
"license": "MIT",
"peer": true,
"dependencies": {
"csstype": "^3.0.2"
}
@@ -3095,7 +3117,6 @@
"integrity": "sha512-qXRuZaOsAdXKFyOhRBg6Lqqc0yay13vN7KrIg4L7N4aaHN68ma9OK3NE1BoDFgFOTfM7zg+3/8+2n8rLUH3OKQ==",
"devOptional": true,
"license": "MIT",
"peer": true,
"peerDependencies": {
"@types/react": "^19.0.0"
}
@@ -3346,6 +3367,11 @@
"integrity": "sha512-ypdmJU/TbBby2Dxibuv7ZLW3Bs1QEmM7nHjEANfohJLvE0XVujisn1qPJcZxg+qDucsr+bP6fLD1rPS3AhJ7EQ==",
"license": "MIT"
},
"node_modules/diacritics": {
"version": "1.3.0",
"resolved": "https://registry.npmjs.org/diacritics/-/diacritics-1.3.0.tgz",
"integrity": "sha512-wlwEkqcsaxvPJML+rDh/2iS824jbREk6DUMUKkEaSlxdYHeS43cClJtsWglvw2RfeXGm6ohKDqsXteJ5sP5enA=="
},
"node_modules/dom-helpers": {
"version": "5.2.1",
"resolved": "https://registry.npmjs.org/dom-helpers/-/dom-helpers-5.2.1.tgz",
@@ -3360,8 +3386,7 @@
"version": "8.6.0",
"resolved": "https://registry.npmjs.org/embla-carousel/-/embla-carousel-8.6.0.tgz",
"integrity": "sha512-SjWyZBHJPbqxHOzckOfo8lHisEaJWmwd23XppYFYVh10bU66/Pn5tkVkbkCMZVdbUE5eTCI2nD8OyIP4Z+uwkA==",
"license": "MIT",
"peer": true
"license": "MIT"
},
"node_modules/embla-carousel-react": {
"version": "8.6.0",
@@ -3527,6 +3552,17 @@
"integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==",
"license": "ISC"
},
"node_modules/i18n-iso-countries": {
"version": "7.14.0",
"resolved": "https://registry.npmjs.org/i18n-iso-countries/-/i18n-iso-countries-7.14.0.tgz",
"integrity": "sha512-nXHJZYtNrfsi1UQbyRqm3Gou431elgLjKl//CYlnBGt5aTWdRPH1PiS2T/p/n8Q8LnqYqzQJik3Q7mkwvLokeg==",
"dependencies": {
"diacritics": "1.3.0"
},
"engines": {
"node": ">= 12"
}
},
"node_modules/immer": {
"version": "11.1.4",
"resolved": "https://registry.npmjs.org/immer/-/immer-11.1.4.tgz",
@@ -3945,7 +3981,6 @@
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz",
"integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==",
"license": "MIT",
"peer": true,
"engines": {
"node": ">=12"
},
@@ -4003,7 +4038,6 @@
"resolved": "https://registry.npmjs.org/react/-/react-18.3.1.tgz",
"integrity": "sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==",
"license": "MIT",
"peer": true,
"dependencies": {
"loose-envify": "^1.1.0"
},
@@ -4030,7 +4064,6 @@
"resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.3.1.tgz",
"integrity": "sha512-5m4nQKp+rZRb09LNH59GM4BxTh9251/ylbKIbpe7TpGxfJ+9kv6BLkLBXIjjspbgbnIBNqlI23tRnTWT0snUIw==",
"license": "MIT",
"peer": true,
"dependencies": {
"loose-envify": "^1.1.0",
"scheduler": "^0.23.2"
@@ -4066,7 +4099,6 @@
"resolved": "https://registry.npmjs.org/react-redux/-/react-redux-9.2.0.tgz",
"integrity": "sha512-ROY9fvHhwOD9ySfrF0wmvu//bKCQ6AeZZq1nJNtbDC+kk5DuSuNX/n6YWYF/SYy7bSba4D4FSz8DJeKY/S/r+g==",
"license": "MIT",
"peer": true,
"dependencies": {
"@types/use-sync-external-store": "^0.0.6",
"use-sync-external-store": "^1.4.0"
@@ -4269,8 +4301,7 @@
"version": "5.0.1",
"resolved": "https://registry.npmjs.org/redux/-/redux-5.0.1.tgz",
"integrity": "sha512-M9/ELqF6fy8FwmkpnF0S3YKOqMyoWJ4+CS5Efg2ct3oY9daQvd/Pc71FpGZsVsbl3Cpb+IIcjBDUnnyBdQbq4w==",
"license": "MIT",
"peer": true
"license": "MIT"
},
"node_modules/redux-thunk": {
"version": "3.1.0",
@@ -4533,7 +4564,6 @@
"resolved": "https://registry.npmjs.org/vite/-/vite-6.3.5.tgz",
"integrity": "sha512-cZn6NDFE7wdTpINgs++ZJ4N49W2vRp8LCKrn3Ob1kYNtOo21vfDoaV5GzBfLU4MovSAB8uNRm4jgzVQZ+mBzPQ==",
"license": "MIT",
"peer": true,
"dependencies": {
"esbuild": "^0.25.0",
"fdir": "^6.4.4",

View File

@@ -30,12 +30,15 @@
"@radix-ui/react-toggle-group": "^1.1.2",
"@radix-ui/react-tooltip": "^1.1.8",
"@reduxjs/toolkit": "^2.11.2",
"@stripe/react-stripe-js": "^6.2.0",
"@stripe/stripe-js": "^9.2.0",
"@tailwindcss/postcss": "^4.1.13",
"@tailwindcss/vite": "^4.1.14",
"class-variance-authority": "^0.7.1",
"clsx": "*",
"cmdk": "^1.1.1",
"embla-carousel-react": "^8.6.0",
"i18n-iso-countries": "^7.14.0",
"input-otp": "^1.4.2",
"lucide-react": "^0.487.0",
"motion": "*",

View File

@@ -37,6 +37,8 @@ import { PaymentDetailsPage } from './pages/PaymentDetailsPage';
import { CartPageDesign } from './pages/CartPageDesign';
import { CheckoutPage2 } from './pages/CheckoutPage2';
import { SuperSavingsDetailsPage } from './pages/SuperSavingsDetailsPage';
import { PaymentSuccessPage } from './pages/PaymentSuccessPage';
import { PaymentCancelPage } from './pages/PaymentCancelPage';
// User type definition
interface User {
@@ -302,6 +304,32 @@ export function AppRouter({
onBackClick={() => navigate(-1)} />
</motion.div>
} />
<Route path="/success" element={
<motion.div key="super-savings" {...pageTransition}>
<PaymentSuccessPage
// onHomeClick={onHomeClick}
// onPassesClick={onPassesClick}
onSignInClick={onSignInClick}
onSignOutClick={onSignOutClick}
currentPage="success"
user={user}
/>
</motion.div>
} />
<Route path="/cancel" element={
<motion.div key="super-savings" {...pageTransition}>
<PaymentCancelPage
// onHomeClick={onHomeClick}
// onPassesClick={onPassesClick}
onSignInClick={onSignInClick}
onSignOutClick={onSignOutClick}
currentPage="cancel"
user={user}
/>
</motion.div>
} />
</Routes>
</AnimatePresence>
</>

View File

@@ -41,7 +41,29 @@ export const cardsApi = createApi({
body: cardBookingDetails
}),
}),
})
payForCard: builder.mutation({
query: (id) => ({
url: `/website/passes/${id}/pay`,
method: "POST",
body: {},
}),
}),
confirmCardPayment: builder.mutation({
query: (id) => ({
url: `/website/passes/${id}/confirm-payment`,
method: "POST",
// body: id,
}),
}),
}),
});
export const {
@@ -49,5 +71,8 @@ export const {
useGetCheckoutPageDataQuery,
useGetCardBookingDetailsQuery,
useStoreRecipientDetailsMutation,
useAddCardToCartMutation
useAddCardToCartMutation,
usePayForCardMutation,
useConfirmCardPaymentMutation
} = cardsApi;

View File

@@ -0,0 +1,89 @@
import React, { useEffect } from 'react';
import { useNavigate } from 'react-router-dom';
import { XCircle } from 'lucide-react';
import Navbar from '../components/Navbar';
import { Footer } from '../components/Footer';
interface PaymentCancelPageProps {
onHomeClick: () => void;
onPassesClick: () => void;
onSignInClick: () => void;
onSignOutClick?: () => void;
currentPage?: string;
user?: { email: string; name: string } | null;
}
export function PaymentCancelPage({
onHomeClick,
onPassesClick,
onSignInClick,
onSignOutClick,
currentPage,
user,
}: PaymentCancelPageProps) {
const navigate = useNavigate();
// ✅ Clear pending booking ID when user cancels
useEffect(() => {
localStorage.removeItem('pendingBookingId');
}, []);
return (
<div className="min-h-screen bg-[#fafafa] font-poppins">
<Navbar
activeCity="Melbourne"
onCityChange={() => {}}
onSignInClick={onSignInClick}
onSignOutClick={onSignOutClick}
onHomeClick={onHomeClick}
onPassesClick={onPassesClick}
onCheckoutClick={() => {}}
onAttractionsClick={() => {}}
onBlogsClick={() => {}}
onHowItWorksClick={() => {}}
onFAQClick={() => {}}
onPrivacyPolicyClick={() => {}}
onAboutUsClick={() => {}}
onProfileClick={() => {}}
onCityCardsClick={() => {}}
onMagicItineraryClick={() => {}}
onPostCardsClick={() => {}}
onOffersClick={() => {}}
onSuperSavingsClick={() => {}}
onEsimsClick={() => {}}
onHotelDiscountsClick={() => {}}
onCartClick={() => {}}
currentPage={currentPage as any}
user={user}
/>
<div className="w-full px-4 sm:px-6 lg:px-10 xl:px-16 pt-32 pb-24 max-w-[1440px] mx-auto flex justify-center items-center min-h-[60vh]">
<div className="bg-white rounded-2xl shadow-lg p-8 max-w-md w-full text-center">
<XCircle className="w-16 h-16 text-yellow-500 mx-auto mb-4" />
<h2 className="text-2xl font-semibold text-[#2a2a2a]">Payment Cancelled</h2>
<p className="text-[#555] mt-2">
You cancelled the payment process. No charges have been made.
</p>
<button
onClick={() => navigate(-1)}
className="mt-6 px-6 py-3 bg-[#F95F62] text-white rounded-xl font-medium hover:bg-[#e8545a] transition"
>
Go Back & Try Again
</button>
</div>
</div>
<Footer
onHomeClick={onHomeClick}
onPassesClick={onPassesClick}
onAttractionsClick={() => {}}
onBlogsClick={() => {}}
onHowItWorksClick={() => {}}
onFAQClick={() => {}}
onPrivacyPolicyClick={() => {}}
onAboutUsClick={() => {}}
onContactUsClick={() => {}}
/>
</div>
);
}

View File

@@ -1,8 +1,7 @@
import React, { useEffect, useState } from 'react';
import { motion, AnimatePresence } from 'motion/react';
import {
ArrowLeft, User, MapPin, Lock, Shield, ChevronDown,
Check, AlertCircle, Pencil, UserCheck, Gift
ArrowLeft, User, Lock, Shield, Pencil, UserCheck, Gift, AlertCircle
} from 'lucide-react';
import Navbar from '../components/Navbar';
import { Footer } from '../components/Footer';
@@ -11,9 +10,16 @@ import { Separator } from '../components/ui/separator';
import { useGetUserProfileDetailsQuery } from '../Redux/services/profile.service';
import LoadingSpinner from '../components/LoadingSpinner';
import { useNavigate, useParams } from 'react-router-dom';
import { useGetCardBookingDetailsQuery, useStoreRecipientDetailsMutation } from '../Redux/services/cards.service';
import {
useGetCardBookingDetailsQuery,
useStoreRecipientDetailsMutation,
usePayForCardMutation,
} from '../Redux/services/cards.service';
import { toast } from 'sonner';
import countries from 'i18n-iso-countries';
import enLocale from 'i18n-iso-countries/langs/en.json';
export interface CheckoutOrderItem {
city: string;
cardType: 'Flexi' | 'Unlimited';
@@ -53,7 +59,20 @@ interface PaymentDetailsPageProps {
user?: { email: string; name: string } | null;
}
/* ─── Editable field ─── */
// Register English locale for country codes
countries.registerLocale(enLocale);
const getCountryCode = (countryName: string): string => {
const code = countries.getAlpha2Code(countryName, 'en');
if (code) return code;
if (countryName.length === 2 && /^[A-Z]{2}$/i.test(countryName)) {
return countryName.toUpperCase();
}
console.warn(`Unknown country name: ${countryName}, defaulting to 'AU'`);
return 'AU';
};
/* ─── Editable field component ─── */
function Field({
label,
value,
@@ -64,7 +83,7 @@ function Field({
maxLength,
inputMode,
prefilled,
disabled = false, // ← New prop
disabled = false,
}: {
label: string;
value: string;
@@ -75,7 +94,7 @@ function Field({
maxLength?: number;
inputMode?: React.HTMLAttributes<HTMLInputElement>['inputMode'];
prefilled?: boolean;
disabled?: boolean; // ← Added
disabled?: boolean;
}) {
const [focused, setFocused] = useState(false);
@@ -94,7 +113,7 @@ function Field({
placeholder={placeholder}
maxLength={maxLength}
inputMode={inputMode}
disabled={disabled} // ← Applied
disabled={disabled}
className={`w-full border rounded-xl px-4 py-3 pr-10 font-poppins text-base font-normal text-[#2a2a2a] outline-none transition-all duration-200 placeholder:text-[#ccc]
${disabled
? 'bg-gray-100 text-gray-500 cursor-not-allowed border-gray-300'
@@ -107,13 +126,10 @@ function Field({
: 'border-gray-200'
}`}
/>
{/* Pencil icon only when prefilled AND not disabled AND not focused */}
{prefilled && !focused && !disabled && (
<Pencil className="absolute right-3 top-1/2 -translate-y-1/2 w-3.5 h-3.5 text-[#F95F62]/40" />
)}
</div>
{error && (
<span className="font-poppins text-xs font-normal text-red-500 flex items-center gap-1">
<AlertCircle className="w-3 h-3" />{error}
@@ -135,11 +151,8 @@ function CardTypeBadge({ cardType }: { cardType: 'Flexi' | 'Unlimited' }) {
);
}
/* ─── Main component ─── */
/* ─── Main Component ─── */
export function PaymentDetailsPage({
checkoutOrder,
onBackClick,
onPaymentComplete,
onHomeClick,
onPassesClick,
onAttractionsClick,
@@ -164,21 +177,19 @@ export function PaymentDetailsPage({
currentPage,
user,
}: PaymentDetailsPageProps) {
/* ── Purchase type ── */
const [selectedTab, setSelectedTab] = useState<'myself' | 'gift'>('myself');
/* ── Gift Recipient Details (Only editable fields) ── */
// Gift fields
const [giftFirstName, setGiftFirstName] = useState('');
const [giftLastName, setGiftLastName] = useState('');
const [giftEmail, setGiftEmail] = useState('');
const [giftPhone, setGiftPhone] = useState('');
const [giftCity, setGiftCity] = useState('');
const [giftCountry, setGiftCountry] = useState('');
const [giftIsd, setGiftIsd] = useState("")
const [giftMessage, setGiftMessage] = useState("")
const [giftIsd, setGiftIsd] = useState('');
const [giftMessage, setGiftMessage] = useState('');
/* ── Profile Data (Same as ProfilePage) ── */
// Profile data
const [formData, setFormData] = useState({
firstName: '',
lastName: '',
@@ -188,19 +199,19 @@ export function PaymentDetailsPage({
address1: '',
address2: '',
city: '',
postalCode: ''
postalCode: '',
});
const navigate = useNavigate()
const userId = localStorage.getItem("userId");
const { bookingId } = useParams()
const navigate = useNavigate();
const userId = localStorage.getItem('userId');
const { bookingId } = useParams();
const { data: userDetails, isLoading } = useGetUserProfileDetailsQuery(userId);
const { data } = useGetCardBookingDetailsQuery(bookingId);
const [storeRecipientDetails, { isLoading: savingChanges }] = useStoreRecipientDetailsMutation();
const [storeRecipientDetails] = useStoreRecipientDetailsMutation();
const [payForCard, { isLoading: isCreatingPayment }] = usePayForCardMutation();
const bookingDetails = data?.bookingDetails ?? null
const bookingDetails = data?.bookingDetails ?? null;
// Populate formData from API (exactly like ProfilePage)
useEffect(() => {
if (userDetails) {
setFormData({
@@ -217,43 +228,38 @@ export function PaymentDetailsPage({
}
}, [userDetails]);
/* ── Validation ── */
const [errors, setErrors] = useState<Record<string, string>>({});
const [submitting, setSubmitting] = useState(false);
const order = checkoutOrder || {
city: 'Melbourne', cardType: 'Flexi' as const,
days: 3, adults: 2, children: 0, quantity: 1, pricePerUnit: 49.50,
};
const subtotal = order.pricePerUnit * order.quantity;
const tax = subtotal * 0.1;
const total = subtotal + tax;
const validate = () => {
const e: Record<string, string> = {};
if (selectedTab === 'gift') {
if (!giftFirstName.trim()) e.giftFirstName = 'Required';
if (!giftLastName.trim()) e.giftLastName = 'Required';
if (!giftIsd.trim()) e.giftIsd = 'Required';
if (!giftMessage.trim()) e.giftMessage = 'Required';
if (!giftEmail.trim() || !/\S+@\S+\.\S+/.test(giftEmail)) {
e.giftEmail = 'Valid email required';
}
if (!giftPhone.trim() || !/^\+?[0-9]{7,15}$/.test(giftPhone)) {
e.giftPhone = 'Valid phone required';
}
if (!giftCity.trim()) e.giftCity = 'Required';
if (!giftCountry.trim()) e.giftCountry = 'Required';
}
return e;
};
const [isRedirecting, setIsRedirecting] = useState(false);
const handlePayment = async () => {
const validationErrors = validate();
setErrors(validationErrors);
if (Object.keys(validationErrors).length > 0) {
toast.error('Please fill all required fields');
return;
}
if (selectedTab === 'gift') {
const recipientDetails = {
isForSelf: true,
recipientFirstName: giftFirstName,
@@ -265,32 +271,40 @@ export function PaymentDetailsPage({
recipientCountry: giftCountry,
giftMessage: giftMessage,
};
const handleSaveProfile = async () => {
try {
console.log("Saving profile...", recipientDetails);
const response = await storeRecipientDetails({ recipientDetails, bookingId });
console.log(response)
toast.success("gift details saved successfully!");
} catch (error) {
console.error("Error saving profile:", error);
toast.error("Failed to update profile. Please try again.");
await storeRecipientDetails({ recipientDetails, bookingId }).unwrap();
toast.success('Gift details saved!');
} catch (err) {
console.error('Failed to save gift details:', err);
toast.error('Failed to save gift details. Please try again.');
return;
}
}
};
const handleSubmit = async () => {
const e = validate();
setErrors(e);
if (Object.keys(e).length > 0) return;
setIsRedirecting(true);
try {
console.log("Saving profile...", recipientDetails);
const response = await storeRecipientDetails({ recipientDetails, bookingId });
console.log(response)
toast.success("gift details saved successfully!");
} catch (error) {
console.error("Error saving profile:", error);
toast.error("Failed to update profile. Please try again.");
const payResponse = await payForCard(bookingId).unwrap();
console.log('payForCard response:', payResponse);
const { checkoutPageUrl } = payResponse;
localStorage.setItem('pendingBookingId', bookingId);
if (!checkoutPageUrl || typeof checkoutPageUrl !== 'string') {
throw new Error('Invalid checkout URL received from server');
}
if (!checkoutPageUrl.startsWith('http://') && !checkoutPageUrl.startsWith('https://')) {
throw new Error('Checkout URL must start with http:// or https://');
}
window.location.href = checkoutPageUrl;
} catch (err: any) {
console.error('Payment initiation error:', err);
const errorMsg = err?.data?.message || err?.message || 'Failed to initiate payment. Please try again.';
toast.error(errorMsg);
setIsRedirecting(false);
}
};
@@ -301,19 +315,33 @@ export function PaymentDetailsPage({
return (
<div className="min-h-screen bg-[#fafafa] font-poppins">
<Navbar
activeCity="Melbourne" onCityChange={() => { }} onSignInClick={onSignInClick} onSignOutClick={onSignOutClick}
onPassesClick={onPassesClick} onCheckoutClick={onCheckoutClick} onHomeClick={onHomeClick}
onAttractionsClick={onAttractionsClick} onBlogsClick={onBlogsClick} onHowItWorksClick={onHowItWorksClick}
onFAQClick={onFAQClick} onPrivacyPolicyClick={onPrivacyPolicyClick} onAboutUsClick={onAboutUsClick}
onProfileClick={onProfileClick} onCityCardsClick={onCityCardsClick} onMagicItineraryClick={onMagicItineraryClick}
onPostCardsClick={onPostCardsClick} onOffersClick={onOffersClick} onSuperSavingsClick={onSuperSavingsClick}
onEsimsClick={onEsimsClick} onHotelDiscountsClick={onHotelDiscountsClick} onCartClick={onCartClick}
currentPage={currentPage as any} user={user}
activeCity="Melbourne"
onCityChange={() => {}}
onSignInClick={onSignInClick}
onSignOutClick={onSignOutClick}
onPassesClick={onPassesClick}
onCheckoutClick={onCheckoutClick}
onHomeClick={onHomeClick}
onAttractionsClick={onAttractionsClick}
onBlogsClick={onBlogsClick}
onHowItWorksClick={onHowItWorksClick}
onFAQClick={onFAQClick}
onPrivacyPolicyClick={onPrivacyPolicyClick}
onAboutUsClick={onAboutUsClick}
onProfileClick={onProfileClick}
onCityCardsClick={onCityCardsClick}
onMagicItineraryClick={onMagicItineraryClick}
onPostCardsClick={onPostCardsClick}
onOffersClick={onOffersClick}
onSuperSavingsClick={onSuperSavingsClick}
onEsimsClick={onEsimsClick}
onHotelDiscountsClick={onHotelDiscountsClick}
onCartClick={onCartClick}
currentPage={currentPage as any}
user={user}
/>
<div className="w-full px-4 sm:px-6 lg:px-10 xl:px-16 pt-32 pb-24 max-w-[1440px] mx-auto">
{/* Back Button */}
<button
onClick={() => navigate(-1)}
className="flex items-center gap-2 text-[#8e8e8e] hover:text-[#2a2a2a] transition-colors font-poppins text-sm font-normal mb-8"
@@ -321,12 +349,10 @@ export function PaymentDetailsPage({
<ArrowLeft className="w-4 h-4" /> Back
</button>
{/* Page heading */}
<motion.div initial={{ opacity: 0, y: 20 }} animate={{ opacity: 1, y: 0 }} className="mb-8">
<div className="flex items-center gap-4 mb-2">
<h1 className="font-poppins text-2xl md:text-3xl lg:text-4xl leading-tight">
<span className="font-light">Review &
</span>{' '}
<span className="font-light">Review & </span>
<span className="font-bold italic bg-gradient-to-r from-[#F95F62] to-[#F95FAF] bg-clip-text text-transparent pr-2">Pay</span>
</h1>
<div className="flex items-center gap-2 text-[#22a86b]">
@@ -335,22 +361,19 @@ export function PaymentDetailsPage({
</div>
</div>
<p className="font-poppins text-sm leading-relaxed font-normal text-[#8e8e8e]">
Complete your purchase securely. Your payment information is protected.
Complete your purchase securely. You will be redirected to Stripe to enter your card details.
</p>
</motion.div>
<div className="grid lg:grid-cols-3 gap-8">
{/* LEFT: Forms */}
<motion.div initial={{ opacity: 0, x: -20 }} animate={{ opacity: 1, x: 0 }} className="lg:col-span-2">
<Card className="shadow-lg border-0 overflow-hidden">
{/* Purchase type tabs */}
<CardHeader className="pb-0 pt-6 px-6 border-b border-gray-100">
<div className="flex gap-2 mb-6">
<button
onClick={() => setSelectedTab('myself')}
className={`flex-1 flex items-center justify-center gap-2 py-3 rounded-xl font-poppins text-sm font-medium transition-all duration-200 ${selectedTab === 'myself'
className={`flex-1 flex items-center justify-center gap-2 py-3 rounded-xl font-poppins text-sm font-medium transition-all duration-200 ${
selectedTab === 'myself'
? 'bg-[#F95F62] text-white shadow-md shadow-[#F95F62]/20'
: 'bg-gray-100 text-[#555] hover:bg-gray-200'
}`}
@@ -360,7 +383,8 @@ export function PaymentDetailsPage({
</button>
<button
onClick={() => setSelectedTab('gift')}
className={`flex-1 flex items-center justify-center gap-2 py-3 rounded-xl font-poppins text-sm font-medium transition-all duration-200 ${selectedTab === 'gift'
className={`flex-1 flex items-center justify-center gap-2 py-3 rounded-xl font-poppins text-sm font-medium transition-all duration-200 ${
selectedTab === 'gift'
? 'bg-[#F95F62] text-white shadow-md shadow-[#F95F62]/20'
: 'bg-gray-100 text-[#555] hover:bg-gray-200'
}`}
@@ -372,8 +396,6 @@ export function PaymentDetailsPage({
</CardHeader>
<CardContent className="px-6 py-6 space-y-6">
{/* Pre-filled notice */}
<motion.div
initial={{ opacity: 0, y: -8 }}
animate={{ opacity: 1, y: 0 }}
@@ -400,12 +422,12 @@ export function PaymentDetailsPage({
<h2 className="font-poppins text-xl leading-snug font-semibold text-[#2a2a2a]">Personal Information</h2>
</div>
<div className="grid grid-cols-1 sm:grid-cols-2 gap-4">
<Field label="First Name" value={formData.firstName} onChange={() => { }} prefilled disabled={true} />
<Field label="Last Name" value={formData.lastName} onChange={() => { }} prefilled disabled={true} />
<Field label="First Name" value={formData.firstName} onChange={() => {}} prefilled disabled />
<Field label="Last Name" value={formData.lastName} onChange={() => {}} prefilled disabled />
</div>
<div className="grid grid-cols-1 sm:grid-cols-2 gap-4">
<Field label="Email Address" value={formData.email} onChange={() => { }} type="email" prefilled disabled={true} />
<Field label="Phone Number" value={formData.phone} onChange={() => { }} type="tel" prefilled disabled={true} />
<Field label="Email Address" value={formData.email} onChange={() => {}} type="email" prefilled disabled />
<Field label="Phone Number" value={formData.phone} onChange={() => {}} type="tel" prefilled disabled />
</div>
</motion.div>
@@ -426,7 +448,6 @@ export function PaymentDetailsPage({
<h3 className="font-poppins text-base font-semibold text-[#2a2a2a]">Gift Recipient Details</h3>
</div>
<div className="grid grid-cols-1 sm:grid-cols-2 gap-4">
<Field
label="Recipient First Name"
value={giftFirstName}
@@ -434,7 +455,6 @@ export function PaymentDetailsPage({
placeholder="Enter recipient's first name"
error={errors.giftFirstName}
/>
<Field
label="Recipient Last Name"
value={giftLastName}
@@ -446,20 +466,17 @@ export function PaymentDetailsPage({
label="Recipient ISD Code"
value={giftIsd}
onChange={setGiftIsd}
placeholder="Enter recipient's ISD"
placeholder="e.g., 61"
error={errors.giftIsd}
/>
<Field
label="Recipient Phone"
value={giftPhone}
onChange={setGiftPhone}
type="number"
type="tel"
placeholder="Enter recipient's phone number"
error={errors.giftPhone}
/>
<Field
label="Recipient Email"
value={giftEmail}
@@ -468,7 +485,6 @@ export function PaymentDetailsPage({
placeholder="Enter recipient's email"
error={errors.giftEmail}
/>
<Field
label="Recipient City"
value={giftCity}
@@ -476,7 +492,6 @@ export function PaymentDetailsPage({
placeholder="Enter recipient's city"
error={errors.giftCity}
/>
<Field
label="Recipient Country"
value={giftCountry}
@@ -488,7 +503,7 @@ export function PaymentDetailsPage({
label="Gift Message"
value={giftMessage}
onChange={setGiftMessage}
placeholder="Enter gift message"
placeholder="Write a heartfelt message"
error={errors.giftMessage}
/>
</div>
@@ -496,6 +511,7 @@ export function PaymentDetailsPage({
</motion.div>
)}
</AnimatePresence>
<Separator />
{/* Billing Address */}
@@ -507,28 +523,23 @@ export function PaymentDetailsPage({
<h2 className="font-poppins text-xl leading-snug font-semibold text-[#2a2a2a]">Billing Address</h2>
</div>
<div className="space-y-4">
<Field label="Address 1" value={formData.address1} onChange={() => { }} prefilled disabled={true} />
<Field label="Address 2" value={formData.address2} onChange={() => { }} prefilled disabled={true} />
<Field label="Address 1" value={formData.address1} onChange={() => {}} prefilled disabled />
<Field label="Address 2" value={formData.address2} onChange={() => {}} prefilled disabled />
<div className="grid grid-cols-1 sm:grid-cols-2 gap-4">
<Field label="City / Suburb" value={formData.city} onChange={() => { }} prefilled disabled={true} />
<Field label="State" value="Victoria" onChange={() => { }} prefilled disabled={true} />
<Field label="City / Suburb" value={formData.city} onChange={() => {}} prefilled disabled />
<Field label="State" value="Victoria" onChange={() => {}} prefilled disabled />
</div>
<div className="grid grid-cols-1 sm:grid-cols-2 gap-4">
<Field label="Postcode" value={formData.postalCode} onChange={() => { }} inputMode="numeric" prefilled disabled={true} />
<div className="flex flex-col gap-1">
<div className="relative">
<Field label="Country" value={formData.country} onChange={() => { }} prefilled disabled={true} />
</div>
</div>
<Field label="Postcode" value={formData.postalCode} onChange={() => {}} inputMode="numeric" prefilled disabled />
<Field label="Country" value={formData.country} onChange={() => {}} prefilled disabled />
</div>
</div>
</motion.div>
</CardContent>
</Card>
</motion.div>
{/* RIGHT: Order Summary (unchanged) */}
{/* Right Column: Order Summary & Payment Button */}
<motion.div initial={{ opacity: 0, x: 20 }} animate={{ opacity: 1, x: 0 }} className="lg:col-span-1">
<div className="lg:sticky lg:top-28 space-y-4">
<div className="bg-white rounded-2xl shadow-[0px_2px_16px_0px_rgba(0,0,0,0.06)] overflow-hidden">
@@ -538,10 +549,13 @@ export function PaymentDetailsPage({
<div className="px-6 py-5 border-b border-gray-100">
<div className="flex items-start gap-4">
<div className={`w-16 h-10 rounded-lg flex-shrink-0 flex items-center justify-center ${bookingDetails?.cardMode.toLowerCase() === 'flexi'
<div
className={`w-16 h-10 rounded-lg flex-shrink-0 flex items-center justify-center ${
bookingDetails?.cardMode?.toLowerCase() === 'flexi'
? 'bg-gradient-to-br from-[#f95faf] to-[#F95F62]'
: 'bg-gradient-to-br from-[#F95F62] to-[#c94245]'
}`}>
}`}
>
<span className="font-poppins text-[10px] font-semibold text-white">{bookingDetails?.cardMode}</span>
</div>
<div className="flex-1 min-w-0">
@@ -550,21 +564,21 @@ export function PaymentDetailsPage({
<CardTypeBadge cardType={bookingDetails?.cardMode} />
</div>
<p className="font-poppins text-sm font-normal text-[#8e8e8e] mt-0.5">
{bookingDetails?.cardMode.toLowerCase() === 'flexi' ? `${bookingDetails?.noOfAttractions} Attractions` : `${bookingDetails?.noOfDays} Days`}
{bookingDetails?.cardMode?.toLowerCase() === 'flexi'
? `${bookingDetails?.noOfAttractions} Attractions`
: `${bookingDetails?.noOfDays} Days`}
</p>
</div>
</div>
<div className="mt-4 space-y-2">
{[
{ label: 'Adults', value: bookingDetails?.totalAdult },
{ label: 'Children', value: bookingDetails?.totalChild },
// { label: 'Qty', value: order.quantity },
].map(({ label, value }) => (
<div key={label} className="flex items-center justify-between">
<span className="font-poppins text-sm font-normal text-[#8e8e8e]">{label}</span>
<span className="font-poppins text-sm font-medium text-[#2a2a2a]">{value}</span>
<div className="flex items-center justify-between">
<span className="font-poppins text-sm font-normal text-[#8e8e8e]">Adults</span>
<span className="font-poppins text-sm font-medium text-[#2a2a2a]">{bookingDetails?.totalAdult}</span>
</div>
<div className="flex items-center justify-between">
<span className="font-poppins text-sm font-normal text-[#8e8e8e]">Children</span>
<span className="font-poppins text-sm font-medium text-[#2a2a2a]">{bookingDetails?.totalChild}</span>
</div>
))}
</div>
</div>
@@ -593,37 +607,45 @@ export function PaymentDetailsPage({
<motion.button
whileHover={{ scale: 1.01 }}
whileTap={{ scale: 0.98 }}
onClick={handleSubmit}
disabled={submitting}
onClick={handlePayment}
disabled={isRedirecting || isCreatingPayment}
className="w-full py-4 rounded-2xl bg-[#F95F62] text-white font-poppins text-base font-semibold hover:bg-[#e8545a] transition-colors shadow-lg shadow-[#F95F62]/20 disabled:opacity-70 flex items-center justify-center gap-2"
>
{submitting ? (
{isRedirecting ? (
<>
<motion.div animate={{ rotate: 360 }} transition={{ duration: 1, repeat: Infinity, ease: 'linear' }} className="w-5 h-5 border-2 border-white/30 border-t-white rounded-full" />
Processing
<motion.div
animate={{ rotate: 360 }}
transition={{ duration: 1, repeat: Infinity, ease: 'linear' }}
className="w-5 h-5 border-2 border-white/30 border-t-white rounded-full"
/>
Redirecting to Stripe...
</>
) : (
<>
<Lock className="w-4 h-4" />
Complete Payment · ${bookingDetails?.totalAmount}
Proceed to Payment · ${bookingDetails?.totalAmount}
</>
)}
</motion.button>
<p className="font-poppins text-xs font-normal text-[#aaa] text-center">
By completing your purchase you agree to our Terms of Service and Privacy Policy
You will be redirected to Stripes secure checkout page to enter your card details.
By completing your purchase you agree to our Terms of Service and Privacy Policy.
</p>
</div>
</motion.div>
</div>
</div>
<Footer
onHomeClick={onHomeClick} onPassesClick={onPassesClick}
onAttractionsClick={onAttractionsClick} onBlogsClick={onBlogsClick}
onHowItWorksClick={onHowItWorksClick} onFAQClick={onFAQClick}
onPrivacyPolicyClick={onPrivacyPolicyClick} onAboutUsClick={onAboutUsClick}
onHomeClick={onHomeClick}
onPassesClick={onPassesClick}
onAttractionsClick={onAttractionsClick}
onBlogsClick={onBlogsClick}
onHowItWorksClick={onHowItWorksClick}
onFAQClick={onFAQClick}
onPrivacyPolicyClick={onPrivacyPolicyClick}
onAboutUsClick={onAboutUsClick}
onContactUsClick={onContactUsClick}
/>
</div>

View File

@@ -0,0 +1,153 @@
import React, { useEffect, useState } from 'react';
import { useNavigate, useSearchParams } from 'react-router-dom';
import { CheckCircle, XCircle, Loader2 } from 'lucide-react';
import { useConfirmCardPaymentMutation } from '../Redux/services/cards.service';
import { toast } from 'sonner';
import Navbar from '../components/Navbar';
import { Footer } from '../components/Footer';
interface PaymentSuccessPageProps {
onHomeClick: () => void;
onPassesClick: () => void;
onSignInClick: () => void;
onSignOutClick?: () => void;
currentPage?: string;
user?: { email: string; name: string } | null;
// Add other handlers if needed (optional)
}
export function PaymentSuccessPage({
onHomeClick,
onPassesClick,
onSignInClick,
onSignOutClick,
currentPage,
user,
}: PaymentSuccessPageProps) {
const [searchParams] = useSearchParams();
const sessionId = searchParams.get('session_id');
const [confirmPayment, { isLoading }] = useConfirmCardPaymentMutation();
const [status, setStatus] = useState<'loading' | 'success' | 'error'>('loading');
const [errorMsg, setErrorMsg] = useState<string>('');
const navigate = useNavigate();
useEffect(() => {
const confirm = async () => {
// Retrieve bookingId from localStorage (set before redirect)
const bookingId = localStorage.getItem('pendingBookingId');
if (!bookingId) {
setStatus('error');
setErrorMsg('Missing booking information. Please contact support.');
return;
}
if (!sessionId) {
setStatus('error');
setErrorMsg('Missing session ID. Please contact support.');
return;
}
try {
// ✅ Send both bookingId and sessionId to backend
await confirmPayment({ bookingId, sessionId }).unwrap();
setStatus('success');
toast.success('Payment confirmed! Your order is complete.');
// Clear the stored bookingId
localStorage.removeItem('pendingBookingId');
} catch (err: any) {
console.error('Payment confirmation error:', err);
setStatus('error');
setErrorMsg(err?.data?.message || 'Payment could not be confirmed. Please contact support.');
toast.error('Payment confirmation failed');
// Optionally clear pending booking on error to avoid infinite loops
localStorage.removeItem('pendingBookingId');
}
};
confirm();
}, [sessionId, confirmPayment]);
return (
<div className="min-h-screen bg-[#fafafa] font-poppins">
<Navbar
activeCity="Melbourne"
onCityChange={() => {}}
onSignInClick={onSignInClick}
onSignOutClick={onSignOutClick}
onHomeClick={onHomeClick}
onPassesClick={onPassesClick}
onCheckoutClick={() => {}}
onAttractionsClick={() => {}}
onBlogsClick={() => {}}
onHowItWorksClick={() => {}}
onFAQClick={() => {}}
onPrivacyPolicyClick={() => {}}
onAboutUsClick={() => {}}
onProfileClick={() => {}}
onCityCardsClick={() => {}}
onMagicItineraryClick={() => {}}
onPostCardsClick={() => {}}
onOffersClick={() => {}}
onSuperSavingsClick={() => {}}
onEsimsClick={() => {}}
onHotelDiscountsClick={() => {}}
onCartClick={() => {}}
currentPage={currentPage as any}
user={user}
/>
<div className="w-full px-4 sm:px-6 lg:px-10 xl:px-16 pt-32 pb-24 max-w-[1440px] mx-auto flex justify-center items-center min-h-[60vh]">
<div className="bg-white rounded-2xl shadow-lg p-8 max-w-md w-full text-center">
{status === 'loading' && (
<>
<Loader2 className="w-16 h-16 text-[#F95F62] animate-spin mx-auto mb-4" />
<h2 className="text-2xl font-semibold text-[#2a2a2a]">Confirming your payment...</h2>
<p className="text-[#555] mt-2">Please wait while we verify your transaction.</p>
</>
)}
{status === 'success' && (
<>
<CheckCircle className="w-16 h-16 text-green-500 mx-auto mb-4" />
<h2 className="text-2xl font-semibold text-[#2a2a2a]">Payment Successful!</h2>
<p className="text-[#555] mt-2">Thank you for your purchase. Your order is now confirmed.</p>
<button
onClick={() => navigate('/my-orders')} // adjust to your orders page route
className="mt-6 px-6 py-3 bg-[#F95F62] text-white rounded-xl font-medium hover:bg-[#e8545a] transition"
>
View My Orders
</button>
</>
)}
{status === 'error' && (
<>
<XCircle className="w-16 h-16 text-red-500 mx-auto mb-4" />
<h2 className="text-2xl font-semibold text-[#2a2a2a]">Payment Confirmation Failed</h2>
<p className="text-red-500 mt-2">{errorMsg}</p>
<button
onClick={() => navigate('/')}
className="mt-6 px-6 py-3 bg-gray-200 text-[#2a2a2a] rounded-xl font-medium hover:bg-gray-300 transition"
>
Go to Homepage
</button>
</>
)}
</div>
</div>
<Footer
onHomeClick={onHomeClick}
onPassesClick={onPassesClick}
onAttractionsClick={() => {}}
onBlogsClick={() => {}}
onHowItWorksClick={() => {}}
onFAQClick={() => {}}
onPrivacyPolicyClick={() => {}}
onAboutUsClick={() => {}}
onContactUsClick={() => {}}
/>
</div>
);
}

1
src/vite-env.d.ts vendored
View File

@@ -1,6 +1,7 @@
interface ImportMetaEnv {
readonly VITE_BASE_URL: string
readonly VITE_GOOGLE_MAP: string
readonly VITE_STRIPE_PUBLISHABLE_KEY: string
}
interface ImportMeta {