feat: Complete checkout overhaul with Stripe PaymentIntent integration and abandoned cart recovery

This commit is contained in:
Codex Agent
2025-10-07 22:25:03 +02:00
parent dd5545605c
commit aa8c6c67c5
38 changed files with 1848 additions and 878 deletions

View File

@@ -1,110 +1,25 @@
import React, { createContext, useCallback, useContext, useMemo, useState } from "react";
import type { CheckoutPackage, CheckoutStepId, CheckoutWizardContextValue, CheckoutWizardState } from "./types";
const cancelCheckout = useCallback(() => {
// Track abandoned checkout (fire and forget)
if (state.authUser || state.selectedPackage) {
fetch('/checkout/track-abandoned', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-CSRF-TOKEN': document.querySelector('meta[name="csrf-token"]')?.getAttribute('content') || '',
},
body: JSON.stringify({
package_id: state.selectedPackage.id,
last_step: ['package', 'auth', 'payment', 'confirmation'].indexOf(state.currentStep) + 1,
user_id: state.authUser?.id,
email: state.authUser?.email,
}),
}).catch(error => {
console.error('Failed to track abandoned checkout:', error);
});
}
interface CheckoutWizardProviderProps {
initialPackage: CheckoutPackage;
packageOptions: CheckoutPackage[];
initialStep?: CheckoutStepId;
initialAuthUser?: CheckoutWizardState['authUser'];
initialIsAuthenticated?: boolean;
children: React.ReactNode;
}
const CheckoutWizardContext = createContext<CheckoutWizardContextValue | undefined>(undefined);
export const CheckoutWizardProvider: React.FC<CheckoutWizardProviderProps> = ({
initialPackage,
packageOptions,
initialStep = 'package',
initialAuthUser = null,
initialIsAuthenticated,
children,
}) => {
const [state, setState] = useState<CheckoutWizardState>(() => ({
currentStep: initialStep,
selectedPackage: initialPackage,
packageOptions,
isAuthenticated: Boolean(initialIsAuthenticated || initialAuthUser),
authUser: initialAuthUser ?? null,
paymentProvider: undefined,
isProcessing: false,
}));
const setStep = useCallback((step: CheckoutStepId) => {
setState((prev) => ({ ...prev, currentStep: step }));
}, []);
const nextStep = useCallback(() => {
setState((prev) => {
const order: CheckoutStepId[] = ['package', 'auth', 'payment', 'confirmation'];
const currentIndex = order.indexOf(prev.currentStep);
const nextIndex = currentIndex === -1 ? 0 : Math.min(order.length - 1, currentIndex + 1);
return { ...prev, currentStep: order[nextIndex] };
});
}, []);
const previousStep = useCallback(() => {
setState((prev) => {
const order: CheckoutStepId[] = ['package', 'auth', 'payment', 'confirmation'];
const currentIndex = order.indexOf(prev.currentStep);
const nextIndex = currentIndex <= 0 ? 0 : currentIndex - 1;
return { ...prev, currentStep: order[nextIndex] };
});
}, []);
const setSelectedPackage = useCallback((pkg: CheckoutPackage) => {
setState((prev) => ({
...prev,
selectedPackage: pkg,
paymentProvider: undefined,
}));
}, []);
const markAuthenticated = useCallback<CheckoutWizardContextValue['markAuthenticated']>((user) => {
setState((prev) => ({
...prev,
isAuthenticated: Boolean(user),
authUser: user ?? null,
}));
}, []);
const setPaymentProvider = useCallback<CheckoutWizardContextValue['setPaymentProvider']>((provider) => {
setState((prev) => ({
...prev,
paymentProvider: provider,
}));
}, []);
const resetPaymentState = useCallback(() => {
setState((prev) => ({
...prev,
paymentProvider: undefined,
isProcessing: false,
}));
}, []);
const value = useMemo<CheckoutWizardContextValue>(() => ({
...state,
setStep,
nextStep,
previousStep,
setSelectedPackage,
markAuthenticated,
setPaymentProvider,
resetPaymentState,
}), [state, setStep, nextStep, previousStep, setSelectedPackage, markAuthenticated, setPaymentProvider, resetPaymentState]);
return (
<CheckoutWizardContext.Provider value={value}>
{children}
</CheckoutWizardContext.Provider>
);
};
export const useCheckoutWizard = () => {
const context = useContext(CheckoutWizardContext);
if (!context) {
throw new Error('useCheckoutWizard must be used within CheckoutWizardProvider');
}
return context;
};
// State aus localStorage entfernen
localStorage.removeItem('checkout-wizard-state');
// Zur Package-Übersicht zurückleiten
window.location.href = '/packages';
}, [state]);