1
0
Fork 0

wip(backend): Work on payment processing

This commit is contained in:
joostdecock 2023-07-31 09:03:55 +02:00
parent 6cccdaf087
commit cc538e5df2
8 changed files with 117 additions and 52 deletions

View file

@ -131,6 +131,18 @@ BACKEND_ENABLE_SANITY=no
# Sanity API version
#SANITY_VERSION=v2022-10-31
#####################################################################
# Payments via Stripe #
# #
# We use Stripe as payments processor for patron contributions #
#####################################################################
# Set this to no to disable Stripe payments altogther
BACKEND_ENABLE_PAYMENTS=no
# Stripe API key with permissions to create a payment intent
#BACKEND_STRIPE_CREATE_INTENT_KEY=yourKeyHere
#####################################################################
# Github integration #
# #

View file

@ -44,6 +44,7 @@ const baseConfig = {
},
sanity: envToBool(process.env.BACKEND_ENABLE_SANITY),
ses: envToBool(process.env.BACKEND_ENABLE_AWS_SES),
stripe: envToBool(process.env.BACKEND_ENABLE_PAYMENTS),
tests: {
base: envToBool(process.env.BACKEND_ENABLE_TESTS),
email: envToBool(process.env.BACKEND_ENABLE_TESTS_EMAIL),
@ -152,6 +153,14 @@ if (baseConfig.use.sanity)
}`,
}
// Stripe config
if (baseConfig.use.stripe)
baseConfig.stripe = {
keys: {
createIntent: process.env.BACKEND_STRIPE_CREATE_INTENT_KEY || false,
},
}
// AWS SES config (for sending out emails)
if (baseConfig.use.ses)
baseConfig.aws = {
@ -209,6 +218,7 @@ const vars = {
BACKEND_ENABLE_GITHUB: 'optional',
BACKEND_ENABLE_OAUTH_GITHUB: 'optional',
BACKEND_ENABLE_OAUTH_GOOGLE: 'optional',
BACKEND_ENABLE_PAYMENTS: 'optional',
BACKEND_ENABLE_TESTS: 'optional',
BACKEND_ALLOW_TESTS_IN_PRODUCTION: 'optional',
BACKEND_ENABLE_DUMP_CONFIG_AT_STARTUP: 'optional',
@ -250,6 +260,11 @@ if (envToBool(process.env.BACKEND_ENABLE_OAUTH_GOOGLE)) {
vars.BACKEND_OAUTH_GOOGLE_CLIENT_ID = 'required'
vars.BACKEND_OAUTH_GOOGLE_CLIENT_SECRET = 'requiredSecret'
}
// Vars for Stripe integration
if (envToBool(process.env.BACKEND_ENABLE_PAYMENTS)) {
vars.BACKEND_STRIPE_CREATE_INTENT_KEY = 'requiredSecret'
}
// Vars for (unit) tests
if (envToBool(process.env.BACKEND_ENABLE_TESTS)) {
vars.BACKEND_TEST_DOMAIN = 'optional'
@ -324,6 +339,11 @@ export function verifyConfig(silent = false) {
config.jwt.secretOrKey.slice(0, 4) + '**redacted**' + config.jwt.secretOrKey.slice(-4),
},
}
if (config.stripe)
dump.stripe = {
...config.stripe.keys,
//token: config.sanity.token.slice(0, 4) + '**redacted**' + config.sanity.token.slice(-4),
}
if (config.sanity)
dump.sanity = {
...config.sanity,

View file

@ -0,0 +1,34 @@
export function PaymentsController() {}
/*
* Create payment intent
*
* This is the endpoint that handles creation of a Stripe payment intent
* See: https://freesewing.dev/reference/backend/api/apikey
*/
PaymentsController.prototype.createIntent = async (req, res, tools) => {
if (!req.body.amount) return this.setResponse(400, 'amountMissing')
if (!req.body.currency) return this.setResponse(400, 'currencyMissing')
const body = new URLSearchParams()
body.append('amount', req.body.amount)
body.append('currency', req.body.currency)
// Call Stripe API with fetch to create the payment inent
const result = await fetch('https://api.stripe.com/v1/payment_intents', {
method: 'POST',
headers: {
'Content-Type': 'application/x-www-form-urlencoded;charset=UTF-8',
authorization: `Bearer ${tools.config.stripe.keys.createIntent}`,
},
body,
})
// Convert response to JSON
const json = await result.json()
// Return status code 201 (created) and clientSecret
return res.status(201).send({
result: 'success',
clientSecret: json.client_secret,
})
}

View file

@ -6,6 +6,7 @@ import { confirmationsRoutes } from './confirmations.mjs'
import { curatedSetsRoutes } from './curated-sets.mjs'
import { issuesRoutes } from './issues.mjs'
import { flowsRoutes } from './flows.mjs'
import { paymentsRoutes } from './payments.mjs'
export const routes = {
apikeysRoutes,
@ -16,4 +17,5 @@ export const routes = {
curatedSetsRoutes,
issuesRoutes,
flowsRoutes,
paymentsRoutes,
}

View file

@ -0,0 +1,15 @@
import { PaymentsController } from '../controllers/payments.mjs'
const Payments = new PaymentsController()
export function paymentsRoutes(tools) {
const { app } = tools
/*
* Only add these routes if payments are enabled in the config
*/
if (tools.config.use.stripe) {
// Create payment intent
app.post('/payments/intent', (req, res) => Payments.createIntent(req, res, tools))
}
}

View file

@ -1,40 +1,18 @@
import { stripeConfig } from 'shared/config/stripe.mjs'
import { useEffect, useState } from 'react'
import { useBackend } from 'shared/hooks/use-backend.mjs'
import { useTheme } from 'shared/hooks/use-theme.mjs'
import { Elements, PaymentElement } from '@stripe/react-stripe-js'
import { loadStripe } from '@stripe/stripe-js'
/*
* The stripe API docs will emphasize to always handle this server-side because
* doing this client-side allows nefarious users to modify the payment amount.
* Which is great advice, but FreeSewing uses a pay-whatever-you-want model so
* if people want to muck about with JS to change the amount rather than change
* it in the input field for the amount, let them.
*
* This is also why we need to use fetch and talk to the API directly, because
* the stripe client-side SDK does not include this functionality.
*/
const createPaymentIntent = async ({ amount, currency }) => {
const body = new URLSearchParams()
body.append('amount', amount)
body.append('currency', currency)
return await fetch('https://api.stripe.com/v1/payment_intents', {
method: 'POST',
headers: {
'Content-Type': 'application/x-www-form-urlencoded;charset=UTF-8',
authorization: `Bearer ${stripeConfig.apiKey}`,
},
body,
})
}
import { Loading } from 'shared/components/spinner.mjs'
export const Payment = ({ amount = 2500, currency = 'eur' }) => {
const appearance = useTheme().stripe
const backend = useBackend()
const [stripe, setStripe] = useState(false)
const [intent, setIntent] = useState(false)
const options = intent
? {
const [clientSecret, setClientSecret] = useState(false)
const options = {
mode: 'payment',
layout: {
type: 'accordion',
@ -48,31 +26,28 @@ export const Payment = ({ amount = 2500, currency = 'eur' }) => {
currency,
appearance,
}
: null
useEffect(() => {
const getPaymentIntent = async () => {
const stripeClient = await loadStripe(stripeConfig.apiKey)
const result = await createPaymentIntent({ amount, currency })
const json = await result.json()
const result = await backend.createPaymentIntent({ amount, currency })
if (result.success && result.data.clientSecret) setClientSecret(result.data.clientSecret)
setStripe(stripeClient)
setIntent(json)
}
getPaymentIntent()
}, [amount, currency])
return intent ? (
return clientSecret ? (
<Elements stripe={stripe} options={options} layout="accordion">
<form>
<PaymentElement options={options} />
<pre>{JSON.stringify(intent, null, 2)}</pre>
<pre>{JSON.stringify(clientSecret, null, 2)}</pre>
</form>
</Elements>
) : (
<>
<p>One moment please...</p>
<pre>{JSON.stringify(options, null, 2)}</pre>
</>
<div className="my-12">
<Loading />
</div>
)
}

View file

@ -1,5 +1,5 @@
export const stripeConfig = {
apiKey: process.env.FS_STRIPE_API_KEY,
apiKey: process.env.NEXT_PUBLIC_STRIPE_API_KEY,
amounts: [5, 10, 25, 50, 100, 250, 500],
periods: ['w', 'm', '3m', '6m', 'y', 'x'],
currencies: {

View file

@ -301,6 +301,13 @@ Backend.prototype.sendLanguageSuggestion = async function (data) {
return responseHandler(await api.post(`/flows/language-suggestion/jwt`, data, this.auth))
}
/*
* Create payment intent
*/
Backend.prototype.createPaymentIntent = async function (data) {
return responseHandler(await api.post(`/payments/intent`, data), 201)
}
export function useBackend(token = false) {
/*
* This backend object is what we'll end up returning