forked from adrienverge/localstripe
-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
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. 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 9, 2024
1 parent
6b0ff20
commit 35b205d
Showing
9 changed files
with
341 additions
and
10 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 | ||
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 | ||
# 4242-4242-4242-4242. |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,26 @@ | ||
<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="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> | ||
<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> | ||
</body> |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,82 @@ | ||
let seti; | ||
let setiClientSecret; | ||
let stripe; | ||
let paymentElement; | ||
|
||
const init = async () => { | ||
const response = await fetch('/setup_intent', {method: "POST"}); | ||
const { | ||
id: id, | ||
client_secret: clientSecret, | ||
stripe_api_pk: publishableKey, | ||
} = await response.json(); | ||
|
||
seti = id; | ||
setiClientSecret = clientSecret; | ||
|
||
stripe = Stripe(publishableKey); | ||
|
||
const elements = stripe.elements({ | ||
clientSecret: clientSecret, | ||
}); | ||
|
||
paymentElement = elements.create('card'); | ||
|
||
paymentElement.mount('#payment-method-element'); | ||
|
||
document.getElementById( | ||
'payment-method-form', | ||
).addEventListener('submit', handlePaymentMethodSubmit); | ||
|
||
document.getElementById( | ||
'payment-form', | ||
).addEventListener('submit', handlePaymentSubmit); | ||
} | ||
|
||
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!"; | ||
} 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(); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,156 @@ | ||
from argparse import ArgumentParser | ||
from dataclasses import dataclass | ||
from importlib.resources import as_file, files | ||
import logging | ||
from os import environ | ||
|
||
from aiohttp import web | ||
import stripe | ||
|
||
use_real_stripe_api = False | ||
stripe_api_pk = 'pk_test_12345' | ||
stripe.api_key = 'sk_test_12345' | ||
|
||
|
||
@dataclass | ||
class CustomerState: | ||
cus: str | None = None | ||
pm: str | None = None | ||
|
||
|
||
# Normally these values would be securely stored in a database, indexed by some | ||
# authenticated customer identifier. For this sample, we have no authentication | ||
# system so just store one global "customer": | ||
customer_state = CustomerState() | ||
|
||
|
||
app = web.Application() | ||
routes = web.RouteTableDef() | ||
|
||
|
||
@routes.get('/stripe.js') | ||
async def stripe_js(request): | ||
del request | ||
|
||
global use_real_stripe_api | ||
|
||
if use_real_stripe_api: | ||
stripe_js_location = 'https://js.stripe.com/v3/' | ||
else: | ||
stripe_js_location = 'http://localhost:8420/js.stripe.com/v3/' | ||
|
||
return web.Response(content_type='application/javascript', text=f"""\ | ||
const script = document.createElement('script'); | ||
script.src = "{stripe_js_location}"; | ||
document.head.appendChild(script); | ||
""") | ||
|
||
|
||
@routes.get('/pm_setup.js') | ||
async def pm_setup_js(request): | ||
del request | ||
|
||
with as_file(files('samples.pm_setup').joinpath('pm_setup.js')) as f: | ||
return web.FileResponse(f) | ||
|
||
|
||
@routes.get('/') | ||
async def index(request): | ||
del request | ||
|
||
global use_real_stripe_api | ||
|
||
with files('samples.pm_setup').joinpath('index.html').open('r') as f: | ||
return web.Response( | ||
text=f.read(), | ||
content_type='text/html', | ||
) | ||
|
||
|
||
@routes.post('/setup_intent') | ||
async def setup_intent(request): | ||
del request | ||
|
||
global customer_state, stripe_api_pk | ||
|
||
cus = stripe.Customer.create() | ||
customer_state.cus = cus.id | ||
|
||
seti = stripe.SetupIntent.create( | ||
customer=cus.id, | ||
payment_method_types=["card"], | ||
) | ||
return web.json_response(dict( | ||
id=seti.id, | ||
client_secret=seti.client_secret, | ||
stripe_api_pk=stripe_api_pk, | ||
)) | ||
|
||
|
||
@routes.post('/payment_method') | ||
async def payment_method(request): | ||
body = await request.json() | ||
|
||
seti = stripe.SetupIntent.retrieve(body['setup_intent']) | ||
|
||
customer_state.pm = seti.payment_method | ||
|
||
return web.Response() | ||
|
||
|
||
@routes.post('/payment_intent') | ||
async def payment_intent(request): | ||
global customer_state | ||
|
||
body = await request.json() | ||
|
||
pi = stripe.PaymentIntent.create( | ||
customer=customer_state.cus, | ||
payment_method=customer_state.pm, | ||
amount=body['amount'], | ||
currency='usd', | ||
) | ||
|
||
return web.json_response(dict( | ||
client_secret=pi.client_secret, | ||
)) | ||
|
||
|
||
app.add_routes(routes) | ||
|
||
|
||
def main(): | ||
global stripe_api_pk, use_real_stripe_api | ||
|
||
parser = ArgumentParser() | ||
parser.add_argument( | ||
'--real-stripe', action='store_true', help="""\ | ||
Use the actual Stripe API. This is useful for verifying this sample and | ||
localstripe are providing an accurate simulation. | ||
To use, you must set the environment variable SK to your Stripe account's | ||
secret API key, and PK to your Stripe account's publishable API key. It is | ||
obviously recommended that you use the test mode variant of your Stripe account | ||
for this. | ||
""") | ||
args = parser.parse_args() | ||
|
||
if args.real_stripe: | ||
use_real_stripe_api = True | ||
stripe.api_key = environ.get('SK') | ||
stripe_api_pk = environ.get('PK') | ||
if not stripe.api_key or not stripe_api_pk: | ||
parser.print_help() | ||
parser.exit(1) | ||
else: | ||
stripe.api_base = 'http://localhost:8420' | ||
|
||
logger = logging.getLogger('aiohttp.access') | ||
logger.setLevel(logging.DEBUG) | ||
logger.addHandler(logging.StreamHandler()) | ||
|
||
web.run_app(app, access_log=logger) | ||
|
||
|
||
if __name__ == '__main__': | ||
main() |
Oops, something went wrong.