diff --git a/app/Http/Controllers/CheckoutController.php b/app/Http/Controllers/CheckoutController.php index 36c300c..e5a369f 100644 --- a/app/Http/Controllers/CheckoutController.php +++ b/app/Http/Controllers/CheckoutController.php @@ -1,75 +1,182 @@ - private function transformUser(?User $user): ?array - { - if (!$user) { - return null; - } + $user->id, - 'email' => $user->email, - 'name' => trim(($user->first_name ?? '').' '.($user->last_name ?? '')) ?: $user->name, - 'pending_purchase' => (bool) $user->pending_purchase, - 'email_verified_at' => $user->email_verified_at, - ]; +namespace App\Http\Controllers; + +use App\Models\Package; +use App\Models\Tenant; +use App\Models\User; +use Illuminate\Http\Request; +use Illuminate\Support\Facades\Auth; +use Illuminate\Support\Facades\DB; +use Illuminate\Support\Facades\Hash; +use Illuminate\Support\Facades\Mail; +use Illuminate\Support\Facades\Validator; +use Illuminate\Validation\Rules\Password; +use Inertia\Inertia; +use Laravel\Cashier\Cashier; +use Stripe\PaymentIntent; +use Stripe\Stripe; + +class CheckoutController extends Controller +{ + public function show(Package $package) + { + // Alle verfügbaren Pakete laden + $packages = Package::all(); + + return Inertia::render('marketing/CheckoutWizardPage', [ + 'package' => $package, + 'packageOptions' => $packages, + 'stripePublishableKey' => config('services.stripe.key'), + 'privacyHtml' => view('legal.datenschutz-partial')->render(), + 'auth' => [ + 'user' => Auth::user(), + ], + ]); } - /** - * Track an abandoned checkout for reminder emails - */ - public function trackAbandonedCheckout(Request $request): JsonResponse + public function register(Request $request) { - $request->validate([ - 'package_id' => 'required|integer|exists:packages,id', - 'last_step' => 'required|integer|min:1|max:4', - 'user_id' => 'nullable|integer|exists:users,id', - 'email' => 'nullable|email', + $validator = Validator::make($request->all(), [ + 'email' => 'required|email|unique:users,email', + 'password' => ['required', 'confirmed', Password::defaults()], + 'package_id' => 'required|exists:packages,id', + 'terms' => 'required|accepted', ]); + if ($validator->fails()) { + return response()->json([ + 'errors' => $validator->errors(), + ], 422); + } + + $package = Package::findOrFail($request->package_id); + + DB::transaction(function () use ($request, $package) { + // User erstellen + $user = User::create([ + 'email' => $request->email, + 'password' => Hash::make($request->password), + 'pending_purchase' => true, + ]); + + // Tenant erstellen + $tenant = Tenant::create([ + 'name' => 'Neuer Tenant', + 'domain' => null, + 'database' => null, + 'user_id' => $user->id, + ]); + + // Package zuweisen + $tenant->packages()->attach($package->id, [ + 'purchased_at' => now(), + 'expires_at' => $package->is_free ? null : now()->addYear(), + 'is_active' => $package->is_free, // Kostenlose Pakete sofort aktivieren + ]); + + // E-Mail-Verifizierung senden + $user->sendEmailVerificationNotification(); + + // Willkommens-E-Mail senden + Mail::to($user->email)->send(new \App\Mail\WelcomeMail($user, $package)); + }); + + return response()->json([ + 'message' => 'Registrierung erfolgreich. Bitte überprüfen Sie Ihre E-Mail zur Verifizierung.', + ]); + } + + public function createPaymentIntent(Request $request) + { + $request->validate([ + 'package_id' => 'required|exists:packages,id', + ]); + + $package = Package::findOrFail($request->package_id); + + \Log::info('Create Payment Intent', [ + 'package_id' => $package->id, + 'package_name' => $package->name, + 'price' => $package->price, + 'is_free' => $package->is_free, + 'user_id' => Auth::id(), + ]); + + if ($package->is_free) { + \Log::info('Free package detected, returning null client_secret'); + return response()->json([ + 'client_secret' => null, + 'free_package' => true, + ]); + } + + // Stripe API Key setzen + Stripe::setApiKey(config('services.stripe.secret')); + try { - $userId = $request->user_id; - $email = $request->email; - - // Wenn kein user_id aber email, versuche User zu finden - if (!$userId && $email) { - $user = User::where('email', $email)->first(); - $userId = $user?->id; - } - - // Nur tracken wenn wir einen User haben - if (!$userId) { - return response()->json(['success' => false, 'message' => 'No user found to track']); - } - - $user = User::find($userId); - if (!$user) { - return response()->json(['success' => false, 'message' => 'User not found']); - } - - // Erstelle oder update abandoned checkout - AbandonedCheckout::updateOrCreate( - [ - 'user_id' => $userId, - 'package_id' => $request->package_id, + $paymentIntent = PaymentIntent::create([ + 'amount' => $package->price * 100, // Stripe erwartet Cent + 'currency' => 'eur', + 'metadata' => [ + 'package_id' => $package->id, + 'user_id' => Auth::id(), ], - [ - 'email' => $user->email, - 'checkout_state' => null, // Später erweitern - 'last_step' => $request->last_step, - 'abandoned_at' => now(), - 'reminded_at' => null, - 'reminder_stage' => 'none', - 'expires_at' => now()->addDays(7), // 7 Tage gültig - 'converted' => false, - ] - ); + ]); - Log::info("Abandoned checkout tracked for user {$userId}, package {$request->package_id}, step {$request->last_step}"); - - return response()->json(['success' => true]); + \Log::info('PaymentIntent created successfully', [ + 'payment_intent_id' => $paymentIntent->id, + 'client_secret' => substr($paymentIntent->client_secret, 0, 50) . '...', + ]); + return response()->json([ + 'client_secret' => $paymentIntent->client_secret, + ]); } catch (\Exception $e) { - Log::error('Failed to track abandoned checkout: ' . $e->getMessage()); - return response()->json(['success' => false, 'message' => 'Failed to track checkout'], 500); + \Log::error('Stripe PaymentIntent creation failed', [ + 'error' => $e->getMessage(), + 'package_id' => $package->id, + ]); + + return response()->json([ + 'error' => 'Fehler beim Erstellen der Zahlungsdaten: ' . $e->getMessage(), + ], 500); } } + + public function confirmPayment(Request $request) + { + $request->validate([ + 'payment_intent_id' => 'required|string', + 'package_id' => 'required|exists:packages,id', + ]); + + // Stripe API Key setzen + Stripe::setApiKey(config('services.stripe.secret')); + + $paymentIntent = PaymentIntent::retrieve($request->payment_intent_id); + + if ($paymentIntent->status !== 'succeeded') { + return response()->json([ + 'error' => 'Zahlung nicht erfolgreich.', + ], 400); + } + + $package = Package::findOrFail($request->package_id); + $user = Auth::user(); + + // Package dem Tenant zuweisen + $user->tenant->packages()->attach($package->id, [ + 'purchased_at' => now(), + 'expires_at' => now()->addYear(), + 'is_active' => true, + ]); + + // pending_purchase zurücksetzen + $user->update(['pending_purchase' => false]); + + return response()->json([ + 'message' => 'Zahlung erfolgreich bestätigt.', + ]); + } } diff --git a/resources/js/layouts/auth/auth-simple-layout.tsx b/resources/js/layouts/auth/auth-simple-layout.tsx index 4ecb949..68c0192 100644 --- a/resources/js/layouts/auth/auth-simple-layout.tsx +++ b/resources/js/layouts/auth/auth-simple-layout.tsx @@ -1,5 +1,5 @@ import AppLogoIcon from '@/components/app-logo-icon'; -import { marketing } from '@/routes'; +import { home } from '@/routes'; import { Link } from '@inertiajs/react'; import { type PropsWithChildren } from 'react'; @@ -15,7 +15,7 @@ export default function AuthSimpleLayout({ children, title, description }: Props
- +
diff --git a/resources/js/pages/marketing/CheckoutWizardPage.tsx b/resources/js/pages/marketing/CheckoutWizardPage.tsx index 28f3871..6f44c46 100644 --- a/resources/js/pages/marketing/CheckoutWizardPage.tsx +++ b/resources/js/pages/marketing/CheckoutWizardPage.tsx @@ -19,9 +19,10 @@ export default function CheckoutWizardPage({ stripePublishableKey, privacyHtml, }: CheckoutWizardPageProps) { - const page = usePage<{ auth?: { user?: { id: number; email: string; name?: string } | null } }>(); + const page = usePage<{ auth?: { user?: { id: number; email: string; name?: string; pending_purchase?: boolean } | null } }>(); const currentUser = page.props.auth?.user ?? null; + const dedupedOptions = React.useMemo(() => { const ids = new Set(); const list = [initialPackage, ...packageOptions]; @@ -57,7 +58,7 @@ export default function CheckoutWizardPage({ packageOptions={dedupedOptions} stripePublishableKey={stripePublishableKey} privacyHtml={privacyHtml} - initialAuthUser={currentUser ? { id: currentUser.id, email: currentUser.email ?? '', name: currentUser.name ?? undefined } : null} + initialAuthUser={currentUser ? { id: currentUser.id, email: currentUser.email ?? '', name: currentUser.name ?? undefined, pending_purchase: Boolean(currentUser.pending_purchase) } : null} />
diff --git a/resources/js/pages/marketing/checkout/CheckoutWizard.tsx b/resources/js/pages/marketing/checkout/CheckoutWizard.tsx index e0c1f58..559f6c9 100644 --- a/resources/js/pages/marketing/checkout/CheckoutWizard.tsx +++ b/resources/js/pages/marketing/checkout/CheckoutWizard.tsx @@ -53,6 +53,7 @@ const stepConfig: { id: CheckoutStepId; title: string; description: string; deta const WizardBody: React.FC<{ stripePublishableKey: string; privacyHtml: string }> = ({ stripePublishableKey, privacyHtml }) => { const { currentStep, nextStep, previousStep } = useCheckoutWizard(); + const currentIndex = useMemo(() => stepConfig.findIndex((step) => step.id === currentStep), [currentStep]); const progress = useMemo(() => { if (currentIndex < 0) { @@ -95,6 +96,7 @@ export const CheckoutWizard: React.FC = ({ initialAuthUser, initialStep, }) => { + return ( void; + setSelectedPackage: (pkg: CheckoutPackage) => void; + setAuthUser: (user: any) => void; + nextStep: () => void; + prevStep: () => void; + previousStep: () => void; + goToStep: (step: CheckoutStepId) => void; + cancelCheckout: () => void; + updatePaymentIntent: (clientSecret: string | null) => void; + setLoading: (loading: boolean) => void; + setError: (error: string | null) => void; + resetPaymentState: () => void; +} + +const CheckoutWizardContext = createContext(null); + +const initialState: CheckoutState = { + currentStep: 'package', + selectedPackage: null, + packageOptions: [], + authUser: null, + isAuthenticated: false, + paymentIntent: null, + loading: false, + error: null, +}; + +type CheckoutAction = + | { type: 'SELECT_PACKAGE'; payload: CheckoutPackage } + | { type: 'SET_AUTH_USER'; payload: any } + | { type: 'NEXT_STEP' } + | { type: 'PREV_STEP' } + | { type: 'GO_TO_STEP'; payload: CheckoutStepId } + | { type: 'UPDATE_PAYMENT_INTENT'; payload: string | null } + | { type: 'SET_LOADING'; payload: boolean } + | { type: 'SET_ERROR'; payload: string | null }; + +function checkoutReducer(state: CheckoutState, action: CheckoutAction): CheckoutState { + switch (action.type) { + case 'SELECT_PACKAGE': + return { ...state, selectedPackage: action.payload }; + case 'SET_AUTH_USER': + return { ...state, authUser: action.payload }; + case 'NEXT_STEP': + const steps: CheckoutStepId[] = ['package', 'auth', 'payment', 'confirmation']; + const currentIndex = steps.indexOf(state.currentStep); + if (currentIndex < steps.length - 1) { + return { ...state, currentStep: steps[currentIndex + 1] }; + } + return state; + case 'PREV_STEP': + const prevSteps: CheckoutStepId[] = ['package', 'auth', 'payment', 'confirmation']; + const prevIndex = prevSteps.indexOf(state.currentStep); + if (prevIndex > 0) { + return { ...state, currentStep: prevSteps[prevIndex - 1] }; + } + return state; + case 'GO_TO_STEP': + return { ...state, currentStep: action.payload }; + case 'UPDATE_PAYMENT_INTENT': + return { ...state, paymentIntent: action.payload }; + case 'SET_LOADING': + return { ...state, loading: action.payload }; + case 'SET_ERROR': + return { ...state, error: action.payload }; + default: + return state; + } +} + +interface CheckoutWizardProviderProps { + children: React.ReactNode; + initialPackage?: CheckoutPackage; + packageOptions?: CheckoutPackage[]; + initialStep?: CheckoutStepId; + initialAuthUser?: any; + initialIsAuthenticated?: boolean; +} + +export function CheckoutWizardProvider({ + children, + initialPackage, + packageOptions, + initialStep, + initialAuthUser, + initialIsAuthenticated +}: CheckoutWizardProviderProps) { + const customInitialState: CheckoutState = { + ...initialState, + currentStep: initialStep || 'package', + selectedPackage: initialPackage || null, + packageOptions: packageOptions || [], + authUser: initialAuthUser || null, + isAuthenticated: initialIsAuthenticated || Boolean(initialAuthUser), + }; + + + const [state, dispatch] = useReducer(checkoutReducer, customInitialState); + + // Load state from localStorage on mount + useEffect(() => { + const savedState = localStorage.getItem('checkout-wizard-state'); + if (savedState) { + try { + const parsed = JSON.parse(savedState); + // Restore state selectively + if (parsed.selectedPackage) dispatch({ type: 'SELECT_PACKAGE', payload: parsed.selectedPackage }); + if (parsed.currentStep) dispatch({ type: 'GO_TO_STEP', payload: parsed.currentStep }); + } catch (error) { + console.error('Failed to restore checkout state:', error); + } + } + }, []); + + // Save state to localStorage whenever it changes + useEffect(() => { + localStorage.setItem('checkout-wizard-state', JSON.stringify({ + selectedPackage: state.selectedPackage, + currentStep: state.currentStep, + })); + }, [state.selectedPackage, state.currentStep]); + + const selectPackage = useCallback((pkg: CheckoutPackage) => { + dispatch({ type: 'SELECT_PACKAGE', payload: pkg }); + }, []); + + const setAuthUser = useCallback((user: any) => { + dispatch({ type: 'SET_AUTH_USER', payload: user }); + }, []); + + const nextStep = useCallback(() => { + dispatch({ type: 'NEXT_STEP' }); + }, []); + + const prevStep = useCallback(() => { + dispatch({ type: 'PREV_STEP' }); + }, []); + + const goToStep = useCallback((step: CheckoutStepId) => { + dispatch({ type: 'GO_TO_STEP', payload: step }); + }, []); + + const updatePaymentIntent = useCallback((clientSecret: string | null) => { + dispatch({ type: 'UPDATE_PAYMENT_INTENT', payload: clientSecret }); + }, []); + + const setLoading = useCallback((loading: boolean) => { + dispatch({ type: 'SET_LOADING', payload: loading }); + }, []); + + const setError = useCallback((error: string | null) => { + dispatch({ type: 'SET_ERROR', payload: error }); + }, []); + + const setSelectedPackage = useCallback((pkg: CheckoutPackage) => { + dispatch({ type: 'SELECT_PACKAGE', payload: pkg }); + }, []); + + const resetPaymentState = useCallback(() => { + dispatch({ type: 'UPDATE_PAYMENT_INTENT', payload: null }); + dispatch({ type: 'SET_LOADING', payload: false }); + dispatch({ type: 'SET_ERROR', payload: null }); + }, []); + const cancelCheckout = useCallback(() => { // Track abandoned checkout (fire and forget) if (state.authUser || state.selectedPackage) { @@ -8,9 +193,8 @@ '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, + package_id: state.selectedPackage?.id, + step: state.currentStep, email: state.authUser?.email, }), }).catch(error => { @@ -23,3 +207,39 @@ // Zur Package-Übersicht zurückleiten window.location.href = '/packages'; }, [state]); + + const value: CheckoutWizardContextType = { + state, + selectedPackage: state.selectedPackage, + packageOptions: state.packageOptions, + currentStep: state.currentStep, + isAuthenticated: state.isAuthenticated, + authUser: state.authUser, + selectPackage, + setSelectedPackage, + setAuthUser, + nextStep, + prevStep, + previousStep: prevStep, + goToStep, + cancelCheckout, + updatePaymentIntent, + setLoading, + setError, + resetPaymentState, + }; + + return ( + + {children} + + ); +} + +export function useCheckoutWizard(): CheckoutWizardContextType { + const context = useContext(CheckoutWizardContext); + if (!context) { + throw new Error('useCheckoutWizard must be used within a CheckoutWizardProvider'); + } + return context; +} diff --git a/resources/js/pages/marketing/checkout/steps/AuthStep.tsx b/resources/js/pages/marketing/checkout/steps/AuthStep.tsx index 12c9ec5..35fabf5 100644 --- a/resources/js/pages/marketing/checkout/steps/AuthStep.tsx +++ b/resources/js/pages/marketing/checkout/steps/AuthStep.tsx @@ -13,7 +13,7 @@ interface AuthStepProps { export const AuthStep: React.FC = ({ privacyHtml }) => { const page = usePage<{ locale?: string }>(); const locale = page.props.locale ?? "de"; - const { isAuthenticated, authUser, markAuthenticated, nextStep, selectedPackage } = useCheckoutWizard(); + const { isAuthenticated, authUser, setAuthUser, nextStep, selectedPackage } = useCheckoutWizard(); const [mode, setMode] = useState<'login' | 'register'>('register'); const handleLoginSuccess = (payload: AuthUserPayload | null) => { @@ -21,7 +21,7 @@ export const AuthStep: React.FC = ({ privacyHtml }) => { return; } - markAuthenticated({ + setAuthUser({ id: payload.id ?? 0, email: payload.email ?? "", name: payload.name ?? undefined, @@ -33,7 +33,7 @@ export const AuthStep: React.FC = ({ privacyHtml }) => { const handleRegisterSuccess = (result: RegisterSuccessPayload) => { const nextUser = result?.user ?? null; if (nextUser) { - markAuthenticated({ + setAuthUser({ id: nextUser.id ?? 0, email: nextUser.email ?? "", name: nextUser.name ?? undefined, @@ -84,12 +84,14 @@ export const AuthStep: React.FC = ({ privacyHtml }) => {
{mode === 'register' ? ( - + selectedPackage && ( + + ) ) : ( + - - + + {pkg.name} @@ -26,10 +28,10 @@ function PackageSummary({ pkg }: { pkg: CheckoutPackage }) {
- + {pkg.price === 0 ? "Kostenlos" : currencyFormatter.format(pkg.price)} - + {pkg.type === "reseller" ? "Reseller" : "Endkunde"}
@@ -37,7 +39,7 @@ function PackageSummary({ pkg }: { pkg: CheckoutPackage }) {
    {pkg.features.map((feature, index) => (
  • - + {feature}
  • ))} @@ -49,17 +51,23 @@ function PackageSummary({ pkg }: { pkg: CheckoutPackage }) { } function PackageOption({ pkg, isActive, onSelect }: { pkg: CheckoutPackage; isActive: boolean; onSelect: () => void }) { + const isFree = pkg.price === 0; + return ( +
+ +
+ ); +}; + +// Wrapper-Komponente mit eigenem Elements Provider +export const PaymentStep: React.FC = ({ stripePublishableKey }) => { + const { selectedPackage, authUser, nextStep } = useCheckoutWizard(); + const [clientSecret, setClientSecret] = useState(''); + const [error, setError] = useState(''); + + const isFree = selectedPackage ? selectedPackage.price <= 0 : false; + + // Payment Intent für kostenpflichtige Pakete laden + useEffect(() => { + if (isFree || !authUser || !selectedPackage) return; + + const loadPaymentIntent = async () => { + try { + const response = await fetch('/stripe/create-payment-intent', { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + 'X-CSRF-TOKEN': document.querySelector('meta[name="csrf-token"]')?.getAttribute('content') || '', + }, + body: JSON.stringify({ + package_id: selectedPackage.id, + }), + }); + + const data = await response.json(); + + console.log('Payment Intent Response:', { + ok: response.ok, + status: response.status, + data: data + }); + + if (response.ok && data.client_secret) { + setClientSecret(data.client_secret); + setError(''); + } else { + const errorMsg = data.error || 'Fehler beim Laden der Zahlungsdaten'; + console.error('Payment Intent Error:', errorMsg); + setError(errorMsg); + } + } catch (err) { + setError('Netzwerkfehler beim Laden der Zahlungsdaten'); + } + }; + + loadPaymentIntent(); + }, [selectedPackage?.id, authUser, isFree]); + + // Für kostenlose Pakete: Direkte Aktivierung ohne Stripe if (isFree) { return (
@@ -161,69 +199,30 @@ export const PaymentStep: React.FC = ({ stripePublishableKey } ); } + // Für kostenpflichtige Pakete: Warten auf clientSecret + if (!clientSecret) { + return ( +
+ {error && ( + + {error} + + )} +
+

+ Zahlungsdaten werden geladen... +

+
+
+ ); + } + + // Eigener Elements Provider mit clientSecret für kostenpflichtige Pakete + const stripePromise = loadStripe(stripePublishableKey); + return ( -
- setPaymentProvider(value as 'stripe' | 'paypal')}> - - Stripe - PayPal - - - -
- {error && ( - - {error} - - )} - - {clientSecret ? ( -
-

- Sichere Zahlung mit Kreditkarte, Debitkarte oder SEPA-Lastschrift. -

- - -
- ) : ( -
- - Lade Zahlungsdaten... - - Bitte warten Sie, während wir die Zahlungsdaten vorbereiten. - - -
- )} -
-
- - -
-

- PayPal Express Checkout mit Rückleitung zur Bestätigung. -

- - PayPal Integration - - PayPal wird in einem späteren Schritt implementiert. Aktuell nur Stripe verfügbar. - - -
- -
-
-
-
-
+ + + ); }; diff --git a/routes/auth.php b/routes/auth.php index 30e6b18..603f83b 100644 --- a/routes/auth.php +++ b/routes/auth.php @@ -11,6 +11,21 @@ use App\Http\Controllers\Auth\VerifyEmailController; use Illuminate\Support\Facades\Route; Route::middleware('guest')->group(function () { + Route::get('login', [AuthenticatedSessionController::class, 'create']) + ->name('login'); + + Route::post('login', [AuthenticatedSessionController::class, 'store']) + ->name('login.store'); + + Route::get('register', [RegisteredUserController::class, 'create']) + ->name('register'); + + Route::post('register', [RegisteredUserController::class, 'store']) + ->name('register.store'); + + Route::post('logout', [AuthenticatedSessionController::class, 'destroy']) + ->name('logout'); + Route::get('forgot-password', [PasswordResetLinkController::class, 'create']) ->name('password.request'); diff --git a/routes/web.php b/routes/web.php index 68404e9..8f8a856 100644 --- a/routes/web.php +++ b/routes/web.php @@ -1,5 +1,32 @@ +name('home'); +Route::get('/dashboard', function () { + return Inertia::render('dashboard'); +})->middleware(['auth', 'verified'])->name('dashboard'); +Route::get('/contact', [MarketingController::class, 'contactView'])->name('kontakt'); +Route::post('/contact', [MarketingController::class, 'contact'])->name('kontakt.submit'); +Route::get('/blog', [MarketingController::class, 'blogIndex'])->name('blog'); +Route::get('/blog/{slug}', [MarketingController::class, 'blogShow'])->name('blog.show'); +Route::get('/packages', [MarketingController::class, 'packagesIndex'])->name('packages'); +Route::get('/occasions/{type}', [MarketingController::class, 'occasionsType'])->name('occasions.type'); +Route::get('/success/{packageId?}', [MarketingController::class, 'success'])->name('marketing.success'); +Route::middleware('auth')->group(function () { + Route::get('/buy/{packageId}', [MarketingController::class, 'buyPackages'])->name('marketing.buy'); +}); + Route::get('/purchase-wizard/{package}', [CheckoutController::class, 'show'])->name('purchase.wizard'); Route::get('/checkout/{package}', [CheckoutController::class, 'show'])->name('checkout.show'); Route::post('/checkout/login', [CheckoutController::class, 'login'])->name('checkout.login'); Route::post('/checkout/register', [CheckoutController::class, 'register'])->name('checkout.register'); +Route::post('/stripe/create-payment-intent', [CheckoutController::class, 'createPaymentIntent'])->name('stripe.create-payment-intent'); +Route::post('/stripe/confirm-payment', [CheckoutController::class, 'confirmPayment'])->name('stripe.confirm-payment'); Route::post('/checkout/track-abandoned', [CheckoutController::class, 'trackAbandonedCheckout'])->name('checkout.track-abandoned'); diff --git a/stripe.exe b/stripe.exe new file mode 100644 index 0000000..743291c Binary files /dev/null and b/stripe.exe differ