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/add checkout for lemonsqueezy #4

Merged
merged 4 commits into from
Feb 18, 2024
Merged
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
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 <kelvin@sailscasts.com>",
"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
Loading