mirror of
https://github.com/laurent22/joplin.git
synced 2025-01-11 18:24:43 +02:00
Server: Allow user to upgrade account
This commit is contained in:
parent
75a421edb1
commit
e83ab93644
@ -1539,6 +1539,9 @@ packages/lib/time.js.map
|
|||||||
packages/lib/utils/credentialFiles.d.ts
|
packages/lib/utils/credentialFiles.d.ts
|
||||||
packages/lib/utils/credentialFiles.js
|
packages/lib/utils/credentialFiles.js
|
||||||
packages/lib/utils/credentialFiles.js.map
|
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.d.ts
|
||||||
packages/lib/uuid.js
|
packages/lib/uuid.js
|
||||||
packages/lib/uuid.js.map
|
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.d.ts
|
||||||
packages/tools/website/build.js
|
packages/tools/website/build.js
|
||||||
packages/tools/website/build.js.map
|
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.d.ts
|
||||||
packages/tools/website/utils/pressCarousel.js
|
packages/tools/website/utils/pressCarousel.js
|
||||||
packages/tools/website/utils/pressCarousel.js.map
|
packages/tools/website/utils/pressCarousel.js.map
|
||||||
|
6
.gitignore
vendored
6
.gitignore
vendored
@ -1524,6 +1524,9 @@ packages/lib/time.js.map
|
|||||||
packages/lib/utils/credentialFiles.d.ts
|
packages/lib/utils/credentialFiles.d.ts
|
||||||
packages/lib/utils/credentialFiles.js
|
packages/lib/utils/credentialFiles.js
|
||||||
packages/lib/utils/credentialFiles.js.map
|
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.d.ts
|
||||||
packages/lib/uuid.js
|
packages/lib/uuid.js
|
||||||
packages/lib/uuid.js.map
|
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.d.ts
|
||||||
packages/tools/website/build.js
|
packages/tools/website/build.js
|
||||||
packages/tools/website/build.js.map
|
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.d.ts
|
||||||
packages/tools/website/utils/pressCarousel.js
|
packages/tools/website/utils/pressCarousel.js
|
||||||
packages/tools/website/utils/pressCarousel.js.map
|
packages/tools/website/utils/pressCarousel.js.map
|
||||||
|
@ -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,
|
const businessAccountEmailBody = `Hello,
|
||||||
|
|
0
packages/server/public/css/index/home.css
Normal file
0
packages/server/public/css/index/home.css
Normal file
3
packages/server/public/css/index/upgrade.css
Normal file
3
packages/server/public/css/index/upgrade.css
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
.basic-plan {
|
||||||
|
opacity: 0.8;
|
||||||
|
}
|
@ -8,6 +8,7 @@ export enum NotificationKey {
|
|||||||
EmailConfirmed = 'emailConfirmed',
|
EmailConfirmed = 'emailConfirmed',
|
||||||
ChangeAdminPassword = 'change_admin_password',
|
ChangeAdminPassword = 'change_admin_password',
|
||||||
UsingSqliteInProd = 'using_sqlite_in_prod',
|
UsingSqliteInProd = 'using_sqlite_in_prod',
|
||||||
|
UpgradedToPro = 'upgraded_to_pro',
|
||||||
}
|
}
|
||||||
|
|
||||||
interface NotificationType {
|
interface NotificationType {
|
||||||
@ -47,6 +48,10 @@ export default class NotificationModel extends BaseModel<Notification> {
|
|||||||
level: NotificationLevel.Important,
|
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.',
|
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];
|
const type = notificationTypes[key];
|
||||||
|
@ -5,7 +5,7 @@ import { AppContext } from '../../utils/types';
|
|||||||
import { contextSessionId } from '../../utils/requestUtils';
|
import { contextSessionId } from '../../utils/requestUtils';
|
||||||
import { ErrorMethodNotAllowed } from '../../utils/errors';
|
import { ErrorMethodNotAllowed } from '../../utils/errors';
|
||||||
import defaultView from '../../utils/defaultView';
|
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 { formatMaxItemSize, formatMaxTotalSize, formatTotalSize, formatTotalSizePercent, yesOrNo } from '../../utils/strings';
|
||||||
import { getCanShareFolder, totalSizeClass } from '../../models/utils/user';
|
import { getCanShareFolder, totalSizeClass } from '../../models/utils/user';
|
||||||
|
|
||||||
@ -16,6 +16,7 @@ router.get('home', async (_path: SubPath, ctx: AppContext) => {
|
|||||||
|
|
||||||
if (ctx.method === 'GET') {
|
if (ctx.method === 'GET') {
|
||||||
const user = ctx.joplin.owner;
|
const user = ctx.joplin.owner;
|
||||||
|
const subscription = await ctx.joplin.models.subscription().byUserId(user.id);
|
||||||
|
|
||||||
const view = defaultView('home', 'Home');
|
const view = defaultView('home', 'Home');
|
||||||
view.content = {
|
view.content = {
|
||||||
@ -57,8 +58,11 @@ router.get('home', async (_path: SubPath, ctx: AppContext) => {
|
|||||||
show: true,
|
show: true,
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
|
showUpgradeProButton: subscription && user.account_type === AccountType.Basic,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
view.cssFiles = ['index/home'];
|
||||||
|
|
||||||
return view;
|
return view;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
75
packages/server/src/routes/index/upgrade.ts
Normal file
75
packages/server/src/routes/index/upgrade.ts
Normal 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;
|
@ -24,6 +24,7 @@ import indexUsers from './index/users';
|
|||||||
import indexStripe from './index/stripe';
|
import indexStripe from './index/stripe';
|
||||||
import indexTerms from './index/terms';
|
import indexTerms from './index/terms';
|
||||||
import indexPrivacy from './index/privacy';
|
import indexPrivacy from './index/privacy';
|
||||||
|
import indexUpgrade from './index/upgrade';
|
||||||
|
|
||||||
import defaultRoute from './default';
|
import defaultRoute from './default';
|
||||||
|
|
||||||
@ -52,6 +53,7 @@ const routes: Routers = {
|
|||||||
'stripe': indexStripe,
|
'stripe': indexStripe,
|
||||||
'terms': indexTerms,
|
'terms': indexTerms,
|
||||||
'privacy': indexPrivacy,
|
'privacy': indexPrivacy,
|
||||||
|
'upgrade': indexUpgrade,
|
||||||
|
|
||||||
'': defaultRoute,
|
'': defaultRoute,
|
||||||
};
|
};
|
||||||
|
@ -1,19 +1,25 @@
|
|||||||
<h1 class="title">Welcome to {{global.appName}}</h1>
|
<h1 class="title">Welcome to {{global.appName}}</h1>
|
||||||
<p class="subtitle">Logged in as <strong>{{global.userDisplayName}}</strong></p>
|
<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>
|
<tbody>
|
||||||
{{#userProps}}
|
{{#userProps}}
|
||||||
{{#show}}
|
{{#show}}
|
||||||
<tr>
|
<tr>
|
||||||
<td class="{{#classes}}{{.}}{{/classes}}">
|
<td class="{{#classes}}{{.}}{{/classes}} prop-name">
|
||||||
<strong>{{label}}</strong>
|
<strong>{{label}}</strong>
|
||||||
</td>
|
</td>
|
||||||
<td class="{{#classes}}{{.}}{{/classes}}">
|
<td class="{{#classes}}{{.}}{{/classes}} prop-value">
|
||||||
{{value}}
|
{{value}}
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
{{/show}}
|
{{/show}}
|
||||||
{{/userProps}}
|
{{/userProps}}
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</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}}
|
38
packages/server/src/views/index/upgrade.mustache
Normal file
38
packages/server/src/views/index/upgrade.mustache
Normal 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>
|
@ -1,9 +1,9 @@
|
|||||||
import * as fs from 'fs-extra';
|
import * as fs from 'fs-extra';
|
||||||
import { insertContentIntoFile, rootDir } from '../tool-utils';
|
import { insertContentIntoFile, rootDir } from '../tool-utils';
|
||||||
import { getPlans } from './utils/plans';
|
|
||||||
import { pressCarouselItems } from './utils/pressCarousel';
|
import { pressCarouselItems } from './utils/pressCarousel';
|
||||||
import { getMarkdownIt, loadMustachePartials, markdownToPageHtml, renderMustache } from './utils/render';
|
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 dirname = require('path').dirname;
|
||||||
const glob = require('glob');
|
const glob = require('glob');
|
||||||
const path = require('path');
|
const path = require('path');
|
||||||
|
@ -1,3 +1,5 @@
|
|||||||
|
import { Plan, StripePublicConfig } from '@joplin/lib/utils/joplinCloud';
|
||||||
|
|
||||||
export enum Env {
|
export enum Env {
|
||||||
Dev = 'dev',
|
Dev = 'dev',
|
||||||
Prod = 'prod',
|
Prod = 'prod',
|
||||||
@ -64,28 +66,8 @@ export interface TemplateParams {
|
|||||||
buildTime?: number;
|
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 {
|
export interface PlanPageParams extends TemplateParams {
|
||||||
plans: Record<string, Plan>;
|
plans: Record<string, Plan>;
|
||||||
faqHtml: string;
|
faqHtml: string;
|
||||||
stripeConfig: StripePublicConfig;
|
stripeConfig: StripePublicConfig;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface StripePublicConfig {
|
|
||||||
publishableKey: string;
|
|
||||||
basicPriceId: string;
|
|
||||||
proPriceId: string;
|
|
||||||
webhookBaseUrl: string;
|
|
||||||
}
|
|
||||||
|
Loading…
Reference in New Issue
Block a user