Skip to content

Commit f6b1803

Browse files
author
Ben Creech
committed
Fix confirmCardPayment in localstripe-v3.js and add an example
I wanted to add an example of using localstripe-v3.js, as a way of testing adrienverge#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).
1 parent 6b0ff20 commit f6b1803

9 files changed

Lines changed: 376 additions & 10 deletions

File tree

README.rst

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -193,6 +193,9 @@ JavaScript source in the web page before it creates card elements:
193193
194194
<script src="http://localhost:8420/js.stripe.com/v3/"></script>
195195
196+
See the ``samples/pm_setup`` directory for an example of this with a
197+
very minimalistic Stripe elements application.
198+
196199
Use webhooks
197200
------------
198201

localstripe/localstripe-v3.js

Lines changed: 30 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -340,24 +340,45 @@ Stripe = (apiKey) => {
340340
confirmCardPayment: async (clientSecret, data) => {
341341
console.log('localstripe: Stripe().confirmCardPayment()');
342342
try {
343-
const success = await openModal(
344-
'3D Secure\nDo you want to confirm or cancel?',
345-
'Complete authentication', 'Fail authentication');
346343
const pi = clientSecret.match(/^(pi_\w+)_secret_/)[1];
347-
const url = `${LOCALSTRIPE_BASE_API}/v1/payment_intents/${pi}` +
348-
`/_authenticate?success=${success}`;
349-
const response = await fetch(url, {
344+
let url = `${LOCALSTRIPE_BASE_API}/v1/payment_intents/${pi}/confirm`;
345+
let response = await fetch(url, {
350346
method: 'POST',
351-
body: JSON.stringify({
347+
body: new URLSearchParams({
352348
key: apiKey,
353349
client_secret: clientSecret,
354350
}),
355351
});
356-
const body = await response.json().catch(() => ({}));
352+
let body = await response.json().catch(() => ({}));
357353
if (response.status !== 200 || body.error) {
358354
return {error: body.error};
359-
} else {
355+
} else if (body.status === 'succeeded') {
360356
return {paymentIntent: body};
357+
} else if (body.status === 'requires_action') {
358+
const success = await openModal(
359+
'3D Secure\nDo you want to confirm or cancel?',
360+
'Complete authentication', 'Fail authentication');
361+
url = `${LOCALSTRIPE_BASE_API}/v1/payment_intents/${pi}` +
362+
`/_authenticate?success=${success}`;
363+
response = await fetch(url, {
364+
method: 'POST',
365+
body: JSON.stringify({
366+
key: apiKey,
367+
client_secret: clientSecret,
368+
}),
369+
});
370+
body = await response.json().catch(() => ({}));
371+
if (response.status !== 200 || body.error) {
372+
return {error: body.error};
373+
} else if (body.status === 'succeeded') {
374+
return {paymentIntent: body};
375+
} else { // 3D Secure authentication cancelled by user:
376+
return {error: {message:
377+
'The latest attempt to confirm the payment has failed ' +
378+
'because authentication failed.'}};
379+
}
380+
} else {
381+
return {error: {message: `payment_intent has status ${body.status}`}};
361382
}
362383
} catch (err) {
363384
if (typeof err === 'object' && err.error) {

localstripe/resources.py

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1971,7 +1971,8 @@ def _api_create(cls, confirm=None, off_session=None, **data):
19711971
return obj
19721972

19731973
@classmethod
1974-
def _api_confirm(cls, id, payment_method=None, **kwargs):
1974+
def _api_confirm(cls, id, payment_method=None, client_secret=None,
1975+
**kwargs):
19751976
if kwargs:
19761977
raise UserError(400, 'Unexpected ' + ', '.join(kwargs.keys()))
19771978

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

19811982
try:
19821983
assert type(id) is str and id.startswith('pi_')
1984+
if client_secret is not None:
1985+
assert type(client_secret) is str
19831986
except AssertionError:
19841987
raise UserError(400, 'Bad request')
19851988

19861989
obj = cls._api_retrieve(id)
19871990

1991+
if client_secret and client_secret != obj.client_secret:
1992+
raise UserError(401, 'Unauthorized')
1993+
19881994
if obj.status != 'requires_confirmation':
19891995
raise UserError(400, 'Bad request')
19901996

localstripe/server.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -168,6 +168,7 @@ async def auth_middleware(request, handler):
168168
r'^/v1/tokens$',
169169
r'^/v1/sources$',
170170
r'^/v1/payment_intents/\w+/_authenticate\b',
171+
r'^/v1/payment_intents/\w+/confirm$',
171172
r'^/v1/setup_intents/\w+/confirm$',
172173
r'^/v1/setup_intents/\w+/cancel$',
173174
)))

samples/pm_setup/README.rst

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
localstripe sample: Set up a payment method for future payments
2+
===============================================================
3+
4+
This is a demonstration of how to inject localstripe for testing a simplistic
5+
client/server Stripe web integration. This is derived from the Stripe
6+
instructions for collecting payment methods on a single-page web app.
7+
8+
**This sample is not intended to represent best practice for production code!**
9+
10+
From the localstripe directory...
11+
12+
.. code:: shell
13+
14+
# Launch localstripe:
15+
python -m localstripe --from-scratch &
16+
# Launch this sample's server:
17+
python -m samples.pm_setup.server
18+
# ... now browse to http://0.0.0.0:8080 and try the test card
19+
# 4242424242424242.

samples/pm_setup/index.html

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
<head>
2+
<title>
3+
localstripe sample: Set up a payment method for future payments
4+
</title>
5+
<!-- this is a proxy script which chooses whether to load the real Stripe.js
6+
or localstripe's mock: -->
7+
<script src="stripe.js"></script>
8+
<script type="module" src="pm_setup.js"></script>
9+
</head>
10+
<body>
11+
<h1>
12+
localstripe sample: Set up a payment method for future payments
13+
</h1>
14+
<form id="setup-form">
15+
<input type="submit" name="setup-submit" value="Start adding a payment method!"/>
16+
</form>
17+
<fieldset disabled="disabled" id="payment-method-form-fieldset">
18+
<form id="payment-method-form">
19+
<p>Enter your (test) card information:</p>
20+
<div id="payment-method-element"></div>
21+
<input type="submit" name="payment-method-submit" value="Submit"/>
22+
<div id="payment-method-result-message"></div>
23+
</form>
24+
</fieldset>
25+
<fieldset disabled="disabled" id="payment-form-fieldset">
26+
<form id="payment-form">
27+
<label for="payment-amount">Make a payment in cents:</label>
28+
<input id="payment-amount" name="amount" type="number"/>
29+
<input type="submit" name="payment-submit" value="Submit"/>
30+
<div id="payment-result-message"></div>
31+
</form>
32+
</fieldset>
33+
</body>

samples/pm_setup/pm_setup.js

Lines changed: 105 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,105 @@
1+
let seti;
2+
let setiClientSecret;
3+
let stripe;
4+
let paymentElement;
5+
6+
const init = async () => {
7+
const response = await fetch('/publishable_key', {method: "GET"});
8+
const {
9+
stripe_api_pk: publishableKey,
10+
} = await response.json();
11+
12+
stripe = Stripe(publishableKey);
13+
14+
document.getElementById(
15+
'setup-form',
16+
).addEventListener('submit', handleSetupSubmit);
17+
18+
document.getElementById(
19+
'payment-method-form',
20+
).addEventListener('submit', handlePaymentMethodSubmit);
21+
22+
document.getElementById(
23+
'payment-form',
24+
).addEventListener('submit', handlePaymentSubmit);
25+
}
26+
27+
const handleSetupSubmit = async (event) => {
28+
event.preventDefault();
29+
30+
const response = await fetch('/setup_intent', {method: "POST"});
31+
const {
32+
id: id,
33+
client_secret: clientSecret,
34+
} = await response.json();
35+
36+
seti = id;
37+
setiClientSecret = clientSecret;
38+
39+
const elements = stripe.elements({
40+
clientSecret: clientSecret,
41+
});
42+
43+
if (paymentElement) {
44+
paymentElement.unmount();
45+
}
46+
47+
paymentElement = elements.create('card');
48+
49+
paymentElement.mount('#payment-method-element');
50+
51+
document.getElementById(
52+
'payment-method-form-fieldset',
53+
).removeAttribute('disabled');
54+
}
55+
56+
let handlePaymentMethodSubmit = async (event) => {
57+
event.preventDefault();
58+
59+
const {error} = await stripe.confirmCardSetup(setiClientSecret, {
60+
payment_method: {
61+
card: paymentElement,
62+
},
63+
});
64+
65+
const container = document.getElementById('payment-method-result-message');
66+
if (error) {
67+
container.textContent = error.message;
68+
} else {
69+
const response = await fetch('/payment_method', {
70+
method: "POST",
71+
body: JSON.stringify({ setup_intent: seti })
72+
});
73+
if (response.ok) {
74+
container.textContent = "Successfully confirmed payment method!";
75+
document.getElementById(
76+
'payment-form-fieldset',
77+
).removeAttribute('disabled');
78+
} else {
79+
container.textContent = "Error confirming payment method!";
80+
}
81+
}
82+
};
83+
84+
let handlePaymentSubmit = async (event) => {
85+
event.preventDefault();
86+
87+
const response = await fetch('/payment_intent', {
88+
method: "POST",
89+
body: JSON.stringify({
90+
amount: document.getElementById('payment-amount').value,
91+
})
92+
});
93+
const {client_secret: clientSecret} = await response.json();
94+
95+
const {error} = await stripe.confirmCardPayment(clientSecret, {});
96+
97+
const container = document.getElementById('payment-result-message');
98+
if (error) {
99+
container.textContent = error.message;
100+
} else {
101+
container.textContent = "Successfully confirmed payment!";
102+
}
103+
};
104+
105+
await init();

0 commit comments

Comments
 (0)