Saltar al contenido

Hace pocos días que Laravel Cashier lanzó su versión 10 para ponerse al día con Stripe y los nuevos requisitos SCA (Strong Customer Authentication). Si estás utilizando Stripe en tu aplicación entonces debes tener en cuenta esta guía de actualización que ofrece el equipo de Laravel Cashier o directamente desde la documentación de Laravel, en ella se explican los pasos necesarios para subir de versión y qué debemos tener en cuenta.

El nuevo requerimiento (SCA) sólo afecta a los pagos en línea que se procesen en Europa a través de Stripe, estas regulaciones fueron impuestas en septiembre de 2019 por la Unión Europea para evitar el fraude de pagos.

Sobrescribir la template SCA

Uno de los temas más importantes que debemos tener en cuenta si debemos cumplir con el SCA ocurre al momento de procesar nuevas suscripciones en nuestra plataforma. Para asegurarnos que todo funciona correctamente deberemos controlar la excepción IncompletePayment de la siguiente forma:

<?php
use Laravel\Cashier\Exceptions\IncompletePayment;

try {
    $subscription = $user->newSubscription('default', $planId)->create($paymentMethod);
} catch (IncompletePayment $exception) {
    return redirect()->route(
        'cashier.payment',
        [$exception->payment->id, 'redirect' => route('home')]
    );
}

Eso lo que hará será controlar si es necesario solicitar datos adicionales al cliente, en caso de ser necesario, Laravel Cashier mostrará el contenido de la template vendor/laravel/cashier/payment.blade.php, la cual te recomiendo sobrescribir utilizando el comando php artisan vendor:publish, una vez lo hagas podrás modificar su código ubicado en el archivo en resources/views/vendor/cashier/payment.blade.php que tiene algo parecido a lo siguiente:

<!DOCTYPE html>
<html lang="{{ str_replace('_', '-', app()->getLocale()) }}" class="h-full">
<head>
    <meta charset="utf-8">
    <meta name="viewport" content="width=device-width, initial-scale=1" />

    <title>{{ __('Confirmación de pago') }} - {{ config('app.name', 'Laravel') }}</title>

    <link href="https://unpkg.com/tailwindcss@^1.0/dist/tailwind.min.css" rel="stylesheet">
    <script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/vue.min.js"></script>

    <script src="https://js.stripe.com/v3"></script>
</head>
<body class="font-sans text-gray-600 bg-gray-200 leading-normal p-4 h-full">
    <div id="app" class="h-full md:flex md:justify-center md:items-center">
        <div class="w-full max-w-lg">
            <!-- Status Messages -->
            <p class="flex items-center mb-4 bg-red-100 border border-red-200 px-5 py-2 rounded-lg text-red-500" v-if="errorMessage">
                <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" class="w-6 h-6">
                    <path class="fill-current text-red-300" d="M12 2a10 10 0 1 1 0 20 10 10 0 0 1 0-20z"/>
                    <path class="fill-current text-red-500" d="M12 18a1.5 1.5 0 1 1 0-3 1.5 1.5 0 0 1 0 3zm1-5.9c-.13 1.2-1.88 1.2-2 0l-.5-5a1 1 0 0 1 1-1.1h1a1 1 0 0 1 1 1.1l-.5 5z"/>
                </svg>

                <span class="ml-3">@{{ errorMessage }}</span>
            </p>

            <p class="flex items-center mb-4 bg-green-100 border border-green-200 px-5 py-4 rounded-lg text-green-700" v-if="paymentProcessed && successMessage">
                <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" class="w-6 h-6">
                    <circle cx="12" cy="12" r="10" class="fill-current text-green-300"/>
                    <path class="fill-current text-green-500" d="M10 14.59l6.3-6.3a1 1 0 0 1 1.4 1.42l-7 7a1 1 0 0 1-1.4 0l-3-3a1 1 0 0 1 1.4-1.42l2.3 2.3z"/>
                </svg>

                <span class="ml-3">@{{ successMessage }}</span>
            </p>

            <div class="bg-white rounded-lg shadow-xl p-4 sm:py-6 sm:px-10 mb-5">
                @if ($payment->isSucceeded())
                    <h1 class="text-xl mt-2 mb-4 text-gray-700">
                        {{ __('Pago completado satisfactoriamente') }}
                    </h1>

                    <p class="mb-6">{{ __('Este pago ya fue confirmado con éxito.') }}</p>
                @elseif ($payment->isCancelled())
                    <h1 class="text-xl mt-2 mb-4 text-gray-700">
                        {{ __('Pago cancelado') }}
                    </h1>

                    <p class="mb-6">{{ __('Este pago fue cancelado.') }}</p>
                @else
                    <div id="payment-elements" v-if="! paymentProcessed">
                        <!-- Instructions -->
                        <h1 class="text-xl mt-2 mb-4 text-gray-700">
                            {{ __('Confirma la cantidad :amount del pago que se va a efectuar', ['amount' => $payment->amount()]) }}
                        </h1>

                        <p class="mb-6">
                            {{ __('Se necesita confirmación adicional para procesar su pago. Confirme su pago completando los detalles de pago a continuación.') }}
                        </p>

                        <!-- Name -->
                        <label for="cardholder-name" class="inline-block text-sm text-gray-700 font-semibold mb-2">{{ __('Nombre completo') }}</label>

                        <input id="cardholder-name" type="text" placeholder="Jane Doe" required
                               class="inline-block bg-gray-200 border border-gray-400 rounded-lg w-full px-4 py-3 mb-3 focus:outline-none"
                               v-model="name">

                        <!-- Card -->
                        <label for="card-element" class="inline-block text-sm text-gray-700 font-semibold mb-2">{{ __('Tarjeta') }}</label>

                        <div id="card-element" class="bg-gray-200 border border-gray-400 rounded-lg p-4 mb-6"></div>

                        <!-- Pay Button -->
                        <button id="card-button"
                                class="inline-block w-full px-4 py-3 mb-4 text-white rounded-lg hover:bg-blue-500"
                                :class="{ 'bg-blue-400': paymentProcessing, 'bg-blue-600': ! paymentProcessing }"
                                @click="confirmPayment"
                                :disabled="paymentProcessing">
                            {{ __('Pagar :amount', ['amount' => $payment->amount()]) }}
                        </button>
                    </div>
                @endif

                <a href="{{ $redirect ?? url('/') }}"
                   class="inline-block w-full px-4 py-3 bg-gray-200 hover:bg-gray-300 text-center text-gray-700 rounded-lg">
                    {{ __('Volver') }}
                </a>
            </div>

            <p class="text-center text-gray-500 text-sm">
                © {{ date('Y') }} {{ config('app.name') }}. {{ __('Todos los derechos reservados.') }}
            </p>
        </div>
    </div>

    <script>
        window.stripe = Stripe('{{ $stripeKey }}');

        var app = new Vue({
            el: '#app',

            data: {
                name: '',
                cardElement: null,
                paymentProcessing: false,
                paymentProcessed: false,
                successMessage: '',
                errorMessage: ''
            },

            mounted: function () {
                const elements = stripe.elements();

                this.cardElement = elements.create('card');
                this.cardElement.mount('#card-element');
            },

            methods: {
                confirmPayment: function () {
                    var self = this;

                    this.paymentProcessing = true;
                    this.paymentProcessed = false;
                    this.successMessage = '';
                    this.errorMessage = '';

                    stripe.handleCardPayment(
                        '{{ $payment->clientSecret() }}', this.cardElement, {
                            payment_method_data: {
                                billing_details: { name: this.name }
                            }
                        }
                    ).then(function (result) {
                        self.paymentProcessing = false;

                        if (result.error) {
                            if (result.error.code === 'parameter_invalid_empty' &&
                                result.error.param === 'payment_method_data[billing_details][name]') {
                                self.errorMessage = '{{ __('Por favor proporcione su nombre.') }}';
                            } else {
                                self.errorMessage = result.error.message;
                            }
                        } else {
                            self.paymentProcessed = true;

                            self.successMessage = '{{ __('El pago fue exitoso.') }}';
                        }
                    });
                },
            },
        })
    </script>
</body>
</html>

Nuevas variables de Entorno de Cashier

Por si todo lo anterior fuese poco, además Laravel Cashier nos ofrece nuevas variables de entorno que yo recomiendo sobrescribir para tener un mayor control y evitar problemas entre otros con los Webhooks:

CASHIER_CURRENCY=EUR #new on cashier 10.0
CASHIER_CURRENCY_LOCALE=es_ES #new on cashier 10.0
CASHIER_MODEL=App\Models\User #new on cashier 10.0

Cada variable habla por sí sola y todas son realmente importantes.

La nueva columna stripe_status de la tabla Subscriptions

La tabla subscriptions ha sido actualizada con una nueva columna que permite conocer el estado de las suscripciones de una forma sencilla, esta es su nueva apariencia:

<?php

Schema::create('subscriptions', function (Blueprint $table) {
    $table->bigIncrements('id');
    $table->unsignedBigInteger('user_id');
    $table->string('name');
    $table->string('stripe_id')->collation('utf8mb4_bin');
    $table->string('stripe_status'); // nueva columna en Cashier 10!
    $table->string('stripe_plan');
    $table->integer('quantity');
    $table->timestamp('trial_ends_at')->nullable();
    $table->timestamp('ends_at')->nullable();
    $table->timestamps();

    $table->index(['user_id', 'stripe_status']);
});

Notificaciones de pago fuera de sesión

Dado que el SCA requiere que los clientes verifiquen ocasionalmente sus detalles de pago incluso cuando su suscripción está activa, Laravel Cashier puede enviar una notificación de pago al cliente cuando se requiere confirmación de pago fuera de sesión. Por ejemplo, esto puede ocurrir cuando una suscripción se renueva. La notificación de pago de Cashier se puede habilitar configurando la variable de entorno CASHIER_PAYMENT_NOTIFICATION. Por defecto, esta notificación está deshabilitada. Por supuesto, Cashier incluye una clase Notification que se puede utilizar para este propósito, pero puedes proporcionar tu propia clase de notificación si lo deseas:

CASHIER_PAYMENT_NOTIFICATION=Laravel\Cashier\Notifications\ConfirmPayment

Dicha notificación tendrá la siguiente apariencia:

<?php

namespace Laravel\Cashier\Notifications;

use Laravel\Cashier\Payment;
use Illuminate\Bus\Queueable;
use Illuminate\Notifications\Notification;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Notifications\Messages\MailMessage;

class ConfirmPayment extends Notification implements ShouldQueue
{
    use Queueable;

    /**
     * The PaymentIntent identifier.
     *
     * @var string
     */
    public $paymentId;

    /**
     * The payment amount.
     *
     * @var string
     */
    public $amount;

    /**
     * Create a new payment confirmation notification.
     *
     * @param  \Laravel\Cashier\Payment  $payment
     * @return void
     */
    public function __construct(Payment $payment)
    {
        $this->paymentId = $payment->id;
        $this->amount = $payment->amount();
    }

    /**
     * Get the notification's delivery channels.
     *
     * @param  mixed  $notifiable
     * @return array
     */
    public function via($notifiable)
    {
        return ['mail'];
    }

    /**
     * Get the mail representation of the notification.
     *
     * @param  mixed  $notifiable
     * @return \Illuminate\Notifications\Messages\MailMessage
     */
    public function toMail($notifiable)
    {
        $url = route('cashier.payment', ['id' => $this->paymentId]);

        return (new MailMessage)
            ->greeting(__('Confirm your :amount payment', ['amount' => $this->amount]))
            ->line(__('Extra confirmation is needed to process your payment. Please continue to the payment page by clicking on the button below.'))
            ->action(__('Confirm Payment'), $url)
            ->line(__('Thanks').',')
            ->line(config('app.name'));
    }
}

Simplemente te recomiendo sobrescribirla para que se adecúe a las necesidades de tu aplicación.

school Curso completo

Curso Laravel 12
Completo 2026

El único curso 100% actualizado que incluye Laravel 12, Livewire 3, Vue 3, React 19 e Inertia 2. Aprende con proyectos reales y las últimas funcionalidades.

access_time 8 horas de contenido
layers 4 tecnologías en 1
update 100% actualizado
code Proyectos prácticos
Ver Curso Laravel 12 arrow_forward

star Incluido en cualquier suscripción

Rutas de aprendizaje