3245 lines
85 KiB
Markdown
3245 lines
85 KiB
Markdown
# Laravel Cashier (Paddle)
|
||
|
||
* Introduction
|
||
* Upgrading Cashier
|
||
* Installation
|
||
* Paddle Sandbox
|
||
* Configuration
|
||
* Billable Model
|
||
* API Keys
|
||
* Paddle JS
|
||
* Currency Configuration
|
||
* Overriding Default Models
|
||
* Quickstart
|
||
* Selling Products
|
||
* Selling Subscriptions
|
||
* Checkout Sessions
|
||
* Overlay Checkout
|
||
* Inline Checkout
|
||
* Guest Checkouts
|
||
* Price Previews
|
||
* Customer Price Previews
|
||
* Discounts
|
||
* Customers
|
||
* Customer Defaults
|
||
* Retrieving Customers
|
||
* Creating Customers
|
||
* Subscriptions
|
||
* Creating Subscriptions
|
||
* Checking Subscription Status
|
||
* Subscription Single Charges
|
||
* Updating Payment Information
|
||
* Changing Plans
|
||
* Subscription Quantity
|
||
* Subscriptions With Multiple Products
|
||
* Multiple Subscriptions
|
||
* Pausing Subscriptions
|
||
* Canceling Subscriptions
|
||
* Subscription Trials
|
||
* With Payment Method Up Front
|
||
* Without Payment Method Up Front
|
||
* Extend or Activate a Trial
|
||
* Handling Paddle Webhooks
|
||
* Defining Webhook Event Handlers
|
||
* Verifying Webhook Signatures
|
||
* Single Charges
|
||
* Charging for Products
|
||
* Refunding Transactions
|
||
* Crediting Transactions
|
||
* Transactions
|
||
* Past and Upcoming Payments
|
||
* Testing
|
||
|
||
## Introduction
|
||
|
||
This documentation is for Cashier Paddle 2.x's integration with Paddle
|
||
Billing. If you're still using Paddle Classic, you should use [Cashier Paddle
|
||
1.x](https://github.com/laravel/cashier-paddle/tree/1.x).
|
||
|
||
[Laravel Cashier Paddle](https://github.com/laravel/cashier-paddle) provides
|
||
an expressive, fluent interface to [Paddle's](https://paddle.com) subscription
|
||
billing services. It handles almost all of the boilerplate subscription
|
||
billing code you are dreading. In addition to basic subscription management,
|
||
Cashier can handle: swapping subscriptions, subscription "quantities",
|
||
subscription pausing, cancelation grace periods, and more.
|
||
|
||
Before digging into Cashier Paddle, we recommend you also review Paddle's
|
||
[concept guides](https://developer.paddle.com/concepts/overview) and [API
|
||
documentation](https://developer.paddle.com/api-reference/overview).
|
||
|
||
## Upgrading Cashier
|
||
|
||
When upgrading to a new version of Cashier, it's important that you carefully
|
||
review [the upgrade guide](https://github.com/laravel/cashier-
|
||
paddle/blob/master/UPGRADE.md).
|
||
|
||
## Installation
|
||
|
||
First, install the Cashier package for Paddle using the Composer package
|
||
manager:
|
||
|
||
|
||
|
||
1composer require laravel/cashier-paddle
|
||
|
||
|
||
composer require laravel/cashier-paddle
|
||
|
||
Next, you should publish the Cashier migration files using the
|
||
`vendor:publish` Artisan command:
|
||
|
||
|
||
|
||
1php artisan vendor:publish --tag="cashier-migrations"
|
||
|
||
|
||
php artisan vendor:publish --tag="cashier-migrations"
|
||
|
||
Then, you should run your application's database migrations. The Cashier
|
||
migrations will create a new `customers` table. In addition, new
|
||
`subscriptions` and `subscription_items` tables will be created to store all
|
||
of your customer's subscriptions. Lastly, a new `transactions` table will be
|
||
created to store all of the Paddle transactions associated with your
|
||
customers:
|
||
|
||
|
||
|
||
1php artisan migrate
|
||
|
||
|
||
php artisan migrate
|
||
|
||
To ensure Cashier properly handles all Paddle events, remember to set up
|
||
Cashier's webhook handling.
|
||
|
||
### Paddle Sandbox
|
||
|
||
During local and staging development, you should [register a Paddle Sandbox
|
||
account](https://sandbox-login.paddle.com/signup). This account will give you
|
||
a sandboxed environment to test and develop your applications without making
|
||
actual payments. You may use Paddle's [test card
|
||
numbers](https://developer.paddle.com/concepts/payment-methods/credit-debit-
|
||
card#test-payment-method) to simulate various payment scenarios.
|
||
|
||
When using the Paddle Sandbox environment, you should set the `PADDLE_SANDBOX`
|
||
environment variable to `true` within your application's `.env` file:
|
||
|
||
|
||
|
||
1PADDLE_SANDBOX=true
|
||
|
||
|
||
PADDLE_SANDBOX=true
|
||
|
||
After you have finished developing your application you may [apply for a
|
||
Paddle vendor account](https://paddle.com). Before your application is placed
|
||
into production, Paddle will need to approve your application's domain.
|
||
|
||
## Configuration
|
||
|
||
### Billable Model
|
||
|
||
Before using Cashier, you must add the `Billable` trait to your user model
|
||
definition. This trait provides various methods to allow you to perform common
|
||
billing tasks, such as creating subscriptions and updating payment method
|
||
information:
|
||
|
||
|
||
|
||
1use Laravel\Paddle\Billable;
|
||
|
||
2
|
||
|
||
3class User extends Authenticatable
|
||
|
||
4{
|
||
|
||
5 use Billable;
|
||
|
||
6}
|
||
|
||
|
||
use Laravel\Paddle\Billable;
|
||
|
||
class User extends Authenticatable
|
||
{
|
||
use Billable;
|
||
}
|
||
|
||
If you have billable entities that are not users, you may also add the trait
|
||
to those classes:
|
||
|
||
|
||
|
||
1use Illuminate\Database\Eloquent\Model;
|
||
|
||
2use Laravel\Paddle\Billable;
|
||
|
||
3
|
||
|
||
4class Team extends Model
|
||
|
||
5{
|
||
|
||
6 use Billable;
|
||
|
||
7}
|
||
|
||
|
||
use Illuminate\Database\Eloquent\Model;
|
||
use Laravel\Paddle\Billable;
|
||
|
||
class Team extends Model
|
||
{
|
||
use Billable;
|
||
}
|
||
|
||
### API Keys
|
||
|
||
Next, you should configure your Paddle keys in your application's `.env` file.
|
||
You can retrieve your Paddle API keys from the Paddle control panel:
|
||
|
||
|
||
|
||
1PADDLE_CLIENT_SIDE_TOKEN=your-paddle-client-side-token
|
||
|
||
2PADDLE_API_KEY=your-paddle-api-key
|
||
|
||
3PADDLE_RETAIN_KEY=your-paddle-retain-key
|
||
|
||
4PADDLE_WEBHOOK_SECRET="your-paddle-webhook-secret"
|
||
|
||
5PADDLE_SANDBOX=true
|
||
|
||
|
||
PADDLE_CLIENT_SIDE_TOKEN=your-paddle-client-side-token
|
||
PADDLE_API_KEY=your-paddle-api-key
|
||
PADDLE_RETAIN_KEY=your-paddle-retain-key
|
||
PADDLE_WEBHOOK_SECRET="your-paddle-webhook-secret"
|
||
PADDLE_SANDBOX=true
|
||
|
||
The `PADDLE_SANDBOX` environment variable should be set to `true` when you are
|
||
using Paddle's Sandbox environment. The `PADDLE_SANDBOX` variable should be
|
||
set to `false` if you are deploying your application to production and are
|
||
using Paddle's live vendor environment.
|
||
|
||
The `PADDLE_RETAIN_KEY` is optional and should only be set if you're using
|
||
Paddle with [Retain](https://developer.paddle.com/concepts/retain/overview).
|
||
|
||
### Paddle JS
|
||
|
||
Paddle relies on its own JavaScript library to initiate the Paddle checkout
|
||
widget. You can load the JavaScript library by placing the `@paddleJS` Blade
|
||
directive right before your application layout's closing `</head>` tag:
|
||
|
||
|
||
|
||
1<head>
|
||
|
||
2 ...
|
||
|
||
3
|
||
|
||
4 @paddleJS
|
||
|
||
5</head>
|
||
|
||
|
||
<head>
|
||
...
|
||
|
||
@paddleJS
|
||
</head>
|
||
|
||
### Currency Configuration
|
||
|
||
You can specify a locale to be used when formatting money values for display
|
||
on invoices. Internally, Cashier utilizes [PHP's `NumberFormatter`
|
||
class](https://www.php.net/manual/en/class.numberformatter.php) to set the
|
||
currency locale:
|
||
|
||
|
||
|
||
1CASHIER_CURRENCY_LOCALE=nl_BE
|
||
|
||
|
||
CASHIER_CURRENCY_LOCALE=nl_BE
|
||
|
||
In order to use locales other than `en`, ensure the `ext-intl` PHP extension
|
||
is installed and configured on your server.
|
||
|
||
### Overriding Default Models
|
||
|
||
You are free to extend the models used internally by Cashier by defining your
|
||
own model and extending the corresponding Cashier model:
|
||
|
||
|
||
|
||
1use Laravel\Paddle\Subscription as CashierSubscription;
|
||
|
||
2
|
||
|
||
3class Subscription extends CashierSubscription
|
||
|
||
4{
|
||
|
||
5 // ...
|
||
|
||
6}
|
||
|
||
|
||
use Laravel\Paddle\Subscription as CashierSubscription;
|
||
|
||
class Subscription extends CashierSubscription
|
||
{
|
||
// ...
|
||
}
|
||
|
||
After defining your model, you may instruct Cashier to use your custom model
|
||
via the `Laravel\Paddle\Cashier` class. Typically, you should inform Cashier
|
||
about your custom models in the `boot` method of your application's
|
||
`App\Providers\AppServiceProvider` class:
|
||
|
||
|
||
|
||
1use App\Models\Cashier\Subscription;
|
||
|
||
2use App\Models\Cashier\Transaction;
|
||
|
||
3
|
||
|
||
4/**
|
||
|
||
5 * Bootstrap any application services.
|
||
|
||
6 */
|
||
|
||
7public function boot(): void
|
||
|
||
8{
|
||
|
||
9 Cashier::useSubscriptionModel(Subscription::class);
|
||
|
||
10 Cashier::useTransactionModel(Transaction::class);
|
||
|
||
11}
|
||
|
||
|
||
use App\Models\Cashier\Subscription;
|
||
use App\Models\Cashier\Transaction;
|
||
|
||
/**
|
||
* Bootstrap any application services.
|
||
*/
|
||
public function boot(): void
|
||
{
|
||
Cashier::useSubscriptionModel(Subscription::class);
|
||
Cashier::useTransactionModel(Transaction::class);
|
||
}
|
||
|
||
## Quickstart
|
||
|
||
### Selling Products
|
||
|
||
Before utilizing Paddle Checkout, you should define Products with fixed prices
|
||
in your Paddle dashboard. In addition, you should configure Paddle's webhook
|
||
handling.
|
||
|
||
Offering product and subscription billing via your application can be
|
||
intimidating. However, thanks to Cashier and [Paddle's Checkout
|
||
Overlay](https://developer.paddle.com/concepts/sell/overlay-checkout), you can
|
||
easily build modern, robust payment integrations.
|
||
|
||
To charge customers for non-recurring, single-charge products, we'll utilize
|
||
Cashier to charge customers with Paddle's Checkout Overlay, where they will
|
||
provide their payment details and confirm their purchase. Once the payment has
|
||
been made via the Checkout Overlay, the customer will be redirected to a
|
||
success URL of your choosing within your application:
|
||
|
||
|
||
|
||
1use Illuminate\Http\Request;
|
||
|
||
2
|
||
|
||
3Route::get('/buy', function (Request $request) {
|
||
|
||
4 $checkout = $request->user()->checkout('pri_deluxe_album')
|
||
|
||
5 ->returnTo(route('dashboard'));
|
||
|
||
6
|
||
|
||
7 return view('buy', ['checkout' => $checkout]);
|
||
|
||
8})->name('checkout');
|
||
|
||
|
||
use Illuminate\Http\Request;
|
||
|
||
Route::get('/buy', function (Request $request) {
|
||
$checkout = $request->user()->checkout('pri_deluxe_album')
|
||
->returnTo(route('dashboard'));
|
||
|
||
return view('buy', ['checkout' => $checkout]);
|
||
})->name('checkout');
|
||
|
||
As you can see in the example above, we will utilize Cashier's provided
|
||
`checkout` method to create a checkout object to present the customer the
|
||
Paddle Checkout Overlay for a given "price identifier". When using Paddle,
|
||
"prices" refer to [defined prices for specific
|
||
products](https://developer.paddle.com/build/products/create-products-prices).
|
||
|
||
If necessary, the `checkout` method will automatically create a customer in
|
||
Paddle and connect that Paddle customer record to the corresponding user in
|
||
your application's database. After completing the checkout session, the
|
||
customer will be redirected to a dedicated success page where you can display
|
||
an informational message to the customer.
|
||
|
||
In the `buy` view, we will include a button to display the Checkout Overlay.
|
||
The `paddle-button` Blade component is included with Cashier Paddle; however,
|
||
you may also manually render an overlay checkout:
|
||
|
||
|
||
|
||
1<x-paddle-button :checkout="$checkout" class="px-8 py-4">
|
||
|
||
2 Buy Product
|
||
|
||
3</x-paddle-button>
|
||
|
||
|
||
<x-paddle-button :checkout="$checkout" class="px-8 py-4">
|
||
Buy Product
|
||
</x-paddle-button>
|
||
|
||
#### Providing Meta Data to Paddle Checkout
|
||
|
||
When selling products, it's common to keep track of completed orders and
|
||
purchased products via `Cart` and `Order` models defined by your own
|
||
application. When redirecting customers to Paddle's Checkout Overlay to
|
||
complete a purchase, you may need to provide an existing order identifier so
|
||
that you can associate the completed purchase with the corresponding order
|
||
when the customer is redirected back to your application.
|
||
|
||
To accomplish this, you may provide an array of custom data to the `checkout`
|
||
method. Let's imagine that a pending `Order` is created within our application
|
||
when a user begins the checkout process. Remember, the `Cart` and `Order`
|
||
models in this example are illustrative and not provided by Cashier. You are
|
||
free to implement these concepts based on the needs of your own application:
|
||
|
||
|
||
|
||
1use App\Models\Cart;
|
||
|
||
2use App\Models\Order;
|
||
|
||
3use Illuminate\Http\Request;
|
||
|
||
4
|
||
|
||
5Route::get('/cart/{cart}/checkout', function (Request $request, Cart $cart) {
|
||
|
||
6 $order = Order::create([
|
||
|
||
7 'cart_id' => $cart->id,
|
||
|
||
8 'price_ids' => $cart->price_ids,
|
||
|
||
9 'status' => 'incomplete',
|
||
|
||
10 ]);
|
||
|
||
11
|
||
|
||
12 $checkout = $request->user()->checkout($order->price_ids)
|
||
|
||
13 ->customData(['order_id' => $order->id]);
|
||
|
||
14
|
||
|
||
15 return view('billing', ['checkout' => $checkout]);
|
||
|
||
16})->name('checkout');
|
||
|
||
|
||
use App\Models\Cart;
|
||
use App\Models\Order;
|
||
use Illuminate\Http\Request;
|
||
|
||
Route::get('/cart/{cart}/checkout', function (Request $request, Cart $cart) {
|
||
$order = Order::create([
|
||
'cart_id' => $cart->id,
|
||
'price_ids' => $cart->price_ids,
|
||
'status' => 'incomplete',
|
||
]);
|
||
|
||
$checkout = $request->user()->checkout($order->price_ids)
|
||
->customData(['order_id' => $order->id]);
|
||
|
||
return view('billing', ['checkout' => $checkout]);
|
||
})->name('checkout');
|
||
|
||
As you can see in the example above, when a user begins the checkout process,
|
||
we will provide all of the cart / order's associated Paddle price identifiers
|
||
to the `checkout` method. Of course, your application is responsible for
|
||
associating these items with the "shopping cart" or order as a customer adds
|
||
them. We also provide the order's ID to the Paddle Checkout Overlay via the
|
||
`customData` method.
|
||
|
||
Of course, you will likely want to mark the order as "complete" once the
|
||
customer has finished the checkout process. To accomplish this, you may listen
|
||
to the webhooks dispatched by Paddle and raised via events by Cashier to store
|
||
order information in your database.
|
||
|
||
To get started, listen for the `TransactionCompleted` event dispatched by
|
||
Cashier. Typically, you should register the event listener in the `boot`
|
||
method of your application's `AppServiceProvider`:
|
||
|
||
|
||
|
||
1use App\Listeners\CompleteOrder;
|
||
|
||
2use Illuminate\Support\Facades\Event;
|
||
|
||
3use Laravel\Paddle\Events\TransactionCompleted;
|
||
|
||
4
|
||
|
||
5/**
|
||
|
||
6 * Bootstrap any application services.
|
||
|
||
7 */
|
||
|
||
8public function boot(): void
|
||
|
||
9{
|
||
|
||
10 Event::listen(TransactionCompleted::class, CompleteOrder::class);
|
||
|
||
11}
|
||
|
||
|
||
use App\Listeners\CompleteOrder;
|
||
use Illuminate\Support\Facades\Event;
|
||
use Laravel\Paddle\Events\TransactionCompleted;
|
||
|
||
/**
|
||
* Bootstrap any application services.
|
||
*/
|
||
public function boot(): void
|
||
{
|
||
Event::listen(TransactionCompleted::class, CompleteOrder::class);
|
||
}
|
||
|
||
In this example, the `CompleteOrder` listener might look like the following:
|
||
|
||
|
||
|
||
1namespace App\Listeners;
|
||
|
||
2
|
||
|
||
3use App\Models\Order;
|
||
|
||
4use Laravel\Paddle\Cashier;
|
||
|
||
5use Laravel\Paddle\Events\TransactionCompleted;
|
||
|
||
6
|
||
|
||
7class CompleteOrder
|
||
|
||
8{
|
||
|
||
9 /**
|
||
|
||
10 * Handle the incoming Cashier webhook event.
|
||
|
||
11 */
|
||
|
||
12 public function handle(TransactionCompleted $event): void
|
||
|
||
13 {
|
||
|
||
14 $orderId = $event->payload['data']['custom_data']['order_id'] ?? null;
|
||
|
||
15
|
||
|
||
16 $order = Order::findOrFail($orderId);
|
||
|
||
17
|
||
|
||
18 $order->update(['status' => 'completed']);
|
||
|
||
19 }
|
||
|
||
20}
|
||
|
||
|
||
namespace App\Listeners;
|
||
|
||
use App\Models\Order;
|
||
use Laravel\Paddle\Cashier;
|
||
use Laravel\Paddle\Events\TransactionCompleted;
|
||
|
||
class CompleteOrder
|
||
{
|
||
/**
|
||
* Handle the incoming Cashier webhook event.
|
||
*/
|
||
public function handle(TransactionCompleted $event): void
|
||
{
|
||
$orderId = $event->payload['data']['custom_data']['order_id'] ?? null;
|
||
|
||
$order = Order::findOrFail($orderId);
|
||
|
||
$order->update(['status' => 'completed']);
|
||
}
|
||
}
|
||
|
||
Please refer to Paddle's documentation for more information on the [data
|
||
contained by the `transaction.completed`
|
||
event](https://developer.paddle.com/webhooks/transactions/transaction-
|
||
completed).
|
||
|
||
### Selling Subscriptions
|
||
|
||
Before utilizing Paddle Checkout, you should define Products with fixed prices
|
||
in your Paddle dashboard. In addition, you should configure Paddle's webhook
|
||
handling.
|
||
|
||
Offering product and subscription billing via your application can be
|
||
intimidating. However, thanks to Cashier and [Paddle's Checkout
|
||
Overlay](https://developer.paddle.com/concepts/sell/overlay-checkout), you can
|
||
easily build modern, robust payment integrations.
|
||
|
||
To learn how to sell subscriptions using Cashier and Paddle's Checkout
|
||
Overlay, let's consider the simple scenario of a subscription service with a
|
||
basic monthly (`price_basic_monthly`) and yearly (`price_basic_yearly`) plan.
|
||
These two prices could be grouped under a "Basic" product (`pro_basic`) in our
|
||
Paddle dashboard. In addition, our subscription service might offer an
|
||
"Expert" plan as `pro_expert`.
|
||
|
||
First, let's discover how a customer can subscribe to our services. Of course,
|
||
you can imagine the customer might click a "subscribe" button for the Basic
|
||
plan on our application's pricing page. This button will invoke a Paddle
|
||
Checkout Overlay for their chosen plan. To get started, let's initiate a
|
||
checkout session via the `checkout` method:
|
||
|
||
|
||
|
||
1use Illuminate\Http\Request;
|
||
|
||
2
|
||
|
||
3Route::get('/subscribe', function (Request $request) {
|
||
|
||
4 $checkout = $request->user()->checkout('price_basic_monthly')
|
||
|
||
5 ->returnTo(route('dashboard'));
|
||
|
||
6
|
||
|
||
7 return view('subscribe', ['checkout' => $checkout]);
|
||
|
||
8})->name('subscribe');
|
||
|
||
|
||
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');
|
||
|
||
In the `subscribe` view, we will include a button to display the Checkout
|
||
Overlay. The `paddle-button` Blade component is included with Cashier Paddle;
|
||
however, you may also manually render an overlay checkout:
|
||
|
||
|
||
|
||
1<x-paddle-button :checkout="$checkout" class="px-8 py-4">
|
||
|
||
2 Subscribe
|
||
|
||
3</x-paddle-button>
|
||
|
||
|
||
<x-paddle-button :checkout="$checkout" class="px-8 py-4">
|
||
Subscribe
|
||
</x-paddle-button>
|
||
|
||
Now, when the Subscribe button is clicked, the customer will be able to enter
|
||
their payment details and initiate their subscription. To know when their
|
||
subscription has actually started (since some payment methods require a few
|
||
seconds to process), you should also configure Cashier's webhook handling.
|
||
|
||
Now that customers can start subscriptions, we need to restrict certain
|
||
portions of our application so that only subscribed users can access them. Of
|
||
course, we can always determine a user's current subscription status via the
|
||
`subscribed` method provided by Cashier's `Billable` trait:
|
||
|
||
|
||
|
||
1@if ($user->subscribed())
|
||
|
||
2 <p>You are subscribed.</p>
|
||
|
||
3@endif
|
||
|
||
|
||
@if ($user->subscribed())
|
||
<p>You are subscribed.</p>
|
||
@endif
|
||
|
||
We can even easily determine if a user is subscribed to specific product or
|
||
price:
|
||
|
||
|
||
|
||
1@if ($user->subscribedToProduct('pro_basic'))
|
||
|
||
2 <p>You are subscribed to our Basic product.</p>
|
||
|
||
3@endif
|
||
|
||
4
|
||
|
||
5@if ($user->subscribedToPrice('price_basic_monthly'))
|
||
|
||
6 <p>You are subscribed to our monthly Basic plan.</p>
|
||
|
||
7@endif
|
||
|
||
|
||
@if ($user->subscribedToProduct('pro_basic'))
|
||
<p>You are subscribed to our Basic product.</p>
|
||
@endif
|
||
|
||
@if ($user->subscribedToPrice('price_basic_monthly'))
|
||
<p>You are subscribed to our monthly Basic plan.</p>
|
||
@endif
|
||
|
||
#### Building a Subscribed Middleware
|
||
|
||
For convenience, you may wish to create a [middleware](/docs/12.x/middleware)
|
||
which determines if the incoming request is from a subscribed user. Once this
|
||
middleware has been defined, you may easily assign it to a route to prevent
|
||
users that are not subscribed from accessing the route:
|
||
|
||
|
||
|
||
1<?php
|
||
|
||
2
|
||
|
||
3namespace App\Http\Middleware;
|
||
|
||
4
|
||
|
||
5use Closure;
|
||
|
||
6use Illuminate\Http\Request;
|
||
|
||
7use Symfony\Component\HttpFoundation\Response;
|
||
|
||
8
|
||
|
||
9class Subscribed
|
||
|
||
10{
|
||
|
||
11 /**
|
||
|
||
12 * Handle an incoming request.
|
||
|
||
13 */
|
||
|
||
14 public function handle(Request $request, Closure $next): Response
|
||
|
||
15 {
|
||
|
||
16 if (! $request->user()?->subscribed()) {
|
||
|
||
17 // Redirect user to billing page and ask them to subscribe...
|
||
|
||
18 return redirect('/subscribe');
|
||
|
||
19 }
|
||
|
||
20
|
||
|
||
21 return $next($request);
|
||
|
||
22 }
|
||
|
||
23}
|
||
|
||
|
||
<?php
|
||
|
||
namespace App\Http\Middleware;
|
||
|
||
use Closure;
|
||
use Illuminate\Http\Request;
|
||
use Symfony\Component\HttpFoundation\Response;
|
||
|
||
class Subscribed
|
||
{
|
||
/**
|
||
* Handle an incoming request.
|
||
*/
|
||
public function handle(Request $request, Closure $next): Response
|
||
{
|
||
if (! $request->user()?->subscribed()) {
|
||
// Redirect user to billing page and ask them to subscribe...
|
||
return redirect('/subscribe');
|
||
}
|
||
|
||
return $next($request);
|
||
}
|
||
}
|
||
|
||
Once the middleware has been defined, you may assign it to a route:
|
||
|
||
|
||
|
||
1use App\Http\Middleware\Subscribed;
|
||
|
||
2
|
||
|
||
3Route::get('/dashboard', function () {
|
||
|
||
4 // ...
|
||
|
||
5})->middleware([Subscribed::class]);
|
||
|
||
|
||
use App\Http\Middleware\Subscribed;
|
||
|
||
Route::get('/dashboard', function () {
|
||
// ...
|
||
})->middleware([Subscribed::class]);
|
||
|
||
#### Allowing Customers to Manage Their Billing Plan
|
||
|
||
Of course, customers may want to change their subscription plan to another
|
||
product or "tier". In our example from above, we'd want to allow the customer
|
||
to change their plan from a monthly subscription to a yearly subscription. For
|
||
this you'll need to implement something like a button that leads to the below
|
||
route:
|
||
|
||
|
||
|
||
1use Illuminate\Http\Request;
|
||
|
||
2
|
||
|
||
3Route::put('/subscription/{price}/swap', function (Request $request, $price) {
|
||
|
||
4 $user->subscription()->swap($price); // With "$price" being "price_basic_yearly" for this example.
|
||
|
||
5
|
||
|
||
6 return redirect()->route('dashboard');
|
||
|
||
7})->name('subscription.swap');
|
||
|
||
|
||
use Illuminate\Http\Request;
|
||
|
||
Route::put('/subscription/{price}/swap', function (Request $request, $price) {
|
||
$user->subscription()->swap($price); // With "$price" being "price_basic_yearly" for this example.
|
||
|
||
return redirect()->route('dashboard');
|
||
})->name('subscription.swap');
|
||
|
||
Besides swapping plans you'll also need to allow your customers to cancel
|
||
their subscription. Like swapping plans, provide a button that leads to the
|
||
following route:
|
||
|
||
|
||
|
||
1use Illuminate\Http\Request;
|
||
|
||
2
|
||
|
||
3Route::put('/subscription/cancel', function (Request $request, $price) {
|
||
|
||
4 $user->subscription()->cancel();
|
||
|
||
5
|
||
|
||
6 return redirect()->route('dashboard');
|
||
|
||
7})->name('subscription.cancel');
|
||
|
||
|
||
use Illuminate\Http\Request;
|
||
|
||
Route::put('/subscription/cancel', function (Request $request, $price) {
|
||
$user->subscription()->cancel();
|
||
|
||
return redirect()->route('dashboard');
|
||
})->name('subscription.cancel');
|
||
|
||
And now your subscription will get canceled at the end of its billing period.
|
||
|
||
As long as you have configured Cashier's webhook handling, Cashier will
|
||
automatically keep your application's Cashier-related database tables in sync
|
||
by inspecting the incoming webhooks from Paddle. So, for example, when you
|
||
cancel a customer's subscription via Paddle's dashboard, Cashier will receive
|
||
the corresponding webhook and mark the subscription as "canceled" in your
|
||
application's database.
|
||
|
||
## Checkout Sessions
|
||
|
||
Most operations to bill customers are performed using "checkouts" via Paddle's
|
||
[Checkout Overlay widget](https://developer.paddle.com/build/checkout/build-
|
||
overlay-checkout) or by utilizing [inline
|
||
checkout](https://developer.paddle.com/build/checkout/build-branded-inline-
|
||
checkout).
|
||
|
||
Before processing checkout payments using Paddle, you should define your
|
||
application's [default payment
|
||
link](https://developer.paddle.com/build/transactions/default-payment-
|
||
link#set-default-link) in your Paddle checkout settings dashboard.
|
||
|
||
### Overlay Checkout
|
||
|
||
Before displaying the Checkout Overlay widget, you must generate a checkout
|
||
session using Cashier. A checkout session will inform the checkout widget of
|
||
the billing operation that should be performed:
|
||
|
||
|
||
|
||
1use Illuminate\Http\Request;
|
||
|
||
2
|
||
|
||
3Route::get('/buy', function (Request $request) {
|
||
|
||
4 $checkout = $user->checkout('pri_34567')
|
||
|
||
5 ->returnTo(route('dashboard'));
|
||
|
||
6
|
||
|
||
7 return view('billing', ['checkout' => $checkout]);
|
||
|
||
8});
|
||
|
||
|
||
use Illuminate\Http\Request;
|
||
|
||
Route::get('/buy', function (Request $request) {
|
||
$checkout = $user->checkout('pri_34567')
|
||
->returnTo(route('dashboard'));
|
||
|
||
return view('billing', ['checkout' => $checkout]);
|
||
});
|
||
|
||
Cashier includes a `paddle-button` [Blade
|
||
component](/docs/12.x/blade#components). You may pass the checkout session to
|
||
this component as a "prop". Then, when this button is clicked, Paddle's
|
||
checkout widget will be displayed:
|
||
|
||
|
||
|
||
1<x-paddle-button :checkout="$checkout" class="px-8 py-4">
|
||
|
||
2 Subscribe
|
||
|
||
3</x-paddle-button>
|
||
|
||
|
||
<x-paddle-button :checkout="$checkout" class="px-8 py-4">
|
||
Subscribe
|
||
</x-paddle-button>
|
||
|
||
By default, this will display the widget using Paddle's default styling. You
|
||
can customize the widget by adding [Paddle supported
|
||
attributes](https://developer.paddle.com/paddlejs/html-data-attributes) like
|
||
the `data-theme='light'` attribute to the component:
|
||
|
||
|
||
|
||
1<x-paddle-button :checkout="$checkout" class="px-8 py-4" data-theme="light">
|
||
|
||
2 Subscribe
|
||
|
||
3</x-paddle-button>
|
||
|
||
|
||
<x-paddle-button :checkout="$checkout" class="px-8 py-4" data-theme="light">
|
||
Subscribe
|
||
</x-paddle-button>
|
||
|
||
The Paddle checkout widget is asynchronous. Once the user creates a
|
||
subscription within the widget, Paddle will send your application a webhook so
|
||
that you may properly update the subscription state in your application's
|
||
database. Therefore, it's important that you properly set up webhooks to
|
||
accommodate for state changes from Paddle.
|
||
|
||
After a subscription state change, the delay for receiving the corresponding
|
||
webhook is typically minimal but you should account for this in your
|
||
application by considering that your user's subscription might not be
|
||
immediately available after completing the checkout.
|
||
|
||
#### Manually Rendering an Overlay Checkout
|
||
|
||
You may also manually render an overlay checkout without using Laravel's
|
||
built-in Blade components. To get started, generate the checkout session as
|
||
demonstrated in previous examples:
|
||
|
||
|
||
|
||
1use Illuminate\Http\Request;
|
||
|
||
2
|
||
|
||
3Route::get('/buy', function (Request $request) {
|
||
|
||
4 $checkout = $user->checkout('pri_34567')
|
||
|
||
5 ->returnTo(route('dashboard'));
|
||
|
||
6
|
||
|
||
7 return view('billing', ['checkout' => $checkout]);
|
||
|
||
8});
|
||
|
||
|
||
use Illuminate\Http\Request;
|
||
|
||
Route::get('/buy', function (Request $request) {
|
||
$checkout = $user->checkout('pri_34567')
|
||
->returnTo(route('dashboard'));
|
||
|
||
return view('billing', ['checkout' => $checkout]);
|
||
});
|
||
|
||
Next, you may use Paddle.js to initialize the checkout. In this example, we
|
||
will create a link that is assigned the `paddle_button` class. Paddle.js will
|
||
detect this class and display the overlay checkout when the link is clicked:
|
||
|
||
|
||
|
||
1<?php
|
||
|
||
2$items = $checkout->getItems();
|
||
|
||
3$customer = $checkout->getCustomer();
|
||
|
||
4$custom = $checkout->getCustomData();
|
||
|
||
5?>
|
||
|
||
6
|
||
|
||
7<a
|
||
|
||
8 href='#!'
|
||
|
||
9 class='paddle_button'
|
||
|
||
10 data-items='{!! json_encode($items) !!}'
|
||
|
||
11 @if ($customer) data-customer-id='{{ $customer->paddle_id }}' @endif
|
||
|
||
12 @if ($custom) data-custom-data='{{ json_encode($custom) }}' @endif
|
||
|
||
13 @if ($returnUrl = $checkout->getReturnUrl()) data-success-url='{{ $returnUrl }}' @endif
|
||
|
||
14>
|
||
|
||
15 Buy Product
|
||
|
||
16</a>
|
||
|
||
|
||
<?php
|
||
$items = $checkout->getItems();
|
||
$customer = $checkout->getCustomer();
|
||
$custom = $checkout->getCustomData();
|
||
?>
|
||
|
||
<a
|
||
href='#!'
|
||
class='paddle_button'
|
||
data-items='{!! json_encode($items) !!}'
|
||
@if ($customer) data-customer-id='{{ $customer->paddle_id }}' @endif
|
||
@if ($custom) data-custom-data='{{ json_encode($custom) }}' @endif
|
||
@if ($returnUrl = $checkout->getReturnUrl()) data-success-url='{{ $returnUrl }}' @endif
|
||
>
|
||
Buy Product
|
||
</a>
|
||
|
||
### Inline Checkout
|
||
|
||
If you don't want to make use of Paddle's "overlay" style checkout widget,
|
||
Paddle also provides the option to display the widget inline. While this
|
||
approach does not allow you to adjust any of the checkout's HTML fields, it
|
||
allows you to embed the widget within your application.
|
||
|
||
To make it easy for you to get started with inline checkout, Cashier includes
|
||
a `paddle-checkout` Blade component. To get started, you should generate a
|
||
checkout session:
|
||
|
||
|
||
|
||
1use Illuminate\Http\Request;
|
||
|
||
2
|
||
|
||
3Route::get('/buy', function (Request $request) {
|
||
|
||
4 $checkout = $user->checkout('pri_34567')
|
||
|
||
5 ->returnTo(route('dashboard'));
|
||
|
||
6
|
||
|
||
7 return view('billing', ['checkout' => $checkout]);
|
||
|
||
8});
|
||
|
||
|
||
use Illuminate\Http\Request;
|
||
|
||
Route::get('/buy', function (Request $request) {
|
||
$checkout = $user->checkout('pri_34567')
|
||
->returnTo(route('dashboard'));
|
||
|
||
return view('billing', ['checkout' => $checkout]);
|
||
});
|
||
|
||
Then, you may pass the checkout session to the component's `checkout`
|
||
attribute:
|
||
|
||
|
||
|
||
1<x-paddle-checkout :checkout="$checkout" class="w-full" />
|
||
|
||
|
||
<x-paddle-checkout :checkout="$checkout" class="w-full" />
|
||
|
||
To adjust the height of the inline checkout component, you may pass the
|
||
`height` attribute to the Blade component:
|
||
|
||
|
||
|
||
1<x-paddle-checkout :checkout="$checkout" class="w-full" height="500" />
|
||
|
||
|
||
<x-paddle-checkout :checkout="$checkout" class="w-full" height="500" />
|
||
|
||
Please consult Paddle's [guide on Inline
|
||
Checkout](https://developer.paddle.com/build/checkout/build-branded-inline-
|
||
checkout) and [available checkout
|
||
settings](https://developer.paddle.com/build/checkout/set-up-checkout-default-
|
||
settings) for further details on the inline checkout's customization options.
|
||
|
||
#### Manually Rendering an Inline Checkout
|
||
|
||
You may also manually render an inline checkout without using Laravel's built-
|
||
in Blade components. To get started, generate the checkout session as
|
||
demonstrated in previous examples:
|
||
|
||
|
||
|
||
1use Illuminate\Http\Request;
|
||
|
||
2
|
||
|
||
3Route::get('/buy', function (Request $request) {
|
||
|
||
4 $checkout = $user->checkout('pri_34567')
|
||
|
||
5 ->returnTo(route('dashboard'));
|
||
|
||
6
|
||
|
||
7 return view('billing', ['checkout' => $checkout]);
|
||
|
||
8});
|
||
|
||
|
||
use Illuminate\Http\Request;
|
||
|
||
Route::get('/buy', function (Request $request) {
|
||
$checkout = $user->checkout('pri_34567')
|
||
->returnTo(route('dashboard'));
|
||
|
||
return view('billing', ['checkout' => $checkout]);
|
||
});
|
||
|
||
Next, you may use Paddle.js to initialize the checkout. In this example, we
|
||
will demonstrate this using [Alpine.js](https://github.com/alpinejs/alpine);
|
||
however, you are free to modify this example for your own frontend stack:
|
||
|
||
|
||
|
||
1<?php
|
||
|
||
2$options = $checkout->options();
|
||
|
||
3
|
||
|
||
4$options['settings']['frameTarget'] = 'paddle-checkout';
|
||
|
||
5$options['settings']['frameInitialHeight'] = 366;
|
||
|
||
6?>
|
||
|
||
7
|
||
|
||
8<div class="paddle-checkout" x-data="{}" x-init="
|
||
|
||
9 Paddle.Checkout.open(@json($options));
|
||
|
||
10">
|
||
|
||
11</div>
|
||
|
||
|
||
<?php
|
||
$options = $checkout->options();
|
||
|
||
$options['settings']['frameTarget'] = 'paddle-checkout';
|
||
$options['settings']['frameInitialHeight'] = 366;
|
||
?>
|
||
|
||
<div class="paddle-checkout" x-data="{}" x-init="
|
||
Paddle.Checkout.open(@json($options));
|
||
">
|
||
</div>
|
||
|
||
### Guest Checkouts
|
||
|
||
Sometimes, you may need to create a checkout session for users that do not
|
||
need an account with your application. To do so, you may use the `guest`
|
||
method:
|
||
|
||
|
||
|
||
1use Illuminate\Http\Request;
|
||
|
||
2use Laravel\Paddle\Checkout;
|
||
|
||
3
|
||
|
||
4Route::get('/buy', function (Request $request) {
|
||
|
||
5 $checkout = Checkout::guest(['pri_34567'])
|
||
|
||
6 ->returnTo(route('home'));
|
||
|
||
7
|
||
|
||
8 return view('billing', ['checkout' => $checkout]);
|
||
|
||
9});
|
||
|
||
|
||
use Illuminate\Http\Request;
|
||
use Laravel\Paddle\Checkout;
|
||
|
||
Route::get('/buy', function (Request $request) {
|
||
$checkout = Checkout::guest(['pri_34567'])
|
||
->returnTo(route('home'));
|
||
|
||
return view('billing', ['checkout' => $checkout]);
|
||
});
|
||
|
||
Then, you may provide the checkout session to the Paddle button or inline
|
||
checkout Blade components.
|
||
|
||
## Price Previews
|
||
|
||
Paddle allows you to customize prices per currency, essentially allowing you
|
||
to configure different prices for different countries. Cashier Paddle allows
|
||
you to retrieve all of these prices using the `previewPrices` method. This
|
||
method accepts the price IDs you wish to retrieve prices for:
|
||
|
||
|
||
|
||
1use Laravel\Paddle\Cashier;
|
||
|
||
2
|
||
|
||
3$prices = Cashier::previewPrices(['pri_123', 'pri_456']);
|
||
|
||
|
||
use Laravel\Paddle\Cashier;
|
||
|
||
$prices = Cashier::previewPrices(['pri_123', 'pri_456']);
|
||
|
||
The currency will be determined based on the IP address of the request;
|
||
however, you may optionally provide a specific country to retrieve prices for:
|
||
|
||
|
||
|
||
1use Laravel\Paddle\Cashier;
|
||
|
||
2
|
||
|
||
3$prices = Cashier::previewPrices(['pri_123', 'pri_456'], ['address' => [
|
||
|
||
4 'country_code' => 'BE',
|
||
|
||
5 'postal_code' => '1234',
|
||
|
||
6]]);
|
||
|
||
|
||
use Laravel\Paddle\Cashier;
|
||
|
||
$prices = Cashier::previewPrices(['pri_123', 'pri_456'], ['address' => [
|
||
'country_code' => 'BE',
|
||
'postal_code' => '1234',
|
||
]]);
|
||
|
||
After retrieving the prices you may display them however you wish:
|
||
|
||
|
||
|
||
1<ul>
|
||
|
||
2 @foreach ($prices as $price)
|
||
|
||
3 <li>{{ $price->product['name'] }} - {{ $price->total() }}</li>
|
||
|
||
4 @endforeach
|
||
|
||
5</ul>
|
||
|
||
|
||
<ul>
|
||
@foreach ($prices as $price)
|
||
<li>{{ $price->product['name'] }} - {{ $price->total() }}</li>
|
||
@endforeach
|
||
</ul>
|
||
|
||
You may also display the subtotal price and tax amount separately:
|
||
|
||
|
||
|
||
1<ul>
|
||
|
||
2 @foreach ($prices as $price)
|
||
|
||
3 <li>{{ $price->product['name'] }} - {{ $price->subtotal() }} (+ {{ $price->tax() }} tax)</li>
|
||
|
||
4 @endforeach
|
||
|
||
5</ul>
|
||
|
||
|
||
<ul>
|
||
@foreach ($prices as $price)
|
||
<li>{{ $price->product['name'] }} - {{ $price->subtotal() }} (+ {{ $price->tax() }} tax)</li>
|
||
@endforeach
|
||
</ul>
|
||
|
||
For more information, [checkout Paddle's API documentation regarding price
|
||
previews](https://developer.paddle.com/api-reference/pricing-preview/preview-
|
||
prices).
|
||
|
||
### Customer Price Previews
|
||
|
||
If a user is already a customer and you would like to display the prices that
|
||
apply to that customer, you may do so by retrieving the prices directly from
|
||
the customer instance:
|
||
|
||
|
||
|
||
1use App\Models\User;
|
||
|
||
2
|
||
|
||
3$prices = User::find(1)->previewPrices(['pri_123', 'pri_456']);
|
||
|
||
|
||
use App\Models\User;
|
||
|
||
$prices = User::find(1)->previewPrices(['pri_123', 'pri_456']);
|
||
|
||
Internally, Cashier will use the user's customer ID to retrieve the prices in
|
||
their currency. So, for example, a user living in the United States will see
|
||
prices in US dollars while a user in Belgium will see prices in Euros. If no
|
||
matching currency can be found, the default currency of the product will be
|
||
used. You can customize all prices of a product or subscription plan in the
|
||
Paddle control panel.
|
||
|
||
### Discounts
|
||
|
||
You may also choose to display prices after a discount. When calling the
|
||
`previewPrices` method, you provide the discount ID via the `discount_id`
|
||
option:
|
||
|
||
|
||
|
||
1use Laravel\Paddle\Cashier;
|
||
|
||
2
|
||
|
||
3$prices = Cashier::previewPrices(['pri_123', 'pri_456'], [
|
||
|
||
4 'discount_id' => 'dsc_123'
|
||
|
||
5]);
|
||
|
||
|
||
use Laravel\Paddle\Cashier;
|
||
|
||
$prices = Cashier::previewPrices(['pri_123', 'pri_456'], [
|
||
'discount_id' => 'dsc_123'
|
||
]);
|
||
|
||
Then, display the calculated prices:
|
||
|
||
|
||
|
||
1<ul>
|
||
|
||
2 @foreach ($prices as $price)
|
||
|
||
3 <li>{{ $price->product['name'] }} - {{ $price->total() }}</li>
|
||
|
||
4 @endforeach
|
||
|
||
5</ul>
|
||
|
||
|
||
<ul>
|
||
@foreach ($prices as $price)
|
||
<li>{{ $price->product['name'] }} - {{ $price->total() }}</li>
|
||
@endforeach
|
||
</ul>
|
||
|
||
## Customers
|
||
|
||
### Customer Defaults
|
||
|
||
Cashier allows you to define some useful defaults for your customers when
|
||
creating checkout sessions. Setting these defaults allow you to pre-fill a
|
||
customer's email address and name so that they can immediately move on to the
|
||
payment portion of the checkout widget. You can set these defaults by
|
||
overriding the following methods on your billable model:
|
||
|
||
|
||
|
||
1/**
|
||
|
||
2 * Get the customer's name to associate with Paddle.
|
||
|
||
3 */
|
||
|
||
4public function paddleName(): string|null
|
||
|
||
5{
|
||
|
||
6 return $this->name;
|
||
|
||
7}
|
||
|
||
8
|
||
|
||
9/**
|
||
|
||
10 * Get the customer's email address to associate with Paddle.
|
||
|
||
11 */
|
||
|
||
12public function paddleEmail(): string|null
|
||
|
||
13{
|
||
|
||
14 return $this->email;
|
||
|
||
15}
|
||
|
||
|
||
/**
|
||
* Get the customer's name to associate with Paddle.
|
||
*/
|
||
public function paddleName(): string|null
|
||
{
|
||
return $this->name;
|
||
}
|
||
|
||
/**
|
||
* Get the customer's email address to associate with Paddle.
|
||
*/
|
||
public function paddleEmail(): string|null
|
||
{
|
||
return $this->email;
|
||
}
|
||
|
||
These defaults will be used for every action in Cashier that generates a
|
||
checkout session.
|
||
|
||
### Retrieving Customers
|
||
|
||
You can retrieve a customer by their Paddle Customer ID using the
|
||
`Cashier::findBillable` method. This method will return an instance of the
|
||
billable model:
|
||
|
||
|
||
|
||
1use Laravel\Paddle\Cashier;
|
||
|
||
2
|
||
|
||
3$user = Cashier::findBillable($customerId);
|
||
|
||
|
||
use Laravel\Paddle\Cashier;
|
||
|
||
$user = Cashier::findBillable($customerId);
|
||
|
||
### Creating Customers
|
||
|
||
Occasionally, you may wish to create a Paddle customer without beginning a
|
||
subscription. You may accomplish this using the `createAsCustomer` method:
|
||
|
||
|
||
|
||
1$customer = $user->createAsCustomer();
|
||
|
||
|
||
$customer = $user->createAsCustomer();
|
||
|
||
An instance of `Laravel\Paddle\Customer` is returned. Once the customer has
|
||
been created in Paddle, you may begin a subscription at a later date. You may
|
||
provide an optional `$options` array to pass in any additional [customer
|
||
creation parameters that are supported by the Paddle
|
||
API](https://developer.paddle.com/api-reference/customers/create-customer):
|
||
|
||
|
||
|
||
1$customer = $user->createAsCustomer($options);
|
||
|
||
|
||
$customer = $user->createAsCustomer($options);
|
||
|
||
## Subscriptions
|
||
|
||
### Creating Subscriptions
|
||
|
||
To create a subscription, first retrieve an instance of your billable model
|
||
from your database, which will typically be an instance of `App\Models\User`.
|
||
Once you have retrieved the model instance, you may use the `subscribe` method
|
||
to create the model's checkout session:
|
||
|
||
|
||
|
||
1use Illuminate\Http\Request;
|
||
|
||
2
|
||
|
||
3Route::get('/user/subscribe', function (Request $request) {
|
||
|
||
4 $checkout = $request->user()->subscribe($premium = 'pri_123', 'default')
|
||
|
||
5 ->returnTo(route('home'));
|
||
|
||
6
|
||
|
||
7 return view('billing', ['checkout' => $checkout]);
|
||
|
||
8});
|
||
|
||
|
||
use Illuminate\Http\Request;
|
||
|
||
Route::get('/user/subscribe', function (Request $request) {
|
||
$checkout = $request->user()->subscribe($premium = 'pri_123', 'default')
|
||
->returnTo(route('home'));
|
||
|
||
return view('billing', ['checkout' => $checkout]);
|
||
});
|
||
|
||
The first argument given to the `subscribe` method is the specific price the
|
||
user is subscribing to. This value should correspond to the price's identifier
|
||
in Paddle. The `returnTo` method accepts a URL that your user will be
|
||
redirected to after they successfully complete the checkout. The second
|
||
argument passed to the `subscribe` method should be the internal "type" of the
|
||
subscription. If your application only offers a single subscription, you might
|
||
call this `default` or `primary`. This subscription type is only for internal
|
||
application usage and is not meant to be displayed to users. In addition, it
|
||
should not contain spaces and it should never be changed after creating the
|
||
subscription.
|
||
|
||
You may also provide an array of custom metadata regarding the subscription
|
||
using the `customData` method:
|
||
|
||
|
||
|
||
1$checkout = $request->user()->subscribe($premium = 'pri_123', 'default')
|
||
|
||
2 ->customData(['key' => 'value'])
|
||
|
||
3 ->returnTo(route('home'));
|
||
|
||
|
||
$checkout = $request->user()->subscribe($premium = 'pri_123', 'default')
|
||
->customData(['key' => 'value'])
|
||
->returnTo(route('home'));
|
||
|
||
Once a subscription checkout session has been created, the checkout session
|
||
may be provided to the `paddle-button` Blade component that is included with
|
||
Cashier Paddle:
|
||
|
||
|
||
|
||
1<x-paddle-button :checkout="$checkout" class="px-8 py-4">
|
||
|
||
2 Subscribe
|
||
|
||
3</x-paddle-button>
|
||
|
||
|
||
<x-paddle-button :checkout="$checkout" class="px-8 py-4">
|
||
Subscribe
|
||
</x-paddle-button>
|
||
|
||
After the user has finished their checkout, a `subscription_created` webhook
|
||
will be dispatched from Paddle. Cashier will receive this webhook and setup
|
||
the subscription for your customer. In order to make sure all webhooks are
|
||
properly received and handled by your application, ensure you have properly
|
||
setup webhook handling.
|
||
|
||
### Checking Subscription Status
|
||
|
||
Once a user is subscribed to your application, you may check their
|
||
subscription status using a variety of convenient methods. First, the
|
||
`subscribed` method returns `true` if the user has a valid subscription, even
|
||
if the subscription is currently within its trial period:
|
||
|
||
|
||
|
||
1if ($user->subscribed()) {
|
||
|
||
2 // ...
|
||
|
||
3}
|
||
|
||
|
||
if ($user->subscribed()) {
|
||
// ...
|
||
}
|
||
|
||
If your application offers multiple subscriptions, you may specify the
|
||
subscription when invoking the `subscribed` method:
|
||
|
||
|
||
|
||
1if ($user->subscribed('default')) {
|
||
|
||
2 // ...
|
||
|
||
3}
|
||
|
||
|
||
if ($user->subscribed('default')) {
|
||
// ...
|
||
}
|
||
|
||
The `subscribed` method also makes a great candidate for a [route
|
||
middleware](/docs/12.x/middleware), allowing you to filter access to routes
|
||
and controllers based on the user's subscription status:
|
||
|
||
|
||
|
||
1<?php
|
||
|
||
2
|
||
|
||
3namespace App\Http\Middleware;
|
||
|
||
4
|
||
|
||
5use Closure;
|
||
|
||
6use Illuminate\Http\Request;
|
||
|
||
7use Symfony\Component\HttpFoundation\Response;
|
||
|
||
8
|
||
|
||
9class EnsureUserIsSubscribed
|
||
|
||
10{
|
||
|
||
11 /**
|
||
|
||
12 * Handle an incoming request.
|
||
|
||
13 *
|
||
|
||
14 * @param \Closure(\Illuminate\Http\Request): (\Symfony\Component\HttpFoundation\Response) $next
|
||
|
||
15 */
|
||
|
||
16 public function handle(Request $request, Closure $next): Response
|
||
|
||
17 {
|
||
|
||
18 if ($request->user() && ! $request->user()->subscribed()) {
|
||
|
||
19 // This user is not a paying customer...
|
||
|
||
20 return redirect('/billing');
|
||
|
||
21 }
|
||
|
||
22
|
||
|
||
23 return $next($request);
|
||
|
||
24 }
|
||
|
||
25}
|
||
|
||
|
||
<?php
|
||
|
||
namespace App\Http\Middleware;
|
||
|
||
use Closure;
|
||
use Illuminate\Http\Request;
|
||
use Symfony\Component\HttpFoundation\Response;
|
||
|
||
class EnsureUserIsSubscribed
|
||
{
|
||
/**
|
||
* Handle an incoming request.
|
||
*
|
||
* @param \Closure(\Illuminate\Http\Request): (\Symfony\Component\HttpFoundation\Response) $next
|
||
*/
|
||
public function handle(Request $request, Closure $next): Response
|
||
{
|
||
if ($request->user() && ! $request->user()->subscribed()) {
|
||
// This user is not a paying customer...
|
||
return redirect('/billing');
|
||
}
|
||
|
||
return $next($request);
|
||
}
|
||
}
|
||
|
||
If you would like to determine if a user is still within their trial period,
|
||
you may use the `onTrial` method. This method can be useful for determining if
|
||
you should display a warning to the user that they are still on their trial
|
||
period:
|
||
|
||
|
||
|
||
1if ($user->subscription()->onTrial()) {
|
||
|
||
2 // ...
|
||
|
||
3}
|
||
|
||
|
||
if ($user->subscription()->onTrial()) {
|
||
// ...
|
||
}
|
||
|
||
The `subscribedToPrice` method may be used to determine if the user is
|
||
subscribed to a given plan based on a given Paddle price ID. In this example,
|
||
we will determine if the user's `default` subscription is actively subscribed
|
||
to the monthly price:
|
||
|
||
|
||
|
||
1if ($user->subscribedToPrice($monthly = 'pri_123', 'default')) {
|
||
|
||
2 // ...
|
||
|
||
3}
|
||
|
||
|
||
if ($user->subscribedToPrice($monthly = 'pri_123', 'default')) {
|
||
// ...
|
||
}
|
||
|
||
The `recurring` method may be used to determine if the user is currently on an
|
||
active subscription and is no longer within their trial period or on a grace
|
||
period:
|
||
|
||
|
||
|
||
1if ($user->subscription()->recurring()) {
|
||
|
||
2 // ...
|
||
|
||
3}
|
||
|
||
|
||
if ($user->subscription()->recurring()) {
|
||
// ...
|
||
}
|
||
|
||
#### Canceled Subscription Status
|
||
|
||
To determine if the user was once an active subscriber but has canceled their
|
||
subscription, you may use the `canceled` method:
|
||
|
||
|
||
|
||
1if ($user->subscription()->canceled()) {
|
||
|
||
2 // ...
|
||
|
||
3}
|
||
|
||
|
||
if ($user->subscription()->canceled()) {
|
||
// ...
|
||
}
|
||
|
||
You may also determine if a user has canceled their subscription, but are
|
||
still on their "grace period" until the subscription fully expires. For
|
||
example, if a user cancels a subscription on March 5th that was originally
|
||
scheduled to expire on March 10th, the user is on their "grace period" until
|
||
March 10th. In addition, the `subscribed` method will still return `true`
|
||
during this time:
|
||
|
||
|
||
|
||
1if ($user->subscription()->onGracePeriod()) {
|
||
|
||
2 // ...
|
||
|
||
3}
|
||
|
||
|
||
if ($user->subscription()->onGracePeriod()) {
|
||
// ...
|
||
}
|
||
|
||
#### Past Due Status
|
||
|
||
If a payment fails for a subscription, it will be marked as `past_due`. When
|
||
your subscription is in this state it will not be active until the customer
|
||
has updated their payment information. You may determine if a subscription is
|
||
past due using the `pastDue` method on the subscription instance:
|
||
|
||
|
||
|
||
1if ($user->subscription()->pastDue()) {
|
||
|
||
2 // ...
|
||
|
||
3}
|
||
|
||
|
||
if ($user->subscription()->pastDue()) {
|
||
// ...
|
||
}
|
||
|
||
When a subscription is past due, you should instruct the user to update their
|
||
payment information.
|
||
|
||
If you would like subscriptions to still be considered valid when they are
|
||
`past_due`, you may use the `keepPastDueSubscriptionsActive` method provided
|
||
by Cashier. Typically, this method should be called in the `register` method
|
||
of your `AppServiceProvider`:
|
||
|
||
|
||
|
||
1use Laravel\Paddle\Cashier;
|
||
|
||
2
|
||
|
||
3/**
|
||
|
||
4 * Register any application services.
|
||
|
||
5 */
|
||
|
||
6public function register(): void
|
||
|
||
7{
|
||
|
||
8 Cashier::keepPastDueSubscriptionsActive();
|
||
|
||
9}
|
||
|
||
|
||
use Laravel\Paddle\Cashier;
|
||
|
||
/**
|
||
* Register any application services.
|
||
*/
|
||
public function register(): void
|
||
{
|
||
Cashier::keepPastDueSubscriptionsActive();
|
||
}
|
||
|
||
When a subscription is in a `past_due` state it cannot be changed until
|
||
payment information has been updated. Therefore, the `swap` and
|
||
`updateQuantity` methods will throw an exception when the subscription is in a
|
||
`past_due` state.
|
||
|
||
#### Subscription Scopes
|
||
|
||
Most subscription states are also available as query scopes so that you may
|
||
easily query your database for subscriptions that are in a given state:
|
||
|
||
|
||
|
||
1// Get all valid subscriptions...
|
||
|
||
2$subscriptions = Subscription::query()->valid()->get();
|
||
|
||
3
|
||
|
||
4// Get all of the canceled subscriptions for a user...
|
||
|
||
5$subscriptions = $user->subscriptions()->canceled()->get();
|
||
|
||
|
||
// Get all valid subscriptions...
|
||
$subscriptions = Subscription::query()->valid()->get();
|
||
|
||
// Get all of the canceled subscriptions for a user...
|
||
$subscriptions = $user->subscriptions()->canceled()->get();
|
||
|
||
A complete list of available scopes is available below:
|
||
|
||
|
||
|
||
1Subscription::query()->valid();
|
||
|
||
2Subscription::query()->onTrial();
|
||
|
||
3Subscription::query()->expiredTrial();
|
||
|
||
4Subscription::query()->notOnTrial();
|
||
|
||
5Subscription::query()->active();
|
||
|
||
6Subscription::query()->recurring();
|
||
|
||
7Subscription::query()->pastDue();
|
||
|
||
8Subscription::query()->paused();
|
||
|
||
9Subscription::query()->notPaused();
|
||
|
||
10Subscription::query()->onPausedGracePeriod();
|
||
|
||
11Subscription::query()->notOnPausedGracePeriod();
|
||
|
||
12Subscription::query()->canceled();
|
||
|
||
13Subscription::query()->notCanceled();
|
||
|
||
14Subscription::query()->onGracePeriod();
|
||
|
||
15Subscription::query()->notOnGracePeriod();
|
||
|
||
|
||
Subscription::query()->valid();
|
||
Subscription::query()->onTrial();
|
||
Subscription::query()->expiredTrial();
|
||
Subscription::query()->notOnTrial();
|
||
Subscription::query()->active();
|
||
Subscription::query()->recurring();
|
||
Subscription::query()->pastDue();
|
||
Subscription::query()->paused();
|
||
Subscription::query()->notPaused();
|
||
Subscription::query()->onPausedGracePeriod();
|
||
Subscription::query()->notOnPausedGracePeriod();
|
||
Subscription::query()->canceled();
|
||
Subscription::query()->notCanceled();
|
||
Subscription::query()->onGracePeriod();
|
||
Subscription::query()->notOnGracePeriod();
|
||
|
||
### Subscription Single Charges
|
||
|
||
Subscription single charges allow you to charge subscribers with a one-time
|
||
charge on top of their subscriptions. You must provide one or multiple price
|
||
ID's when invoking the `charge` method:
|
||
|
||
|
||
|
||
1// Charge a single price...
|
||
|
||
2$response = $user->subscription()->charge('pri_123');
|
||
|
||
3
|
||
|
||
4// Charge multiple prices at once...
|
||
|
||
5$response = $user->subscription()->charge(['pri_123', 'pri_456']);
|
||
|
||
|
||
// Charge a single price...
|
||
$response = $user->subscription()->charge('pri_123');
|
||
|
||
// Charge multiple prices at once...
|
||
$response = $user->subscription()->charge(['pri_123', 'pri_456']);
|
||
|
||
The `charge` method will not actually charge the customer until the next
|
||
billing interval of their subscription. If you would like to bill the customer
|
||
immediately, you may use the `chargeAndInvoice` method instead:
|
||
|
||
|
||
|
||
1$response = $user->subscription()->chargeAndInvoice('pri_123');
|
||
|
||
|
||
$response = $user->subscription()->chargeAndInvoice('pri_123');
|
||
|
||
### Updating Payment Information
|
||
|
||
Paddle always saves a payment method per subscription. If you want to update
|
||
the default payment method for a subscription, you should redirect your
|
||
customer to Paddle's hosted payment method update page using the
|
||
`redirectToUpdatePaymentMethod` method on the subscription model:
|
||
|
||
|
||
|
||
1use Illuminate\Http\Request;
|
||
|
||
2
|
||
|
||
3Route::get('/update-payment-method', function (Request $request) {
|
||
|
||
4 $user = $request->user();
|
||
|
||
5
|
||
|
||
6 return $user->subscription()->redirectToUpdatePaymentMethod();
|
||
|
||
7});
|
||
|
||
|
||
use Illuminate\Http\Request;
|
||
|
||
Route::get('/update-payment-method', function (Request $request) {
|
||
$user = $request->user();
|
||
|
||
return $user->subscription()->redirectToUpdatePaymentMethod();
|
||
});
|
||
|
||
When a user has finished updating their information, a `subscription_updated`
|
||
webhook will be dispatched by Paddle and the subscription details will be
|
||
updated in your application's database.
|
||
|
||
### Changing Plans
|
||
|
||
After a user has subscribed to your application, they may occasionally want to
|
||
change to a new subscription plan. To update the subscription plan for a user,
|
||
you should pass the Paddle price's identifier to the subscription's `swap`
|
||
method:
|
||
|
||
|
||
|
||
1use App\Models\User;
|
||
|
||
2
|
||
|
||
3$user = User::find(1);
|
||
|
||
4
|
||
|
||
5$user->subscription()->swap($premium = 'pri_456');
|
||
|
||
|
||
use App\Models\User;
|
||
|
||
$user = User::find(1);
|
||
|
||
$user->subscription()->swap($premium = 'pri_456');
|
||
|
||
If you would like to swap plans and immediately invoice the user instead of
|
||
waiting for their next billing cycle, you may use the `swapAndInvoice` method:
|
||
|
||
|
||
|
||
1$user = User::find(1);
|
||
|
||
2
|
||
|
||
3$user->subscription()->swapAndInvoice($premium = 'pri_456');
|
||
|
||
|
||
$user = User::find(1);
|
||
|
||
$user->subscription()->swapAndInvoice($premium = 'pri_456');
|
||
|
||
#### Prorations
|
||
|
||
By default, Paddle prorates charges when swapping between plans. The
|
||
`noProrate` method may be used to update the subscriptions without prorating
|
||
the charges:
|
||
|
||
|
||
|
||
1$user->subscription('default')->noProrate()->swap($premium = 'pri_456');
|
||
|
||
|
||
$user->subscription('default')->noProrate()->swap($premium = 'pri_456');
|
||
|
||
If you would like to disable proration and invoice customers immediately, you
|
||
may use the `swapAndInvoice` method in combination with `noProrate`:
|
||
|
||
|
||
|
||
1$user->subscription('default')->noProrate()->swapAndInvoice($premium = 'pri_456');
|
||
|
||
|
||
$user->subscription('default')->noProrate()->swapAndInvoice($premium = 'pri_456');
|
||
|
||
Or, to not bill your customer for a subscription change, you may utilize the
|
||
`doNotBill` method:
|
||
|
||
|
||
|
||
1$user->subscription('default')->doNotBill()->swap($premium = 'pri_456');
|
||
|
||
|
||
$user->subscription('default')->doNotBill()->swap($premium = 'pri_456');
|
||
|
||
For more information on Paddle's proration policies, please consult Paddle's
|
||
[proration
|
||
documentation](https://developer.paddle.com/concepts/subscriptions/proration).
|
||
|
||
### Subscription Quantity
|
||
|
||
Sometimes subscriptions are affected by "quantity". For example, a project
|
||
management application might charge $10 per month per project. To easily
|
||
increment or decrement your subscription's quantity, use the
|
||
`incrementQuantity` and `decrementQuantity` methods:
|
||
|
||
|
||
|
||
1$user = User::find(1);
|
||
|
||
2
|
||
|
||
3$user->subscription()->incrementQuantity();
|
||
|
||
4
|
||
|
||
5// Add five to the subscription's current quantity...
|
||
|
||
6$user->subscription()->incrementQuantity(5);
|
||
|
||
7
|
||
|
||
8$user->subscription()->decrementQuantity();
|
||
|
||
9
|
||
|
||
10// Subtract five from the subscription's current quantity...
|
||
|
||
11$user->subscription()->decrementQuantity(5);
|
||
|
||
|
||
$user = User::find(1);
|
||
|
||
$user->subscription()->incrementQuantity();
|
||
|
||
// Add five to the subscription's current quantity...
|
||
$user->subscription()->incrementQuantity(5);
|
||
|
||
$user->subscription()->decrementQuantity();
|
||
|
||
// Subtract five from the subscription's current quantity...
|
||
$user->subscription()->decrementQuantity(5);
|
||
|
||
Alternatively, you may set a specific quantity using the `updateQuantity`
|
||
method:
|
||
|
||
|
||
|
||
1$user->subscription()->updateQuantity(10);
|
||
|
||
|
||
$user->subscription()->updateQuantity(10);
|
||
|
||
The `noProrate` method may be used to update the subscription's quantity
|
||
without prorating the charges:
|
||
|
||
|
||
|
||
1$user->subscription()->noProrate()->updateQuantity(10);
|
||
|
||
|
||
$user->subscription()->noProrate()->updateQuantity(10);
|
||
|
||
#### Quantities for Subscriptions With Multiple Products
|
||
|
||
If your subscription is a subscription with multiple products, you should pass
|
||
the ID of the price whose quantity you wish to increment or decrement as the
|
||
second argument to the increment / decrement methods:
|
||
|
||
|
||
|
||
1$user->subscription()->incrementQuantity(1, 'price_chat');
|
||
|
||
|
||
$user->subscription()->incrementQuantity(1, 'price_chat');
|
||
|
||
### Subscriptions With Multiple Products
|
||
|
||
[Subscription with multiple
|
||
products](https://developer.paddle.com/build/subscriptions/add-remove-
|
||
products-prices-addons) allow you to assign multiple billing products to a
|
||
single subscription. For example, imagine you are building a customer service
|
||
"helpdesk" application that has a base subscription price of $10 per month but
|
||
offers a live chat add-on product for an additional $15 per month.
|
||
|
||
When creating subscription checkout sessions, you may specify multiple
|
||
products for a given subscription by passing an array of prices as the first
|
||
argument to the `subscribe` method:
|
||
|
||
|
||
|
||
1use Illuminate\Http\Request;
|
||
|
||
2
|
||
|
||
3Route::post('/user/subscribe', function (Request $request) {
|
||
|
||
4 $checkout = $request->user()->subscribe([
|
||
|
||
5 'price_monthly',
|
||
|
||
6 'price_chat',
|
||
|
||
7 ]);
|
||
|
||
8
|
||
|
||
9 return view('billing', ['checkout' => $checkout]);
|
||
|
||
10});
|
||
|
||
|
||
use Illuminate\Http\Request;
|
||
|
||
Route::post('/user/subscribe', function (Request $request) {
|
||
$checkout = $request->user()->subscribe([
|
||
'price_monthly',
|
||
'price_chat',
|
||
]);
|
||
|
||
return view('billing', ['checkout' => $checkout]);
|
||
});
|
||
|
||
In the example above, the customer will have two prices attached to their
|
||
`default` subscription. Both prices will be charged on their respective
|
||
billing intervals. If necessary, you may pass an associative array of key /
|
||
value pairs to indicate a specific quantity for each price:
|
||
|
||
|
||
|
||
1$user = User::find(1);
|
||
|
||
2
|
||
|
||
3$checkout = $user->subscribe('default', ['price_monthly', 'price_chat' => 5]);
|
||
|
||
|
||
$user = User::find(1);
|
||
|
||
$checkout = $user->subscribe('default', ['price_monthly', 'price_chat' => 5]);
|
||
|
||
If you would like to add another price to an existing subscription, you must
|
||
use the subscription's `swap` method. When invoking the `swap` method, you
|
||
should also include the subscription's current prices and quantities as well:
|
||
|
||
|
||
|
||
1$user = User::find(1);
|
||
|
||
2
|
||
|
||
3$user->subscription()->swap(['price_chat', 'price_original' => 2]);
|
||
|
||
|
||
$user = User::find(1);
|
||
|
||
$user->subscription()->swap(['price_chat', 'price_original' => 2]);
|
||
|
||
The example above will add the new price, but the customer will not be billed
|
||
for it until their next billing cycle. If you would like to bill the customer
|
||
immediately you may use the `swapAndInvoice` method:
|
||
|
||
|
||
|
||
1$user->subscription()->swapAndInvoice(['price_chat', 'price_original' => 2]);
|
||
|
||
|
||
$user->subscription()->swapAndInvoice(['price_chat', 'price_original' => 2]);
|
||
|
||
You may remove prices from subscriptions using the `swap` method and omitting
|
||
the price you want to remove:
|
||
|
||
|
||
|
||
1$user->subscription()->swap(['price_original' => 2]);
|
||
|
||
|
||
$user->subscription()->swap(['price_original' => 2]);
|
||
|
||
You may not remove the last price on a subscription. Instead, you should
|
||
simply cancel the subscription.
|
||
|
||
### Multiple Subscriptions
|
||
|
||
Paddle allows your customers to have multiple subscriptions simultaneously.
|
||
For example, you may run a gym that offers a swimming subscription and a
|
||
weight-lifting subscription, and each subscription may have different pricing.
|
||
Of course, customers should be able to subscribe to either or both plans.
|
||
|
||
When your application creates subscriptions, you may provide the type of the
|
||
subscription to the `subscribe` method as the second argument. The type may be
|
||
any string that represents the type of subscription the user is initiating:
|
||
|
||
|
||
|
||
1use Illuminate\Http\Request;
|
||
|
||
2
|
||
|
||
3Route::post('/swimming/subscribe', function (Request $request) {
|
||
|
||
4 $checkout = $request->user()->subscribe($swimmingMonthly = 'pri_123', 'swimming');
|
||
|
||
5
|
||
|
||
6 return view('billing', ['checkout' => $checkout]);
|
||
|
||
7});
|
||
|
||
|
||
use Illuminate\Http\Request;
|
||
|
||
Route::post('/swimming/subscribe', function (Request $request) {
|
||
$checkout = $request->user()->subscribe($swimmingMonthly = 'pri_123', 'swimming');
|
||
|
||
return view('billing', ['checkout' => $checkout]);
|
||
});
|
||
|
||
In this example, we initiated a monthly swimming subscription for the
|
||
customer. However, they may want to swap to a yearly subscription at a later
|
||
time. When adjusting the customer's subscription, we can simply swap the price
|
||
on the `swimming` subscription:
|
||
|
||
|
||
|
||
1$user->subscription('swimming')->swap($swimmingYearly = 'pri_456');
|
||
|
||
|
||
$user->subscription('swimming')->swap($swimmingYearly = 'pri_456');
|
||
|
||
Of course, you may also cancel the subscription entirely:
|
||
|
||
|
||
|
||
1$user->subscription('swimming')->cancel();
|
||
|
||
|
||
$user->subscription('swimming')->cancel();
|
||
|
||
### Pausing Subscriptions
|
||
|
||
To pause a subscription, call the `pause` method on the user's subscription:
|
||
|
||
|
||
|
||
1$user->subscription()->pause();
|
||
|
||
|
||
$user->subscription()->pause();
|
||
|
||
When a subscription is paused, Cashier will automatically set the `paused_at`
|
||
column in your database. This column is used to determine when the `paused`
|
||
method should begin returning `true`. For example, if a customer pauses a
|
||
subscription on March 1st, but the subscription was not scheduled to recur
|
||
until March 5th, the `paused` method will continue to return `false` until
|
||
March 5th. This is because a user is typically allowed to continue using an
|
||
application until the end of their billing cycle.
|
||
|
||
By default, pausing happens at the next billing interval so the customer can
|
||
use the remainder of the period they paid for. If you want to pause a
|
||
subscription immediately, you may use the `pauseNow` method:
|
||
|
||
|
||
|
||
1$user->subscription()->pauseNow();
|
||
|
||
|
||
$user->subscription()->pauseNow();
|
||
|
||
Using the `pauseUntil` method, you can pause the subscription until a specific
|
||
moment in time:
|
||
|
||
|
||
|
||
1$user->subscription()->pauseUntil(now()->addMonth());
|
||
|
||
|
||
$user->subscription()->pauseUntil(now()->addMonth());
|
||
|
||
Or, you may use the `pauseNowUntil` method to immediately pause the
|
||
subscription until a given point in time:
|
||
|
||
|
||
|
||
1$user->subscription()->pauseNowUntil(now()->addMonth());
|
||
|
||
|
||
$user->subscription()->pauseNowUntil(now()->addMonth());
|
||
|
||
You may determine if a user has paused their subscription but are still on
|
||
their "grace period" using the `onPausedGracePeriod` method:
|
||
|
||
|
||
|
||
1if ($user->subscription()->onPausedGracePeriod()) {
|
||
|
||
2 // ...
|
||
|
||
3}
|
||
|
||
|
||
if ($user->subscription()->onPausedGracePeriod()) {
|
||
// ...
|
||
}
|
||
|
||
To resume a paused subscription, you may invoke the `resume` method on the
|
||
subscription:
|
||
|
||
|
||
|
||
1$user->subscription()->resume();
|
||
|
||
|
||
$user->subscription()->resume();
|
||
|
||
A subscription cannot be modified while it is paused. If you want to swap to a
|
||
different plan or update quantities you must resume the subscription first.
|
||
|
||
### Canceling Subscriptions
|
||
|
||
To cancel a subscription, call the `cancel` method on the user's subscription:
|
||
|
||
|
||
|
||
1$user->subscription()->cancel();
|
||
|
||
|
||
$user->subscription()->cancel();
|
||
|
||
When a subscription is canceled, Cashier will automatically set the `ends_at`
|
||
column in your database. This column is used to determine when the
|
||
`subscribed` method should begin returning `false`. For example, if a customer
|
||
cancels a subscription on March 1st, but the subscription was not scheduled to
|
||
end until March 5th, the `subscribed` method will continue to return `true`
|
||
until March 5th. This is done because a user is typically allowed to continue
|
||
using an application until the end of their billing cycle.
|
||
|
||
You may determine if a user has canceled their subscription but are still on
|
||
their "grace period" using the `onGracePeriod` method:
|
||
|
||
|
||
|
||
1if ($user->subscription()->onGracePeriod()) {
|
||
|
||
2 // ...
|
||
|
||
3}
|
||
|
||
|
||
if ($user->subscription()->onGracePeriod()) {
|
||
// ...
|
||
}
|
||
|
||
If you wish to cancel a subscription immediately, you may call the `cancelNow`
|
||
method on the subscription:
|
||
|
||
|
||
|
||
1$user->subscription()->cancelNow();
|
||
|
||
|
||
$user->subscription()->cancelNow();
|
||
|
||
To stop a subscription on its grace period from canceling, you may invoke the
|
||
`stopCancelation` method:
|
||
|
||
|
||
|
||
1$user->subscription()->stopCancelation();
|
||
|
||
|
||
$user->subscription()->stopCancelation();
|
||
|
||
Paddle's subscriptions cannot be resumed after cancelation. If your customer
|
||
wishes to resume their subscription, they will have to create a new
|
||
subscription.
|
||
|
||
## Subscription Trials
|
||
|
||
### With Payment Method Up Front
|
||
|
||
If you would like to offer trial periods to your customers while still
|
||
collecting payment method information up front, you should use set a trial
|
||
time in the Paddle dashboard on the price your customer is subscribing to.
|
||
Then, initiate the checkout session as normal:
|
||
|
||
|
||
|
||
1use Illuminate\Http\Request;
|
||
|
||
2
|
||
|
||
3Route::get('/user/subscribe', function (Request $request) {
|
||
|
||
4 $checkout = $request->user()
|
||
|
||
5 ->subscribe('pri_monthly')
|
||
|
||
6 ->returnTo(route('home'));
|
||
|
||
7
|
||
|
||
8 return view('billing', ['checkout' => $checkout]);
|
||
|
||
9});
|
||
|
||
|
||
use Illuminate\Http\Request;
|
||
|
||
Route::get('/user/subscribe', function (Request $request) {
|
||
$checkout = $request->user()
|
||
->subscribe('pri_monthly')
|
||
->returnTo(route('home'));
|
||
|
||
return view('billing', ['checkout' => $checkout]);
|
||
});
|
||
|
||
When your application receives the `subscription_created` event, Cashier will
|
||
set the trial period ending date on the subscription record within your
|
||
application's database as well as instruct Paddle to not begin billing the
|
||
customer until after this date.
|
||
|
||
If the customer's subscription is not canceled before the trial ending date
|
||
they will be charged as soon as the trial expires, so you should be sure to
|
||
notify your users of their trial ending date.
|
||
|
||
You may determine if the user is within their trial period using either the
|
||
`onTrial` method of the user instance:
|
||
|
||
|
||
|
||
1if ($user->onTrial()) {
|
||
|
||
2 // ...
|
||
|
||
3}
|
||
|
||
|
||
if ($user->onTrial()) {
|
||
// ...
|
||
}
|
||
|
||
To determine if an existing trial has expired, you may use the
|
||
`hasExpiredTrial` methods:
|
||
|
||
|
||
|
||
1if ($user->hasExpiredTrial()) {
|
||
|
||
2 // ...
|
||
|
||
3}
|
||
|
||
|
||
if ($user->hasExpiredTrial()) {
|
||
// ...
|
||
}
|
||
|
||
To determine if a user is on trial for a specific subscription type, you may
|
||
provide the type to the `onTrial` or `hasExpiredTrial` methods:
|
||
|
||
|
||
|
||
1if ($user->onTrial('default')) {
|
||
|
||
2 // ...
|
||
|
||
3}
|
||
|
||
4
|
||
|
||
5if ($user->hasExpiredTrial('default')) {
|
||
|
||
6 // ...
|
||
|
||
7}
|
||
|
||
|
||
if ($user->onTrial('default')) {
|
||
// ...
|
||
}
|
||
|
||
if ($user->hasExpiredTrial('default')) {
|
||
// ...
|
||
}
|
||
|
||
### Without Payment Method Up Front
|
||
|
||
If you would like to offer trial periods without collecting the user's payment
|
||
method information up front, you may set the `trial_ends_at` column on the
|
||
customer record attached to your user to your desired trial ending date. This
|
||
is typically done during user registration:
|
||
|
||
|
||
|
||
1use App\Models\User;
|
||
|
||
2
|
||
|
||
3$user = User::create([
|
||
|
||
4 // ...
|
||
|
||
5]);
|
||
|
||
6
|
||
|
||
7$user->createAsCustomer([
|
||
|
||
8 'trial_ends_at' => now()->addDays(10)
|
||
|
||
9]);
|
||
|
||
|
||
use App\Models\User;
|
||
|
||
$user = User::create([
|
||
// ...
|
||
]);
|
||
|
||
$user->createAsCustomer([
|
||
'trial_ends_at' => now()->addDays(10)
|
||
]);
|
||
|
||
Cashier refers to this type of trial as a "generic trial", since it is not
|
||
attached to any existing subscription. The `onTrial` method on the `User`
|
||
instance will return `true` if the current date is not past the value of
|
||
`trial_ends_at`:
|
||
|
||
|
||
|
||
1if ($user->onTrial()) {
|
||
|
||
2 // User is within their trial period...
|
||
|
||
3}
|
||
|
||
|
||
if ($user->onTrial()) {
|
||
// User is within their trial period...
|
||
}
|
||
|
||
Once you are ready to create an actual subscription for the user, you may use
|
||
the `subscribe` method as usual:
|
||
|
||
|
||
|
||
1use Illuminate\Http\Request;
|
||
|
||
2
|
||
|
||
3Route::get('/user/subscribe', function (Request $request) {
|
||
|
||
4 $checkout = $request->user()
|
||
|
||
5 ->subscribe('pri_monthly')
|
||
|
||
6 ->returnTo(route('home'));
|
||
|
||
7
|
||
|
||
8 return view('billing', ['checkout' => $checkout]);
|
||
|
||
9});
|
||
|
||
|
||
use Illuminate\Http\Request;
|
||
|
||
Route::get('/user/subscribe', function (Request $request) {
|
||
$checkout = $request->user()
|
||
->subscribe('pri_monthly')
|
||
->returnTo(route('home'));
|
||
|
||
return view('billing', ['checkout' => $checkout]);
|
||
});
|
||
|
||
To retrieve the user's trial ending date, you may use the `trialEndsAt`
|
||
method. This method will return a Carbon date instance if a user is on a trial
|
||
or `null` if they aren't. You may also pass an optional subscription type
|
||
parameter if you would like to get the trial ending date for a specific
|
||
subscription other than the default one:
|
||
|
||
|
||
|
||
1if ($user->onTrial('default')) {
|
||
|
||
2 $trialEndsAt = $user->trialEndsAt();
|
||
|
||
3}
|
||
|
||
|
||
if ($user->onTrial('default')) {
|
||
$trialEndsAt = $user->trialEndsAt();
|
||
}
|
||
|
||
You may use the `onGenericTrial` method if you wish to know specifically that
|
||
the user is within their "generic" trial period and has not created an actual
|
||
subscription yet:
|
||
|
||
|
||
|
||
1if ($user->onGenericTrial()) {
|
||
|
||
2 // User is within their "generic" trial period...
|
||
|
||
3}
|
||
|
||
|
||
if ($user->onGenericTrial()) {
|
||
// User is within their "generic" trial period...
|
||
}
|
||
|
||
### Extend or Activate a Trial
|
||
|
||
You can extend an existing trial period on a subscription by invoking the
|
||
`extendTrial` method and specifying the moment in time that the trial should
|
||
end:
|
||
|
||
|
||
|
||
1$user->subscription()->extendTrial(now()->addDays(5));
|
||
|
||
|
||
$user->subscription()->extendTrial(now()->addDays(5));
|
||
|
||
Or, you may immediately activate a subscription by ending its trial by calling
|
||
the `activate` method on the subscription:
|
||
|
||
|
||
|
||
1$user->subscription()->activate();
|
||
|
||
|
||
$user->subscription()->activate();
|
||
|
||
## Handling Paddle Webhooks
|
||
|
||
Paddle can notify your application of a variety of events via webhooks. By
|
||
default, a route that points to Cashier's webhook controller is registered by
|
||
the Cashier service provider. This controller will handle all incoming webhook
|
||
requests.
|
||
|
||
By default, this controller will automatically handle canceling subscriptions
|
||
that have too many failed charges, subscription updates, and payment method
|
||
changes; however, as we'll soon discover, you can extend this controller to
|
||
handle any Paddle webhook event you like.
|
||
|
||
To ensure your application can handle Paddle webhooks, be sure to [configure
|
||
the webhook URL in the Paddle control
|
||
panel](https://vendors.paddle.com/notifications-v2). By default, Cashier's
|
||
webhook controller responds to the `/paddle/webhook` URL path. The full list
|
||
of all webhooks you should enable in the Paddle control panel are:
|
||
|
||
* Customer Updated
|
||
* Transaction Completed
|
||
* Transaction Updated
|
||
* Subscription Created
|
||
* Subscription Updated
|
||
* Subscription Paused
|
||
* Subscription Canceled
|
||
|
||
Make sure you protect incoming requests with Cashier's included [webhook
|
||
signature verification](/docs/12.x/cashier-paddle#verifying-webhook-
|
||
signatures) middleware.
|
||
|
||
#### Webhooks and CSRF Protection
|
||
|
||
Since Paddle webhooks need to bypass Laravel's [CSRF
|
||
protection](/docs/12.x/csrf), you should ensure that Laravel does not attempt
|
||
to verify the CSRF token for incoming Paddle webhooks. To accomplish this, you
|
||
should exclude `paddle/*` from CSRF protection in your application's
|
||
`bootstrap/app.php` file:
|
||
|
||
|
||
|
||
1->withMiddleware(function (Middleware $middleware) {
|
||
|
||
2 $middleware->validateCsrfTokens(except: [
|
||
|
||
3 'paddle/*',
|
||
|
||
4 ]);
|
||
|
||
5})
|
||
|
||
|
||
->withMiddleware(function (Middleware $middleware) {
|
||
$middleware->validateCsrfTokens(except: [
|
||
'paddle/*',
|
||
]);
|
||
})
|
||
|
||
#### Webhooks and Local Development
|
||
|
||
For Paddle to be able to send your application webhooks during local
|
||
development, you will need to expose your application via a site sharing
|
||
service such as [Ngrok](https://ngrok.com/) or
|
||
[Expose](https://expose.dev/docs/introduction). If you are developing your
|
||
application locally using [Laravel Sail](/docs/12.x/sail), you may use Sail's
|
||
[site sharing command](/docs/12.x/sail#sharing-your-site).
|
||
|
||
### Defining Webhook Event Handlers
|
||
|
||
Cashier automatically handles subscription cancelation on failed charges and
|
||
other common Paddle webhooks. However, if you have additional webhook events
|
||
you would like to handle, you may do so by listening to the following events
|
||
that are dispatched by Cashier:
|
||
|
||
* `Laravel\Paddle\Events\WebhookReceived`
|
||
* `Laravel\Paddle\Events\WebhookHandled`
|
||
|
||
Both events contain the full payload of the Paddle webhook. For example, if
|
||
you wish to handle the `transaction.billed` webhook, you may register a
|
||
[listener](/docs/12.x/events#defining-listeners) that will handle the event:
|
||
|
||
|
||
|
||
1<?php
|
||
|
||
2
|
||
|
||
3namespace App\Listeners;
|
||
|
||
4
|
||
|
||
5use Laravel\Paddle\Events\WebhookReceived;
|
||
|
||
6
|
||
|
||
7class PaddleEventListener
|
||
|
||
8{
|
||
|
||
9 /**
|
||
|
||
10 * Handle received Paddle webhooks.
|
||
|
||
11 */
|
||
|
||
12 public function handle(WebhookReceived $event): void
|
||
|
||
13 {
|
||
|
||
14 if ($event->payload['event_type'] === 'transaction.billed') {
|
||
|
||
15 // Handle the incoming event...
|
||
|
||
16 }
|
||
|
||
17 }
|
||
|
||
18}
|
||
|
||
|
||
<?php
|
||
|
||
namespace App\Listeners;
|
||
|
||
use Laravel\Paddle\Events\WebhookReceived;
|
||
|
||
class PaddleEventListener
|
||
{
|
||
/**
|
||
* Handle received Paddle webhooks.
|
||
*/
|
||
public function handle(WebhookReceived $event): void
|
||
{
|
||
if ($event->payload['event_type'] === 'transaction.billed') {
|
||
// Handle the incoming event...
|
||
}
|
||
}
|
||
}
|
||
|
||
Cashier also emit events dedicated to the type of the received webhook. In
|
||
addition to the full payload from Paddle, they also contain the relevant
|
||
models that were used to process the webhook such as the billable model, the
|
||
subscription, or the receipt:
|
||
|
||
* `Laravel\Paddle\Events\CustomerUpdated`
|
||
* `Laravel\Paddle\Events\TransactionCompleted`
|
||
* `Laravel\Paddle\Events\TransactionUpdated`
|
||
* `Laravel\Paddle\Events\SubscriptionCreated`
|
||
* `Laravel\Paddle\Events\SubscriptionUpdated`
|
||
* `Laravel\Paddle\Events\SubscriptionPaused`
|
||
* `Laravel\Paddle\Events\SubscriptionCanceled`
|
||
|
||
You can also override the default, built-in webhook route by defining the
|
||
`CASHIER_WEBHOOK` environment variable in your application's `.env` file. This
|
||
value should be the full URL to your webhook route and needs to match the URL
|
||
set in your Paddle control panel:
|
||
|
||
|
||
|
||
1CASHIER_WEBHOOK=https://example.com/my-paddle-webhook-url
|
||
|
||
|
||
CASHIER_WEBHOOK=https://example.com/my-paddle-webhook-url
|
||
|
||
### Verifying Webhook Signatures
|
||
|
||
To secure your webhooks, you may use [Paddle's webhook
|
||
signatures](https://developer.paddle.com/webhooks/signature-verification). For
|
||
convenience, Cashier automatically includes a middleware which validates that
|
||
the incoming Paddle webhook request is valid.
|
||
|
||
To enable webhook verification, ensure that the `PADDLE_WEBHOOK_SECRET`
|
||
environment variable is defined in your application's `.env` file. The webhook
|
||
secret may be retrieved from your Paddle account dashboard.
|
||
|
||
## Single Charges
|
||
|
||
### Charging for Products
|
||
|
||
If you would like to initiate a product purchase for a customer, you may use
|
||
the `checkout` method on a billable model instance to generate a checkout
|
||
session for the purchase. The `checkout` method accepts one or multiple price
|
||
ID's. If necessary, an associative array may be used to provide the quantity
|
||
of the product that is being purchased:
|
||
|
||
|
||
|
||
1use Illuminate\Http\Request;
|
||
|
||
2
|
||
|
||
3Route::get('/buy', function (Request $request) {
|
||
|
||
4 $checkout = $request->user()->checkout(['pri_tshirt', 'pri_socks' => 5]);
|
||
|
||
5
|
||
|
||
6 return view('buy', ['checkout' => $checkout]);
|
||
|
||
7});
|
||
|
||
|
||
use Illuminate\Http\Request;
|
||
|
||
Route::get('/buy', function (Request $request) {
|
||
$checkout = $request->user()->checkout(['pri_tshirt', 'pri_socks' => 5]);
|
||
|
||
return view('buy', ['checkout' => $checkout]);
|
||
});
|
||
|
||
After generating the checkout session, you may use Cashier's provided `paddle-
|
||
button` Blade component to allow the user to view the Paddle checkout widget
|
||
and complete the purchase:
|
||
|
||
|
||
|
||
1<x-paddle-button :checkout="$checkout" class="px-8 py-4">
|
||
|
||
2 Buy
|
||
|
||
3</x-paddle-button>
|
||
|
||
|
||
<x-paddle-button :checkout="$checkout" class="px-8 py-4">
|
||
Buy
|
||
</x-paddle-button>
|
||
|
||
A checkout session has a `customData` method, allowing you to pass any custom
|
||
data you wish to the underlying transaction creation. Please consult [the
|
||
Paddle documentation](https://developer.paddle.com/build/transactions/custom-
|
||
data) to learn more about the options available to you when passing custom
|
||
data:
|
||
|
||
|
||
|
||
1$checkout = $user->checkout('pri_tshirt')
|
||
|
||
2 ->customData([
|
||
|
||
3 'custom_option' => $value,
|
||
|
||
4 ]);
|
||
|
||
|
||
$checkout = $user->checkout('pri_tshirt')
|
||
->customData([
|
||
'custom_option' => $value,
|
||
]);
|
||
|
||
### Refunding Transactions
|
||
|
||
Refunding transactions will return the refunded amount to your customer's
|
||
payment method that was used at the time of purchase. If you need to refund a
|
||
Paddle purchase, you may use the `refund` method on a
|
||
`Cashier\Paddle\Transaction` model. This method accepts a reason as the first
|
||
argument, one or more price ID's to refund with optional amounts as an
|
||
associative array. You may retrieve the transactions for a given billable
|
||
model using the `transactions` method.
|
||
|
||
For example, imagine we want to refund a specific transaction for prices
|
||
`pri_123` and `pri_456`. We want to fully refund `pri_123`, but only refund
|
||
two dollars for `pri_456`:
|
||
|
||
|
||
|
||
1use App\Models\User;
|
||
|
||
2
|
||
|
||
3$user = User::find(1);
|
||
|
||
4
|
||
|
||
5$transaction = $user->transactions()->first();
|
||
|
||
6
|
||
|
||
7$response = $transaction->refund('Accidental charge', [
|
||
|
||
8 'pri_123', // Fully refund this price...
|
||
|
||
9 'pri_456' => 200, // Only partially refund this price...
|
||
|
||
10]);
|
||
|
||
|
||
use App\Models\User;
|
||
|
||
$user = User::find(1);
|
||
|
||
$transaction = $user->transactions()->first();
|
||
|
||
$response = $transaction->refund('Accidental charge', [
|
||
'pri_123', // Fully refund this price...
|
||
'pri_456' => 200, // Only partially refund this price...
|
||
]);
|
||
|
||
The example above refunds specific line items in a transaction. If you want to
|
||
refund the entire transaction, simply provide a reason:
|
||
|
||
|
||
|
||
1$response = $transaction->refund('Accidental charge');
|
||
|
||
|
||
$response = $transaction->refund('Accidental charge');
|
||
|
||
For more information on refunds, please consult [Paddle's refund
|
||
documentation](https://developer.paddle.com/build/transactions/create-
|
||
transaction-adjustments).
|
||
|
||
Refunds must always be approved by Paddle before fully processing.
|
||
|
||
### Crediting Transactions
|
||
|
||
Just like refunding, you can also credit transactions. Crediting transactions
|
||
will add the funds to the customer's balance so it may be used for future
|
||
purchases. Crediting transactions can only be done for manually-collected
|
||
transactions and not for automatically-collected transactions (like
|
||
subscriptions) since Paddle handles subscription credits automatically:
|
||
|
||
|
||
|
||
1$transaction = $user->transactions()->first();
|
||
|
||
2
|
||
|
||
3// Credit a specific line item fully...
|
||
|
||
4$response = $transaction->credit('Compensation', 'pri_123');
|
||
|
||
|
||
$transaction = $user->transactions()->first();
|
||
|
||
// Credit a specific line item fully...
|
||
$response = $transaction->credit('Compensation', 'pri_123');
|
||
|
||
For more info, [see Paddle's documentation on
|
||
crediting](https://developer.paddle.com/build/transactions/create-transaction-
|
||
adjustments).
|
||
|
||
Credits can only be applied for manually-collected transactions.
|
||
Automatically-collected transactions are credited by Paddle themselves.
|
||
|
||
## Transactions
|
||
|
||
You may easily retrieve an array of a billable model's transactions via the
|
||
`transactions` property:
|
||
|
||
|
||
|
||
1use App\Models\User;
|
||
|
||
2
|
||
|
||
3$user = User::find(1);
|
||
|
||
4
|
||
|
||
5$transactions = $user->transactions;
|
||
|
||
|
||
use App\Models\User;
|
||
|
||
$user = User::find(1);
|
||
|
||
$transactions = $user->transactions;
|
||
|
||
Transactions represent payments for your products and purchases and are
|
||
accompanied by invoices. Only completed transactions are stored in your
|
||
application's database.
|
||
|
||
When listing the transactions for a customer, you may use the transaction
|
||
instance's methods to display the relevant payment information. For example,
|
||
you may wish to list every transaction in a table, allowing the user to easily
|
||
download any of the invoices:
|
||
|
||
|
||
|
||
1<table>
|
||
|
||
2 @foreach ($transactions as $transaction)
|
||
|
||
3 <tr>
|
||
|
||
4 <td>{{ $transaction->billed_at->toFormattedDateString() }}</td>
|
||
|
||
5 <td>{{ $transaction->total() }}</td>
|
||
|
||
6 <td>{{ $transaction->tax() }}</td>
|
||
|
||
7 <td><a href="{{ route('download-invoice', $transaction->id) }}" target="_blank">Download</a></td>
|
||
|
||
8 </tr>
|
||
|
||
9 @endforeach
|
||
|
||
10</table>
|
||
|
||
|
||
<table>
|
||
@foreach ($transactions as $transaction)
|
||
<tr>
|
||
<td>{{ $transaction->billed_at->toFormattedDateString() }}</td>
|
||
<td>{{ $transaction->total() }}</td>
|
||
<td>{{ $transaction->tax() }}</td>
|
||
<td><a href="{{ route('download-invoice', $transaction->id) }}" target="_blank">Download</a></td>
|
||
</tr>
|
||
@endforeach
|
||
</table>
|
||
|
||
The `download-invoice` route may look like the following:
|
||
|
||
|
||
|
||
1use Illuminate\Http\Request;
|
||
|
||
2use Laravel\Paddle\Transaction;
|
||
|
||
3
|
||
|
||
4Route::get('/download-invoice/{transaction}', function (Request $request, Transaction $transaction) {
|
||
|
||
5 return $transaction->redirectToInvoicePdf();
|
||
|
||
6})->name('download-invoice');
|
||
|
||
|
||
use Illuminate\Http\Request;
|
||
use Laravel\Paddle\Transaction;
|
||
|
||
Route::get('/download-invoice/{transaction}', function (Request $request, Transaction $transaction) {
|
||
return $transaction->redirectToInvoicePdf();
|
||
})->name('download-invoice');
|
||
|
||
### Past and Upcoming Payments
|
||
|
||
You may use the `lastPayment` and `nextPayment` methods to retrieve and
|
||
display a customer's past or upcoming payments for recurring subscriptions:
|
||
|
||
|
||
|
||
1use App\Models\User;
|
||
|
||
2
|
||
|
||
3$user = User::find(1);
|
||
|
||
4
|
||
|
||
5$subscription = $user->subscription();
|
||
|
||
6
|
||
|
||
7$lastPayment = $subscription->lastPayment();
|
||
|
||
8$nextPayment = $subscription->nextPayment();
|
||
|
||
|
||
use App\Models\User;
|
||
|
||
$user = User::find(1);
|
||
|
||
$subscription = $user->subscription();
|
||
|
||
$lastPayment = $subscription->lastPayment();
|
||
$nextPayment = $subscription->nextPayment();
|
||
|
||
Both of these methods will return an instance of `Laravel\Paddle\Payment`;
|
||
however, `lastPayment` will return `null` when transactions have not been
|
||
synced by webhooks yet, while `nextPayment` will return `null` when the
|
||
billing cycle has ended (such as when a subscription has been canceled):
|
||
|
||
|
||
|
||
1Next payment: {{ $nextPayment->amount() }} due on {{ $nextPayment->date()->format('d/m/Y') }}
|
||
|
||
|
||
Next payment: {{ $nextPayment->amount() }} due on {{ $nextPayment->date()->format('d/m/Y') }}
|
||
|
||
## Testing
|
||
|
||
While testing, you should manually test your billing flow to make sure your
|
||
integration works as expected.
|
||
|
||
For automated tests, including those executed within a CI environment, you may
|
||
use [Laravel's HTTP Client](/docs/12.x/http-client#testing) to fake HTTP calls
|
||
made to Paddle. Although this does not test the actual responses from Paddle,
|
||
it does provide a way to test your application without actually calling
|
||
Paddle's API.
|
||
|