Chore: Doc: Add (currently disabled) website/checkout logic for a self-hosted plan (#15675)

This commit is contained in:
Henry Heino
2026-06-17 22:22:01 +01:00
committed by GitHub
parent dafb6b05f1
commit 6701668e69
4 changed files with 169 additions and 30 deletions
@@ -36,7 +36,18 @@
{{#featureLabelsOff}}
<p class="unchecked-text"><i class="fas fa-times feature feature-off"></i>{{label}}</p>
{{/featureLabelsOff}}
{{#pricingTable}}
<strong translate>Pricing</strong>
<table class="table">
<tbody>
{{#rows}}
<tr><td>{{condition}}</td><td>{{priceYearly}}</td></tr>
{{/rows}}
</tbody>
</table>
{{/pricingTable}}
<p class="text-center subscribe-wrapper">
<a id="subscribeButton-{{name}}" href="{{cfaUrl}}" class="button-link btn-white subscribeButton cfa-button">{{cfaLabel}}</a>
@@ -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 = () => {
@@ -250,6 +250,8 @@
$('.toggle-button-self').click((event) => {
event.preventDefault();
// Self-hosting is currently yearly-only
applyPeriod('yearly');
setHostingType('self');
});
+96 -10
View File
@@ -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<PlanName, Plan> {
// 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<PlanName, Pla
featuresOff: [],
featureLabelsOn: getFeatureLabelsByPlan(PlanName.JoplinServerBusiness, true),
featureLabelsOff: [],
cfaLabel: _('Get a quote'),
cfaUrl: 'https://tally.so/r/D4BlOE',
...(selfServiceSelfHostingEnabled ? {
pricingTable: {
rows: getTieredPricingTable(findPrice(stripeConfig, {
accountType: 5,
period: PricePeriod.Yearly,
})),
},
cfaLabel: _('Try it now'),
cfaUrl: '',
priceYearly: findPrice(stripeConfig, {
accountType: 5,
period: PricePeriod.Yearly,
}),
} : {
cfaLabel: _('Get a quote'),
cfaUrl: 'https://tally.so/r/D4BlOE',
}),
footnote: '',
learnMoreUrl: 'https://joplinapp.org/help/apps/joplin_server_business',
hostingType: PlanHostingType.Self,
+42
View File
@@ -58,6 +58,27 @@
"period": "yearly",
"amount": "95.88",
"currency": "EUR"
},
{
"accountType": 5,
"id": "price_1ThCwVL9ZkKzC9sXU9AVyDB1",
"period": "yearly",
"quantityMinimum": 2,
"amounts": [
{
"amount": "40.00",
"users": [2, 10]
},
{
"amount": "30.00",
"users": [11, 50]
},
{
"amount": "20.00",
"users": [51, "infinity"]
}
],
"currency": "EUR"
}
],
"archivedPrices": []
@@ -121,6 +142,27 @@
"period": "yearly",
"amount": "95.88",
"currency": "EUR"
},
{
"accountType": 5,
"id": "price_1TgZ0zLx4fybOTqJl0xF4kHr",
"period": "yearly",
"quantityMinimum": 2,
"amounts": [
{
"amount": "40.00",
"users": [2, 10]
},
{
"amount": "30.00",
"users": [11, 50]
},
{
"amount": "20.00",
"users": [51, "infinity"]
}
],
"currency": "EUR"
}
],
"archivedPrices": [