I'm attempting to create a webpage for my startup, which is subscription-based. We have Stripe in live mode, and are essentially storing cards for future use, allowing us to have different payment options available when creating new subscriptions. Subscriptions will then be assigned to a device (similar to the way Office 365 assigns licenses to users).
When a user signs up, our code automatically creates a Stripe customer ID with Stripe and stores it on the backend. We then have a settings page, which contains a section for "Payment Methods", which dynamically loads them server-side, and allowing new payment methods to be added by clicking a "+" button in the same area. The "+" button is set to onclick="addPaymentOption()"
, which calls this nifty piece of code (Clipped for relevancy):
function addPaymentOption(){
//initiated by front end, loads stripe payment form
const attachto = document.getElementById("payment-dash");
const ppw_t = document.createElement("li");
ppw_t.setAttribute("id", "payment_add_form_cover");
ppw_t.setAttribute("style", "border-top: 1px solid #262626; margin-top: 6px;");
ppw_t.innerHTML = '<h3>Add a Payment method</h3>';
const ppw = document.createElement("div");
ppw.setAttribute("id", "payment_add_form");
ppw.setAttribute("style", "padding: 6px; max-width: 400px; margin: 0px auto;");
ppw.innerHTML = '<form id="payment-form" action=""><div id="payment-element"></div><button id="submit" type="submit" style="width:100%;">Save card details</button><div id="error-message"></div></form><form id="address-element"><div id="address-element"></div></form>';
attachto.appendChild(ppw_t);
ppw_t.appendChild(ppw);
(async () => {
const response = await fetch('/settings/payments/secret'); //<-- Renders client secret in backend, in JSON format.
const { client_secret: clientSecret } = await response.json();
const options = {
clientSecret: clientSecret
};
const stripe = Stripe('pk_live_51Mm3X7HIvvDoRmVUJay6Ib7MFBZgXvRtwwSosMh34h5GqCHZ0EKtnehc05DxFMfkggp2tzi6nu7ekNQnYyknMeC300T2iYxOHU');
console.log(clientSecret);
const elements = stripe.elements(options);
const paymentElement = elements.create('payment'); // <-- create payment element
paymentElement.mount('#payment-element');
loadStripe();
})();
}
function loadStripe(){
const form = document.getElementById('payment-form');
form.addEventListener('submit', async (event) => {
event.preventDefault();
const {error} = await stripe.confirmSetup({
//`Elements` instance that was used to create the Payment Element
elements,
confirmParams: {
return_url: 'https://**********.com/settings/payments',
}
});
if (error) {
// This point will only be reached if there is an immediate error when
// confirming the payment. Show error to your customer (for example, payment
// details incomplete)
const messageContainer = document.querySelector('#error-message');
messageContainer.textContent = error.message;
} else {
// 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`.
}
});
}
The HTML where the code references (clipped for relevancy) is:
<ul class="Dashboard" id="payment-dash">
<li>
<contextmenu>
<ol>
<li onclick="addSubscription()">+</li>
<li onclick="doHelp()">?</li>
</ol>
</contextmenu>
<img src="/assets/img/ui/pay/subscriptions.png" alt=""/>
<h3>Subscriptions</h3>
<ul class="DashboardSub">
<li>
<header>
<img src="/assets/img/static/lock_logo.png"/>
<h4>Subscription #1</h4>
</header>
<span>Monthly cost: $12.99<br/>Linked to: <i>Device name</i></span>
<opts><a href="#">Remove</a> | <a href="#">Assign</a></opts>
</li>
<li>
<header>
<img src="/assets/img/static/lock_logo.png"/>
<h4>Device #1</h4>
</header>
<span>Monthly cost: $12.99<br/>Linked to: <i>Device name</i></span>
<opts><a href="#">Remove</a> | <a href="#">Assign</a></opts>
</li>
<li>
<header>
<img src="/assets/img/static/lock_logo.png"/>
<h4>Device #1</h4>
</header>
<span>Monthly cost: $12.99<br/>Linked to: <i>Device name</i></span>
<opts><a href="#">Remove</a> | <a href="#">Assign</a></opts>
</li>
<li>
<header>
<img src="/assets/img/icons/static/guardian-gpt.png"/>
<h4>AI Family</h4>
</header>
<span>Monthly cost: $5.99<br/></span>
<opts><a href="#">Remove</a></opts>
</li>
<div>
<b>Monthly total</b>: $42.99
</div>
</ul>
</li>
<li>
<contextmenu>
<ol>
<li onclick="addPaymentOption()">+</li>
<li>?</li>
</ol>
</contextmenu>
<img src="/assets/img/ui/pay/payment_methods.png" alt=""/>
<h3>Payment Methods</h3>
<ul class="DashboardSub" id="payments">
<li>
<header>
<img src="/assets/img/ui/pay/mc.png"/>
<h4>Mastercard *1234</h4>
</header>
<span>Expires in 2025<br/>Last use: </span>
<opts><a href="#">Remove</a> | <a href="#">Set Default</a></opts>
</li>
<li>
<header>
<img src="/assets/img/ui/pay/visa.png"/>
<h4>Mastercard *1234</h4>
</header>
<span>Expires in 2025<br/>Last use: </span>
<opts><a href="#">Remove</a> | <a href="#">Set Default</a></opts>
</li>
<li>
<header>
<img src="/assets/img/ui/pay/amex.png"/>
<h4>Mastercard *1234</h4>
</header>
<span>Expires in 2025<br/>Last use: </span>
<opts><a href="#">Remove</a> | <a href="#">Set Default</a></opts>
</li>
<li>
<header>
<img src="/assets/img/ui/pay/discover.png"/>
<h4>Mastercard *1234</h4>
</header>
<span>Expires in 2025<br/>Last use: </span>
<opts><a href="#">Remove</a> | <a href="#">Set Default</a></opts>
</li>
<li>
<header>
<img src="/assets/img/ui/pay/ach_pay.png"/>
<h4>Bank Acct. *1234</h4>
</header>
<span>Expires in 2025<br/>Pre-auth: </span>
<opts><a href="#">Remove</a> | <a href="#">Set Default</a></opts>
</li>
</ul>
</li>
<li>
<img src="/assets/img/ui/pay/history.png" alt=""/>
<h3>Payment History</h3>
<table>
<tr>
<td><b>Subscriptions</b></td>
<td>4</td>
</tr>
<tr>
<td><b>Last Payment</b></td>
<td><b>$45.<sup>00</sup>.</td>
</tr>
</table>
</li>
<li>
<img src="/assets/img/ui/pay/cancel.png" alt=""/>
<h3>Cancel Service</h3>
<table>
<tr>
<td><b>Subscriptions</b></td>
<td>4</td>
</tr>
<tr>
<td><b>Last Payment</b></td>
<td><b>$45.<sup>00</sup>.</td>
</tr>
</table>
</li>
</ul>
Also, the code for /secrets
is:
if(isLoggedIn()){
$customer_id = fetchstripeid();
$intent = $stripe->setupIntents->create(
[
'customer' => $customer_id,
'payment_method_types' => ['card_present'],
]);
echo json_encode(array('client_secret' => $intent->client_secret));
ob_flush();flush();
die();
}
A few revisions ago, it would properly create the form elements for credit card info and billing ZIP, but the "Save card details" button would disappear. I rearranged the code because it appeared that the element was being loaded and unloaded. Then, a few revisions later, the "Save card details" button loads into the form, but the credit card entry form elements refuse to load. All of the time, it gave me little to no error messages in the console.
Does anyone see something obvious here I'm missing? Is something loading before or after it's supposed to? My Stripe experience is limited, maybe there's a concept I don't get here?