emericanec/lemon-pay

一个易于将您的 Laravel 应用程序与 Lemon Squeezy 集成的包。

资助包维护!
driesvints

dev-main / 1.x-dev 2023-05-09 16:17 UTC

This package is not auto-updated.

Last update: 2024-09-25 21:07:08 UTC


README

Readme header

Lemon Squeezy for Laravel

Tests Coding Standards Latest Stable Version Total Downloads

一个易于将您的 Laravel 应用程序与 Lemon Squeezy 集成的包。它简化了设置结账体验的过程。轻松设置产品的付款或让您的客户订阅您的产品计划。处理宽限期、暂停订阅或提供免费试用。

此包受到了 Cashier 的启发,由 Taylor Otwell 创建。

Lemon Squeezy for Laravel 由 Dries Vints 维护。任何对 资助此包开发 的赞助都将是极大的赞赏 ❤️

我们还建议您阅读 Lemon Squeezy 的 文档开发者指南

此包仍在开发中。只要没有 v1.0.0 版本,v0.x 版本的发布中可能会发生重大更改。不会提供 v0.x 版本之间的升级路径。

路线图

以下功能尚未包含在此包中,但计划在未来添加

  • 订阅发票
  • 许可证密钥
  • 营销电子邮件检查
  • 产品及变体列表
  • 自定义定价结账
  • 创建折扣代码

要求

  • PHP 8.1 或更高版本
  • Laravel 10.0 或更高版本

安装

您需要执行几个步骤来安装此包

  1. 通过 Composer 需求包
  2. 创建 API 密钥
  3. 连接您的商店
  4. 配置可收费模型
  5. 连接到 Lemon JS
  6. 设置 webhook

以下将详细介绍这些步骤。

Composer

使用 Composer 安装包

composer require lemonsqueezy/laravel

API 密钥

接下来,配置您的 API 密钥。在 Lemon Squeezy 控制面板 中以测试模式创建一个新的密钥,并将其粘贴到下面的 .env 文件中,如下所示

LEMON_SQUEEZY_API_KEY=your-lemon-squeezy-api-key

当您将应用程序部署到生产环境时,您必须创建一个新的生产模式密钥以处理实时数据。

商店标识符

您的商店标识符将在创建产品结账时使用。转到 您的 Lemon Squeezy 通用设置,并将商店 ID(# 符号后面的部分)复制到下面的 env 值中

LEMON_SQUEEZY_STORE=your-lemon-squeezy-store-id

可收费模型

为了确保我们实际上可以为我们的客户创建结账,我们需要配置一个模型作为我们的 "可收费" 模型。这通常是应用程序的 User 模型。为此,在您的模型上导入并使用 Billable trait

use LemonSqueezy\Laravel\Billable;
 
class User extends Authenticatable
{
    use Billable;
}

现在,您的用户模型将有权访问我们包中的方法,以在 Lemon Squeezy 中为您的产品创建结账。

Lemon JS

Lemon Squeezy 使用自己的 JavaScript 库来初始化其结账小部件。我们可以通过在我们的应用程序的 head 部分的 Blade 指令中加载它,在 </head> 标签之前,来利用它。

<head>
    ...
 
    @lemonJS
</head>

Webhooks

最后,请确保设置好传入的Webhook。这在开发和生产环境中都是必需的。前往您的Lemon Squeezy的Webhook设置,并将URL指向您公开的本地应用。您可以使用NgrokExpose或您偏好的其他工具。

请确保选择所有事件类型。您应该指向的路径是默认的/lemon-squeezy/webhook。强烈建议您验证Webhook签名

Webhooks & CSRF保护

传入的Webhook不应受CSRF保护的影响。为了防止这种情况,请将您的Webhook路径添加到您的App\Http\Middleware\VerifyCsrfToken中间件的排除列表中

protected $except = [
    'lemon-squeezy/*',
];

升级

在升级到新版本时,请查看我们的升级指南

配置

该软件包提供了多种配置方式,以便您能够更好地与Lemon Squeezy集成。

验证Webhook签名

为了确保传入的Webhook确实是来自Lemon Squeezy,我们可以为它们配置一个签名密钥。前往Lemon Squeezy仪表板中的Webhook设置,点击您的应用Webhook,并将签名密钥复制到下面的环境变量中

LEMON_SQUEEZY_SIGNING_SECRET=your-webhook-signing-secret

现在,任何传入的Webhook在执行之前都会首先被验证。

结账

使用此软件包,您可以为您的客户轻松创建结账。

单次付款

例如,要为单次付款创建结账,使用您想要销售的产品变体的变体ID,并使用以下代码片段创建结账

use Illuminate\Http\Request;
 
Route::get('/buy', function (Request $request) {
    return $request->user()->checkout('variant-id');
});

这将自动将您的客户重定向到Lemon Squeezy结账页面,客户可以在那里购买您的产品。

注意 当为您的商店创建结账时,每次您重定向结账对象或调用结账对象的url方法时,都会对Lemon Squeezy进行一次API调用。这些调用非常昂贵,并且可能会消耗您的应用的时间和资源。如果您需要反复创建相同的会话,您可能需要缓存这些URL。

覆盖小部件

除了将客户重定向到结账屏幕外,您还可以创建一个结账按钮,该按钮将在您的页面上渲染结账覆盖层。为此,将$checkout对象传递到视图

use Illuminate\Http\Request;
 
Route::get('/buy', function (Request $request) {
    $checkout = $request->user()->checkout('variant-id');

    return view('billing', ['checkout' => $checkout]);
});

现在,使用软件包提供的Laravel Blade组件创建按钮

<x-lemon-button :href="$checkout" class="px-8 py-4">
    Buy Product
</x-lemon-button>

当用户点击此按钮时,它将触发Lemon Squeezy结账覆盖层。您还可以选择将其渲染为暗模式

<x-lemon-button :href="$checkout" class="px-8 py-4" dark>
    Buy Product
</x-lemon-button>

如果您正在结账订阅,并且不想显示“您将被收费...”文本,您可以通过在结账对象上调用withoutSubscriptionPreview方法来禁用它

$request->user()->subscribe('variant-id')
    ->withoutSubscriptionPreview();

如果您想为结账按钮设置不同的颜色,您可以通过withButtonColor传递十六进制颜色代码(带前导#符号)

$request->user()->checkout('variant-id')
    ->withButtonColor('#FF2E1F');

预填用户数据

您可以通过覆盖以下方法在可收费模型上轻松预填结账的用户数据

public function lemonSqueezyName(): ?string; // name
public function lemonSqueezyEmail(): ?string; // email
public function lemonSqueezyCountry(): ?string; // country
public function lemonSqueezyZip(): ?string; // zip
public function lemonSqueezyTaxNumber(): ?string; // tax_number

默认情况下,将使用方法右侧注释中显示的属性。

此外,您还可以通过以下方法动态传递此数据

use Illuminate\Http\Request;
 
Route::get('/buy', function (Request $request) {
    return $request->user()->checkout('variant-id')
        ->withName('John Doe')
        ->withEmail('john@example.com')
        ->withBillingAddress('US', '10038') // Country & Zip Code
        ->withTaxNumber('123456679')
        ->withDiscountCode('PROMO');
});

购买后的重定向

要将在购买后将客户重定向回您的应用,请使用redirectTo方法

$request->user()->checkout('variant-id')
    ->redirectTo(url('/'));

您还可以通过配置配置文件中的lemon-squeezy.redirect_url来设置此默认URL

'redirect_url' => 'https://my-app.com/dashboard',

结账过期

您可以通过调用其上的expiresAt方法来指定结账会话应该保持活跃的时间

$request->user()->checkout('variant-id')
    ->expiresAt(now()->addDays(3));

自定义数据

您还可以通过结账传递自定义数据。为此,请使用结账方法发送键/值对

use Illuminate\Http\Request;
 
Route::get('/buy', function (Request $request) {
    return $request->user()->checkout('variant-id', custom: ['foo' => 'bar']);
});

这些数据将在相关的webhooks中稍后可用。

保留关键字

在处理自定义数据时,该库有一些保留关键字

  • billable_id
  • billable_type
  • subscription_type

尝试使用这些关键字中的任何一个都会抛出异常。

订阅

设置订阅产品

设置不同计划和间隔的订阅产品需要以特定方式进行。Lemon Squeezy有一个很好的指南,介绍了如何进行此操作。

虽然您可以选择如何设置产品和计划,但选择第二个选项为每个计划类型创建一个产品会更简单。例如,当您有一个“基本”和“专业”计划,并且两者都有月度和年费时,最好为这些计划创建两个不同的产品,然后为每个产品的月度和年费添加两个变体。

这使您可以在以后利用hasProduct方法来检查订阅,这允许您仅检查订阅是否在特定计划类型上,而无需担心它是按月还是按年付费。

创建订阅

开始订阅很简单。为此,我们需要产品的变体ID。复制变体ID,并从您的收费模型启动新的订阅结账

use Illuminate\Http\Request;
 
Route::get('/subscribe', function (Request $request) {
    return $request->user()->subscribe('variant-id');
});

当客户完成结账后, incoming SubscriptionCreated webhook会将它与数据库中的收费模型相关联。然后您可以从收费模型检索订阅

$subscription = $user->subscription();

检查订阅状态

一旦客户订阅了您的服务,您可以使用各种方法来检查订阅的各种状态。最基本的一个例子是检查客户是否订阅了有效的订阅

if ($user->subscribed()) {
    // ...
}

您可以在应用程序的多个地方使用此方法,如中间件、策略等,以提供您的服务。要检查单个订阅是否有效,您可以使用valid方法

if ($user->subscription()->valid()) {
    // ...
}

此方法以及subscribed方法将返回true,如果您的订阅是活跃的、试用期的、逾期、暂停免费或在其取消宽限期内。

您还可以检查订阅是否在特定产品上

if ($user->subscription()->hasProduct('your-product-id')) {
    // ...
}

或在特定变体上

if ($user->subscription()->hasVariant('your-variant-id')) {
    // ...
}

如果您想检查订阅是否在特定变体上,同时有效,则可以使用

if ($user->subscribedToVariant('your-variant-id')) {
    // ...
}

或者如果您正在使用多个订阅类型,您可以将类型作为额外参数传递

if ($user->subscribed('swimming')) {
    // ...
}

if ($user->subscribedToVariant('your-variant-id', 'swimming')) {
    // ...
}

已取消状态

要检查用户是否已取消其订阅,您可以使用cancelled方法

if ($user->subscription()->cancelled()) {
    // ...
}

当他们处于宽限期时,您可以使用onGracePeriod检查

if ($user->subscription()->onGracePeriod()) {
    // ...
}

如果订阅已完全取消且不再处于宽限期,您可以使用expired检查

if ($user->subscription()->expired()) {
    // ...
}

逾期状态

如果订阅的定期付款失败,订阅将转换为逾期状态。这意味着它仍然是有效的订阅,但您的客户将在两周内支付将重试。

if ($user->subscription()->pastDue()) {
    // ...
}

在此状态下,您应指导客户更新他们的支付信息。在Lemon Squeezy中,失败的付款将重试几次。有关此以及催收过程的更多信息,请参阅Lemon Squeezy文档

订阅范围

提供了多种订阅范围,用于查询特定状态的订阅

// Get all active subscriptions...
$subscriptions = Subscription::query()->active()->get();
 
// Get all of the cancelled subscriptions for a specific user...
$subscriptions = $user->subscriptions()->cancelled()->get();

以下是所有可用的范围

Subscription::query()->onTrial();
Subscription::query()->active();
Subscription::query()->paused();
Subscription::query()->pastDue();
Subscription::query()->unpaid();
Subscription::query()->cancelled();
Subscription::query()->expired();

更新支付信息

为了允许客户更新他们的支付详情,例如信用卡信息,您可以按照以下方法重定向他们

use Illuminate\Http\Request;
 
Route::get('/update-payment-info', function (Request $request) {
    $subscription = $request->user()->subscription();

    return view('billing', [
        'paymentMethodUrl' => $subscription->updatePaymentMethodUrl(),
    ]);
});

或者,如果您想以更无缝的方式在您的应用上打开URL(类似于结账覆盖层),您可以使用Lemon.js,通过LemonSqueezy.Url.Open()方法打开URL。首先,将URL传递给视图

use Illuminate\Http\Request;
 
Route::get('/update-payment-info', function (Request $request) {
    $subscription = $request->user()->subscription();

    return view('billing', [
        'paymentMethodUrl' => $subscription->updatePaymentMethodUrl(),
    ]);
});

然后通过按钮触发它

<script defer>
    function updatePM() {
        LemonSqueezy.Url.Open('{!! $paymentMethodUrl !!}');
    }
</script>

<button onclick="updatePM()">
    Update payment method
</button>

这要求您已设置Lemon.js

更改计划

当客户订阅了月度计划时,他们可能想要升级到更好的计划,将他们的支付方式改为年付计划或降级到更便宜的计划。在这些情况下,您可以允许他们通过将不同的变体ID传递给swap方法来切换计划

use App\Models\User;

$user = User::find(1);

$user->subscription()->swap('variant-id');

这将使客户切换到新的订阅计划,但计费将仅在下一个计费周期进行。如果您想立即向客户开账单,则可以使用swapAndInvoice方法代替

$user = User::find(1);

$user->subscription()->swapAndInvoice('variant-id');

分摊

默认情况下,Lemon Squeezy在更改计划时会按比例分配金额。如果您想防止这种情况,您可以在执行交换之前使用noProrate方法

$user = User::find(1);

$user->subscription()->noProrate()->swap('variant-id');

多个订阅

在某些情况下,您可能希望允许客户订阅多种订阅类型。例如,健身房可能提供游泳和举重订阅。您可以允许客户订阅其中一种或两种。

要处理不同的订阅,您可以在启动新的订阅时将订阅的type作为subscribe的第二个参数提供

$user = User::find(1);

$checkout = $user->subscribe('variant-id', 'swimming');

现在,您可以通过在检索时提供type参数来始终引用此特定订阅类型

$user = User::find(1);

// Retrieve the swimming subscription type...
$subscription = $user->subscription('swimming');

// Swap plans for the gym subscription type...
$user->subscription('gym')->swap('variant-id');

// Cancel the swimming subscription...
$user->subscription('swimming')->cancel();

暂停订阅

暂停订阅,请在订阅上调用pause方法

$user = User::find(1);

$user->subscription()->pause();

可选地,提供一个日期,在此日期后订阅可以恢复

$user = User::find(1);

$user->subscription()->pause(
    now()->addDays(5)
);

这将填写客户的resumes_at时间戳。要了解您的订阅是否处于暂停期间,您可以使用onPausedPeriod方法

if ($user->subscription()->onPausedPeriod()) {
    // ...
}

要取消暂停,只需在订阅上调用该方法

$user->subscription()->unpause();

暂停状态

默认情况下,暂停订阅将在暂停期间剩余时间内取消其使用。如果您希望客户免费使用您的服务,则可以使用pauseForFree方法

$user->subscription()->pauseForFree();

取消订阅

取消订阅,请在订阅上调用cancel方法

$user = User::find(1);

$user->subscription()->cancel();

这将使您的订阅设置为取消。如果您的订阅在周期中途取消,它将进入宽限期,并将设置ends_at列。客户仍然可以访问该周期剩余提供的服务。您可以通过调用onGracePeriod方法检查其宽限期

if ($user->subscription()->onGracePeriod()) {
    // ...
}

使用Lemon Squeezy无法立即取消。要在宽限期仍然有效时恢复订阅,请调用resume方法

$user->subscription()->resume();

当取消的订阅达到宽限期的结束时,它将过渡到过期状态,并且无法再恢复。

订阅试用期

关于在 Lemon Squeezy 中进行试用的详细说明,请查看他们的指南:点击这里.

无需付款

为了让人们能够注册您的产品而不需要填写付款详情,您可以在创建客户时设置 trial_ends_at 列表。

use App\Models\User;
 
$user = User::create([
    // ...
]);
 
$user->createAsCustomer([
    'trial_ends_at' => now()->addDays(10)
]);

这被称为“通用试用”,因为它没有与任何订阅相关联。您可以使用 onTrial 方法来检查客户是否正在试用您的应用程序。

if ($user->onTrial()) {
    // User is within their trial period...
}

或者如果您还想确保这是一个通用试用,您可以使用 onGenericTrial 方法。

if ($user->onGenericTrial()) {
    // User is within their "generic" trial period...
}

您还可以通过调用 trialEndsAt 方法来检索试用的结束日期。

if ($user->onTrial()) {
    $trialEndsAt = $user->trialEndsAt();
}

一旦您的客户准备就绪,或者试用期结束后,他们可以开始他们的订阅。

use Illuminate\Http\Request;

Route::get('/buy', function (Request $request) {
    return $request->user()->subscribe('variant-id');
});

请注意,当客户在通用试用期间开始订阅时,他们的试用将被取消,因为他们已经开始为您的产品付款。

需要付款

另一种选择是,当人们想要试用您的产品时要求他们提供付款详情。这意味着试用期结束后,他们将立即订阅您的产品。要开始此操作,您需要在产品设置中 配置试用期限。然后,让客户开始订阅。

use Illuminate\Http\Request;

Route::get('/buy', function (Request $request) {
    return $request->user()->subscribe('variant-id');
});

您的客户订阅后,他们将进入您配置的试用期,并且在您配置的日期之前不会收费。如果他们想在这个时间之前取消订阅,您需要给他们这个选项。

要检查您的客户是否正在免费试用中,您可以在可计费或单个订阅上使用 onTrial 方法。

if ($user->onTrial()) {
    // ...
}
 
if ($user->subscription()->onTrial()) {
    // ...
}

要确定试用是否已过期,您可以使用 hasExpiredTrial 方法。

if ($user->hasExpiredTrial()) {
    // ...
}
 
if ($user->subscription()->hasExpiredTrial()) {
    // ...
}

处理 Webhooks

Lemon Squeezy 可以向您的应用程序发送 webhooks,您可以对它们做出反应。默认情况下,此包已经为您做了大部分工作。 如果您已正确设置 webhooks,它将监听任何传入的事件并相应地更新您的数据库。我们建议启用所有事件类型,这样您在未来升级时会更方便。

要监听传入的 webhooks,我们有两个将被触发的事件

  • LemonSqueezy\Laravel\Events\WebhookReceived
  • LemonSqueezy\Laravel\Events\WebhookHandled

WebhookReceived 事件将在 webhook 到来但尚未由包的 WebhookController 处理时触发。当 webhook 被包处理时,将触发 WebhookHandled 事件。这两个事件都将包含传入 webhook 的完整有效负载。

如果您想对这些事件做出反应,您必须创建监听器。例如,您可能想对订阅更新做出反应。

<?php
 
namespace App\Listeners;
 
use LemonSqueezy\Laravel\Events\WebhookHandled;
 
class LemonSqueezyEventListener
{
    /**
     * Handle received Lemon Squeezy webhooks.
     */
    public function handle(WebhookHandled $event): void
    {
        if ($event->payload['meta']['event_name'] === 'subscription_updated') {
            // Handle the incoming event...
        }
    }
}

有关示例有效负载,请参阅 Lemon Squeezy API 文档

一旦您有了监听器,您需要在应用程序的 EventServiceProvider 中连接它。

<?php
 
namespace App\Providers;
 
use App\Listeners\LemonSqueezyEventListener;
use Illuminate\Foundation\Support\Providers\EventServiceProvider as ServiceProvider;
use LemonSqueezy\Laravel\Events\WebhookHandled;
 
class EventServiceProvider extends ServiceProvider
{
    protected $listen = [
        WebhookHandled::class => [
            LemonSqueezyEventListener::class,
        ],
    ];
}

Webhook 事件

除了监听 WebhookHandled 事件之外,您还可以订阅以下在 webhook 被处理后触发的专用包事件之一

  • LemonSqueezy\Laravel\Events\SubscriptionCreated
  • LemonSqueezy\Laravel\Events\SubscriptionUpdated
  • LemonSqueezy\Laravel\Events\SubscriptionCancelled
  • LemonSqueezy\Laravel\Events\SubscriptionResumed
  • LemonSqueezy\Laravel\Events\SubscriptionExpired
  • LemonSqueezy\Laravel\Events\SubscriptionPaused
  • LemonSqueezy\Laravel\Events\SubscriptionUnpaused

所有这些事件都包含一个可计费的 $model 实例、一个 $subscription 对象和一个事件 $payload。它们可以通过它们的公共属性访问。

变更日志

请查看此存储库中的所有最新更改的 CHANGELOG

许可

适用于 Laravel 的 Lemon Squeezy 是一个开源软件,许可协议为 MIT 许可协议