I am trying to implement Stripe to create a subscription (with a price depending on the quantity chosen), in my Symfony 5 app (backend : PHP, fronted JS)
I would like this scenario:
The user chooses the quantity for his subscription
It is redirected to the checkout page (created myself, not the Stripe Checkout utility hosted by themselves)
He enters his payment information.
If necessary, SCA authentication (3D SECURE).
If everything is OK, I create the subscription.
So, from step 3, I tried this :
My back-end endpoint :
/**
* @Route("/paiement/process", name="website_payment_process", options={"expose"=true})
*/
public function process(Request $request, $stripeSecretKey, ShoppingCart $cart, StripeClient $stripeClient, EntityManagerInterface $manager)
{
\Stripe\Stripe::setApiKey($stripeSecretKey);
$json_obj = json_decode($request->getContent());
$intent = null;
try {
if (isset($json_obj->payment_method_id)) {
// Create the PaymentIntent
$intent = \Stripe\PaymentIntent::create([
'payment_method' => $json_obj->payment_method_id,
'confirmation_method' => 'manual',
'confirm' => true,
'amount' => $cart->getTotalWithDiscount(false) * 100,
'currency' => 'eur',
'description' => 'Mon paiement',
'setup_future_usage' => 'off_session',
]);
}
if (isset($json_obj->payment_intent_id)) {
$intent = \Stripe\PaymentIntent::retrieve(
$json_obj->payment_intent_id
);
$intent->confirm();
}
if ('requires_action' == $intent->status &&
'use_stripe_sdk' == $intent->next_action->type) {
// Tell the client to handle the action
return new JsonResponse([
'requires_action' => true,
'payment_intent_client_secret' => $intent->client_secret,
]);
} elseif ('succeeded' == $intent->status) {
// Paiement Stripe accepté
$customer = \Stripe\Customer::create([
'description' => 'Abonnement à mon application',
'preferred_locales' => ['fr'],
'email' => $json_obj->email,
'payment_method' => $intent->payment_method,
]);
$stripeCustomer = new StripeCustomer();
$stripeCustomer->setStripeCustomerId($customer->id);
$this->getUser()->setStripeCustomer($stripeCustomer);
$manager->persist($this->getUser());
$manager->flush();
// Created the subscription in association with Customer Id
$stripeClient->createSubscription(
$this->getUser(),
$cart->getPlan(),
true,
$cart->getQuantity(),
$cart->getCouponId()
);
return new JsonResponse([
'success' => true,
]);
} else {
return new JsonResponse(['error' => 'Invalid PaymentIntent status'], 500);
}
} catch (\Exception $e) {
// Display error on client
return new JsonResponse([
'error' => $e->getMessage(),
]);
}
}
And the JS :
if($("#payment-form").length !== 0){
const STRIPE_PUBLIC_KEY = process.env.STRIPE_PUBLIC_KEY;
// Stripe.setPublishableKey(STRIPE_PUBLIC_KEY);
var stripe = Stripe(STRIPE_PUBLIC_KEY);
var processUrl = Routing.generate('website_payment_process');
var elements = stripe.elements();
var card = elements.create('card', {
style: {
base: {
iconColor: '#666EE8',
color: '#31325F',
lineHeight: '40px',
fontWeight: 300,
fontFamily: 'Helvetica Neue',
fontSize: '15px',
'::placeholder': {
color: '#CFD7E0',
},
},
}
});
card.mount('#card-element');
function setOutcome(result) {
var errorElement = document.querySelector('.error');
errorElement.classList.remove('visible');
if (result.token) {
$('#stripeToken').val(result.token.id);
$('#payment-form').submit();
} else if (result.error) {
errorElement.textContent = result.error.message;
errorElement.classList.add('visible');
}
}
card.on('change', function(event) {
setOutcome(event);
});
document.getElementById('payment-form').addEventListener('submit', function(e) {
e.preventDefault();
var form = document.getElementById('payment-form');
stripe.createPaymentMethod('card', card, {
billing_details: {name: form.querySelector('input[name=cardholder-name]').value}
}).then(function(result) {
if (result.error) {
var errorElement = document.querySelector('.error');
errorElement.textContent = result.error.message;
errorElement.classList.add('visible');
} else {
$('#buttonPayment').hide();
$('#spanWaitPayement').show();
// Otherwise send paymentMethod.id to your server (see Step 2)
fetch(processUrl, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-CSRF-TOKEN': $('meta[name="csrf-token"]').attr('content'),
},
body: JSON.stringify({ payment_method_id: result.paymentMethod.id })
}).then(function(result) {
result.json().then(function(json) {
handleServerResponse(json);
})
});
}
});
});
function handleServerResponse(response) {
if (response.error) {
$('#buttonPayment').show();
$('#spanWaitPayement').hide();
var errorElement = document.querySelector('.error');
errorElement.textContent = result.error.message;
errorElement.classList.add('visible');
} else if (response.requires_action) {
// Use Stripe.js to handle required card action
stripe.handleCardAction(
response.payment_intent_client_secret
).then(function(result) {
if (result.error) {
$('#buttonPayment').show();
$('#spanWaitPayement').hide();
var errorElement = document.querySelector('.error');
errorElement.textContent = result.error.message;
errorElement.classList.add('visible');
} else {
// The card action has been handled
// The PaymentIntent can be confirmed again on the server
fetch(processUrl, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-CSRF-TOKEN': $('meta[name="csrf-token"]').attr('content'),
},
body: JSON.stringify({
payment_intent_id: result.paymentIntent.id,
email: $('#email').val(),
})
}).then(function(confirmResult) {
return confirmResult.json();
}).then(handleServerResponse);
}
});
} else {
// window.location.replace('/paiement-ok');
}
}
}
I manage to retrieve the payment information, use the SCA authentication, and make the payment. But I must certainly be wrong for the rest because if the payment is successful, it's just after that I create the customer, and the subscription, and associate the PaymentIntent previously used. And inevitably it does not work, the subscription is "incomplete" and he tries again to rebill the customer