Skip to content

Commit

Permalink
Feat/add checkout for lemonsqueezy (#4)
Browse files Browse the repository at this point in the history
* feat(sails-lemonsqueezy): add checkout method

* feat(sails-pay): add basic setup for hook

* chore(playground): change node engine

* chore(sails-pay): add lemonsqueezy
  • Loading branch information
DominusKelvin authored Feb 18, 2024
1 parent fb0e576 commit ae1e14c
Show file tree
Hide file tree
Showing 17 changed files with 377 additions and 946 deletions.
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1 +1,2 @@
node_modules
package
955 changes: 42 additions & 913 deletions package-lock.json

Large diffs are not rendered by default.

2 changes: 0 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,4 @@
{
"name": "sails-pay",
"version": "0.0.1",
"private": true,
"keywords": [
"Lemon Squeezy",
Expand Down
23 changes: 0 additions & 23 deletions packages/sails-hook-pay/index.js

This file was deleted.

6 changes: 6 additions & 0 deletions packages/sails-lemonsqueezy/adapter.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
const methods = require('./machines')
module.exports = {
identity: 'sails-lemonsqueezy',
config: {},
checkout: methods.checkout
}
34 changes: 34 additions & 0 deletions packages/sails-lemonsqueezy/helpers/fetch.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
const { fetch: undiciFetch } = require('undici')

const baseUrl = 'https://api.lemonsqueezy.com'
const defaultHeaders = {
Accept: 'application/vnd.api+json',
'Content-Type': 'application/vnd.api+json'
}

const fetchImpl =
typeof global.fetch !== 'undefined' ? global.fetch : undiciFetch

const fetch = async (path, options = {}) => {
const url = new URL(`/v1${path}`, baseUrl).toString()
const mergedOptions = {
...options,
headers: {
...defaultHeaders,
...(options.headers || {})
}
}

try {
const response = await fetchImpl(url, mergedOptions)

const jsonResponse = await response.json()

return jsonResponse
} catch (error) {
console.error('Error occurred during fetch:', error)
throw error
}
}

module.exports = fetch
21 changes: 21 additions & 0 deletions packages/sails-lemonsqueezy/helpers/generate-json-api-payload.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
/**
* Generates a JSON:API payload for making requests.
* @param {string} type - The type of the resource.
* @param {Object} data - The attributes of the resource.
* @param {Object} [relationships={}] - The relationships of the resource.
* @returns {string} - A JSON string representing the JSON:API request body.
*/
module.exports = function generateJsonApiPayload(
type,
data,
relationships = {}
) {
const payload = {
data: {
type: type,
attributes: data,
relationships: relationships
}
}
return JSON.stringify(payload)
}
32 changes: 32 additions & 0 deletions packages/sails-lemonsqueezy/helpers/parameters.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
/**
* Common input definitions (i.e. parameter definitions) that are shared by multiple files.
*
* @type {Dictionary}
* @constant
*/

module.exports = {
LEMON_SQUEEZY_API_KEY: {
type: 'string',
friendlyName: 'API Key',
description: 'A valid Lemon Squeezy API Key',
protect: true,
whereToGet: {
url: 'https://app.lemonsqueezy.com/settings/api',
description: 'Generate an API key in your Lemon Squeezy dashboard.',
extendedDescription:
'To generate an API key, you will first need to log in to your Lemon Squeezy account, or sign up for one if you have not already done so.'
}
},
LEMON_SQUEEZY_STORE_ID: {
type: 'string',
friendlyName: 'Store ID',
description: 'A valid Lemon Squeezy store ID',
whereToGet: {
url: 'https://app.lemonsqueezy.com/settings/stores',
description: 'The ID is the number next to the store name.',
extendedDescription:
'To find your Lemon Squeezy Store ID, visit your Stores page in the Lemon Squeezy dashboard.'
}
}
}
166 changes: 166 additions & 0 deletions packages/sails-lemonsqueezy/machines/checkout.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,166 @@
const fetch = require('../helpers/fetch')
const generateJsonApiPayload = require('../helpers/generate-json-api-payload')
module.exports = require('machine').build({
friendlyName: 'Checkout',
description:
'Creates and return a unique checkout URL for a specific variant.',
moreInfoUrl: 'https://docs.lemonsqueezy.com/api/checkouts',
inputs: {
apiKey: require('../helpers/parameters').LEMON_SQUEEZY_API_KEY,
store: require('../helpers/parameters').LEMON_SQUEEZY_STORE_ID,
variant: {
type: 'string',
description: 'The ID of the variant associated with this checkout.'
},
customPrice: {
type: 'number',
description:
'Represents a positive integer in cents representing the custom price of the variant.'
},
productOptions: {
type: 'ref',
description:
'An object containing any overridden product options for this checkout. ',
example: {
name: '',
description: '',
media: [],
redirect_url: '',
receipt_button_text: '',
receipt_link_url: '',
receipt_thank_you_note: '',
enabled_variants: [1]
}
},
checkoutOptions: {
type: 'ref',
description: 'An object containing checkout options for this checkout.',
example: {
embed: false,
media: true,
logo: true,
desc: true,
discount: true,
dark: false,
subscription_preview: true,
button_color: '#2DD272'
}
},
checkoutData: {
type: 'ref',
description:
'An object containing any prefill or custom data to be used in the checkout.',
example: {
email: '',
name: '',
billing_address: [],
tax_number: '',
discount_code: '',
custom: [],
variant_quantities: []
}
},
preview: {
type: 'boolean',
description:
'A boolean indicating whether to return a preview of the checkout. If true, the checkout will include a preview object with the checkout preview data.',
example: true
},
testMode: {
type: 'boolean',
description:
'A boolean indicating whether the checkout should be created in test mode.',
example: false
},
expiresAt: {
type: 'string',
description:
'An ISO 8601 formatted date-time string indicating when the checkout expires. Can be null if the checkout is perpetual.',
example: '2022-10-30T15:20:06.000000Z'
}
},
exits: {
success: {
description: 'The checkout url.',
outputVariableName: 'checkoutUrl',
outputType: 'string'
},
couldNotCreateCheckoutUrl: {
description: 'Checkout URL could not be created.',
extendedDescription:
'This indicates that an error was encountered during checkout url creation.',
outputFriendlyName: 'Create checkout URL error report.',
outputVariableName: 'errors',
outputType: [
{
detail:
'The POST method is not supported for route checkouts. Supported methods: GET, HEAD.',
status: '405',
title: 'Method Not Allowed'
}
]
}
},

fn: async function (
{
apiKey,
store,
variant,
customPrice,
productOptions,
checkoutOptions,
checkoutData,
preview,
testMode,
expiresAt
},
exits
) {
const adapterConfig = require('../adapter').config
const payload = generateJsonApiPayload(
'checkouts',
{
custom_price: customPrice || null,
product_options: {
redirect_url: adapterConfig.redirectUrl || null,
...productOptions
},
checkout_options: checkoutOptions,
checkout_data: checkoutData,
preview,
test_mode: testMode,
expires_at: expiresAt || null
},
{
store: {
data: {
type: 'stores',
id: store || adapterConfig.store
}
},
variant: {
data: {
type: 'variants',
id: variant
}
}
}
)

const checkout = await fetch('/checkouts', {
method: 'POST',
headers: {
authorization: `Bearer ${apiKey || adapterConfig.apiKey}`
},
body: payload
})
if (checkout.errors) {
const errors = checkout.errors
return exits.couldNotCreateCheckoutUrl(errors)
}

const checkoutUrl = checkout.data.attributes.url
return exits.success(checkoutUrl)
}
})
3 changes: 3 additions & 0 deletions packages/sails-lemonsqueezy/machines/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
module.exports = {
checkout: require('./checkout')
}
10 changes: 7 additions & 3 deletions packages/sails-lemonsqueezy/package.json
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
{
"name": "sails-lemonsqueezy",
"name": "@sails-pay/lemonsqueezy",
"version": "0.0.1",
"description": "Lemon Squeezy adapter for Sails Pay",
"main": "index.js",
"main": "adapter.js",
"scripts": {
"test": "npm run test"
},
Expand Down Expand Up @@ -32,5 +32,9 @@
"ecommerce"
],
"author": "Kelvin Omereshone <[email protected]>",
"license": "MIT"
"license": "MIT",
"dependencies": {
"machine": "^15.2.3",
"undici": "^6.6.2"
}
}
File renamed without changes.
File renamed without changes.
58 changes: 58 additions & 0 deletions packages/sails-pay/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
/**
* pay hook
*
* @description :: A hook definition. Extends Sails by adding shadow routes, implicit actions, and/or initialization logic.
* @docs :: https://sailsjs.com/docs/concepts/extending-sails/hooks
*/

module.exports = function (sails) {
function extractProviderName(fullName) {
const parts = fullName.split('/')
return parts[parts.length - 1]
}
return {
defaults: {
pay: {
provider: 'default',
providers: {}
}
},
/**
* Runs when this Sails app loads/lifts.
*/
initialize: async function () {
function getPaymentProvider(provider) {
if (!sails.config.pay.providers[provider]) {
throw new Error('The provided payment provider coult not be found.')
}
const providerName = extractProviderName(
sails.config.pay.providers[provider].adapter
)
switch (providerName) {
case 'lemonsqueezy':
console.log()
const paymentProvider = require(
sails.config.pay.providers[provider].adapter
)
paymentProvider.config = sails.config.pay.providers[provider]
return paymentProvider
default:
throw new Error(
'Invalid payment provider provided, supported stores are redis or memcached.'
)
}
}

sails.hooks.pay.paymentProvider = getPaymentProvider(
sails.config.pay.provider
)
sails.hooks.pay.paymentProvider.provider = function (provider) {
return getPaymentProvider(provider)
}

sails.pay = sails.hooks.pay.paymentProvider

sails.log.info('Initializing custom hook (`pay`)')
}
}
}
Loading

0 comments on commit ae1e14c

Please sign in to comment.