支持3D安全卡Stripe支付开始订阅

Flo*_*ler 4 laravel stripe-payments

我的应用程序的支付模型想法非常简单:拥有一个带有会员区域和一些特殊功能的 (laravel) 网站,而会员帐户的费用为 19.90/年。我想将Stripe集成到我的注册流程中以允许付款。付款成功后,我会创建一个订阅,然后每年都会自动续订此付款。

到目前为止一切顺利 - 我设法使用有关如何设置Stripe 订阅的指南使其正常工作。然而,需要3D安全认证的卡还不能使用,而这是必须具备的。

所以我进一步阅读并使用了PaymentIntentAPI 文档)。但是,当前的行为如下:

  • 我创建一个 PaymentIntent 并将公钥传递给前端
  • 客户输入凭据并提交
  • 3D 安全身份验证正确发生,返回我 payment_method_id
  • 在服务器端,我再次检索 PaymentIntent。它具有状态succeeded,并且付款已在我的 Stripe 仪表板上收到。
  • 然后,我创建客户对象(使用从 PaymentIntent 获得的付款方法),并与该客户一起创建订阅
  • 订阅具有状态incomplete,并且订阅似乎尝试再次向客户收费,但由于第二次需要进行 3D 安全验证而失败。

所以我的实际问题是:如何创建一个订阅,以某种方式注意到客户已经使用我的 PaymentIntent 和我传递给它的 PaymentMethod 进行了付款?

一些代码

创建 PaymentIntent 并将其传递给前端

\Stripe\Stripe::setApiKey(env('STRIPE_SECRET_KEY'));
$intent = \Stripe\PaymentIntent::create([
   'amount' => '1990',
   'currency' => 'chf',
]);
$request->session()->put('stripePaymentIntentId',$intent->id);
return view('payment.checkout')->with('intentClientSecret',$intent->client_secret);
Run Code Online (Sandbox Code Playgroud)

单击“购买”时前端结账

// I have stripe elements (the card input field) ready and working
// using the variable "card". The Stripe instance is saved in "stripe".
// Using "confirmCardPayment", the 3DS authentication is performed successfully.
stripe.confirmCardPayment(intentClientSecret,{
    payment_method: {card: mycard},
    setup_future_usage: 'off_session'
}).then(function(result) {
    $('#card-errors').text(result.error ? result.error.message : '');
    if (!result.error) {
        submitMyFormToBackend(result.paymentIntent.payment_method);
    }
    else {
        unlockPaymentForm();
    }
});
Run Code Online (Sandbox Code Playgroud)

提交后后台

// Get the PaymentMethod id from the frontend that was submitted
$payment_method_id = $request->get('stripePaymentMethodId');
// Get the PaymentIntent id which we created in the beginning
$payment_intent_id = $request->session()->get('stripePaymentIntentId');
\Stripe\Stripe::setApiKey(env('STRIPE_SECRET_KEY'));
// Get the Laravel User
$user = auth()->user();

// Firstly load Payment Intent to have this failing first if anything is not right
$intent = \Stripe\PaymentIntent::retrieve($payment_intent_id);
if ($intent instanceof \Stripe\PaymentIntent) {
    // PaymentIntent loaded successfully.

    if ($intent->status == 'succeeded') {

        // The intent succeeded and at this point I believe the money
        // has already been transferred to my account, so it's paid.
        // Setting up the user with the paymentMethod given from the frontend (from
        // the 3DS confirmation).
        $customer = \Stripe\Customer::create([
            'payment_method' => $payment_method_id,
            'email' => $user->email,
            'invoice_settings' => [
                'default_payment_method' => $payment_method_id,
            ],
        ]);

        $stripeSub = \Stripe\Subscription::create([
            'customer' => $customer->id,
            'items' => [
                [
                    'plan' => env('STRIPE_PLAN_ID'),
                ]
            ],
            'collection_method' => 'charge_automatically',
            'off_session' => false,
        ]);

        // If the state of the subscription would be "active" or "trialing", we would be fine
        // (depends on the trial settings on the plan), but both would be ok.
        if (in_array($stripeSub->status,['active','trialing'])) {
            return "SUCCESS";
        }

        // HOWEVER the state that I get here is "incomplete", thus it's an error.
        else {
            return "ERROR";
        }
    }
}
Run Code Online (Sandbox Code Playgroud)

Flo*_*ler 5

我终于得到了一个适用于我的网站的可行解决方案。事情是这样的:

1 - 后端:创建一个SetupIntent

我创建了一个SetupIntentSetupIntent API 文档)来完全涵盖结账流程。与PaymentIntentPaymentIntent API 文档)的区别在于,PaymentIntent 从收集卡详细信息、准备付款并有效地将金额转入帐户,而 SetupIntent 仅准备收卡,但尚未执行付款。您将从中获得一个PaymentMethod ( PaymentMethod API Docs ),您可以稍后使用。

$intent = SetupIntent::create([
    'payment_method_types' => ['card'],
]);
Run Code Online (Sandbox Code Playgroud)

然后我将$intent->client_secret密钥传递给客户端 JavaScript。

2 - 前端:使用 Elements 收集卡片详细信息

在前端,我放置了 Stripe 卡元素来收集卡详细信息。

var stripe = Stripe(your_stripe_public_key);
var elements = stripe.elements();
var style = { /* my custom style definitions */ };
var card = elements.create('card',{style:style});
card.mount('.my-cards-element-container');

// Add live error message listener 
card.addEventListener('change',function(event) {
    $('.my-card-errors-container').text(event.error ? event.error.message : '');
}

// Add payment button listener
$('.my-payment-submit-button').on('click',function() {
    // Ensure to lock the Payment Form while performing async actions
    lockMyPaymentForm();
    // Confirm the setup without charging it yet thanks to the SetupIntent.
    // With 3D Secure 2 cards, this will trigger the confirmation window.
    // With 3D Secure cards, this will not trigger a confirmation.
    stripe.confirmCardSetup(setup_intent_client_secret, {
        payment_method: {card: card} // <- the latter is the card object variable
    }).then(function(result) {
        $('.my-card-errors-container').text(event.error ? event.error.message : '');
        if (!result.error) {
            submitPaymentMethodIdToBackend(result.setupIntent.payment_method);
        }
        else {
            // There was an error so unlock the payment form again.
            unlockMyPaymentForm();
        }
    });
}

function lockMyPaymentForm() {
    $('.my-payment-submit-button').addClass('disabled'); // From Bootstrap
    // Get the card element here and disable it
    // This variable is not global so this is just sample code that does not work.
    card.update({disabled: true});
}

function unlockMyPaymentForm() {
    $('.my-payment-submit-button').removeClass('disabled'); // From Bootstrap
    // Get the card element here and enable it again
    // This variable is not global so this is just sample code that does not work.
    card.update({disabled: false});
}
Run Code Online (Sandbox Code Playgroud)

3 - 后端:创建客户和订阅

在后端,我收到了$payment_method_id我从前端提交的数据。首先,如果客户尚不存在,我们现在需要创建一个客户客户 API 文档)。对于客户,我们将附加来自 SetupIntent 的付款方式。然后,我们创建订阅订阅 API 文档),它将从 SetupIntent 开始收费。

$customer = \Stripe\Customer::create([
    'email' => $user->email, // A field from my previously registered laravel user
]);

$paymentMethod = \Stripe\PaymentMethod::retrieve($payment_method_id);

$paymentMethod->attach([
    'customer' => $customer->id,
]);

$customer = \Stripe\Customer::update($customer->id,[
    'invoice_settings' => [
        'default_payment_method' => $paymentMethod->id,
    ],
]);

$subscription = \Stripe\Subscription::create([
    'customer' => $customer->id,
    'items' => [
        [
            'plan' => 'MY_STRIPE_PLAN_ID',
        ],
    ],
    'off_session' => TRUE, //for use when the subscription renews
]);
Run Code Online (Sandbox Code Playgroud)

现在我们有一个 Subscription 对象。对于普通卡,状态应为activetrialing,具体取决于您在订阅上设置的试用天数。然而,在处理 3D Secure 测试卡时,我的订阅仍然处于状态incomplete。根据我的 Stripe 支持联系人的说法,这也可能是一个问题,因为 3D Secure 测试卡尚未完全正常工作。不过,我认为这种情况也可能发生在使用某种卡的生产环境中,因此我们必须处理它。

在具有状态的订阅中,incomplete您可以从中检索最新的发票,$subscription->latest_invoice如下所示:

$invoice = \Stripe\Invoice::retrieve($subscription->latest_invoice); 
Run Code Online (Sandbox Code Playgroud)

在您的发票对象上,您将找到 astatus和 a hosted_invoice_url。当status仍然 时open,我现在向用户提供他必须首先完成的托管发票的 URL。我让他在新窗口中打开链接,其中显示了由 stripe 托管的漂亮发票。在那里,他可以再次确认他的信用卡详细信息,包括 3D Secure 工作流程。如果他在那里成功,则在您从 Stripe 重新检索订阅后或之后$subscription->status进行更改。activetrialing

这是某种万无一失的策略,如果您的实施出现任何问题,只需将其发送到 Stripe 即可完成。请务必提示用户,如果他必须确认他的卡两次,则不会收取两次费用,而只会收取一次费用!

我无法创建 @snieguu 解决方案的工作版本,因为我想使用 Elements,而不是单独收集信用卡详细信息,然后自己创建 PaymentMethod。