wip(backend): Work on payment processing
This commit is contained in:
parent
6cccdaf087
commit
cc538e5df2
8 changed files with 117 additions and 52 deletions
|
@ -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 #
|
||||
# #
|
||||
|
|
|
@ -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,
|
||||
|
|
34
sites/backend/src/controllers/payments.mjs
Normal file
34
sites/backend/src/controllers/payments.mjs
Normal 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,
|
||||
})
|
||||
}
|
|
@ -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,
|
||||
}
|
||||
|
|
15
sites/backend/src/routes/payments.mjs
Normal file
15
sites/backend/src/routes/payments.mjs
Normal 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))
|
||||
}
|
||||
}
|
|
@ -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>
|
||||
)
|
||||
}
|
||||
|
||||
|
|
|
@ -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: {
|
||||
|
|
|
@ -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
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue