2021-07-31 15:42:56 +02:00
|
|
|
import * as fs from 'fs-extra';
|
2022-04-07 16:35:15 +02:00
|
|
|
import markdownUtils, { MarkdownTableHeader, MarkdownTableRow } from '../markdownUtils';
|
2022-11-28 18:16:32 +02:00
|
|
|
import { _ } from '../locale';
|
2022-04-07 16:35:15 +02:00
|
|
|
|
|
|
|
type FeatureId = string;
|
|
|
|
|
|
|
|
export enum PlanName {
|
|
|
|
Basic = 'basic',
|
|
|
|
Pro = 'pro',
|
|
|
|
Teams = 'teams',
|
|
|
|
}
|
|
|
|
|
|
|
|
interface PlanFeature {
|
|
|
|
title: string;
|
|
|
|
basic: boolean;
|
|
|
|
pro: boolean;
|
|
|
|
teams: boolean;
|
|
|
|
basicInfo?: string;
|
|
|
|
proInfo?: string;
|
|
|
|
teamsInfo?: string;
|
|
|
|
basicInfoShort?: string;
|
|
|
|
proInfoShort?: string;
|
|
|
|
teamsInfoShort?: string;
|
|
|
|
}
|
2021-07-31 15:42:56 +02:00
|
|
|
|
2021-07-23 21:34:30 +02:00
|
|
|
export interface Plan {
|
|
|
|
name: string;
|
|
|
|
title: string;
|
2021-07-31 15:42:56 +02:00
|
|
|
priceMonthly: StripePublicConfigPrice;
|
|
|
|
priceYearly: StripePublicConfigPrice;
|
2021-07-23 21:34:30 +02:00
|
|
|
featured: boolean;
|
|
|
|
iconName: string;
|
2022-04-07 16:35:15 +02:00
|
|
|
featuresOn: FeatureId[];
|
|
|
|
featuresOff: FeatureId[];
|
|
|
|
featureLabelsOn: string[];
|
|
|
|
featureLabelsOff: string[];
|
2021-07-23 21:34:30 +02:00
|
|
|
cfaLabel: string;
|
|
|
|
cfaUrl: string;
|
2022-04-07 16:35:15 +02:00
|
|
|
footnote: string;
|
2021-07-23 21:34:30 +02:00
|
|
|
}
|
2021-07-10 12:16:13 +02:00
|
|
|
|
2021-07-31 15:42:56 +02:00
|
|
|
export enum PricePeriod {
|
|
|
|
Monthly = 'monthly',
|
|
|
|
Yearly = 'yearly',
|
|
|
|
}
|
|
|
|
|
|
|
|
export enum PriceCurrency {
|
|
|
|
EUR = 'EUR',
|
|
|
|
GBP = 'GBP',
|
|
|
|
USD = 'USD',
|
|
|
|
}
|
|
|
|
|
|
|
|
export interface StripePublicConfigPrice {
|
|
|
|
accountType: number; // AccountType
|
|
|
|
id: string;
|
|
|
|
period: PricePeriod;
|
|
|
|
amount: string;
|
|
|
|
formattedAmount: string;
|
|
|
|
formattedMonthlyAmount: string;
|
|
|
|
currency: PriceCurrency;
|
|
|
|
}
|
|
|
|
|
2021-07-23 21:34:30 +02:00
|
|
|
export interface StripePublicConfig {
|
|
|
|
publishableKey: string;
|
2021-07-31 15:42:56 +02:00
|
|
|
prices: StripePublicConfigPrice[];
|
2021-07-23 21:34:30 +02:00
|
|
|
webhookBaseUrl: string;
|
|
|
|
}
|
|
|
|
|
2021-07-31 15:42:56 +02:00
|
|
|
function formatPrice(amount: string | number, currency: PriceCurrency): string {
|
|
|
|
amount = typeof amount === 'number' ? (Math.ceil(amount * 100) / 100).toFixed(2) : amount;
|
|
|
|
if (currency === PriceCurrency.EUR) return `${amount}€`;
|
|
|
|
if (currency === PriceCurrency.GBP) return `£${amount}`;
|
|
|
|
if (currency === PriceCurrency.USD) return `$${amount}`;
|
|
|
|
throw new Error(`Unsupported currency: ${currency}`);
|
|
|
|
}
|
|
|
|
|
|
|
|
interface FindPriceQuery {
|
|
|
|
accountType?: number;
|
|
|
|
period?: PricePeriod;
|
|
|
|
priceId?: string;
|
|
|
|
}
|
|
|
|
|
|
|
|
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}`);
|
|
|
|
|
|
|
|
config.prices = config.prices.map(p => {
|
|
|
|
return {
|
|
|
|
...p,
|
|
|
|
formattedAmount: formatPrice(p.amount, p.currency),
|
|
|
|
formattedMonthlyAmount: p.period === PricePeriod.Monthly ? formatPrice(p.amount, p.currency) : formatPrice(Number(p.amount) / 12, p.currency),
|
|
|
|
};
|
|
|
|
});
|
|
|
|
|
|
|
|
return config;
|
|
|
|
}
|
|
|
|
|
|
|
|
export function findPrice(prices: StripePublicConfigPrice[], query: FindPriceQuery): StripePublicConfigPrice {
|
|
|
|
let output: StripePublicConfigPrice = null;
|
|
|
|
|
|
|
|
if (query.accountType && query.period) {
|
|
|
|
output = prices.filter(p => p.accountType === query.accountType).find(p => p.period === query.period);
|
|
|
|
} else if (query.priceId) {
|
|
|
|
output = prices.find(p => p.id === query.priceId);
|
|
|
|
} else {
|
|
|
|
throw new Error(`Invalid query: ${JSON.stringify(query)}`);
|
|
|
|
}
|
|
|
|
|
|
|
|
if (!output) throw new Error(`Not found: ${JSON.stringify(query)}`);
|
|
|
|
|
|
|
|
return output;
|
|
|
|
}
|
|
|
|
|
2022-11-28 18:16:32 +02:00
|
|
|
const features = (): Record<FeatureId, PlanFeature> => {
|
|
|
|
return {
|
|
|
|
maxItemSize: {
|
|
|
|
title: _('Max note or attachment size'),
|
|
|
|
basic: true,
|
|
|
|
pro: true,
|
|
|
|
teams: true,
|
|
|
|
basicInfo: _('%d MB per note or attachment', 10),
|
|
|
|
proInfo: _('%d MB per note or attachment', 200),
|
|
|
|
teamsInfo: _('%d MB per note or attachment', 200),
|
|
|
|
basicInfoShort: _('%d MB', 10),
|
|
|
|
proInfoShort: _('%d MB', 200),
|
|
|
|
teamsInfoShort: _('%d MB', 200),
|
|
|
|
},
|
|
|
|
maxStorage: {
|
2022-12-20 14:58:03 +02:00
|
|
|
title: _('Storage space'),
|
2022-11-28 18:16:32 +02:00
|
|
|
basic: true,
|
|
|
|
pro: true,
|
|
|
|
teams: true,
|
|
|
|
basicInfo: _('%d GB storage space', 1),
|
|
|
|
proInfo: _('%d GB storage space', 10),
|
|
|
|
teamsInfo: _('%d GB storage space', 10),
|
|
|
|
basicInfoShort: _('%d GB', 1),
|
|
|
|
proInfoShort: _('%d GB', 10),
|
|
|
|
teamsInfoShort: _('%d GB', 10),
|
|
|
|
},
|
|
|
|
publishNote: {
|
|
|
|
title: _('Publish notes to the internet'),
|
|
|
|
basic: true,
|
|
|
|
pro: true,
|
|
|
|
teams: true,
|
|
|
|
},
|
|
|
|
sync: {
|
|
|
|
title: _('Sync as many devices as you want'),
|
|
|
|
basic: true,
|
|
|
|
pro: true,
|
|
|
|
teams: true,
|
|
|
|
},
|
|
|
|
clipper: {
|
|
|
|
title: _('Web Clipper'),
|
|
|
|
basic: true,
|
|
|
|
pro: true,
|
|
|
|
teams: true,
|
|
|
|
},
|
|
|
|
collaborate: {
|
2023-08-04 17:24:02 +02:00
|
|
|
title: _('Collaborate on a notebook with others'),
|
|
|
|
basic: true,
|
|
|
|
pro: true,
|
|
|
|
teams: true,
|
|
|
|
},
|
|
|
|
share: {
|
|
|
|
title: _('Share a notebook with others'),
|
2022-11-28 18:16:32 +02:00
|
|
|
basic: false,
|
|
|
|
pro: true,
|
|
|
|
teams: true,
|
|
|
|
},
|
2023-07-26 19:07:29 +02:00
|
|
|
emailToNote: {
|
|
|
|
title: _('Email to Note'),
|
|
|
|
basic: false,
|
|
|
|
pro: true,
|
|
|
|
teams: true,
|
|
|
|
},
|
2022-11-28 18:16:32 +02:00
|
|
|
multiUsers: {
|
|
|
|
title: _('Manage multiple users'),
|
|
|
|
basic: false,
|
|
|
|
pro: false,
|
|
|
|
teams: true,
|
|
|
|
},
|
|
|
|
consolidatedBilling: {
|
|
|
|
title: _('Consolidated billing'),
|
|
|
|
basic: false,
|
|
|
|
pro: false,
|
|
|
|
teams: true,
|
|
|
|
},
|
|
|
|
sharingAccessControl: {
|
|
|
|
title: _('Sharing access control'),
|
|
|
|
basic: false,
|
|
|
|
pro: false,
|
|
|
|
teams: true,
|
|
|
|
},
|
|
|
|
prioritySupport: {
|
|
|
|
title: _('Priority support'),
|
|
|
|
basic: false,
|
|
|
|
pro: false,
|
|
|
|
teams: true,
|
|
|
|
},
|
|
|
|
};
|
2022-04-07 16:35:15 +02:00
|
|
|
};
|
2021-07-10 12:16:13 +02:00
|
|
|
|
2022-04-07 16:35:15 +02:00
|
|
|
export const getFeatureIdsByPlan = (planName: PlanName, featureOn: boolean): FeatureId[] => {
|
|
|
|
const output: FeatureId[] = [];
|
2021-07-10 12:16:13 +02:00
|
|
|
|
2022-11-28 18:16:32 +02:00
|
|
|
for (const [k, v] of Object.entries(features())) {
|
2022-04-07 16:35:15 +02:00
|
|
|
if (v[planName] === featureOn) {
|
|
|
|
output.push(k);
|
|
|
|
}
|
|
|
|
}
|
2021-07-10 12:16:13 +02:00
|
|
|
|
2022-04-07 16:35:15 +02:00
|
|
|
return output;
|
|
|
|
};
|
2021-07-10 12:16:13 +02:00
|
|
|
|
2022-04-07 16:35:15 +02:00
|
|
|
export const getFeatureLabelsByPlan = (planName: PlanName, featureOn: boolean): string[] => {
|
|
|
|
const output: FeatureId[] = [];
|
2021-07-10 12:16:13 +02:00
|
|
|
|
2022-11-28 18:16:32 +02:00
|
|
|
for (const [featureId, v] of Object.entries(features())) {
|
2022-04-07 16:35:15 +02:00
|
|
|
if (v[planName] === featureOn) {
|
|
|
|
output.push(getFeatureLabel(planName, featureId));
|
|
|
|
}
|
|
|
|
}
|
2021-07-10 12:16:13 +02:00
|
|
|
|
2022-04-07 16:35:15 +02:00
|
|
|
return output;
|
|
|
|
};
|
|
|
|
|
|
|
|
export const getAllFeatureIds = (): FeatureId[] => {
|
|
|
|
return Object.keys(features);
|
|
|
|
};
|
|
|
|
|
|
|
|
export const getFeatureById = (featureId: FeatureId): PlanFeature => {
|
2022-11-28 18:16:32 +02:00
|
|
|
return features()[featureId];
|
2022-04-07 16:35:15 +02:00
|
|
|
};
|
|
|
|
|
|
|
|
export const getFeaturesByPlan = (planName: PlanName, featureOn: boolean): PlanFeature[] => {
|
|
|
|
const output: PlanFeature[] = [];
|
|
|
|
|
|
|
|
for (const [, v] of Object.entries(features)) {
|
|
|
|
if (v[planName] === featureOn) {
|
|
|
|
output.push(v);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
return output;
|
|
|
|
};
|
|
|
|
|
|
|
|
export const getFeatureLabel = (planName: PlanName, featureId: FeatureId): string => {
|
2022-11-28 18:16:32 +02:00
|
|
|
const feature = features()[featureId];
|
2022-04-07 16:35:15 +02:00
|
|
|
const k = `${planName}Info`;
|
|
|
|
if ((feature as any)[k]) return (feature as any)[k];
|
|
|
|
return feature.title;
|
|
|
|
};
|
|
|
|
|
|
|
|
export const getFeatureEnabled = (planName: PlanName, featureId: FeatureId): boolean => {
|
2022-11-28 18:16:32 +02:00
|
|
|
const feature = features()[featureId];
|
2022-04-07 16:35:15 +02:00
|
|
|
return feature[planName];
|
|
|
|
};
|
|
|
|
|
|
|
|
export const createFeatureTableMd = () => {
|
|
|
|
const headers: MarkdownTableHeader[] = [
|
|
|
|
{
|
|
|
|
name: 'featureLabel',
|
|
|
|
label: 'Feature',
|
|
|
|
},
|
|
|
|
{
|
|
|
|
name: 'basic',
|
|
|
|
label: 'Basic',
|
|
|
|
},
|
|
|
|
{
|
|
|
|
name: 'pro',
|
|
|
|
label: 'Pro',
|
|
|
|
},
|
|
|
|
{
|
|
|
|
name: 'teams',
|
|
|
|
label: 'Teams',
|
|
|
|
},
|
|
|
|
];
|
|
|
|
|
|
|
|
const rows: MarkdownTableRow[] = [];
|
|
|
|
|
|
|
|
const getCellInfo = (planName: PlanName, feature: PlanFeature) => {
|
|
|
|
if (!feature[planName]) return '-';
|
|
|
|
const infoShort: string = (feature as any)[`${planName}InfoShort`];
|
|
|
|
if (infoShort) return infoShort;
|
|
|
|
return '✔️';
|
2021-07-10 12:16:13 +02:00
|
|
|
};
|
|
|
|
|
2022-11-28 18:16:32 +02:00
|
|
|
for (const [, feature] of Object.entries(features())) {
|
2022-04-07 16:35:15 +02:00
|
|
|
const row: MarkdownTableRow = {
|
|
|
|
featureLabel: feature.title,
|
|
|
|
basic: getCellInfo(PlanName.Basic, feature),
|
|
|
|
pro: getCellInfo(PlanName.Pro, feature),
|
|
|
|
teams: getCellInfo(PlanName.Teams, feature),
|
|
|
|
};
|
|
|
|
|
|
|
|
rows.push(row);
|
|
|
|
}
|
|
|
|
|
|
|
|
return markdownUtils.createMarkdownTable(headers, rows);
|
|
|
|
};
|
|
|
|
|
|
|
|
export function getPlans(stripeConfig: StripePublicConfig): Record<PlanName, Plan> {
|
2021-07-10 12:16:13 +02:00
|
|
|
return {
|
|
|
|
basic: {
|
|
|
|
name: 'basic',
|
2022-11-28 18:16:32 +02:00
|
|
|
title: _('Basic'),
|
2021-07-31 15:42:56 +02:00
|
|
|
priceMonthly: findPrice(stripeConfig.prices, {
|
|
|
|
accountType: 1,
|
|
|
|
period: PricePeriod.Monthly,
|
|
|
|
}),
|
|
|
|
priceYearly: findPrice(stripeConfig.prices, {
|
|
|
|
accountType: 1,
|
|
|
|
period: PricePeriod.Yearly,
|
|
|
|
}),
|
2021-07-10 12:16:13 +02:00
|
|
|
featured: false,
|
|
|
|
iconName: 'basic-icon',
|
2022-04-07 16:35:15 +02:00
|
|
|
featuresOn: getFeatureIdsByPlan(PlanName.Basic, true),
|
|
|
|
featuresOff: getFeatureIdsByPlan(PlanName.Basic, false),
|
|
|
|
featureLabelsOn: getFeatureLabelsByPlan(PlanName.Basic, true),
|
|
|
|
featureLabelsOff: getFeatureLabelsByPlan(PlanName.Basic, false),
|
2022-11-28 18:16:32 +02:00
|
|
|
cfaLabel: _('Try it now'),
|
2021-07-10 12:16:13 +02:00
|
|
|
cfaUrl: '',
|
2022-04-07 16:35:15 +02:00
|
|
|
footnote: '',
|
2021-07-10 12:16:13 +02:00
|
|
|
},
|
|
|
|
|
|
|
|
pro: {
|
|
|
|
name: 'pro',
|
2022-11-28 18:16:32 +02:00
|
|
|
title: _('Pro'),
|
2021-07-31 15:42:56 +02:00
|
|
|
priceMonthly: findPrice(stripeConfig.prices, {
|
|
|
|
accountType: 2,
|
|
|
|
period: PricePeriod.Monthly,
|
|
|
|
}),
|
|
|
|
priceYearly: findPrice(stripeConfig.prices, {
|
|
|
|
accountType: 2,
|
|
|
|
period: PricePeriod.Yearly,
|
|
|
|
}),
|
2021-07-10 12:16:13 +02:00
|
|
|
featured: true,
|
|
|
|
iconName: 'pro-icon',
|
2022-04-07 16:35:15 +02:00
|
|
|
featuresOn: getFeatureIdsByPlan(PlanName.Pro, true),
|
|
|
|
featuresOff: getFeatureIdsByPlan(PlanName.Pro, false),
|
|
|
|
featureLabelsOn: getFeatureLabelsByPlan(PlanName.Pro, true),
|
|
|
|
featureLabelsOff: getFeatureLabelsByPlan(PlanName.Pro, false),
|
2022-11-28 18:16:32 +02:00
|
|
|
cfaLabel: _('Try it now'),
|
2021-07-10 12:16:13 +02:00
|
|
|
cfaUrl: '',
|
2022-04-07 16:35:15 +02:00
|
|
|
footnote: '',
|
2021-07-10 12:16:13 +02:00
|
|
|
},
|
|
|
|
|
2022-04-07 16:35:15 +02:00
|
|
|
teams: {
|
|
|
|
name: 'teams',
|
2022-11-28 18:16:32 +02:00
|
|
|
title: _('Teams'),
|
2022-04-07 16:35:15 +02:00
|
|
|
priceMonthly: findPrice(stripeConfig.prices, {
|
|
|
|
accountType: 3,
|
|
|
|
period: PricePeriod.Monthly,
|
|
|
|
}),
|
|
|
|
priceYearly: findPrice(stripeConfig.prices, {
|
|
|
|
accountType: 3,
|
|
|
|
period: PricePeriod.Yearly,
|
|
|
|
}),
|
2021-07-10 12:16:13 +02:00
|
|
|
featured: false,
|
|
|
|
iconName: 'business-icon',
|
2022-04-07 16:35:15 +02:00
|
|
|
featuresOn: getFeatureIdsByPlan(PlanName.Teams, true),
|
|
|
|
featuresOff: getFeatureIdsByPlan(PlanName.Teams, false),
|
|
|
|
featureLabelsOn: getFeatureLabelsByPlan(PlanName.Teams, true),
|
|
|
|
featureLabelsOff: getFeatureLabelsByPlan(PlanName.Teams, false),
|
2022-11-28 18:16:32 +02:00
|
|
|
cfaLabel: _('Try it now'),
|
2022-04-07 16:35:15 +02:00
|
|
|
cfaUrl: '',
|
2022-11-28 18:16:32 +02:00
|
|
|
footnote: _('Per user. Minimum of %d users.', 2),
|
2021-07-10 12:16:13 +02:00
|
|
|
},
|
|
|
|
};
|
|
|
|
}
|