Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(unlock-app): Checkout hooks #15234

Open
wants to merge 29 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
159 changes: 159 additions & 0 deletions locksmith/src/controllers/v2/checkoutController.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@ import {
deleteCheckoutConfigById,
} from '../../operations/checkoutConfigOperations'
import { PaywallConfig } from '@unlock-protocol/core'
import { Payload } from '../../models/payload'
import { addJob } from '../../worker/worker'

/**
* Create or update a checkout configuration.
Expand Down Expand Up @@ -141,3 +143,160 @@ export const deleteCheckoutConfig: RequestHandler = async (
})
return
}

export const updateCheckoutHooks: RequestHandler = async (
request,
response
) => {
const { id } = request.params
const userAddress = request.user!.walletAddress
const payload = request.body

try {
const existingConfig = await getCheckoutConfigById(id)

if (!existingConfig) {
response.status(404).send({
message: 'No config found',
})
return
}

const updatedConfig = {
...existingConfig.config,
hooks: {
...existingConfig.config.hooks,
...payload,
},
}

const checkoutConfig = await PaywallConfig.strip().parseAsync(updatedConfig)

const storedConfig = await saveCheckoutConfig({
id,
name: existingConfig.name,
config: checkoutConfig,
user: userAddress,
})

response.status(200).send({
id: storedConfig.id,
by: storedConfig.createdBy,
name: storedConfig.name,
config: storedConfig.config,
updatedAt: storedConfig.updatedAt.toISOString(),
createdAt: storedConfig.createdAt.toISOString(),
})
} catch (error) {
if (
error instanceof Error &&
error.message === 'User not authorized to update this configuration'
) {
response.status(403).send({
message: error.message,
})
return
}

if (error instanceof Error) {
response.status(400).send({
message: 'Invalid hooks payload',
error: error.message,
})
return
}

throw error
}
}

export const getCheckoutHookJobs: RequestHandler = async (
request,
response
) => {
const userAddress = request.user!.walletAddress

try {
const jobs = await Payload.findAll({
where: {
payload: {
by: userAddress,
read: false,
},
},
order: [['createdAt', 'DESC']],
})

if (!jobs) {
response
.status(404)
.send({ message: 'No unread checkout hook jobs found for this user.' })
return
}

response.status(200).send(jobs)
} catch (error: any) {
response.status(400).send({ message: 'Could not retrieve jobs.' })
}
}

export const addCheckoutHookJob: RequestHandler = async (request, response) => {
const { id } = request.params

try {
const checkout = await getCheckoutConfigById(id)
const payloadData = request.body

const payload = new Payload()
payload.payload = {
checkoutId: id,
by: checkout?.by,
status: 'pending',
read: false,
...payloadData,
}
await payload.save()

const job = await addJob('checkoutHookJob', payload)

response.status(200).send({
message: 'Job added successfully',
job,
})
} catch (error) {
response.status(400).send({ message: 'Could not add job.' })
}
}

export const updateCheckoutHookJob: RequestHandler = async (
request,
response
) => {
const payloadId = request.params.id
const userAddress = request.user!.walletAddress

try {
const job = await Payload.findByPk(payloadId)
if (!job) {
response.status(404).send({ message: 'No existing job found to update.' })
return
}

if (job.payload.by !== userAddress) {
response.status(403).send({ message: 'Not authorized to update job.' })
}

job.payload = {
...job.payload,
read: true,
}
await job.save()

response.status(200).send({
message: 'Job marked as read successfully',
job,
})
} catch (error) {
response.status(400).send({ message: 'Could not update job.' })
}
}
8 changes: 8 additions & 0 deletions locksmith/src/routes/v2/checkoutConfigs.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,18 @@ import {
createOrUpdateCheckoutConfig,
deleteCheckoutConfig,
getCheckoutConfigs,
updateCheckoutHooks,
getCheckoutHookJobs,
addCheckoutHookJob,
updateCheckoutHookJob,
} from '../../controllers/v2/checkoutController'
const router: express.Router = express.Router({ mergeParams: true })

router.get('/list', authenticatedMiddleware, getCheckoutConfigs)
router.get('/hooks/all', authenticatedMiddleware, getCheckoutHookJobs)
router.post('/hooks/:id', authenticatedMiddleware, addCheckoutHookJob)
router.put('/hooks/:id', authenticatedMiddleware, updateCheckoutHooks)
router.patch('/hooks/:id', authenticatedMiddleware, updateCheckoutHookJob)
router.put('/:id?', authenticatedMiddleware, createOrUpdateCheckoutConfig)
router.get('/:id', getCheckoutConfig)
router.delete('/:id', authenticatedMiddleware, deleteCheckoutConfig)
Expand Down
38 changes: 38 additions & 0 deletions locksmith/src/worker/tasks/checkoutHooks.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
import { Task } from 'graphile-worker'
import { Payload } from '../../models/payload'
import logger from '../../logger'
import axios from 'axios'
import { getCheckoutConfigById } from '../../operations/checkoutConfigOperations'

export const checkoutHookJob: Task = async (payload: any) => {
const { id } = payload
const { checkoutId, event, data } = payload.payload

const job = await Payload.findByPk(id)

if (!job) {
logger.warn(`No job found with id ${id}`)
return
}

const checkout: any = await getCheckoutConfigById(checkoutId)
const url = checkout?.config?.hooks && checkout.config.hooks[event]

if (url) {
try {
await axios.post(url, data)

job.payload = {
...job.payload,
status: 'processed',
}
await job.save()
} catch (error) {
throw new Error('\nCould not send data to webhook')
}
} else {
throw new Error('\nurl not found')
}

logger.info(`checkoutHookJob processed job with id ${id}`)
}
2 changes: 2 additions & 0 deletions locksmith/src/worker/worker.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ import { downloadReceipts } from './tasks/receipts'
import { createEventCasterEvent } from './tasks/eventCaster/createEventCasterEvent'
import { rsvpForEventCasterEvent } from './tasks/eventCaster/rsvpForEventCasterEvent'
import exportKeysJob from './tasks/exportKeysJob'
import { checkoutHookJob } from './tasks/checkoutHooks'

const crontabProduction = `
*/5 * * * * monitor
Expand Down Expand Up @@ -109,6 +110,7 @@ export async function startWorker() {
downloadReceipts,
createEventCasterEvent,
rsvpForEventCasterEvent,
checkoutHookJob,
},
})

Expand Down
24 changes: 24 additions & 0 deletions packages/core/src/schema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -313,6 +313,30 @@ export const PaywallConfig = z
claim: z.boolean().optional(),
})
.optional(),
hooks: z
.object({
status: z
.string({
description: 'URL to be called on status change.',
})
.optional(),
authenticated: z
.string({
description: 'URL to be called when the user is authenticated.',
})
.optional(),
transactionSent: z
.string({
description: 'URL to be called when a transaction is sent.',
})
.optional(),
metadata: z
.string({
description: 'URL to be called for metadata updates.',
})
.optional(),
})
.optional(),
})
.passthrough()

Expand Down
Loading
Loading