From 6701668e69c3ba4b3a57827c14059a8a524112b0 Mon Sep 17 00:00:00 2001 From: Henry Heino <46334387+personalizedrefrigerator@users.noreply.github.com> Date: Wed, 17 Jun 2026 14:22:01 -0700 Subject: [PATCH] Chore: Doc: Add (currently disabled) website/checkout logic for a self-hosted plan (#15675) --- .../templates/partials/plan.mustache | 49 ++++---- Assets/WebsiteAssets/templates/plans.mustache | 2 + packages/lib/utils/joplinCloud/index.ts | 106 ++++++++++++++++-- packages/server/stripeConfig.json | 42 +++++++ 4 files changed, 169 insertions(+), 30 deletions(-) diff --git a/Assets/WebsiteAssets/templates/partials/plan.mustache b/Assets/WebsiteAssets/templates/partials/plan.mustache index fc58e17de6..c291f0517b 100644 --- a/Assets/WebsiteAssets/templates/partials/plan.mustache +++ b/Assets/WebsiteAssets/templates/partials/plan.mustache @@ -36,7 +36,18 @@ {{#featureLabelsOff}}

{{label}}

{{/featureLabelsOff}} - + + {{#pricingTable}} + Pricing + + + {{#rows}} + + {{/rows}} + +
{{condition}}{{priceYearly}}
+ {{/pricingTable}} +

{{cfaLabel}} @@ -58,29 +69,27 @@ const buttonId = 'subscribeButton-' + planName; const buttonElement = document.getElementById(buttonId); - if (stripePricesIds.monthly) { - function handleResult() { - console.info('Redirected to checkout'); + function handleResult() { + console.info('Redirected to checkout'); + } + + buttonElement.addEventListener("click", function(evt) { + const priceId = stripePricesIds[subscriptionPeriod]; + + if (!priceId) { + console.error('Invalid period: ' + subscriptionPeriod); + return; } - buttonElement.addEventListener("click", function(evt) { - evt.preventDefault(); + evt.preventDefault(); - const priceId = stripePricesIds[subscriptionPeriod]; - - if (!priceId) { - console.error('Invalid period: ' + subscriptionPeriod); - return; - } - - createCheckoutSession(priceId).then(function(data) { - stripe.redirectToCheckout({ - sessionId: data.sessionId - }) - .then(handleResult); - }); + createCheckoutSession(priceId).then(function(data) { + stripe.redirectToCheckout({ + sessionId: data.sessionId + }) + .then(handleResult); }); - } + }); for (const button of document.querySelectorAll('button.action-toggleIncreaseStorage')) { button.onclick = () => { diff --git a/Assets/WebsiteAssets/templates/plans.mustache b/Assets/WebsiteAssets/templates/plans.mustache index 62a480b15e..471357e703 100644 --- a/Assets/WebsiteAssets/templates/plans.mustache +++ b/Assets/WebsiteAssets/templates/plans.mustache @@ -250,6 +250,8 @@ $('.toggle-button-self').click((event) => { event.preventDefault(); + // Self-hosting is currently yearly-only + applyPeriod('yearly'); setHostingType('self'); }); diff --git a/packages/lib/utils/joplinCloud/index.ts b/packages/lib/utils/joplinCloud/index.ts index 8ac777672a..dd2e9bb1ce 100644 --- a/packages/lib/utils/joplinCloud/index.ts +++ b/packages/lib/utils/joplinCloud/index.ts @@ -38,11 +38,17 @@ enum PlanHostingType { Self = 'self', } +export interface PlanTieredPricingTableRow { + condition: string; + priceYearly: string; +} + export interface Plan { name: string; title: string; priceMonthly?: StripePublicConfigPrice; priceYearly?: StripePublicConfigPrice; + pricingTable?: { rows: PlanTieredPricingTableRow[] }; featured: boolean; iconName: string; featuresOn: FeatureId[]; @@ -67,16 +73,39 @@ export enum PriceCurrency { USD = 'USD', } -export interface StripePublicConfigPrice { +export interface StripePublicConfigTieredAmount { + amount: string; + formattedAmount: string; + users: [number, number|'infinity']; + userRange: { min: number; max: number }; +} + +export interface StripePublicConfigBasePrice { accountType: number; // AccountType id: string; period: PricePeriod; + currency: PriceCurrency; +} + +export interface StripePublicConfigFixedPrice extends StripePublicConfigBasePrice { amount: string; formattedAmount: string; formattedMonthlyAmount: string; - currency: PriceCurrency; + + amounts: undefined; } +export interface StripePublicConfigTieredPrice extends StripePublicConfigBasePrice { + amounts: StripePublicConfigTieredAmount[]; + + quantityMinimum: number; + amount: undefined; + formattedAmount: undefined; + formattedMonthlyAmount: undefined; +} + +export type StripePublicConfigPrice = StripePublicConfigFixedPrice | StripePublicConfigTieredPrice; + export interface StripePublicConfig { publishableKey: string; prices: StripePublicConfigPrice[]; @@ -92,17 +121,28 @@ function formatPrice(amount: string | number, currency: PriceCurrency): string { throw new Error(`Unsupported currency: ${currency}`); } -interface FindPriceQuery { - accountType?: number; - period?: PricePeriod; - priceId?: string; -} +export const isTieredPrice = (p: StripePublicConfigPrice): p is StripePublicConfigTieredPrice => { + return 'amounts' in p; +}; export function loadStripeConfig(env: string, filePath: string): StripePublicConfig { const config: StripePublicConfig = JSON.parse(fs.readFileSync(filePath, 'utf8'))[env]; if (!config) throw new Error(`Invalid env: ${env}`); - const decoratePrices = (p: StripePublicConfigPrice) => { + const decoratePrices = (p: StripePublicConfigPrice): StripePublicConfigPrice => { + if (isTieredPrice(p)) { + return { + ...p, + amounts: p.amounts.map(amount => ({ + ...amount, + formattedAmount: formatPrice(amount.amount, p.currency), + userRange: { + min: amount.users[0], + max: amount.users[1] === 'infinity' ? Number.POSITIVE_INFINITY : amount.users[1], + }, + })), + }; + } return { ...p, formattedAmount: formatPrice(p.amount, p.currency), @@ -116,6 +156,12 @@ export function loadStripeConfig(env: string, filePath: string): StripePublicCon return config; } +interface FindPriceQuery { + accountType?: number; + period?: PricePeriod; + priceId?: string; +} + export function findPrice(config: StripePublicConfig, query: FindPriceQuery): StripePublicConfigPrice { let output: StripePublicConfigPrice = null; @@ -455,7 +501,32 @@ export const createFeatureTableMd = () => { return markdownUtils.createMarkdownTable(headers, rows); }; +const getTieredPricingTable = (price: StripePublicConfigPrice) => { + if (!isTieredPrice(price)) throw new Error(`Not a tiered price: ${price.id}`); + + const rows: PlanTieredPricingTableRow[] = []; + for (const amount of price.amounts) { + const formatUserCount = (count: number) => { + if (count === Number.POSITIVE_INFINITY) { + return '∞'; + } + return String(count); + }; + rows.push({ + condition: `${ + formatUserCount(amount.userRange.min) + }—${ + formatUserCount(amount.userRange.max) + } users`, + priceYearly: `${amount.formattedAmount} / user / year`, + }); + } + return rows; +}; + export function getPlans(stripeConfig: StripePublicConfig): Record { + // TODO: Set to true to enable self-hosting self-service. + const selfServiceSelfHostingEnabled = false; return { basic: { name: 'basic', @@ -560,8 +631,23 @@ export function getPlans(stripeConfig: StripePublicConfig): Record