Implement stripe and add success and cancel page
This commit is contained in:
54
package-lock.json
generated
54
package-lock.json
generated
@@ -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",
|
||||
|
||||
@@ -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": "*",
|
||||
|
||||
@@ -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>
|
||||
</>
|
||||
|
||||
@@ -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;
|
||||
89
src/pages/PaymentCancelPage.tsx
Normal file
89
src/pages/PaymentCancelPage.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
@@ -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,80 +228,83 @@ 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 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 (
|
||||
<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,35 +361,33 @@ 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'
|
||||
? 'bg-[#F95F62] text-white shadow-md shadow-[#F95F62]/20'
|
||||
: 'bg-gray-100 text-[#555] hover:bg-gray-200'
|
||||
}`}
|
||||
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'
|
||||
}`}
|
||||
>
|
||||
<User className="w-4 h-4" />
|
||||
<span>For myself</span>
|
||||
</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'
|
||||
? 'bg-[#F95F62] text-white shadow-md shadow-[#F95F62]/20'
|
||||
: 'bg-gray-100 text-[#555] hover:bg-gray-200'
|
||||
}`}
|
||||
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'
|
||||
}`}
|
||||
>
|
||||
<Gift className="w-4 h-4" />
|
||||
<span>To gift Someone</span>
|
||||
@@ -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'
|
||||
? 'bg-gradient-to-br from-[#f95faf] to-[#F95F62]'
|
||||
: 'bg-gradient-to-br from-[#F95F62] to-[#c94245]'
|
||||
}`}>
|
||||
<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>
|
||||
))}
|
||||
<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,39 +607,47 @@ 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 Stripe’s 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>
|
||||
);
|
||||
}
|
||||
}
|
||||
153
src/pages/PaymentSuccessPage.tsx
Normal file
153
src/pages/PaymentSuccessPage.tsx
Normal 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
1
src/vite-env.d.ts
vendored
@@ -1,6 +1,7 @@
|
||||
interface ImportMetaEnv {
|
||||
readonly VITE_BASE_URL: string
|
||||
readonly VITE_GOOGLE_MAP: string
|
||||
readonly VITE_STRIPE_PUBLISHABLE_KEY: string
|
||||
}
|
||||
|
||||
interface ImportMeta {
|
||||
|
||||
Reference in New Issue
Block a user