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 +| {{condition}} | {{priceYearly}} |
{{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