diff --git a/.eslintignore b/.eslintignore index ac185cca7..b7f7372f4 100644 --- a/.eslintignore +++ b/.eslintignore @@ -1539,6 +1539,9 @@ packages/lib/time.js.map packages/lib/utils/credentialFiles.d.ts packages/lib/utils/credentialFiles.js packages/lib/utils/credentialFiles.js.map +packages/lib/utils/joplinCloud.d.ts +packages/lib/utils/joplinCloud.js +packages/lib/utils/joplinCloud.js.map packages/lib/uuid.d.ts packages/lib/uuid.js packages/lib/uuid.js.map @@ -1698,9 +1701,6 @@ packages/tools/update-readme-sponsors.js.map packages/tools/website/build.d.ts packages/tools/website/build.js packages/tools/website/build.js.map -packages/tools/website/utils/plans.d.ts -packages/tools/website/utils/plans.js -packages/tools/website/utils/plans.js.map packages/tools/website/utils/pressCarousel.d.ts packages/tools/website/utils/pressCarousel.js packages/tools/website/utils/pressCarousel.js.map diff --git a/.gitignore b/.gitignore index a74be40a6..c9b9fb1d4 100644 --- a/.gitignore +++ b/.gitignore @@ -1524,6 +1524,9 @@ packages/lib/time.js.map packages/lib/utils/credentialFiles.d.ts packages/lib/utils/credentialFiles.js packages/lib/utils/credentialFiles.js.map +packages/lib/utils/joplinCloud.d.ts +packages/lib/utils/joplinCloud.js +packages/lib/utils/joplinCloud.js.map packages/lib/uuid.d.ts packages/lib/uuid.js packages/lib/uuid.js.map @@ -1683,9 +1686,6 @@ packages/tools/update-readme-sponsors.js.map packages/tools/website/build.d.ts packages/tools/website/build.js packages/tools/website/build.js.map -packages/tools/website/utils/plans.d.ts -packages/tools/website/utils/plans.js -packages/tools/website/utils/plans.js.map packages/tools/website/utils/pressCarousel.d.ts packages/tools/website/utils/pressCarousel.js packages/tools/website/utils/pressCarousel.js.map diff --git a/packages/tools/website/utils/plans.ts b/packages/lib/utils/joplinCloud.ts similarity index 75% rename from packages/tools/website/utils/plans.ts rename to packages/lib/utils/joplinCloud.ts index 22f9c3987..026d89dc8 100644 --- a/packages/tools/website/utils/plans.ts +++ b/packages/lib/utils/joplinCloud.ts @@ -1,6 +1,47 @@ -/* eslint-disable import/prefer-default-export */ +export interface Plan { + name: string; + title: string; + price: string; + stripePriceId: string; + featured: boolean; + iconName: string; + featuresOn: string[]; + featuresOff: string[]; + cfaLabel: string; + cfaUrl: string; +} -import { Plan, StripePublicConfig } from './types'; +export interface StripePublicConfig { + publishableKey: string; + basicPriceId: string; + proPriceId: string; + webhookBaseUrl: string; +} + +export interface PlanFeature { + label: string; + enabled: boolean; +} + +export function getFeatureList(plan: Plan): PlanFeature[] { + const output: PlanFeature[] = []; + + for (const f of plan.featuresOn) { + output.push({ + label: f, + enabled: true, + }); + } + + for (const f of plan.featuresOff) { + output.push({ + label: f, + enabled: false, + }); + } + + return output; +} const businessAccountEmailBody = `Hello, diff --git a/packages/server/public/css/index/home.css b/packages/server/public/css/index/home.css new file mode 100644 index 000000000..e69de29bb diff --git a/packages/server/public/css/index/upgrade.css b/packages/server/public/css/index/upgrade.css new file mode 100644 index 000000000..26c4ddc86 --- /dev/null +++ b/packages/server/public/css/index/upgrade.css @@ -0,0 +1,3 @@ +.basic-plan { + opacity: 0.8; +} \ No newline at end of file diff --git a/packages/server/src/models/NotificationModel.ts b/packages/server/src/models/NotificationModel.ts index 24d82b642..a5b0197c5 100644 --- a/packages/server/src/models/NotificationModel.ts +++ b/packages/server/src/models/NotificationModel.ts @@ -8,6 +8,7 @@ export enum NotificationKey { EmailConfirmed = 'emailConfirmed', ChangeAdminPassword = 'change_admin_password', UsingSqliteInProd = 'using_sqlite_in_prod', + UpgradedToPro = 'upgraded_to_pro', } interface NotificationType { @@ -47,6 +48,10 @@ export default class NotificationModel extends BaseModel { level: NotificationLevel.Important, message: 'The server is currently using SQLite3 as a database. It is not recommended in production as it is slow and can cause locking issues. Please see the README for information on how to change it.', }, + [NotificationKey.UpgradedToPro]: { + level: NotificationLevel.Normal, + message: 'Thank you! Your account has been successfully upgraded to Pro.', + }, }; const type = notificationTypes[key]; diff --git a/packages/server/src/routes/index/home.ts b/packages/server/src/routes/index/home.ts index 26e2518b7..01ac8bb3e 100644 --- a/packages/server/src/routes/index/home.ts +++ b/packages/server/src/routes/index/home.ts @@ -5,7 +5,7 @@ import { AppContext } from '../../utils/types'; import { contextSessionId } from '../../utils/requestUtils'; import { ErrorMethodNotAllowed } from '../../utils/errors'; import defaultView from '../../utils/defaultView'; -import { accountTypeToString } from '../../models/UserModel'; +import { AccountType, accountTypeToString } from '../../models/UserModel'; import { formatMaxItemSize, formatMaxTotalSize, formatTotalSize, formatTotalSizePercent, yesOrNo } from '../../utils/strings'; import { getCanShareFolder, totalSizeClass } from '../../models/utils/user'; @@ -16,6 +16,7 @@ router.get('home', async (_path: SubPath, ctx: AppContext) => { if (ctx.method === 'GET') { const user = ctx.joplin.owner; + const subscription = await ctx.joplin.models.subscription().byUserId(user.id); const view = defaultView('home', 'Home'); view.content = { @@ -57,8 +58,11 @@ router.get('home', async (_path: SubPath, ctx: AppContext) => { show: true, }, ], + showUpgradeProButton: subscription && user.account_type === AccountType.Basic, }; + view.cssFiles = ['index/home']; + return view; } diff --git a/packages/server/src/routes/index/upgrade.ts b/packages/server/src/routes/index/upgrade.ts new file mode 100644 index 000000000..349b57e30 --- /dev/null +++ b/packages/server/src/routes/index/upgrade.ts @@ -0,0 +1,75 @@ +import { SubPath, redirect } from '../../utils/routeUtils'; +import Router from '../../utils/Router'; +import { RouteType } from '../../utils/types'; +import { AppContext } from '../../utils/types'; +import { getFeatureList, getPlans } from '@joplin/lib/utils/joplinCloud'; +import config from '../../config'; +import defaultView from '../../utils/defaultView'; +import { stripeConfig, updateSubscriptionType } from '../../utils/stripe'; +import { bodyFields } from '../../utils/requestUtils'; +import { NotificationKey } from '../../models/NotificationModel'; +import { AccountType } from '../../models/UserModel'; +import { ErrorBadRequest } from '../../utils/errors'; + +interface FormFields { + upgrade_button: string; +} + +const router: Router = new Router(RouteType.Web); + +function upgradeUrl() { + return `${config().baseUrl}/upgrade`; +} + +router.get('upgrade', async (_path: SubPath, _ctx: AppContext) => { + interface PlanRow { + basicLabel: string; + proLabel: string; + } + + const plans = getPlans(stripeConfig()); + const basicFeatureList = getFeatureList(plans.basic); + const proFeatureList = getFeatureList(plans.pro); + + const planRows: PlanRow[] = []; + + for (let i = 0; i < basicFeatureList.length; i++) { + const basic = basicFeatureList[i]; + const pro = proFeatureList[i]; + + if (basic.label === pro.label && basic.enabled === pro.enabled) continue; + + planRows.push({ + basicLabel: basic.enabled ? basic.label : '-', + proLabel: pro.label, + }); + } + + const view = defaultView('upgrade', 'Upgrade'); + view.content = { + planRows, + basicPrice: plans.basic.price, + proPrice: plans.pro.price, + postUrl: upgradeUrl(), + }; + view.cssFiles = ['index/upgrade']; + return view; +}); + +router.post('upgrade', async (_path: SubPath, ctx: AppContext) => { + const fields = await bodyFields(ctx.req); + + const joplin = ctx.joplin; + const models = joplin.models; + + if (fields.upgrade_button) { + await updateSubscriptionType(models, joplin.owner.id, AccountType.Pro); + await models.user().save({ id: joplin.owner.id, account_type: AccountType.Pro }); + await models.notification().add(joplin.owner.id, NotificationKey.UpgradedToPro); + return redirect(ctx, `${config().baseUrl}/home`); + } + + throw new ErrorBadRequest('Invalid button'); +}); + +export default router; diff --git a/packages/server/src/routes/routes.ts b/packages/server/src/routes/routes.ts index c31acfd5c..01f25d0a9 100644 --- a/packages/server/src/routes/routes.ts +++ b/packages/server/src/routes/routes.ts @@ -24,6 +24,7 @@ import indexUsers from './index/users'; import indexStripe from './index/stripe'; import indexTerms from './index/terms'; import indexPrivacy from './index/privacy'; +import indexUpgrade from './index/upgrade'; import defaultRoute from './default'; @@ -52,6 +53,7 @@ const routes: Routers = { 'stripe': indexStripe, 'terms': indexTerms, 'privacy': indexPrivacy, + 'upgrade': indexUpgrade, '': defaultRoute, }; diff --git a/packages/server/src/views/index/home.mustache b/packages/server/src/views/index/home.mustache index b8dc07333..5d5c6eb54 100644 --- a/packages/server/src/views/index/home.mustache +++ b/packages/server/src/views/index/home.mustache @@ -1,19 +1,25 @@ -

Welcome to {{global.appName}}

-

Logged in as {{global.userDisplayName}}

+

Welcome to {{global.appName}}

+

Logged in as {{global.userDisplayName}}

- - - {{#userProps}} - {{#show}} - - - - - {{/show}} - {{/userProps}} - -
- {{label}} - - {{value}} -
+ + + {{#userProps}} + {{#show}} + + + + + {{/show}} + {{/userProps}} + +
+ {{label}} + + {{value}} +
+ +{{#showUpgradeProButton}} +

+ Upgrade to a Pro account to benefit from collaborate on notebooks, to increase the max note size, or the max total size. +

+{{/showUpgradeProButton}} \ No newline at end of file diff --git a/packages/server/src/views/index/upgrade.mustache b/packages/server/src/views/index/upgrade.mustache new file mode 100644 index 000000000..0f59759c7 --- /dev/null +++ b/packages/server/src/views/index/upgrade.mustache @@ -0,0 +1,38 @@ +

Upgrade your account

+

Upgrading to a Pro account to get the following benefits.

+ +
+ + + + + + + + {{#planRows}} + + + + + {{/planRows}} + + + + + + +
Basic - {{basicPrice}}Pro - {{proPrice}}
+ {{basicLabel}} + + {{proLabel}} +
+
+ + \ No newline at end of file diff --git a/packages/tools/website/build.ts b/packages/tools/website/build.ts index 9760f9574..7cdd003f3 100644 --- a/packages/tools/website/build.ts +++ b/packages/tools/website/build.ts @@ -1,9 +1,9 @@ import * as fs from 'fs-extra'; import { insertContentIntoFile, rootDir } from '../tool-utils'; -import { getPlans } from './utils/plans'; import { pressCarouselItems } from './utils/pressCarousel'; import { getMarkdownIt, loadMustachePartials, markdownToPageHtml, renderMustache } from './utils/render'; -import { Env, PlanPageParams, Sponsors, StripePublicConfig, TemplateParams } from './utils/types'; +import { Env, PlanPageParams, Sponsors, TemplateParams } from './utils/types'; +import { getPlans, StripePublicConfig } from '@joplin/lib/utils/joplinCloud'; const dirname = require('path').dirname; const glob = require('glob'); const path = require('path'); diff --git a/packages/tools/website/utils/types.ts b/packages/tools/website/utils/types.ts index fb366be81..732717d0d 100644 --- a/packages/tools/website/utils/types.ts +++ b/packages/tools/website/utils/types.ts @@ -1,3 +1,5 @@ +import { Plan, StripePublicConfig } from '@joplin/lib/utils/joplinCloud'; + export enum Env { Dev = 'dev', Prod = 'prod', @@ -64,28 +66,8 @@ export interface TemplateParams { buildTime?: number; } -export interface Plan { - name: string; - title: string; - price: string; - stripePriceId: string; - featured: boolean; - iconName: string; - featuresOn: string[]; - featuresOff: string[]; - cfaLabel: string; - cfaUrl: string; -} - export interface PlanPageParams extends TemplateParams { plans: Record; faqHtml: string; stripeConfig: StripePublicConfig; } - -export interface StripePublicConfig { - publishableKey: string; - basicPriceId: string; - proPriceId: string; - webhookBaseUrl: string; -}