Joe
Joe

Reputation: 31

Laravel Cashier (Paddle): Not Storing Transactions & Subscriptions

I am using Laravel 12.x with Inertia, React, Typescript, WorkOS Auth, and Cashier (Paddle).

With my current implementation I can get the checkout overlay to open and process payment, but in my database I am not storing the transaction & subscription. Cashier does not seem to be aware a transaction occurred and a user is subscribed after checkout. On Paddle's end, everything is as expected when I open the customer portal. I am just failing to process my user's transaction and subscription.

I believe I am incorrectly implementing Cashier by manually calling $user->createAsCustomer(); myself in my controller, rather than creating a checkout with Cashier and letting Cashier handle the record creation. I think I am totally doing this wrong.

Cashier (Paddle) Documentation

How could I fix my implementation and leverage Cashier correctly? I struggle with the documentation because of my React/Inertia stack (rather than blade). I see in the documentation $checkout = $user->checkout('pri_34567')->returnTo(route('dashboard')); but I couldn't get that working with my front-end.

SubscriptionController.php

class SubscriptionController extends Controller
{
    public function store(Request $request)
    {
        try {

            $user = $request->user();

            $priceId = config('subscription.paddle_price_id');

            $user->createAsCustomer();

            $redirectUrl = route('dashboard');

            // Return the data for the frontend ... I think this is the wrong approach
            return response()->json([
                'priceId' => $priceId,
                'redirectUrl' => $redirectUrl
            ]);

        } catch (\Exception $e) {
            Log::error('CHECKOUT - Subscription error: ' . $e->getMessage());
            Log::error($e->getTraceAsString());
            return response()->json(['error' => $e->getMessage()], 500);
        }
    }
}

My react component has a button that onClick calls this function:

useHandleCheckoutOpen.tsx


import axios from 'axios';
import { type User } from '@/types';
import { router } from '@inertiajs/react';

export function useHandleCheckoutOpen(user: User) {
    return async () => {
        try {
            const response = await axios.post('/subscribe');

            if (response.data.error) {
                console.error('Server returned an error:', response.data.error);
                return;
            }

            const { priceId, redirectUrl } = response.data;

            if (window.Paddle && window.Paddle.Checkout) {
                window.Paddle.Checkout.open({
                    items: [
                        {
                            priceId,
                            quantity: 1,
                        },
                    ],
                    settings: {
                        displayMode: 'overlay',
                        allowLogout: false,
                        // successUrl: redirectUrl,
                    },
                    customer: {
                        name: user.name,
                        email: user.email,
                    },
                });
            } else {
                console.error('Paddle script not loaded or window.Paddle not defined.');
            }
        } catch (error: any) {
            console.error('Error starting subscription:', error);
            if (error.response) {
                console.error('Server response:', error.response.data);
            }
        }
    };
}

I have included use Billable in my User Model. My web.php is:

web.php

Route::middleware([
    'auth',
    ValidateSessionWithWorkOS::class,
])->group(function () {
    Route::post('/paddle/webhook', WebhookController::class);
    Route::post('/subscribe', [SubscriptionController::class, 'store'])->name('subscribe');
    Route::get('/customer-portal', [CustomerPortalController::class, 'show'])->name('customer-portal');
});

I have tried to implement what the Cashier (paddle) documentation references, but because the examples are in blade I am not sure how to relate that to my react front-end. Most examples I can find outside of the documentation are not up to date and refer to old methods that Cashier no longer supports like ->create()

The docs say

use Illuminate\Http\Request;

Route::get('/subscribe', function (Request $request) {
    $checkout = $request->user()->checkout('price_basic_monthly')
        ->returnTo(route('dashboard'));

    return view('subscribe', ['checkout' => $checkout]);
})->name('subscribe');

with the blade component:

<x-paddle-button :checkout="$checkout" class="px-8 py-4">
    Subscribe
</x-paddle-button>

Upvotes: 1

Views: 30

Answers (1)

Joe
Joe

Reputation: 31

Cashier automatically listens/processes webhooks from Paddle if configured correctly with the .env variables. By adding the webhook route to the web.php file, I was overriding the default behavior of Cashier because this was taking precedence:

Route::post('/paddle/webhook', WebhookController::class);

The solution would be to remove this line so Cashier can process webhooks and create the Transaction & Subscription records when the webhooks are received. Defining that route in web.php is not necessary.

Upvotes: 2

Related Questions