Skip to content

Commit

Permalink
Fix confirmCardPayment in localstripe-v3.js and add an example
Browse files Browse the repository at this point in the history
I wanted to add an example of using localstripe-v3.js, as a way of testing
#240 and also because it seems
generally useful as documentation.

The example includes a --real-stripe argument so you can quickly compare the
real (test) Stripe API and Stripe.js against localstripe.

Along the way I discovered confirmCardPayment isn't quite right; it skips
/confirm and goes right to /_authenticate. This fails on cards that don't
require 3D Secure authentication because the /_authenticate endpoint is
confused about why it's being called at all. I fixed this, modeled on how
confirmCardSetup works.

This in turn required a small fix to the localstripe backend to allow calls to
/confirm from the browser (i.e., with a client_secret and key as form data).
  • Loading branch information
Ben Creech committed Nov 10, 2024
1 parent 6b0ff20 commit ecef02f
Show file tree
Hide file tree
Showing 9 changed files with 372 additions and 10 deletions.
3 changes: 3 additions & 0 deletions README.rst
Original file line number Diff line number Diff line change
Expand Up @@ -193,6 +193,9 @@ JavaScript source in the web page before it creates card elements:
<script src="http://localhost:8420/js.stripe.com/v3/"></script>
See the ``samples/pm_setup`` directory for an example of this with a
very minimalistic Stripe elements application.
Use webhooks
------------
Expand Down
39 changes: 30 additions & 9 deletions localstripe/localstripe-v3.js
Original file line number Diff line number Diff line change
Expand Up @@ -340,24 +340,45 @@ Stripe = (apiKey) => {
confirmCardPayment: async (clientSecret, data) => {
console.log('localstripe: Stripe().confirmCardPayment()');
try {
const success = await openModal(
'3D Secure\nDo you want to confirm or cancel?',
'Complete authentication', 'Fail authentication');
const pi = clientSecret.match(/^(pi_\w+)_secret_/)[1];
const url = `${LOCALSTRIPE_BASE_API}/v1/payment_intents/${pi}` +
`/_authenticate?success=${success}`;
const response = await fetch(url, {
let url = `${LOCALSTRIPE_BASE_API}/v1/payment_intents/${pi}/confirm`;
let response = await fetch(url, {
method: 'POST',
body: JSON.stringify({
body: new URLSearchParams({
key: apiKey,
client_secret: clientSecret,
}),
});
const body = await response.json().catch(() => ({}));
let body = await response.json().catch(() => ({}));
if (response.status !== 200 || body.error) {
return {error: body.error};
} else {
} else if (body.status === 'succeeded') {
return {paymentIntent: body};
} else if (body.status === 'requires_action') {
const success = await openModal(
'3D Secure\nDo you want to confirm or cancel?',
'Complete authentication', 'Fail authentication');
url = `${LOCALSTRIPE_BASE_API}/v1/payment_intents/${pi}` +
`/_authenticate?success=${success}`;
response = await fetch(url, {
method: 'POST',
body: JSON.stringify({
key: apiKey,
client_secret: clientSecret,
}),
});
body = await response.json().catch(() => ({}));
if (response.status !== 200 || body.error) {
return {error: body.error};
} else if (body.status === 'succeeded') {
return {paymentIntent: body};
} else { // 3D Secure authentication cancelled by user:
return {error: {message:
'The latest attempt to confirm the payment has failed ' +
'because authentication failed.'}};
}
} else {
return {error: {message: `payment_intent has status ${body.status}`}};
}
} catch (err) {
if (typeof err === 'object' && err.error) {
Expand Down
8 changes: 7 additions & 1 deletion localstripe/resources.py
Original file line number Diff line number Diff line change
Expand Up @@ -1971,7 +1971,8 @@ def _api_create(cls, confirm=None, off_session=None, **data):
return obj

@classmethod
def _api_confirm(cls, id, payment_method=None, **kwargs):
def _api_confirm(cls, id, payment_method=None, client_secret=None,
**kwargs):
if kwargs:
raise UserError(400, 'Unexpected ' + ', '.join(kwargs.keys()))

Expand All @@ -1980,11 +1981,16 @@ def _api_confirm(cls, id, payment_method=None, **kwargs):

try:
assert type(id) is str and id.startswith('pi_')
if client_secret is not None:
assert type(client_secret) is str
except AssertionError:
raise UserError(400, 'Bad request')

obj = cls._api_retrieve(id)

if client_secret and client_secret != obj.client_secret:
raise UserError(401, 'Unauthorized')

if obj.status != 'requires_confirmation':
raise UserError(400, 'Bad request')

Expand Down
1 change: 1 addition & 0 deletions localstripe/server.py
Original file line number Diff line number Diff line change
Expand Up @@ -168,6 +168,7 @@ async def auth_middleware(request, handler):
r'^/v1/tokens$',
r'^/v1/sources$',
r'^/v1/payment_intents/\w+/_authenticate\b',
r'^/v1/payment_intents/\w+/confirm$',
r'^/v1/setup_intents/\w+/confirm$',
r'^/v1/setup_intents/\w+/cancel$',
)))
Expand Down
19 changes: 19 additions & 0 deletions samples/pm_setup/README.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
localstripe sample: Set up a payment method for future payments
===============================================================

This is a demonstration of how to inject localstripe for testing a simplistic
client/server Stripe web integration. This is derived from the Stripe
instructions for collecting payment methods on a single-page web app.

**This sample is not intended to represent best practice for production code!**

From the localstripe directory...

.. code:: shell
# Launch localstripe:
python -m localstripe --from-scratch &
# Launch this sample's server:
python -m samples.pm_setup.server
# ... now browse to http://0.0.0.0:8080 and try the test card
# 4242424242424242.
33 changes: 33 additions & 0 deletions samples/pm_setup/index.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
<head>
<title>
localstripe sample: Set up a payment method for future payments
</title>
<!-- this is a proxy script which chooses whether to load the real Stripe.js
or localstripe's mock: -->
<script src="stripe.js"></script>
<script type="module" src="pm_setup.js"></script>
</head>
<body>
<h1>
localstripe sample: Set up a payment method for future payments
</h1>
<form id="setup-form">
<button id="setup-submit">Start adding a payment method!</button>
</form>
<fieldset disabled="disabled" id="payment-method-form-fieldset">
<form id="payment-method-form">
<p>Enter your (test) card information:</p>
<div id="payment-method-element"></div>
<button id="payment-method-submit">Submit</button>
<div id="payment-method-result-message"></div>
</form>
</fieldset>
<fieldset disabled="disabled" id="payment-form-fieldset">
<form id="payment-form">
<label for="payment-amount">Make a payment in cents:</label>
<input id="payment-amount" name="amount" type="number"/>
<button id="payment-submit">Submit</button>
<div id="payment-result-message"></div>
</form>
</fieldset>
</body>
101 changes: 101 additions & 0 deletions samples/pm_setup/pm_setup.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
let seti;
let setiClientSecret;
let stripe;
let paymentElement;

const init = async () => {
const response = await fetch('/publishable_key', {method: "GET"});
const {
stripe_api_pk: publishableKey,
} = await response.json();

stripe = Stripe(publishableKey);

document.getElementById(
'setup-form',
).addEventListener('submit', handleSetupSubmit);

document.getElementById(
'payment-method-form',
).addEventListener('submit', handlePaymentMethodSubmit);

document.getElementById(
'payment-form',
).addEventListener('submit', handlePaymentSubmit);
}

const handleSetupSubmit = async (event) => {
event.preventDefault();

const response = await fetch('/setup_intent', {method: "POST"});
const {
id: id,
client_secret: clientSecret,
} = await response.json();

seti = id;
setiClientSecret = clientSecret;

const elements = stripe.elements({
clientSecret: clientSecret,
});

paymentElement = elements.create('card');

paymentElement.mount('#payment-method-element');

document.getElementById(
'payment-method-form-fieldset',
).removeAttribute('disabled');
}

let handlePaymentMethodSubmit = async (event) => {
event.preventDefault();

const {error} = await stripe.confirmCardSetup(setiClientSecret, {
payment_method: {
card: paymentElement,
},
});

const container = document.getElementById('payment-method-result-message');
if (error) {
container.textContent = error.message;
} else {
const response = await fetch('/payment_method', {
method: "POST",
body: JSON.stringify({ setup_intent: seti })
});
if (response.ok) {
container.textContent = "Successfully confirmed payment method!";
document.getElementById(
'payment-form-fieldset',
).removeAttribute('disabled');
} else {
container.textContent = "Error confirming payment method!";
}
}
};

let handlePaymentSubmit = async (event) => {
event.preventDefault();

const response = await fetch('/payment_intent', {
method: "POST",
body: JSON.stringify({
amount: document.getElementById('payment-amount').value,
})
});
const {client_secret: clientSecret} = await response.json();

const {error} = await stripe.confirmCardPayment(clientSecret, {});

const container = document.getElementById('payment-result-message');
if (error) {
container.textContent = error.message;
} else {
container.textContent = "Successfully confirmed payment!";
}
};

await init();
Loading

0 comments on commit ecef02f

Please sign in to comment.