From 205b19ae50b0cb099deabc465173cd1cd67c2af6 Mon Sep 17 00:00:00 2001 From: Hemant Vishwakarma Date: Wed, 22 Apr 2026 16:51:15 +0530 Subject: [PATCH] Implement stripe and add success and cancel page --- package-lock.json | 54 ++++- package.json | 3 + src/AppRouter.tsx | 28 +++ src/Redux/services/cards.service.ts | 29 ++- src/pages/PaymentCancelPage.tsx | 89 ++++++++ src/pages/PaymentDetailsPage.tsx | 338 +++++++++++++++------------- src/pages/PaymentSuccessPage.tsx | 153 +++++++++++++ src/vite-env.d.ts | 1 + 8 files changed, 523 insertions(+), 172 deletions(-) create mode 100644 src/pages/PaymentCancelPage.tsx create mode 100644 src/pages/PaymentSuccessPage.tsx diff --git a/package-lock.json b/package-lock.json index 071ee2a..abfa862 100644 --- a/package-lock.json +++ b/package-lock.json @@ -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", diff --git a/package.json b/package.json index 078328a..544a147 100644 --- a/package.json +++ b/package.json @@ -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": "*", diff --git a/src/AppRouter.tsx b/src/AppRouter.tsx index 0b5fb55..eba004f 100644 --- a/src/AppRouter.tsx +++ b/src/AppRouter.tsx @@ -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)} /> } /> + + + + + + } /> + + + + } /> diff --git a/src/Redux/services/cards.service.ts b/src/Redux/services/cards.service.ts index fbc43e3..efac3eb 100644 --- a/src/Redux/services/cards.service.ts +++ b/src/Redux/services/cards.service.ts @@ -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; \ No newline at end of file diff --git a/src/pages/PaymentCancelPage.tsx b/src/pages/PaymentCancelPage.tsx new file mode 100644 index 0000000..108cf7b --- /dev/null +++ b/src/pages/PaymentCancelPage.tsx @@ -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 ( +
+ {}} + 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} + /> + +
+
+ +

Payment Cancelled

+

+ You cancelled the payment process. No charges have been made. +

+ +
+
+ +
{}} + onBlogsClick={() => {}} + onHowItWorksClick={() => {}} + onFAQClick={() => {}} + onPrivacyPolicyClick={() => {}} + onAboutUsClick={() => {}} + onContactUsClick={() => {}} + /> +
+ ); +} \ No newline at end of file diff --git a/src/pages/PaymentDetailsPage.tsx b/src/pages/PaymentDetailsPage.tsx index 08b5ad3..ed315a6 100644 --- a/src/pages/PaymentDetailsPage.tsx +++ b/src/pages/PaymentDetailsPage.tsx @@ -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['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 && ( )} - {error && ( {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,80 +228,83 @@ export function PaymentDetailsPage({ } }, [userDetails]); - /* ── Validation ── */ const [errors, setErrors] = useState>({}); - 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 = {}; - 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 recipientDetails = { - isForSelf: true, - recipientFirstName: giftFirstName, - recipientLastName: giftLastName, - recipientEmail: giftEmail, - recipientIsdCode: `+${giftIsd}`, - recipientPhone: giftPhone, - recipientCity: giftCity, - recipientCountry: giftCountry, - giftMessage: giftMessage, - }; + const [isRedirecting, setIsRedirecting] = useState(false); - - 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."); + const handlePayment = async () => { + const validationErrors = validate(); + setErrors(validationErrors); + if (Object.keys(validationErrors).length > 0) { + toast.error('Please fill all required fields'); + return; } - }; - const handleSubmit = async () => { - const e = validate(); - setErrors(e); - if (Object.keys(e).length > 0) return; + if (selectedTab === 'gift') { + const recipientDetails = { + isForSelf: true, + recipientFirstName: giftFirstName, + recipientLastName: giftLastName, + recipientEmail: giftEmail, + recipientIsdCode: `+${giftIsd}`, + recipientPhone: giftPhone, + recipientCity: giftCity, + recipientCountry: giftCountry, + giftMessage: giftMessage, + }; + try { + 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; + } + } + + 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 (
{ }} 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} />
- - {/* Back Button */} - {/* Page heading */}

- Review & - {' '} + Review & Pay

@@ -335,35 +361,33 @@ export function PaymentDetailsPage({

- Complete your purchase securely. Your payment information is protected. + Complete your purchase securely. You will be redirected to Stripe to enter your card details.

- - {/* LEFT: Forms */} - - {/* Purchase type tabs */}
- { }} prefilled disabled={true} /> - { }} prefilled disabled={true} /> + {}} prefilled disabled /> + {}} prefilled disabled />
- { }} type="email" prefilled disabled={true} /> - { }} type="tel" prefilled disabled={true} /> + {}} type="email" prefilled disabled /> + {}} type="tel" prefilled disabled />
@@ -426,7 +448,6 @@ export function PaymentDetailsPage({

Gift Recipient Details

- - - - - - -
@@ -496,6 +511,7 @@ export function PaymentDetailsPage({ )} + {/* Billing Address */} @@ -507,28 +523,23 @@ export function PaymentDetailsPage({

Billing Address

- { }} prefilled disabled={true} /> - { }} prefilled disabled={true} /> + {}} prefilled disabled /> + {}} prefilled disabled />
- { }} prefilled disabled={true} /> - { }} prefilled disabled={true} /> + {}} prefilled disabled /> + {}} prefilled disabled />
- { }} inputMode="numeric" prefilled disabled={true} /> -
-
- { }} prefilled disabled={true} /> -
-
+ {}} inputMode="numeric" prefilled disabled /> + {}} prefilled disabled />
- - {/* RIGHT: Order Summary (unchanged) */} + {/* Right Column: Order Summary & Payment Button */}
@@ -538,10 +549,13 @@ export function PaymentDetailsPage({
-
+
{bookingDetails?.cardMode}
@@ -550,21 +564,21 @@ export function PaymentDetailsPage({

- {bookingDetails?.cardMode.toLowerCase() === 'flexi' ? `${bookingDetails?.noOfAttractions} Attractions` : `${bookingDetails?.noOfDays} Days`} + {bookingDetails?.cardMode?.toLowerCase() === 'flexi' + ? `${bookingDetails?.noOfAttractions} Attractions` + : `${bookingDetails?.noOfDays} Days`}

- {[ - { label: 'Adults', value: bookingDetails?.totalAdult }, - { label: 'Children', value: bookingDetails?.totalChild }, - // { label: 'Qty', value: order.quantity }, - ].map(({ label, value }) => ( -
- {label} - {value} -
- ))} +
+ Adults + {bookingDetails?.totalAdult} +
+
+ Children + {bookingDetails?.totalChild} +
@@ -593,39 +607,47 @@ export function PaymentDetailsPage({ - {submitting ? ( + {isRedirecting ? ( <> - - Processing… + + Redirecting to Stripe... ) : ( <> - Complete Payment · ${bookingDetails?.totalAmount} + Proceed to Payment · ${bookingDetails?.totalAmount} )}

- By completing your purchase you agree to our Terms of Service and Privacy Policy + You will be redirected to Stripe’s secure checkout page to enter your card details. + By completing your purchase you agree to our Terms of Service and Privacy Policy.

-