1
0
mirror of https://github.com/laurent22/joplin.git synced 2024-12-24 10:27:10 +02:00

Server: Allow user to upgrade account

This commit is contained in:
Laurent Cozic 2021-07-23 20:34:30 +01:00
parent 75a421edb1
commit e83ab93644
13 changed files with 205 additions and 49 deletions

View File

@ -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

6
.gitignore vendored
View File

@ -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

View File

@ -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,

View File

@ -0,0 +1,3 @@
.basic-plan {
opacity: 0.8;
}

View File

@ -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<Notification> {
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];

View File

@ -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;
}

View File

@ -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<FormFields>(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;

View File

@ -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,
};

View File

@ -1,15 +1,15 @@
<h1 class="title">Welcome to {{global.appName}}</h1>
<p class="subtitle">Logged in as <strong>{{global.userDisplayName}}</strong></p>
<table class="table is-hoverable">
<table class="table is-hoverable user-props-table">
<tbody>
{{#userProps}}
{{#show}}
<tr>
<td class="{{#classes}}{{.}}{{/classes}}">
<td class="{{#classes}}{{.}}{{/classes}} prop-name">
<strong>{{label}}</strong>
</td>
<td class="{{#classes}}{{.}}{{/classes}}">
<td class="{{#classes}}{{.}}{{/classes}} prop-value">
{{value}}
</td>
</tr>
@ -17,3 +17,9 @@
{{/userProps}}
</tbody>
</table>
{{#showUpgradeProButton}}
<p class="block">
<a href="{{baseUrl}}/upgrade" class="upgrade-button">Upgrade to a Pro account</a> to benefit from collaborate on notebooks, to increase the max note size, or the max total size.
</p>
{{/showUpgradeProButton}}

View File

@ -0,0 +1,38 @@
<h1 class="title">Upgrade your account</h1>
<p class="subtitle">Upgrading to a Pro account to get the following benefits.</p>
<form id="upgrade_form" action="{{{postUrl}}}" method="POST">
<table class="table is-hoverable user-props-table">
<tbody>
<tr>
<th class="basic-plan">Basic - {{basicPrice}}</th>
<th>Pro - {{proPrice}}</th>
</tr>
{{#planRows}}
<tr>
<td class="basic-plan">
{{basicLabel}}
</td>
<td>
{{proLabel}}
</td>
</tr>
{{/planRows}}
<tr>
<td></td>
<td><input type="submit" id="upgrade_button" name="upgrade_button" class="button is-success" value="Upgrade to Pro" /></td>
</tr>
</tbody>
</table>
</form>
<script>
$(() => {
$('#upgrade_form').submit((event) => {
const ok = confirm('Your account is going to be upgraded to Pro. Do you wish to continue?');
if (!ok) event.preventDefault();
});
});
</script>

View File

@ -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');

View File

@ -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<string, Plan>;
faqHtml: string;
stripeConfig: StripePublicConfig;
}
export interface StripePublicConfig {
publishableKey: string;
basicPriceId: string;
proPriceId: string;
webhookBaseUrl: string;
}