laravel - 支持3D Secure卡Stripe支付开始订阅

标签 laravel stripe-payments

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

到目前为止一切顺利 - 我设法使用 Guide on how to set up a subscription 使其正常工作通过 Stripe 。但是,需要 3D Secure 身份验证的卡还不能使用,这是必须具备的。

所以我进一步阅读并使用了 PaymentIntent (API Docs)。但是,当前行为如下:

  • 我创建一个 PaymentIntent 并将公钥传递给前端
  • 客户输入凭据并提交
  • 3D Secure Authentication 正确进行,返回 payment_method_id
  • 在服务器端,我再次检索 PaymentIntent。它的状态为成功,我的 Stripe Dashboard 已收到付款。
  • 然后我创建客户对象(使用我从 PaymentIntent 获得的付款方式),并与该客户一起创建订阅
  • 订阅状态为未完成,似乎订阅尝试再次向客户收费,但由于第二次需要 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);

点击“购买”时的前端结账

// 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();
    }
});

提交后后台处理

// 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";
        }
    }
}

最佳答案

我终于得到了一个为我的网站运行的有效解决方案。它是这样的:

1 - 后端:创建一个 SetupIntent

我创建了一个 SetupIntent ( SetupIntent API Docs ) 以完全覆盖结帐流程。与 PaymentIntent 的区别( PaymentIntent API Docs ) 是 PaymentIntent 从收集卡详细信息、准备付款并有效地将金额转移到帐户,而 SetupIntent 仅准备收卡,但尚未执行付款。你会得到一个 PaymentMethod ( PaymentMethod API Docs ),您可以稍后使用。

$intent = SetupIntent::create([
    'payment_method_types' => ['card'],
]);

然后我将 $intent->client_secret key 传递给我的客户端 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});
}

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

在后端,我收到了我从前端提交的$payment_method_id。 首先,我们现在需要创建一个 Customer ( Customer API Docs ) 如果它还不存在。对于客户,我们将从 SetupIntent 中附加付款方式。然后,我们创建 Subscription ( Subscription API Docs ) 将从 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
]);

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

对于状态为 incomplete 的订阅,您可以像这样从 $subscription->latest_invoice 检索最新的发票:

$invoice = \Stripe\Invoice::retrieve($subscription->latest_invoice); 

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

这是一种万无一失的策略,如果您的实现出现任何问题,只需将它们发送给 Stripe 即可完成。一定要提示用户,如果他必须两次确认他的卡,它不会扣两次,只会扣一次!

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

关于laravel - 支持3D Secure卡Stripe支付开始订阅,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/59021814/

相关文章:

php - 拉拉维尔 5.2 :create and edit function

laravel - Guzzle - Laravel。如何使用 x-www-form-url-encoded 发出请求

javascript - 我应该如何将 `cardNumberElement` 、 `cardExpiryElement` 和 `cardCvcElement` 传递到 `stripe.confirmCardPayment` 的 `payment_method.card` 中?

ruby-on-rails - 带 Stripe 结账的 Rails : How can i seed my db with sample charges and use stripe checkout to generate tokens for each charge?

php - Stripe curl 命令行 POST

html - 将两列放在另一列下方,带侧边栏

php - Laravel 哪里不工作

java - com.stripe.exception.InvalidRequestException : Missing required param: amount. ;请求 ID : req_6mshZrGwQeiE8c?

java - Firebase 云功能( strip : AddPaymentSource)

javascript - 如何使用get发送数据而不在url中显示参数?