You've already forked joplin
mirror of
https://github.com/laurent22/joplin.git
synced 2025-08-24 20:19:10 +02:00
Compare commits
4 Commits
server-v3.
...
website_te
Author | SHA1 | Date | |
---|---|---|---|
|
362bc75c49 | ||
|
860838967c | ||
|
17896eda81 | ||
|
3bcff9b602 |
@@ -1073,6 +1073,10 @@ footer .bottom-links-row p {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.joplin-cloud-feature-list table {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.price-row .plan-type {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
@@ -6,11 +6,11 @@
|
||||
</div>
|
||||
|
||||
<div class="plan-price plan-price-monthly">
|
||||
{{priceMonthly.formattedMonthlyAmount}}<sub class="per-month"> /month</sub>
|
||||
{{priceMonthly.formattedMonthlyAmount}}<sub class="per-month"> /month{{#footnote}} (*){{/footnote}}</sub>
|
||||
</div>
|
||||
|
||||
<div class="plan-price plan-price-yearly">
|
||||
{{priceYearly.formattedMonthlyAmount}}<sub class="per-month"> /month</sub>
|
||||
{{priceYearly.formattedMonthlyAmount}}<sub class="per-month"> /month{{#footnote}} (*){{/footnote}}</sub>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -20,17 +20,19 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{{#featuresOn}}
|
||||
{{#featureLabelsOn}}
|
||||
<p><i class="fas fa-check feature feature-on"></i>{{.}}</p>
|
||||
{{/featuresOn}}
|
||||
{{/featureLabelsOn}}
|
||||
|
||||
{{#featuresOff}}
|
||||
{{#featureLabelsOff}}
|
||||
<p class="unchecked-text"><i class="fas fa-times feature feature-off"></i>{{.}}</p>
|
||||
{{/featuresOff}}
|
||||
{{/featureLabelsOff}}
|
||||
|
||||
<p class="text-center subscribe-wrapper">
|
||||
<a id="subscribeButton-{{name}}" href="{{cfaUrl}}" class="button-link btn-white subscribeButton">{{cfaLabel}}</a>
|
||||
</p>
|
||||
|
||||
{{#footnote}}<sub>(*) {{.}}</sub>{{/footnote}}
|
||||
</div>
|
||||
|
||||
<script>
|
||||
|
@@ -42,13 +42,23 @@
|
||||
{{> plan}}
|
||||
{{/plans.pro}}
|
||||
|
||||
{{#plans.business}}
|
||||
{{#plans.teams}}
|
||||
{{> plan}}
|
||||
{{/plans.business}}
|
||||
{{/plans.teams}}
|
||||
|
||||
<p class="joplin-cloud-login-info">Already have a Joplin Cloud account? <a href="https://joplincloud.com">Login now</a></p>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<div>
|
||||
<h1>Feature comparison</h1>
|
||||
<div class="joplin-cloud-feature-list">
|
||||
{{{featureListHtml}}}
|
||||
</div>
|
||||
<p> </p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
{{{faqHtml}}}
|
||||
</div>
|
||||
|
@@ -1,4 +1,26 @@
|
||||
import * as fs from 'fs-extra';
|
||||
import markdownUtils, { MarkdownTableHeader, MarkdownTableRow } from '../markdownUtils';
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
export interface Plan {
|
||||
name: string;
|
||||
@@ -7,10 +29,13 @@ export interface Plan {
|
||||
priceYearly: StripePublicConfigPrice;
|
||||
featured: boolean;
|
||||
iconName: string;
|
||||
featuresOn: string[];
|
||||
featuresOff: string[];
|
||||
featuresOn: FeatureId[];
|
||||
featuresOff: FeatureId[];
|
||||
featureLabelsOn: string[];
|
||||
featureLabelsOff: string[];
|
||||
cfaLabel: string;
|
||||
cfaUrl: string;
|
||||
footnote: string;
|
||||
}
|
||||
|
||||
export enum PricePeriod {
|
||||
@@ -40,31 +65,6 @@ export interface StripePublicConfig {
|
||||
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;
|
||||
}
|
||||
|
||||
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}€`;
|
||||
@@ -110,28 +110,181 @@ export function findPrice(prices: StripePublicConfigPrice[], query: FindPriceQue
|
||||
return output;
|
||||
}
|
||||
|
||||
const businessAccountEmailBody = `Hello,
|
||||
const features: Record<FeatureId, PlanFeature> = {
|
||||
maxItemSize: {
|
||||
title: 'Publish notes to the internet',
|
||||
basic: true,
|
||||
pro: true,
|
||||
teams: true,
|
||||
basicInfo: '10 MB per note or attachment',
|
||||
proInfo: '200 MB per note or attachment',
|
||||
teamsInfo: '200 MB per note or attachment',
|
||||
basicInfoShort: '10 MB',
|
||||
proInfoShort: '200 MB',
|
||||
teamsInfoShort: '200 MB',
|
||||
},
|
||||
maxStorage: {
|
||||
title: 'Storage space',
|
||||
basic: true,
|
||||
pro: true,
|
||||
teams: true,
|
||||
basicInfo: '1 GB storage space',
|
||||
proInfo: '200 GB storage space',
|
||||
teamsInfo: '200 GB storage space',
|
||||
basicInfoShort: '1 GB',
|
||||
proInfoShort: '200 GB',
|
||||
teamsInfoShort: '200 GB',
|
||||
},
|
||||
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: {
|
||||
title: 'Share and collaborate on a notebook',
|
||||
basic: false,
|
||||
pro: true,
|
||||
teams: true,
|
||||
},
|
||||
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,
|
||||
},
|
||||
};
|
||||
|
||||
This is an automatically generated email. The Business feature is coming soon, and in the meantime we offer a business discount if you would like to register multiple users.
|
||||
export const getFeatureIdsByPlan = (planName: PlanName, featureOn: boolean): FeatureId[] => {
|
||||
const output: FeatureId[] = [];
|
||||
|
||||
If so please let us know the following details and we will get back to you as soon as possible:
|
||||
for (const [k, v] of Object.entries(features)) {
|
||||
if (v[planName] === featureOn) {
|
||||
output.push(k);
|
||||
}
|
||||
}
|
||||
|
||||
- Name:
|
||||
return output;
|
||||
};
|
||||
|
||||
- Email:
|
||||
export const getFeatureLabelsByPlan = (planName: PlanName, featureOn: boolean): string[] => {
|
||||
const output: FeatureId[] = [];
|
||||
|
||||
- Number of users: `;
|
||||
for (const [featureId, v] of Object.entries(features)) {
|
||||
if (v[planName] === featureOn) {
|
||||
output.push(getFeatureLabel(planName, featureId));
|
||||
}
|
||||
}
|
||||
|
||||
export function getPlans(stripeConfig: StripePublicConfig): Record<string, Plan> {
|
||||
const features = {
|
||||
publishNote: 'Publish notes to the internet',
|
||||
sync: 'Sync as many devices as you want',
|
||||
clipper: 'Web Clipper',
|
||||
collaborate: 'Share and collaborate on a notebook',
|
||||
multiUsers: 'Up to 10 users',
|
||||
prioritySupport: 'Priority support',
|
||||
return output;
|
||||
};
|
||||
|
||||
export const getAllFeatureIds = (): FeatureId[] => {
|
||||
return Object.keys(features);
|
||||
};
|
||||
|
||||
export const getFeatureById = (featureId: FeatureId): PlanFeature => {
|
||||
return features[featureId];
|
||||
};
|
||||
|
||||
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 => {
|
||||
const feature = features[featureId];
|
||||
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 => {
|
||||
const feature = features[featureId];
|
||||
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 '✔️';
|
||||
};
|
||||
|
||||
for (const [, feature] of Object.entries(features)) {
|
||||
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> {
|
||||
return {
|
||||
basic: {
|
||||
name: 'basic',
|
||||
@@ -146,20 +299,13 @@ export function getPlans(stripeConfig: StripePublicConfig): Record<string, Plan>
|
||||
}),
|
||||
featured: false,
|
||||
iconName: 'basic-icon',
|
||||
featuresOn: [
|
||||
'Max 10 MB per note or attachment',
|
||||
features.publishNote,
|
||||
features.sync,
|
||||
features.clipper,
|
||||
'1 GB storage space',
|
||||
],
|
||||
featuresOff: [
|
||||
features.collaborate,
|
||||
features.multiUsers,
|
||||
features.prioritySupport,
|
||||
],
|
||||
featuresOn: getFeatureIdsByPlan(PlanName.Basic, true),
|
||||
featuresOff: getFeatureIdsByPlan(PlanName.Basic, false),
|
||||
featureLabelsOn: getFeatureLabelsByPlan(PlanName.Basic, true),
|
||||
featureLabelsOff: getFeatureLabelsByPlan(PlanName.Basic, false),
|
||||
cfaLabel: 'Try it now',
|
||||
cfaUrl: '',
|
||||
footnote: '',
|
||||
},
|
||||
|
||||
pro: {
|
||||
@@ -175,42 +321,35 @@ export function getPlans(stripeConfig: StripePublicConfig): Record<string, Plan>
|
||||
}),
|
||||
featured: true,
|
||||
iconName: 'pro-icon',
|
||||
featuresOn: [
|
||||
'Max 200 MB per note or attachment',
|
||||
features.publishNote,
|
||||
features.sync,
|
||||
features.clipper,
|
||||
'10 GB storage space',
|
||||
features.collaborate,
|
||||
],
|
||||
featuresOff: [
|
||||
features.multiUsers,
|
||||
features.prioritySupport,
|
||||
],
|
||||
featuresOn: getFeatureIdsByPlan(PlanName.Pro, true),
|
||||
featuresOff: getFeatureIdsByPlan(PlanName.Pro, false),
|
||||
featureLabelsOn: getFeatureLabelsByPlan(PlanName.Pro, true),
|
||||
featureLabelsOff: getFeatureLabelsByPlan(PlanName.Pro, false),
|
||||
cfaLabel: 'Try it now',
|
||||
cfaUrl: '',
|
||||
footnote: '',
|
||||
},
|
||||
|
||||
business: {
|
||||
name: 'business',
|
||||
title: 'Business',
|
||||
priceMonthly: { accountType: 3, formattedMonthlyAmount: '49.99€' } as any,
|
||||
priceYearly: { accountType: 3, formattedMonthlyAmount: '39.99€', formattedAmount: '479.88€' } as any,
|
||||
teams: {
|
||||
name: 'teams',
|
||||
title: 'Teams',
|
||||
priceMonthly: findPrice(stripeConfig.prices, {
|
||||
accountType: 3,
|
||||
period: PricePeriod.Monthly,
|
||||
}),
|
||||
priceYearly: findPrice(stripeConfig.prices, {
|
||||
accountType: 3,
|
||||
period: PricePeriod.Yearly,
|
||||
}),
|
||||
featured: false,
|
||||
iconName: 'business-icon',
|
||||
featuresOn: [
|
||||
'Max 200 MB per note or attachment',
|
||||
features.publishNote,
|
||||
features.sync,
|
||||
features.clipper,
|
||||
'10 GB storage space',
|
||||
features.collaborate,
|
||||
features.multiUsers,
|
||||
features.prioritySupport,
|
||||
],
|
||||
featuresOff: [],
|
||||
cfaLabel: 'Contact us',
|
||||
cfaUrl: `mailto:business@joplincloud.com?subject=${encodeURIComponent('Joplin Cloud Business Account Order')}&body=${encodeURIComponent(businessAccountEmailBody)}`,
|
||||
featuresOn: getFeatureIdsByPlan(PlanName.Teams, true),
|
||||
featuresOff: getFeatureIdsByPlan(PlanName.Teams, false),
|
||||
featureLabelsOn: getFeatureLabelsByPlan(PlanName.Teams, true),
|
||||
featureLabelsOff: getFeatureLabelsByPlan(PlanName.Teams, false),
|
||||
cfaLabel: 'Try it now',
|
||||
cfaUrl: '',
|
||||
footnote: 'Per user. Minimum of 2 users.',
|
||||
},
|
||||
};
|
||||
}
|
||||
|
@@ -2,7 +2,7 @@ import { SubPath, redirect } from '../../utils/routeUtils';
|
||||
import Router from '../../utils/Router';
|
||||
import { RouteType } from '../../utils/types';
|
||||
import { AppContext } from '../../utils/types';
|
||||
import { findPrice, getFeatureList, getPlans, PricePeriod } from '@joplin/lib/utils/joplinCloud';
|
||||
import { findPrice, PricePeriod, PlanName, getFeatureLabel, getFeatureEnabled, getAllFeatureIds } from '@joplin/lib/utils/joplinCloud';
|
||||
import config from '../../config';
|
||||
import defaultView from '../../utils/defaultView';
|
||||
import { stripeConfig, stripePriceIdByUserId, updateSubscriptionType } from '../../utils/stripe';
|
||||
@@ -28,21 +28,23 @@ router.get('upgrade', async (_path: SubPath, ctx: AppContext) => {
|
||||
proLabel: string;
|
||||
}
|
||||
|
||||
const plans = getPlans(stripeConfig());
|
||||
const basicFeatureList = getFeatureList(plans.basic);
|
||||
const proFeatureList = getFeatureList(plans.pro);
|
||||
const featureIds = getAllFeatureIds();
|
||||
|
||||
const planRows: PlanRow[] = [];
|
||||
|
||||
for (let i = 0; i < basicFeatureList.length; i++) {
|
||||
const basic = basicFeatureList[i];
|
||||
const pro = proFeatureList[i];
|
||||
for (let i = 0; i < featureIds.length; i++) {
|
||||
const featureId = featureIds[i];
|
||||
|
||||
if (basic.label === pro.label && basic.enabled === pro.enabled) continue;
|
||||
const basicLabel = getFeatureLabel(PlanName.Basic, featureId);
|
||||
const proLabel = getFeatureLabel(PlanName.Pro, featureId);
|
||||
const basicEnabled = getFeatureEnabled(PlanName.Basic, featureId);
|
||||
const proEnabled = getFeatureEnabled(PlanName.Pro, featureId);
|
||||
|
||||
if (basicLabel === proLabel && basicEnabled === proEnabled) continue;
|
||||
|
||||
planRows.push({
|
||||
basicLabel: basic.enabled ? basic.label : '-',
|
||||
proLabel: pro.label,
|
||||
basicLabel: basicEnabled ? basicLabel : '-',
|
||||
proLabel: proLabel,
|
||||
});
|
||||
}
|
||||
|
||||
|
@@ -30,6 +30,20 @@
|
||||
"period": "yearly",
|
||||
"amount": "57.48",
|
||||
"currency": "EUR"
|
||||
},
|
||||
{
|
||||
"accountType": 3,
|
||||
"id": "price_1Kl9uBLx4fybOTqJhx7q4zzj",
|
||||
"period": "monthly",
|
||||
"amount": "7.99",
|
||||
"currency": "EUR"
|
||||
},
|
||||
{
|
||||
"accountType": 3,
|
||||
"id": "price_1Kl9uNLx4fybOTqJpsB2l3Kg",
|
||||
"period": "yearly",
|
||||
"amount": "80.28",
|
||||
"currency": "EUR"
|
||||
}
|
||||
]
|
||||
},
|
||||
@@ -64,6 +78,20 @@
|
||||
"period": "yearly",
|
||||
"amount": "57.48",
|
||||
"currency": "EUR"
|
||||
},
|
||||
{
|
||||
"accountType": 3,
|
||||
"id": "price_1Kl9jyLx4fybOTqJN0i1A88B",
|
||||
"period": "monthly",
|
||||
"amount": "7.99",
|
||||
"currency": "EUR"
|
||||
},
|
||||
{
|
||||
"accountType": 3,
|
||||
"id": "price_1Kl9nLLx4fybOTqJYTtts35z",
|
||||
"period": "yearly",
|
||||
"amount": "80.28",
|
||||
"currency": "EUR"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
@@ -3,7 +3,7 @@ import { rootDir } from '../tool-utils';
|
||||
import { pressCarouselItems } from './utils/pressCarousel';
|
||||
import { getMarkdownIt, loadMustachePartials, markdownToPageHtml, renderMustache } from './utils/render';
|
||||
import { AssetUrls, Env, PlanPageParams, Sponsors, TemplateParams } from './utils/types';
|
||||
import { getPlans, loadStripeConfig } from '@joplin/lib/utils/joplinCloud';
|
||||
import { createFeatureTableMd, getPlans, loadStripeConfig } from '@joplin/lib/utils/joplinCloud';
|
||||
import { MarkdownAndFrontMatter, stripOffFrontMatter } from './utils/frontMatter';
|
||||
import { dirname, basename } from 'path';
|
||||
import { readmeFileTitle, replaceGitHubByWebsiteLinks } from './utils/parser';
|
||||
@@ -14,7 +14,7 @@ const glob = require('glob');
|
||||
const path = require('path');
|
||||
const md5File = require('md5-file/promise');
|
||||
|
||||
const env = Env.Prod;
|
||||
const env = Env.Dev;
|
||||
|
||||
const docDir = `${dirname(dirname(dirname(dirname(__dirname))))}/joplin-website/docs`;
|
||||
|
||||
@@ -281,6 +281,7 @@ async function main() {
|
||||
templateHtml: plansTemplateHtml,
|
||||
plans: getPlans(stripeConfig),
|
||||
faqHtml: planPageFaqHtml,
|
||||
featureListHtml: getMarkdownIt().render(createFeatureTableMd(), {}),
|
||||
stripeConfig,
|
||||
};
|
||||
|
||||
|
@@ -24,15 +24,11 @@ export function renderMustache(contentHtml: string, templateParams: TemplatePara
|
||||
}
|
||||
|
||||
export function getMarkdownIt() {
|
||||
return new MarkdownIt({
|
||||
const markdownIt = new MarkdownIt({
|
||||
breaks: true,
|
||||
linkify: true,
|
||||
html: true,
|
||||
});
|
||||
}
|
||||
|
||||
export function markdownToPageHtml(md: string, templateParams: TemplateParams): string {
|
||||
const markdownIt = getMarkdownIt();
|
||||
|
||||
markdownIt.core.ruler.push('tableClass', (state: any) => {
|
||||
const tokens = state.tokens;
|
||||
@@ -47,7 +43,11 @@ export function markdownToPageHtml(md: string, templateParams: TemplateParams):
|
||||
}
|
||||
});
|
||||
|
||||
markdownIt.use(headerAnchor);
|
||||
return markdownIt;
|
||||
}
|
||||
|
||||
export function markdownToPageHtml(md: string, templateParams: TemplateParams): string {
|
||||
const markdownIt = getMarkdownIt();
|
||||
markdownIt.use(headerAnchor);
|
||||
return renderMustache(markdownIt.render(md), templateParams);
|
||||
}
|
||||
|
@@ -79,5 +79,6 @@ export interface TemplateParams {
|
||||
export interface PlanPageParams extends TemplateParams {
|
||||
plans: Record<string, Plan>;
|
||||
faqHtml: string;
|
||||
featureListHtml: string;
|
||||
stripeConfig: StripePublicConfig;
|
||||
}
|
||||
|
@@ -12,10 +12,6 @@
|
||||
|
||||
Moreover, by getting a subscription you are supporting the development of the project as a whole, including the open source applications. Such support is needed in the long term to provide bug and security fixes, add new features, and provide support.
|
||||
|
||||
## What if I exceed the storage space?
|
||||
|
||||
If you exceed the storage space, you will not be able to upload new notes. You may however delete notes and attachments so as to free up space. If you are on a Basic plan, you may also upgrade to Pro. If you are on a Pro or Business plan please contact us and let us know that you need more space and we will increase your storage space.
|
||||
|
||||
## Do you offer discounts?
|
||||
|
||||
We offer a 50% Education Discount for students and teachers. To claim it, please [contact us](mailto:support@joplincloud.com) from your university or school email address. You will then receive a URL you can use to subscribe to Joplin Cloud while benefiting from the 50% discount. This is valid for a whole year and can be renewed for as long as you are in education by contacting us again.
|
||||
|
Reference in New Issue
Block a user