LemonSqueezy integration

This commit is contained in:
Ben Allfree 2024-01-07 18:56:29 +00:00
parent 2876c86c9f
commit a1973a0b81
6 changed files with 202 additions and 8 deletions

View File

@ -5,13 +5,16 @@
"dockerode",
"eventsource",
"frontends",
"getenv",
"maildev",
"memorystream",
"mothership",
"noaxis",
"nofile",
"PBOUNCE",
"pocketbase",
"pockethost",
"POCKETSTREAM",
"rizzdown",
"superadmin",
"unzipper",

View File

@ -5,7 +5,7 @@
import { client } from '$src/pocketbase-client'
import FAQSection from '$src/routes/account/FAQSection.svelte'
import PricingCard from '$src/routes/account/PricingCard.svelte'
import { isUserLegacy, userSubscriptionType } from '$util/stores'
import { isUserLegacy, userStore, userSubscriptionType } from '$util/stores'
import { onMount } from 'svelte'
import { writable } from 'svelte/store'
@ -71,8 +71,8 @@
description="Want all your PocketHost projects in one place? That's what the Pro tier is all about."
priceMonthly={[20, 'month']}
priceAnnually={[199, 'year (save 20%)']}
checkoutMonthURL="https://store.pockethost.io/checkout/buy/8e7cfb35-846a-4fd6-adcb-c2db5589275d"
checkoutYearURL="https://store.pockethost.io/checkout/buy/96e4ab4b-f646-4fb2-b830-5584db983e73"
checkoutMonthURL="https://store.pockethost.io/checkout/buy/8e7cfb35-846a-4fd6-adcb-c2db5589275d?checkout[custom][user_id]={$userStore?.id}"
checkoutYearURL="https://store.pockethost.io/checkout/buy/96e4ab4b-f646-4fb2-b830-5584db983e73?checkout[custom][user_id]={$userStore?.id}"
active={$userSubscriptionType === SubscriptionType.Premium}
/>
@ -82,8 +82,8 @@
description="Super elite! The Founder's Edition is our way of saying thanks for supporting PocketHost in these early days. Choose between lifetime and annual options."
priceMonthly={[299, 'once, use forever']}
priceAnnually={[99, 'year (save 55%)']}
checkoutMonthURL="https://store.pockethost.io/checkout/buy/e71cbfb5-cec3-4745-97a7-d877f6776503"
checkoutYearURL="https://store.pockethost.io/checkout/buy/e5660329-5b99-4ed6-8f36-0d387803e1d6"
checkoutMonthURL="https://store.pockethost.io/checkout/buy/e71cbfb5-cec3-4745-97a7-d877f6776503?checkout[custom][user_id]={$userStore?.id}"
checkoutYearURL="https://store.pockethost.io/checkout/buy/e5660329-5b99-4ed6-8f36-0d387803e1d6?checkout[custom][user_id]={$userStore?.id}"
active={$userSubscriptionType === SubscriptionType.Lifetime}
/>
</div>

View File

@ -2,6 +2,7 @@ import {
DATA_ROOT,
DEBUG,
DefaultSettingsService,
LS_WEBHOOK_SECRET,
mkContainerHomePath,
MOTHERSHIP_APP_DIR,
MOTHERSHIP_HOOKS_DIR,
@ -63,6 +64,7 @@ global.EventSource = EventSource
dev: DEBUG(),
env: {
DATA_ROOT: mkContainerHomePath(`data`),
LS_WEBHOOK_SECRET: LS_WEBHOOK_SECRET(),
},
extraBinds: [
`${DATA_ROOT()}:${mkContainerHomePath(`data`)}`,

View File

@ -96,6 +96,8 @@ export const SETTINGS = {
DISCORD_POCKETSTREAM_URL: mkString(''),
TEST_EMAIL: mkString(),
LS_WEBHOOK_SECRET: mkString(''),
}
;(() => {
let passed = true
@ -219,6 +221,8 @@ export const DISCORD_POCKETSTREAM_URL = () =>
export const TEST_EMAIL = () => settings().TEST_EMAIL
export const LS_WEBHOOK_SECRET = () => settings().LS_WEBHOOK_SECRET
/**
* Helpers
*/

View File

@ -0,0 +1,185 @@
/// <reference path="../types/types.d.ts" />
routerAdd('POST', '/api/ls', (c) => {
const log = (...s) =>
console.log(
`*** [ls]`,
...s.map((p) => {
if (typeof p === 'object') return JSON.stringify(p, null, 2)
return p
}),
)
const error = (...s) => console.error(`***`, ...s)
const audit = (key, note) => {
log(note)
const collection = $app.dao().findCollectionByNameOrId('audit')
const record = new Record(collection, {
...key,
note,
})
$app.dao().saveRecord(record)
}
const secret = $os.getenv('LS_WEBHOOK_SECRET')
log(`Secret`, secret)
const raw = readerToString(c.request().body)
const data = JSON.parse(raw)
log(`payload`, JSON.stringify(data, null, 2))
const {
meta: {
custom_data: { user_id },
},
data: {
type,
attributes: {
order_id,
product_name,
product_id,
status,
user_email: email,
},
},
} = data
log({ user_id, order_id, product_name, product_id, status, email })
if (status !== `active`) {
audit({ email, event: `LS_ERR` }, `Unsupported status ${status}: ${raw}`)
return c.json(500, { status: 'unsupported status' })
} else {
log(`status`, status)
}
if (!user_id) {
audit({ email, event: `LS_ERR` }, `No user ID: ${raw}`)
return c.json(500, { status: 'no user ID' })
} else {
log(`user ID ok`, user_id)
}
if (!order_id) {
audit({ email, event: `LS_ERR` }, `No order ID: ${raw}`)
return c.json(500, { status: 'no order ID' })
} else {
log(`order ID ok`, order_id)
}
const user = (() => {
try {
return $app.dao().findFirstRecordByData('users', 'id', user_id)
} catch (e) {}
})()
if (!user) {
audit({ email, event: `LS_ERR` }, `User ${user_id} not found: ${raw}`)
return c.json(500, { status: 'no user ID' })
} else {
log(`user record ok`, user)
}
const editions = {
// Founder's annual
159792: () => {
user.set(`subscription`, `premium`)
user.set(`isFounder`, true)
},
// Founder's lifetime
159794: () => {
user.set(`subscription`, `lifetime`)
user.set(`isFounder`, true)
},
// Pro annual
159791: () => {
user.set(`subscription`, `premium`)
},
// Pro monthly
159790: () => {
user.set(`subscription`, `premium`)
},
}
const applyEditionSpecifics = editions[product_id]
if (!applyEditionSpecifics) {
audit(
{ email, user: user_id, event: `LS_ERR` },
`Product edition ${product_id} not found: ${raw}`,
)
return c.json(500, { status: 'invalid product ID' })
} else {
log(`product id ok`, product_id)
}
applyEditionSpecifics()
const collection = $app.dao().findCollectionByNameOrId('payments')
const payment = new Record(collection, {
user: user_id,
payment_id: `ls_${order_id}`,
})
try {
$app.dao().runInTransaction((txDao) => {
txDao.saveRecord(user)
txDao.saveRecord(payment)
txDao
.db()
.newQuery(
`update settings set value=value-1 where name='founders-edition-count'`,
)
.execute()
})
log(`database updated`)
} catch (e) {
audit(
{ email, user: user_id, event: `LS_ERR` },
`Failed to update database: ${e}: ${raw}`,
)
return c.json(500, { status: 'failed to update database' })
}
try {
const res = $http.send({
url: `https://discord.com/api/webhooks/1193619183594901575/JVDfdUz2HPEUk-nG1RfI3BK2Czyx5vw1YmeH7cNfgvXbHNGPH0oJncOYqxMA_u5b2u57`,
method: 'POST',
body: JSON.stringify({
content: `someone just subscribed to ${product_name}`,
}),
headers: { 'content-type': 'application/json' },
timeout: 5, // in seconds
})
} catch (e) {
audit({ email, event: `LS_ERR` }, `Failed to notify discord: ${e}: ${raw}`)
return c.json(500, { status: 'failed to notify discord' })
}
log(`discord notified`)
try {
$app.newMailClient().send(
new MailerMessage({
from: {
address: $app.settings().meta.senderAddress,
name: $app.settings().meta.senderName,
},
to: [{ address: user.email() }],
subject: `Activated - ${product_name}`,
html: [
`<p>Hello, welcome to the PocketHost Pro family!`,
`<p>Please go find @noaxis on <a href="https://discord.com/channels/1128192380500193370/1128192380500193373">Discord</a> and say hi.`,
`<p>Thank you *so much!!!* for supporting PocketHost in these early days. If you have any questions or concerns, don't hesitate to reach out.`,
`<p>~noaxis`,
].join(`\n`),
}),
)
} catch (e) {
audit({ email, event: `LS_ERR` }, `Failed to send email: ${e}: ${raw}`)
return c.json(500, { status: 'failed to send email' })
}
log(`email sent`)
audit({ email, user: user_id, event: `LS_OK` }, `Payment successful`)
return c.json(200, { status: 'ok' })
})

View File

@ -88,7 +88,7 @@ routerAdd('POST', '/api/sns', (c) => {
default:
audit(
{ event: `SNS` },
`Unrecognized bounce type ${bounceType}: ${data}`,
`Unrecognized bounce type ${bounceType}: ${raw}`,
)
}
break
@ -124,12 +124,12 @@ routerAdd('POST', '/api/sns', (c) => {
default:
audit(
{ event: `SNS` },
`Unrecognized notification type ${notificationType}: ${data}`,
`Unrecognized notification type ${notificationType}: ${raw}`,
)
}
break
default:
audit({ event: `SNS` }, `Message ${Type} not handled: ${data}`)
audit({ event: `SNS` }, `Message ${Type} not handled: ${raw}`)
}
return c.json(200, { status: 'ok' })