I am using Stripe quick start code found here https://stripe.com/docs/payments/quickstart
I have narrowed down the problem to the "items" array in the checkout.js not being recognized or not properly constructed or whatever in the create.php file.
My customers will only be purchasing one type of item at different dollar amounts so this function
function calculateOrderAmount(array $items): int {
// Replace this constant with a calculation of the order's amount
// Calculate the order total on the server to prevent
// people from directly manipulating the amount on the client
return 1400;
}
is useless to me and the amount can just go into this
$paymentIntent = \Stripe\PaymentIntent::create([
'amount' => calculateOrderAmount($jsonObj->items),
'currency' => 'eur',
'automatic_payment_methods' => [
'enabled' => true,
],
]);
as the total.
I have tried replacing calculateOrderAmount($jsonObj->items) with array_values($$items)[0] as well as everything else I can think of. Stripe support has been of no help.
Thank you in advance
The HTML: The price is in cents from the "value"
<select id="item-options"">
<option value="">---- SELECT----</option>
<option value="9000">300 ITEMS - $90.00 USD</option>
<option value="8000">200 ITEMS - $80.00 USD</option>
<option value="5000">100 ITEMS - $50.00 USD</option>
<option value="3000">50 ITEMS - $30.00 USD</option>
<option value="1750">25 ITEMS - $17.50 USD</option>
<option value="800">10 ITEMS - $8.00 USD</option>
<option value="450">5 ITEMS - $4.50 USD</option>
<option value="100">1 ITEMS - $1.00 USD</option>
</select>
The full javascript:
var price = $('#item-options option:selected').val();
// This is your test publishable API key.
const stripe = Stripe("pk_test_xxxxxxxxxxx....");
// The items the customer wants to buy I have change this to (amount: price)
const items = [{ id: "xl-tshirt" }];
let elements;
initialize();
checkStatus();
document
.querySelector("#payment-form")
.addEventListener("submit", handleSubmit);
// Fetches a payment intent and captures the client secret
async function initialize() {
const { clientSecret } = await fetch("/create.php", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ items }),
}).then((r) => r.json());
elements = stripe.elements({ clientSecret });
const paymentElement = elements.create("payment");
paymentElement.mount("#payment-element");
}
async function handleSubmit(e) {
e.preventDefault();
setLoading(true);
const { error } = await stripe.confirmPayment({
elements,
confirmParams: {
// Make sure to change this to your payment completion page
return_url: "http://localhost:4242/public/checkout.html",
receipt_email: document.getElementById("email").value,
},
});
// This point will only be reached if there is an immediate error when
// confirming the payment. Otherwise, your customer will be redirected to
// your `return_url`. For some payment methods like iDEAL, your customer will
// be redirected to an intermediate site first to authorize the payment, then
// redirected to the `return_url`.
if (error.type === "card_error" || error.type === "validation_error") {
showMessage(error.message);
} else {
showMessage("An unexpected error occured.");
}
setLoading(false);
}
// Fetches the payment intent status after payment submission
async function checkStatus() {
const clientSecret = new URLSearchParams(window.location.search).get(
"payment_intent_client_secret"
);
if (!clientSecret) {
return;
}
const { paymentIntent } = await stripe.retrievePaymentIntent(clientSecret);
switch (paymentIntent.status) {
case "succeeded":
showMessage("Payment succeeded!");
break;
case "processing":
showMessage("Your payment is processing.");
break;
case "requires_payment_method":
showMessage("Your payment was not successful, please try again.");
break;
default:
showMessage("Something went wrong.");
break;
}
}
// ------- UI helpers -------
function showMessage(messageText) {
const messageContainer = document.querySelector("#payment-message");
messageContainer.classList.remove("hidden");
messageContainer.textContent = messageText;
setTimeout(function () {
messageContainer.classList.add("hidden");
messageText.textContent = "";
}, 4000);
}
// Show a spinner on payment submission
function setLoading(isLoading) {
if (isLoading) {
// Disable the button and show a spinner
document.querySelector("#submit").disabled = true;
document.querySelector("#spinner").classList.remove("hidden");
document.querySelector("#button-text").classList.add("hidden");
} else {
document.querySelector("#submit").disabled = false;
document.querySelector("#spinner").classList.add("hidden");
document.querySelector("#button-text").classList.remove("hidden");
}
}
The php :
<?php
require 'vendor/autoload.php';
// This is your test secret API key.
\Stripe\Stripe::setApiKey('sk_test_XXXXXX.........');
function calculateOrderAmount(array $items): int {
// Replace this constant with a calculation of the order's amount
// Calculate the order total on the server to prevent
// people from directly manipulating the amount on the client
return 1400;
}
header('Content-Type: application/json');
try {
// retrieve JSON from POST body
$jsonStr = file_get_contents('php://input');
$jsonObj = json_decode($jsonStr);
// Create a PaymentIntent with amount and currency
$paymentIntent = \Stripe\PaymentIntent::create([
'amount' => calculateOrderAmount($jsonObj->items),
'currency' => 'eur',
'automatic_payment_methods' => [
'enabled' => true,
],
]);
$output = [
'clientSecret' => $paymentIntent->client_secret,
];
echo json_encode($output);
} catch (Error $e) {
http_response_code(500);
echo json_encode(['error' => $e->getMessage()]);
}
To prevent price manipulation on the client side you should pass to your PHP script ids of products, not the prices directly. By knowing the product ids, you can search for prices on the server side. That's what the calculateOrderAmount function is for in your example code.
Since you intended to sell only one type of product, the price of which will depend on the volume, you may want to send a number of items from the client. Then take a look at tiered pricing and make the code match your pricing model.
I solved this by doing a var_dump($items) in the php file and the console.log showed it as a stdClass Object. Using "$amount = $items[0]->amount;" in the "function calculateOrderAmount(array $items)" now gives me the correct price for the "$paymentIntent = \Stripe\PaymentIntent::create([
'amount' => calculateOrderAmount($jsonObj->items),"
Thank you Justin for responding to my question.
Related
I set to publish and secret keys in live mode. but when I use the 42424242424242 card number, it's valid. I don't find any solution, how to fix it.
$options = [
'amount' => \floatval($req->cost) * 100,
'currency' => 'aud',
'automatic_payment_methods' => ['enabled' => true],
];
$stripKey = \Utility::isLocalHost() ? $_ENV["STRIP_SECRET_KEY"] : $_ENV["STRIP_SECRET_KEY_lIVE"];
Stripe::setApiKey($stripKey);
$intent = PaymentIntent::create($options);
as you can see, my backend is written in PHP language that generates a client secrete key and in front-end, I build an element and stripe form by that key.
this.strip = Stripe(this.key);
const options = { clientSecret: payment.clientSecret };
this.stripElement = this.strip.elements(options);
const paymentElement = this.stripElement.create('payment');
paymentElement.mount('#payment-element');
paymentElement.on('change', (event) => {
if (event.complete && !event.empty) {
this.submitBtn.disabled = false;
}
});
However, I move the code on my server and set the live mode for stripe. The test credit card '42424 ...' is still valid.
I have an endpoint uploads images and updates a database table.
I send 3 requests to this endpoint at same time. Actually this problem happens when I send more than 2 API requests.
First request that comes to endpoint uploads images and updates database table successfully.
Second request that comes to endpoint uploads images sees database changes of first request, and updates database table successfully.
Third request that comes to endpoint uploads images, doesn't see database changes of second request, and updates database table successfully.
As a result; only database changes of first request and third request apply. Database changes of second request is not able to applied successfully or is overridden, somehow.
I use pg npm package.
Is problem in my code or in pg package.
How can this problem be solved?
Controller:
#UseStaffPermissionsGuards('upsert', 'VehicleCondition')
#ApiBody({ type: VehiclePhotoConditionInfoImageDTO })
#ApiResponse({ status: 201 })
#Post(':id/photos/:photoConditionId/image')
#ApiConsumes('multipart/form-data')
#UseInterceptors(FilesInterceptor('images'), FilesToBodyInterceptor)
async upsertImages(
#Param('id') vehicleId: string,
#Param('photoConditionId') photoConditionId: string,
#Body() vehiclePhotoConditionInfoImages: VehiclePhotoConditionInfoImageDTO,
): Promise<void> {
return this.vehiclePhotoConditionService.upsertImages(
vehicleId,
photoConditionId,
vehiclePhotoConditionInfoImages,
);
}
Service:
async upsertImages(
vehicleId: string,
vehiclePhotoConditionId: string,
vehiclePhotoConditionImage: VehiclePhotoConditionInfoImageDTO,
): Promise<void> {
await this.isVehicleExist(vehicleId);
const vehiclePhotoCondition = await this.getOne(vehicleId, vehiclePhotoConditionId);
if (!vehiclePhotoCondition) {
throw new BadRequestException(
`The vehicle photo condition ${vehiclePhotoConditionId} is not found`,
);
}
const imageKeys = await this.handleImages(vehiclePhotoConditionId, vehiclePhotoConditionImage);
const updatedVehiclePhotoConditions = vehiclePhotoCondition.info.map((data) => {
if (data.vehiclePart === vehiclePhotoConditionImage.vehiclePart) {
data.uploadedImagesKeys.push(...imageKeys);
}
return data;
});
const query = sql
.update('vehicle_photo_condition', {
info: JSON.stringify(updatedVehiclePhotoConditions),
updated_at: sql('now()'),
})
.where({ id: vehiclePhotoConditionId });
await this.db.query(query.toParams());
}
I solved the problem. I am posting correct code.
Here is the explanation:
In previous code, I was updating inner jsonb array of objects in code and because of the fact that the below code took some time and asynchronicity of the NodeJS, previous request can take more time than the other requests that will come later and this situation can cause data inconsistency.
Here is the previous code:
const updatedVehiclePhotoConditions = vehiclePhotoCondition.info.map((data) => {
if (data.vehiclePart === vehiclePhotoConditionImage.vehiclePart) {
data.uploadedImagesKeys.push(...imageKeys);
}
return data;
});
In current code, I am updating inner jsonb array of object in database and let database do this operation. So, no data consistency happened.
Here is current code:
const query = {
text: `
UPDATE vehicle_photo_condition
SET info = s.json_array
FROM (
SELECT
jsonb_agg(
CASE WHEN obj ->> 'vehiclePart' = $1 THEN
jsonb_set(obj, '{uploadedImagesKeys}', $2)
ELSE obj END
) as json_array
FROM vehicle_photo_condition, jsonb_array_elements(info) obj WHERE id = $3
) s WHERE id = $3`,
values: [
vehiclePhotoConditionImage.vehiclePart,
JSON.stringify(imageKeys),
vehiclePhotoConditionId,
],
};
async upsertImages(
vehicleId: string,
vehiclePhotoConditionId: string,
vehiclePhotoConditionImage: VehiclePhotoConditionInfoImageDTO,
): Promise<void> {
const vehiclePhotoCondition = await this.getOne(vehicleId, vehiclePhotoConditionId);
if (!vehiclePhotoCondition) {
throw new BadRequestException(
`The vehicle photo condition ${vehiclePhotoConditionId} is not found`,
);
}
const imageKeys = await this.handleImages(vehiclePhotoConditionId, vehiclePhotoConditionImage);
const query = {
text: `
UPDATE vehicle_photo_condition
SET info = s.json_array
FROM (
SELECT
jsonb_agg(
CASE WHEN obj ->> 'vehiclePart' = $1 THEN
jsonb_set(obj, '{uploadedImagesKeys}', $2)
ELSE obj END
) as json_array
FROM vehicle_photo_condition, jsonb_array_elements(info) obj WHERE id = $3
) s WHERE id = $3`,
values: [
vehiclePhotoConditionImage.vehiclePart,
JSON.stringify(imageKeys),
vehiclePhotoConditionId,
],
};
await this.db.query(query);
}
I am trying to use the Stripe API to create a payment form as detailed here:
https://stripe.com/docs/payments/integration-builder
I would like to send the amout (that the user is charged) from the front-end so have attempted to add it to the fetch request as shown below:
var purchase = {
//items: [{ id: "xl-tshirt", price: 400 }]
amount: 2000
};
// Disable the button until we have Stripe set up on the page
document.querySelector("button").disabled = true;
fetch("/create.php", {
method: "POST",
headers: {
"Content-Type": "application/json"
},
body: JSON.stringify(purchase)
});
However the value (currently hardcoded at 2000) is not pulling through to the POST body successfully and the payment intent is failing. Below is the code I am using:
try {
// retrieve JSON from POST body
$json_str = file_get_contents('php://input');
$json_obj = json_decode($json_str, false);
$paymentIntent = \Stripe\PaymentIntent::create([
//'amount' => calculateOrderAmount($json_obj->items),
'amount' => $json_obj['amount'],
'currency' => 'usd',
]);
$output = [
'clientSecret' => $paymentIntent->client_secret,
];
echo json_encode($output);
} catch (Error $e) {
http_response_code(500);
echo json_encode(['error' => $e->getMessage()]);
}
Any advice is much appreciated.
You are sending false here, that converts it into an object
$json_obj = json_decode($json_str, false);
Then you are trying to use it as an array here
'amount' => $json_obj['amount'],
Try using
'amount' => $json_obj->amount,
Or
$json_obj = json_decode($json_str, true);
without changing anything else.
UPDATED I built a pricing page that uses Stripe Checkout to use both a One-Time payment button for product 1 and a Subscription payment button for product 2.
My goal is to redirect the one time payment button to Stripe Checkout with a one time payment, and separately redirect the subscription payment to a checkout with a recurring payment.
According to STRIPE this can be done using Subscription as the Mode in the CheckoutSession in create-checkout-session.php (sample project) :
The mode of the Checkout Session. Required when using prices or setup
mode. Pass subscription if the Checkout Session includes at least one
recurring item.
Contrary to the Stripe Docs the following line of code: 'mode' => 'subscription', activates subscription payments ONLY, but it doesnt redirect one time payments. For one-time payments to work I must change it to: 'mode' => 'payment', but then subscription payments don't work.
Here's the php code in question:
<?php
require_once 'shared.php';
$domain_url = $config['domain'];
// Create new Checkout Session for the order
// Other optional params include:
// [billing_address_collection] - to display billing address details on the page
// [customer] - if you have an existing Stripe Customer ID
// [payment_intent_data] - lets capture the payment later
// [customer_email] - lets you prefill the email input in the form
// For full details see https://stripe.com/docs/api/checkout/sessions/create
// ?session_id={CHECKOUT_SESSION_ID} means the redirect will have the session ID set as a query param
$checkout_session = \Stripe\Checkout\Session::create([
'success_url' => $domain_url . '/success.html?session_id={CHECKOUT_SESSION_ID}',
'cancel_url' => $domain_url . '/canceled.html',
'payment_method_types' => ['card'],
'mode' => 'subscription',
'line_items' => [[
'price' => $body->priceId,
'quantity' => 1,
]]
]);
echo json_encode(['sessionId' => $checkout_session['id']]);
And here's the javascript code:
// Create a Checkout Session with the selected plan ID
var createCheckoutSession = function(priceId) {
return fetch("./create-checkout-session.php", {
method: "POST",
headers: {
"Content-Type": "application/json"
},
body: JSON.stringify({
priceId: priceId
})
}).then(function(result) {
return result.json();
});
};
// Handle any errors returned from Checkout
var handleResult = function(result) {
if (result.error) {
var displayError = document.getElementById("error-message");
displayError.textContent = result.error.message;
}
};
/* Get your Stripe publishable key to initialize Stripe.js */
fetch("./config.php")
.then(function(result) {
return result.json();
})
.then(function(json) {
var publishableKey = json.publishableKey;
var subscriptionPriceId = json.subscriptionPrice;
var onetimePriceId = json.onetimePrice;
var stripe = Stripe(publishableKey);
// Setup event handler to create a Checkout Session when button is clicked
document
.getElementById("subscription-btn")
.addEventListener("click", function(evt) {
createCheckoutSession(subscriptionPriceId).then(function(data) {
// Call Stripe.js method to redirect to the new Checkout page
stripe
.redirectToCheckout({
sessionId: data.sessionId
})
.then(handleResult);
});
});
// Setup event handler to create a Checkout Session when button is clicked
document
.getElementById("onetime-btn")
.addEventListener("click", function(evt) {
createCheckoutSession(onetimePriceId).then(function(data) {
// Call Stripe.js method to redirect to the new Checkout page
stripe
.redirectToCheckout({
sessionId: data.sessionId
})
.then(handleResult);
});
});
});
Is it even possible to have both one time payments and recurring payments on the same page with Stripe Checkout? How can I accomplish this?
Update according to Bemn:
$checkout_session = \Stripe\Checkout\Session::create([
'success_url' => $domain_url . '/success.html?session_id={CHECKOUT_SESSION_ID}',
'cancel_url' => $domain_url . '/canceled.html',
'payment_method_types' => ['card'],
'mode' => $body->mode
'line_items' => [[
'price' => $body->price_xxx,
// For metered billing, do not pass quantity
'quantity' => 1,
]],
'line_items' => [[
'price' => $body->price_zzz,
// For metered billing, do not pass quantity
'quantity' => 1,
]]
]);
echo json_encode(['sessionId' => $checkout_session['id']]);
And the JS:
// Create a Checkout Session with the selected plan ID
var createCheckoutSession = function(priceId, mode) {
return fetch("./create-checkout-session.php", {
method: "POST",
headers: {
"Content-Type": "application/json"
},
body: JSON.stringify({
priceId: priceId,
mode: mode // <-- passing the mode, e.g. 'payment' or 'subscription'
})
}).then(function(result) {
return result.json();
});
};
And the HTML:
<div data-stripe-priceid="pricexxx" data-stripe-mode="payment" id="onetime-btn" class="bold mt-2 d-inline-block w-100-after-md max-width-xxs py-2 btn btn-secondary">Ore Time</div>
<div data-stripe-priceid="pricexxx" data-stripe-mode="subscription" id="subscription-btn" class="bold mt-2 d-inline-block w-100-after-md max-width-xxs py-2 btn btn-secondary">Ore Time</div>
Is it even possible to have both one time payments and recurring payments on the same page with Stripe Checkout?
Yes. The key is you should pass the correct options to generate the corresponding Stripe Checkout session ID.
How can I accomplish this?
Backend: Have a function to accept Stripe's price ID and payment mode as input and return a Stripe Checkout session ID as the output.
Frontend: Pass payment mode information to /create-checkout-session.php. (see the Note if you are unable to do so)
Details
The following solution assuming that:
You generate a Stripe Checkout Session ID at the backend. That ID will then pass to .createCheckoutSession() in js frontend.
You have a 1-time product (let's call it PAY) and a recurrent subscription (let's call it SUB).
Frontend
I think you are close. What you need to do is passing the mode information to your API endpoint as well:
// Create a Checkout Session with the selected plan ID
var createCheckoutSession = function(priceId, mode) { // <-- add a mode parameter
return fetch("./create-checkout-session.php", {
method: "POST",
headers: {
"Content-Type": "application/json"
},
body: JSON.stringify({
priceId: priceId,
mode: mode // <-- passing the mode, e.g. 'payment' or 'subscription'
})
}).then(function(result) {
return result.json();
});
};
If so, each checkout button in the page should have corresponding info of the priceId and payment mode. You can do so by storing them using data attribute:
<div data-stripe-priceid="price_yyy" data-stripe-mode="subscription">Recurrent</div>
<div data-stripe-priceid="price_zzz" data-stripe-mode="payment">1-time</div>
If so, you can get the data attributes by e.g. a click event.
Note: If you cannot add an extra param to indicate mode, you neeed to identify if the given price ID is a 1-time or recurrent product in the backend. See https://stripe.com/docs/api/prices/object?lang=php#price_object-type for more details.
Backend
Here are 2 sample code snippets from Stripe's documentation. Direct copying of them does not work.
Reference for PAY: https://stripe.com/docs/checkout/integration-builder
$checkout_session = \Stripe\Checkout\Session::create([
'payment_method_types' => ['card'],
'line_items' => [[
'price_data' => [
'currency' => 'usd',
'unit_amount' => 2000,
'product_data' => [
'name' => 'Stubborn Attachments',
'images' => ["https://i.imgur.com/EHyR2nP.png"],
],
],
'quantity' => 1,
]],
'mode' => 'payment',
'success_url' => $YOUR_DOMAIN . '/success.html',
'cancel_url' => $YOUR_DOMAIN . '/cancel.html',
]);
In your case, you may not need to define 'price_data'. Instead, you should use 'price', like the next example.
Reference for SUB: https://stripe.com/docs/billing/subscriptions/checkout#create-session
$checkout_session = \Stripe\Checkout\Session::create([
'success_url' => 'https://example.com/success.html?session_id={CHECKOUT_SESSION_ID}',
'cancel_url' => 'https://example.com/canceled.html',
'payment_method_types' => ['card'],
'mode' => 'subscription',
'line_items' => [[
'price' => $body->priceId,
// For metered billing, do not pass quantity
'quantity' => 1,
]],
]);
Take a look at this reference: https://stripe.com/docs/api/checkout/sessions/create. For line_items, you can just simply using 'price' and pass the price ID (e.g. price_xxx), which means your 'line_items' will looks like this:
'line_items' => [[
'price' => $body->priceId,
'quantity' => 1,
]],
For 'mode', use the value from your API request. It should be something like:
'mode' => $body->mode
Which means in your backend you better define a function (e.g. generate_checkout_session) to:
parse the json body received in the API request
get priceId and mode from the parsed data
use the priceId and mode in \Stripe\Checkout\Session::create and
returns the checkout_session ID
Hope this (and the reference urls) can help you.
When you create your Session you can pass both a Price for the recurring amount charged on the subscription and another Price for the one-time fee you want to charge. You can combine multiple recurring Prices and one-time Prices overall.
$checkout_session = \Stripe\Checkout\Session::create([
'success_url' => $domain_url . '/success.html?session_id={CHECKOUT_SESSION_ID}',
'cancel_url' => $domain_url . '/canceled.html',
'payment_method_types' => ['card'],
'mode' => 'subscription',
'line_items' => [
// Add a one-time Price for $10
[
'price' => 'price_123',
'quantity' => 1,
],
// Add another one-time Price for $23
[
'price' => 'price_345',
'quantity' => 1,
],
// Add a recurring Price for $100 monthly
[
'price' => 'price_ABC',
'quantity' => 1,
],
]);
The code above will create a session with 3 line items. One for $100 monthly, one for $10 just once and one for $23 just once. The total for the session would be $133 on the first payment. It will also start a subscription for $100 a month and future invoices will be for $100 as expected.
What I have got is that you just need to add a check for either it is one-time or subscription here is what you can do:
JS FILE CHANGES:
var createCheckoutSession = function(priceId, $mode) {
return fetch("./create-checkout-session.php", {
method: "POST",
headers: {
"Content-Type": "application/json"
},
body: JSON.stringify({
priceId: priceId,
paymentType: $mode, // This vary based on the button clicked either one-time or subscription.
})
}).then(function(result) {
return result.json();
});
};
/* Get your Stripe publishable key to initialize Stripe.js */
fetch("./config.php")
.then(function(result) {
return result.json();
})
.then(function(json) {
var publishableKey = json.publishableKey;
var subscriptionPriceId = json.subscriptionPrice;
var onetimePriceId = json.onetimePrice;
var stripe = Stripe(publishableKey);
// Setup event handler to create a Checkout Session when button is clicked
document
.getElementById("subscription-btn")
.addEventListener("click", function(evt) {
createCheckoutSession(subscriptionPriceId, 'subscription').then(function(data) {
// Call Stripe.js method to redirect to the new Checkout page
stripe
.redirectToCheckout({
sessionId: data.sessionId
})
.then(handleResult);
});
});
// Setup event handler to create a Checkout Session when button is clicked
document
.getElementById("onetime-btn")
.addEventListener("click", function(evt) {
createCheckoutSession(onetimePriceId, 'onetime').then(function(data) {
// Call Stripe.js method to redirect to the new Checkout page
stripe
.redirectToCheckout({
sessionId: data.sessionId
})
.then(handleResult);
});
});
});
Now we need to make changes in PHP file:
PHP FILE CHANGES:
$checkout_session = \Stripe\Checkout\Session::create([
'success_url' => $domain_url . '/success.html?session_id={CHECKOUT_SESSION_ID}',
'cancel_url' => $domain_url . '/canceled.html',
'payment_method_types' => ['card'],
'mode' => $body->paymentType, // Here is what we have got from front-end
'line_items' => [[
'price' => $body->priceId,
'quantity' => 1,
]]
]);
For subscription we actually need to set interval, which is not need to set in one time purses. Probably this error happening for this reason.
Add recurring can be solve recurring error.
recurring: {
interval: 'month' // 'month' | 'year'
}
I am using the PaymentIntent API to integrate Stripe payments using stripe-php SDK and Stripe.js V3.
Following This guide https://stripe.com/docs/payments/payment-intents/migration#saving-cards-checkout. I am getting what successful payments in my Stripe Dashboard done with test cards which do not require 3d-secure. But The Stripe's new SCA 3d secure Popup(according to their docs.) is not popping up, Which leads payments done with 3dsecure ENABLED cards to "Incomplete Payments" Tab in stripe Dashboard.
I have examine the code thoroughly multiple times and tested. I have noticed that my code skips(somtimes) OR throws an error "Unexpected end of JSON input" in the "Fetch Part" on the client side code.. which leads the 3d-secure cards payments to be incomplete.The JavaScript Fetch function is not fetching the "payment_method_id" from the specified file(url).
My Payment.js File:
var elements = stripe.elements();
var style = {
base: {
color: '#32325d',
lineHeight: '18px',
fontFamily: '"Helvetica Neue", Helvetica, sans-serif',
fontSmoothing: 'antialiased',
fontSize: '16px',
'::placeholder': {
color: '#aab7c4'
}
},
invalid: {
color: '#fa755a',
iconColor: '#fa755a'
}
};
var cardNumber = elements.create('cardNumber', {
style: style
});
cardNumber.mount('#cardNumber');
var cardExpiry = elements.create('cardExpiry', {
style: style
});
cardExpiry.mount('#cardExpiry');
var cardCvc = elements.create('cardCvc', {
style: style
});
cardCvc.mount('#cardCVC');
var cardholderName = $('#custName').val();
var amount = $('#amount').val();
$(document).ready(function () {
$("#paymentForm").submit(function (event) {
//event.preventDefault();
stripe.createPaymentMethod('card', cardNumber, {
billing_details: {name: cardholderName.value}
}).then(function (result) {
console.log(result);
if (result.error) {
var errorElement = document.getElementById('card-error');
errorElement.textContent = result.error.massage;
} else {
stripeTokenHandler(result);
fetch('example.com/stripe/index.php', {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify({
payment_method_id: result.paymentMethod.id
})
}).then(function (result) {
// Handle server response (see Step 3)
result.json().then(function (result) {
handleServerResponse(result);
})
});
}
});
//return false;
});
function stripeTokenHandler(result) {
var payForm = $("#paymentForm");
var paymentMethodID = result.paymentMethod.id;
//set the token into the form hidden input to make payment
payForm.append("<input type='hidden' name='payment_method_id' value='" + paymentMethodID + "' />");
// payForm.submit();
payForm.submit();
}
}
My Index.php File
header('Content-Type: application/json');
if(isset($_POST['submit'])){
//include Stripe PHP library
require_once('stripe-php/init.php');
//set Stripe Secret Key
\Stripe\Stripe::setApiKey('sk_test_key');
//add customer to stripe
$customer = \Stripe\Customer::create(array(
'email' => $custEmail,
));
function generatePaymentResponse($intent) {
if ($intent->status == 'requires_action' &&
$intent->next_action->type == 'use_stripe_sdk') {
# Tell the client to handle the action
echo json_encode([
'requires_action' => true,
'payment_intent_client_secret' => $intent->client_secret
]);
} else if ($intent->status == 'succeeded') {
# The payment didn’t need any additional actions and completed!
# Handle post-payment fulfillment
echo json_encode([
'success' => true
]);
} else {
# Invalid status
http_response_code(500);
echo json_encode(['error' => 'Invalid PaymentIntent status']);
}
}
# retrieve json from POST body
$json_str = file_get_contents('php://input');
$json1 = json_encode($_POST);
$json_obj = json_decode($json1);
$intent = null;
try {
if (isset($json_obj->payment_method_id)) {
$intent = \Stripe\PaymentIntent::create([
'payment_method' => $json_obj->payment_method_id,
'customer' => $customer->id,
'amount' => 1099,
'currency' => 'gbp',
'confirmation_method' => 'manual',
'confirm' => true,
]);
}
generatePaymentResponse($intent);
}catch (\Stripe\Error\Base $e) {
# Display error on client
echo json_encode([
'error' => $e->getMessage()
]);
}
}
?>
As it can be seen my stripeTokenHandler is appending the payment_method.id into HTML form and the process goes on. but the "fetch" section of JS code should get payment_method_id to generate "Response" and to proceed for "next_action" if the payment status is "requires_action".
So, In order to achieve what i wanted to, what i did was
- removed stripeTokenHandler()
Because i was using it in my previous charges API integration and thought it will work with the new PaymentIntent. And i guess many people misunderstood or misguided buy bunch of different methods stripe has in its docs.I saw a lot of questions on internet people complaining that stripe's "poorly managed" Documentation has confused them.
- Learned Fetch Api.
As a newbie i didnt know what it was for.
- removed isset post submit from my php code
reason: payment.js was unable to POST the paymentMethod.id to the server using Fetch Api and get the response body back from the server to proceed the code further.
I MUST say that Stripe needs to improve its Docs about this SCA ready PaymentIntent thing.