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' ;
2023-09-22 16:35:02 +02:00
import { htmlentities } from '@joplin/utils/html' ;
2022-04-07 16:35:15 +02:00
type FeatureId = string ;
export enum PlanName {
Basic = 'basic' ,
Pro = 'pro' ,
Teams = 'teams' ,
}
interface PlanFeature {
title : string ;
2023-09-22 16:35:02 +02:00
description? : string ;
2022-04-07 16:35:15 +02:00
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 [ ] ;
2023-09-24 14:20:22 +02:00
featureLabelsOn : string [ ] ;
2022-04-07 16:35:15 +02:00
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 [ ] ;
2023-10-06 21:53:23 +02:00
archivedPrices : 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 } ` ) ;
2023-10-06 21:53:23 +02:00
const decoratePrices = ( p : StripePublicConfigPrice ) = > {
2021-07-31 15:42:56 +02:00
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 ) ,
} ;
2023-10-06 21:53:23 +02:00
} ;
config . prices = config . prices . map ( decoratePrices ) ;
config . archivedPrices = config . archivedPrices . map ( decoratePrices ) ;
2021-07-31 15:42:56 +02:00
return config ;
}
2023-10-06 21:53:23 +02:00
export function findPrice ( config : StripePublicConfig , query : FindPriceQuery ) : StripePublicConfigPrice {
2021-07-31 15:42:56 +02:00
let output : StripePublicConfigPrice = null ;
2023-10-06 21:53:23 +02:00
for ( const prices of [ config . prices , config . archivedPrices ] ) {
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 ) break ;
2021-07-31 15:42:56 +02:00
}
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 > = > {
2023-09-22 16:35:02 +02:00
const shareNotebookTitle = _ ( 'Share a notebook with others' ) ;
2022-11-28 18:16:32 +02:00
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 ,
2023-10-23 19:18:36 +02:00
basicInfo : _ ( '%d GB storage space' , 2 ) ,
proInfo : _ ( '%d GB storage space' , 30 ) ,
teamsInfo : _ ( '%d GB storage space' , 50 ) ,
basicInfoShort : _ ( '%d GB' , 2 ) ,
proInfoShort : _ ( '%d GB' , 30 ) ,
teamsInfoShort : _ ( '%d GB' , 50 ) ,
2022-11-28 18:16:32 +02:00
} ,
publishNote : {
title : _ ( 'Publish notes to the internet' ) ,
2024-01-10 14:11:28 +02:00
description : 'You can [publish a note](https://joplinapp.org/help/apps/publish_note) from the Joplin app. You will get a link that you can share with other users, who can then view the note in their browser.' ,
2022-11-28 18:16:32 +02:00
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' ) ,
2024-01-10 14:11:28 +02:00
description : _ ( 'The [Web Clipper](%s) is a browser extension that allows you to save web pages and screenshots from your browser.' , 'https://joplinapp.org/help/apps/clipper' ) ,
2022-11-28 18:16:32 +02:00
basic : true ,
pro : true ,
teams : true ,
} ,
collaborate : {
2023-08-04 17:24:02 +02:00
title : _ ( 'Collaborate on a notebook with others' ) ,
2023-09-22 16:35:02 +02:00
description : _ ( 'This allows another user to share a notebook with you, and you can then both collaborate on it. It does not however allow you to share a notebook with someone else, unless you have the feature "%s".' , shareNotebookTitle ) ,
2023-08-04 17:24:02 +02:00
basic : true ,
pro : true ,
teams : true ,
} ,
share : {
2023-09-22 16:35:02 +02:00
title : shareNotebookTitle ,
2024-04-27 18:10:10 +02:00
description : 'You can [share a notebook](https://joplinapp.org/help/apps/share_notebook/) with other Joplin Cloud users, who can then view the notes and edit them.' ,
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' ) ,
2024-01-10 14:11:28 +02:00
description : '[Email to Note](https://joplinapp.org/help/apps/email_to_note/) allows you to save your emails in Joplin Cloud by forwarding your emails to a special email address. The subject of the email will become the note title, and the email body will become the note content.' ,
2023-07-26 19:07:29 +02:00
basic : false ,
pro : true ,
teams : true ,
} ,
2023-10-06 21:32:21 +02:00
customBanner : {
title : _ ( 'Customise the note publishing banner' ) ,
2024-01-10 14:11:28 +02:00
description : 'You can [customise the banner](https://joplinapp.org/help/apps/publish_note#customising-the-publishing-banner) that appears on top of your published notes, for example by adding a custom logo and text, and changing the banner colour.' ,
2023-10-06 21:32:21 +02:00
basic : false ,
pro : true ,
teams : true ,
} ,
2022-11-28 18:16:32 +02:00
multiUsers : {
title : _ ( 'Manage multiple users' ) ,
2024-01-10 14:11:28 +02:00
description : 'The [Teams functionality](https://joplinapp.org/help/apps/teams/) enables the efficient administration of multiple users within a team. Serving as a centralized hub, it provides an overview of all users within your organisations, facilitating easy addition or removal of members, as well as centralised billing.' ,
2022-11-28 18:16:32 +02:00
basic : false ,
pro : false ,
teams : true ,
} ,
consolidatedBilling : {
title : _ ( 'Consolidated billing' ) ,
2024-01-10 14:11:28 +02:00
description : 'Billing is consolidated, ensuring a single monthly or yearly invoice, based on your chosen plan. The billing is automatically adjusted in accordance with the number of team members' ,
2022-11-28 18:16:32 +02:00
basic : false ,
pro : false ,
teams : true ,
} ,
2023-09-22 16:35:02 +02:00
sharePermissions : {
title : _ ( 'Share permissions' ) ,
2024-01-10 14:11:28 +02:00
description : '[Share permissions](https://joplinapp.org/help/apps/share_permissions/) allow you to define whether a notebook you share with someone can be edited or is read-only. It can be useful for example to share documentation that you do not want to be modified.' ,
2022-11-28 18:16:32 +02:00
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 [ ] = > {
2023-10-06 21:32:21 +02:00
return Object . keys ( features ( ) ) ;
2022-04-07 16:35:15 +02:00
} ;
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 [ ] = [ ] ;
2023-09-22 16:35:02 +02:00
for ( const [ , v ] of Object . entries ( features ( ) ) ) {
2022-04-07 16:35:15 +02:00
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 ` ;
2024-04-05 13:16:49 +02:00
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied
2022-04-07 16:35:15 +02:00
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' ,
2023-09-22 16:35:02 +02:00
disableHtmlEscape : true ,
2022-04-07 16:35:15 +02:00
} ,
{
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 '-' ;
2024-04-05 13:16:49 +02:00
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied
2022-04-07 16:35:15 +02:00
const infoShort : string = ( feature as any ) [ ` ${ planName } InfoShort ` ] ;
if ( infoShort ) return infoShort ;
return '✔️' ;
2021-07-10 12:16:13 +02:00
} ;
2023-09-22 16:35:02 +02:00
const makeFeatureLabel = ( featureId : string , feature : PlanFeature ) = > {
const output : string [ ] = [
` ${ htmlentities ( feature . title ) } ` ,
] ;
if ( feature . description ) {
output . push ( ` <a data-id= ${ htmlentities ( featureId ) } class="feature-title" name="feature- ${ htmlentities ( featureId ) } " href="#feature- ${ htmlentities ( featureId ) } ">i</a> ` ) ;
output . push ( ` <div class="feature-description feature-description- ${ htmlentities ( featureId ) } "> ${ htmlentities ( feature . description ) } </div> ` ) ;
}
return output . join ( '' ) ;
} ;
for ( const [ id , feature ] of Object . entries ( features ( ) ) ) {
2022-04-07 16:35:15 +02:00
const row : MarkdownTableRow = {
2023-09-22 16:35:02 +02:00
featureLabel : makeFeatureLabel ( id , feature ) ,
2022-04-07 16:35:15 +02:00
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' ) ,
2023-10-06 21:53:23 +02:00
priceMonthly : findPrice ( stripeConfig , {
2021-07-31 15:42:56 +02:00
accountType : 1 ,
period : PricePeriod.Monthly ,
} ) ,
2023-10-06 21:53:23 +02:00
priceYearly : findPrice ( stripeConfig , {
2021-07-31 15:42:56 +02:00
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 ) ,
2023-09-24 14:20:22 +02:00
featureLabelsOn : getFeatureLabelsByPlan ( PlanName . Basic , true ) ,
2022-04-07 16:35:15 +02:00
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' ) ,
2023-10-06 21:53:23 +02:00
priceMonthly : findPrice ( stripeConfig , {
2021-07-31 15:42:56 +02:00
accountType : 2 ,
period : PricePeriod.Monthly ,
} ) ,
2023-10-06 21:53:23 +02:00
priceYearly : findPrice ( stripeConfig , {
2021-07-31 15:42:56 +02:00
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 ) ,
2023-09-24 14:20:22 +02:00
featureLabelsOn : getFeatureLabelsByPlan ( PlanName . Pro , true ) ,
2022-04-07 16:35:15 +02:00
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' ) ,
2023-10-06 21:53:23 +02:00
priceMonthly : findPrice ( stripeConfig , {
2022-04-07 16:35:15 +02:00
accountType : 3 ,
period : PricePeriod.Monthly ,
} ) ,
2023-10-06 21:53:23 +02:00
priceYearly : findPrice ( stripeConfig , {
2022-04-07 16:35:15 +02:00
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 ) ,
2023-09-24 14:20:22 +02:00
featureLabelsOn : getFeatureLabelsByPlan ( PlanName . Teams , true ) ,
2022-04-07 16:35:15 +02:00
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
} ,
} ;
}