FLUX-SE / PayumStripe

Payum Stripe gateways (with SCA support)
MIT License
28 stars 15 forks source link

How to reuse a saved card #7

Closed francesco-laricchia closed 3 years ago

francesco-laricchia commented 3 years ago

Hi, I'm migrating from legacy Payum/Stripe in order to comply to new SCA regulations. This is working beautifully with simple one-time payments, still I can't figure out how to charge a saved card.

Previously I just had to retrieve the "customer" token, I had stored on my db, and capture would charge the corresponding card. It looks like this library does not support this flow at all, yet.

I'm studyng Stripe docs and this is the point I reached:

then

In Postman, this does work.

Now I'm bashing my head on Payum docs and examples in order to understand how to achieve this. Any plan to implement this in this library?

EDIT: I've already set up my custom GatewayFactory which extends StripeCheckoutSessionGatewayFactory and started creating custom Actions.

Prometee commented 3 years ago

Hello @francesco-laricchia

The gateway stripe_checkout_session allow to pay, subscribe or store a credit card thanks to a Stripe hosted portal, then at the end of the process returning to your website the payment/subscription/setup is completed. But nothing disallow you to use each API Action outside those Stripe Checkout processes to make your own re-payment flow.

There is now (master branch) a CreatePaymentIntent request which will allow you to create your own PaymentIntent, here is an exemple :

$model = [
    'amount' => 2000,
    'currency' => 'eur',
    'payment_method' => 'pm_123456',
    'customer' => 'cus_123456',
];
$request = new CreatePaymentIntent($model);
$payum->getGateway('stripe_checkout_session')->execute($request);

$paymentIntent = $request->getApiResource();

Let me know if you succeeded.

francesco-laricchia commented 3 years ago

Let me know if you succeeded.

I eventually managed to complete a payment with a previously saved card, even if it requires to go through SCA flow again every time. Thank you for the hint!

With legacy Payum/Stripe I used GetCreditCardTokenAction to save card token, which was only the "customer" field, to my database so I could Stripe::charge() the same card later without asking the user for the card data again.

I'm still using it with your library, but now after the first payment succeeds I set

$token = [
    'customer' => $model['customer'],
    'payment_method' => $model['payment_method'],
];

$request->token = serialize($token);

In CaptureAction, I'm just adding

$model->offsetSet('return_url', $token->getTargetUrl());

in order to manage SCA authentication request. And this is all I need to create a new PaymentIntent. If no SCA is required again, that's it. If SCA il enforced, I created a new action so:

$redirectToScaPage = new RedirectToScaPage($paymentIntent->toArray());
$this->gateway->execute($redirectToScaPage);

redirects the user's browser to Stripe hosted authorization page which will manage it all for me. Then it goes back to return_url, the payment is captured and the order fulfilled.

Payum is not the easiest stuff to learn, but I'm getting to the grips with it.

Prometee commented 3 years ago

@francesco-laricchia using Stripe JS Element through stripe_js gateway is also possible I made a test project with your workflow and this work also well with or without SCA.

Here is the payment object I set (prepare.php if we follow the original Payum doc):

use Payum\Core\Model\Payment;

$storage = $payum->getStorage(Payment::class);

/** @var Payment $payment */
$payment = $storage->create();
$payment->setNumber(uniqid());
$payment->setCurrencyCode('EUR');
$payment->setTotalAmount(123);
$payment->setDescription('A description');
$payment->setClientId('anId');
$payment->setClientEmail('foo@example.com');
$payment->setDetails([
    'customer' => 'cus_123456',
    'payment_method' => 'card_123456'
]);

$storage->update($payment);

Here is the twig overrides I build to handle your case :

{# templates/bundles/FluxSEPayumStripe/Action/pay.html.twig #}
{% extends "@!FluxSEPayumStripe/Action/pay.html.twig" %}

{% block payum_body %}
    {{ parent() }}
<form action="{{ action_url|default('') }}" method="POST" name="payment-form" id="payment-form" data-secret="{{ model.client_secret }}">
    <!-- We'll put the error messages in this element -->
    <div id="card-errors" role="alert"></div>
</form>
{% endblock %}

{% block payum_javascripts_stripejs_common %}
<script type="text/javascript">
    var stripe = Stripe('{{ publishable_key }}');    
    var form = document.getElementById('payment-form');

    var showError = function(errorMsgText) {
        var displayError = document.getElementById('card-errors');
        displayError.textContent = errorMsgText;
    };

    stripe.confirmCardPayment(form.dataset['secret'], {
        payment_method: {{ model.payment_method|json_encode|raw }}
    }).then(function (result) {
        if (result.error) {
            showError(result.error.message);
        } else {
            // The payment has been processed!
            var paymentIntent = result.paymentIntent;
            if (paymentIntent.status === 'succeeded') {
                // Show a success message to your customer
                // There's a risk of the customer closing the window before callback
                // execution. Set up a webhook or plugin to listen for the
                // payment_intent.succeeded event that handles any business critical
                // post-payment actions.
                form.submit();
            }
        }
    });
</script>
{% endblock %}

The SCA popup will be prompt if needed then the customer is redirected to the end of the Payum process.

So you have choices between using stripe checkout session portal or a pure Stripe JS implementation ;)