Skip to content

Commit 75eb851

Browse files
committed
using tier with checkout
To run this demo, make sure that you have a version of tier installed that has support for checkout links. $ brew upgrade tierrun/tap/tier Note that the /payment page is now just a redirect, so there's no need to do a payment method collection template, or any of that.
1 parent 4f8a730 commit 75eb851

File tree

6 files changed

+74
-192
lines changed

6 files changed

+74
-192
lines changed

lib/routes.mjs

Lines changed: 28 additions & 84 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,8 @@ import {
1212
validateNewUser,
1313
} from './users.mjs'
1414

15+
import { actualRequestUrl } from 'actual-request-url'
16+
1517
import {
1618
defaultTemplateData,
1719
showPage,
@@ -52,9 +54,27 @@ routes.get('/pricing', log, async (req, res) => {
5254
routes.post('/plan', log, mustLogin, async (req, res) => {
5355
const { plan } = req.body
5456
const user = req.cookies.user
55-
await tier.subscribe(`org:${user}`, plan)
57+
const reqUrl = actualRequestUrl(req)
58+
const successUrl = String(new URL('/checkout_success?plan='+plan, reqUrl))
59+
const cancelUrl = String(new URL('/checkout_cancel', reqUrl))
60+
const { url } = await tier.checkout(`org:${user}`, successUrl, {
61+
cancelUrl,
62+
features: plan,
63+
})
64+
res.redirect(url)
65+
})
66+
67+
// just set the cookie and take them back to the pricing page
68+
routes.get('/checkout_success', log, mustLogin, async (req, res) => {
69+
const { plan } = req.query
5670
res.cookie('plan', plan, { httpOnly: true })
57-
res.send({ ok: 'updated plan' })
71+
res.redirect('/pricing')
72+
})
73+
74+
// just redirect back to the pricing page
75+
// In a real app, you'd probably want to do something here
76+
routes.get('/checkout_cancel', log, mustLogin, async (_, res) => {
77+
res.redirect('/pricing')
5878
})
5979

6080
// This is the application endpoint, where we convert temperatures
@@ -66,7 +86,7 @@ routes.post('/convert', log, mustLogin, async (req, res) => {
6686

6787
// Check whether the user has access to this feature. This call
6888
// will tell us the limit and usage of the feature we care about.
69-
const usage = await tier.limit(`org:${user}`, 'feature:convert')
89+
const usage = await tier.lookupLimit(`org:${user}`, 'feature:convert')
7090
if (usage.used >= usage.limit) {
7191
return res.status(402).send({ error: 'not allowed by plan' })
7292
}
@@ -121,7 +141,7 @@ const loginUser = async (res, user) => {
121141
// get the user's current plan
122142
// We don't need this for much, but it's handy when we show
123143
// the pricing page to be able to highlight their current plan.
124-
const phase = await tier.phase(`org:${user.id}`)
144+
const phase = await tier.lookupPhase(`org:${user.id}`)
125145
res.cookie('plan', phase.plans[0])
126146

127147
await setSecureLoginCookies(res, user)
@@ -196,84 +216,8 @@ routes.use('/ping', log, (req, res) => {
196216
// let them do a few things before actually requiring a credit card.
197217
routes.get('/payment', log, mustLogin, async (req, res) => {
198218
const user = req.cookies.user
199-
// This is one of the rare cases where we need to know the actual
200-
// Stripe identifier for this customer.
201-
const { stripe_id: customerID } = await tier.whois(`org:${user}`)
202-
203-
const options = {
204-
customer: await getStripeCustomer(customerID),
205-
setupIntent: await createStripeSetupIntent(customerID),
206-
stripePublishableKey: process.env.STRIPE_PUBLISHABLE_KEY,
207-
}
208-
showPage(req, res, 'payment.ejs', options)
209-
})
210-
211-
const getStripeCustomer = async (customerID) => {
212-
const url = new URL('https://api.stripe.com/v1/customers/' + customerID)
213-
url.searchParams.set('expand[]', 'invoice_settings')
214-
url.searchParams.append('expand[]', 'invoice_settings.default_payment_method')
215-
const stripeRes = await fetch(url.href, {
216-
method: 'GET',
217-
headers: {
218-
authorization: `Bearer ${process.env.STRIPE_KEY}`,
219-
},
220-
})
221-
return await stripeRes.json()
222-
}
223-
224-
const createStripeSetupIntent = async (customerID) => {
225-
const setupIntent = await fetch('https://api.stripe.com/v1/setup_intents', {
226-
method: 'POST',
227-
headers: {
228-
authorization: `Bearer ${process.env.STRIPE_KEY}`,
229-
'content-type': 'application/x-www-form-urlencoded',
230-
},
231-
body: new URLSearchParams({
232-
customer: customerID,
233-
'payment_method_types[]': 'card',
234-
}),
235-
})
236-
const seti = await setupIntent.json()
237-
if (seti.error) {
238-
throw seti.error
239-
}
240-
return seti
241-
}
242-
243-
// redirected here by Stripe
244-
// /attach-payment-method?setup_intent=seti_...&setup_intent_client_secret=seti_..._secret_...&redirect_status=succeeded
245-
routes.get('/attach-payment-method', log, mustLogin, async (req, res) => {
246-
const parsed = new URLSearchParams(req.url.substring(req.url.indexOf('?')))
247-
const setup_intent = parsed.get('setup_intent')
248-
const redirect_status = parsed.get('redirect_status')
249-
250-
const u = new URL(`https://api.stripe.com/v1/setup_intents/${setup_intent}`)
251-
const setupIntent = await fetch(u.href, {
252-
method: 'GET',
253-
headers: {
254-
authorization: `Bearer ${process.env.STRIPE_KEY}`,
255-
},
256-
})
257-
const seti = await setupIntent.json()
258-
if (redirect_status === 'succeeded' && seti.payment_method) {
259-
// attach to customer
260-
const { stripe_id: customerID } = await tier.whois(`org:${req.cookies.user}`)
261-
const url = 'https://api.stripe.com/v1/customers/' + customerID
262-
const stripeRes = await fetch(url, {
263-
method: 'POST',
264-
headers: {
265-
authorization: `Bearer ${process.env.STRIPE_KEY}`,
266-
'content-type': 'application/x-www-form-urlencoded',
267-
},
268-
body: new URLSearchParams({
269-
'invoice_settings[default_payment_method]': seti.payment_method,
270-
}).toString(),
271-
})
272-
await stripeRes.json()
273-
return res.redirect(303, '/payment')
274-
}
275-
276-
// some kind of error or thing requiring user action
277-
// TODO: pass this info to the payment page to resolve
278-
return res.json([ seti, redirect_status ])
219+
const reqUrl = actualRequestUrl(req)
220+
const success = String(new URL('/', reqUrl))
221+
const { url } = await tier.checkout(`org:${user}`, success)
222+
res.redirect(url)
279223
})

lib/templates/header.include.ejs

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,6 @@
22
<html>
33
<head>
44
<title><%= title %></title>
5-
<script src="https://js.stripe.com/v3/"></script>
65
<body>
76
<h1><%= typeof header === 'undefined' ? title : `${title} - ${header}` %></h1>
87
<%- include('./nav.include.ejs') %>

lib/templates/payment.ejs

Lines changed: 0 additions & 42 deletions
This file was deleted.

lib/templates/pricing.ejs

Lines changed: 0 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -85,20 +85,3 @@ for (const p of plans) {
8585
</tr>
8686

8787
</table>
88-
89-
<script>
90-
document.body.addEventListener('submit', async e => {
91-
e.preventDefault()
92-
const form = e.target
93-
const url = form.action
94-
const formData = new FormData(form)
95-
const res = await fetch(form.action, {
96-
method: 'POST',
97-
headers: {
98-
'content-type': 'application/x-www-form-urlencoded',
99-
},
100-
body: `plan=${formData.get('plan')}`,
101-
})
102-
location.reload()
103-
})
104-
</script>

0 commit comments

Comments
 (0)