From 82fdf826f8113c9a717bf319bbb0f427a55787b3 Mon Sep 17 00:00:00 2001 From: Thomas Kaul <4159106+dtslvr@users.noreply.github.com> Date: Mon, 2 Nov 2026 20:22:19 +0100 Subject: [PATCH] Feature/Max plan on pricing page (#8376) * Setup Ghostfolio Max plan * Update changelog --- CHANGELOG.md | 1 + .../src/app/endpoints/info/info.service.ts | 7 +- .../pages/pricing/pricing-page.component.ts | 12 +- .../src/app/pages/pricing/pricing-page.html | 358 ++++++++++-------- libs/common/src/lib/config.ts | 19 + .../src/lib/interfaces/info-item.interface.ts | 1 + .../lib/types/subscription-offer-key.type.ts | 1 + 7 files changed, 243 insertions(+), 156 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 9a792bb55e..2f63aac4b3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Changed +- Improved the content of the pricing page - Upgraded `stripe` from version `23.0.0` to `23.0.1` ### Fixed diff --git a/apps/api/src/app/endpoints/info/info.service.ts b/apps/api/src/app/endpoints/info/info.service.ts index 666b11dd1c..ecff1c16b5 100644 --- a/apps/api/src/app/endpoints/info/info.service.ts +++ b/apps/api/src/app/endpoints/info/info.service.ts @@ -109,14 +109,16 @@ export class InfoService { isUserSignupEnabled, latestFearAndGreedStocksMarketData, statistics, - subscriptionOffer + subscriptionOffer, + subscriptionOfferMax ] = await Promise.all([ this.benchmarkService.getBenchmarkAssetProfiles(), this.getDemoAuthToken(), this.propertyService.isUserSignupEnabled(), latestFearAndGreedStocksMarketDataPromise, this.getStatistics(), - this.subscriptionService.getSubscriptionOffer({ key: 'default' }) + this.subscriptionService.getSubscriptionOffer({ key: 'default' }), + this.subscriptionService.getSubscriptionOffer({ key: 'max' }) ]); if (isUserSignupEnabled) { @@ -131,6 +133,7 @@ export class InfoService { isReadOnlyMode, statistics, subscriptionOffer, + subscriptionOfferMax, baseCurrency: DEFAULT_CURRENCY, currencies: this.exchangeRateDataService.getCurrencies(), fearAndGreedStocksMarketPrice: diff --git a/apps/client/src/app/pages/pricing/pricing-page.component.ts b/apps/client/src/app/pages/pricing/pricing-page.component.ts index 8b4e07ffb7..4a2fd22a7b 100644 --- a/apps/client/src/app/pages/pricing/pricing-page.component.ts +++ b/apps/client/src/app/pages/pricing/pricing-page.component.ts @@ -1,4 +1,5 @@ import { UserService } from '@ghostfolio/client/services/user/user.service'; +import { MAX_PLAN_MAIL_HREF } from '@ghostfolio/common/config'; import { User } from '@ghostfolio/common/interfaces'; import { hasPermission, permissions } from '@ghostfolio/common/permissions'; import { publicRoutes } from '@ghostfolio/common/routes/routes'; @@ -64,7 +65,9 @@ export class GfPricingPageComponent implements OnInit { ); protected label: string | undefined; + protected readonly maxPlanMailHref = MAX_PLAN_MAIL_HREF; protected price: number | undefined; + protected priceMax: number | undefined; protected readonly professionalDataProviderTooltipPremium = translate( 'PROFESSIONAL_DATA_PROVIDER_TOOLTIP_PREMIUM' @@ -92,8 +95,12 @@ export class GfPricingPageComponent implements OnInit { } public ngOnInit() { - const { baseCurrency, globalPermissions, subscriptionOffer } = - this.dataService.fetchInfo(); + const { + baseCurrency, + globalPermissions, + subscriptionOffer, + subscriptionOfferMax + } = this.dataService.fetchInfo(); this.baseCurrency = baseCurrency; this.coupon = subscriptionOffer?.coupon; @@ -106,6 +113,7 @@ export class GfPricingPageComponent implements OnInit { this.label = subscriptionOffer?.label; this.price = subscriptionOffer?.price; + this.priceMax = subscriptionOfferMax?.price; this.userService.stateChanged .pipe(takeUntilDestroyed(this.destroyRef)) diff --git a/apps/client/src/app/pages/pricing/pricing-page.html b/apps/client/src/app/pages/pricing/pricing-page.html index a8b36ea0e8..7cdaf6c89f 100644 --- a/apps/client/src/app/pages/pricing/pricing-page.html +++ b/apps/client/src/app/pages/pricing/pricing-page.html @@ -10,99 +10,8 @@ hosting infrastructure and professional data providers, and to fund ongoing development.

-

- If you prefer to run Ghostfolio on your own infrastructure, please - find the source code and further instructions on - GitHub. -

-
-
- - -
-

Open Source

-

- For tech-savvy investors who prefer to run Ghostfolio on their - own infrastructure. -

-
    -
  • -   -
  • -
  • - - Unlimited Transactions -
  • -
  • - - Unlimited Accounts -
  • -
  • - - Portfolio Performance -
  • -
  • - - Portfolio Summary -
  • -
  • - - Portfolio Allocations -
  • -
  • - - Performance Benchmarks -
  • -
  • - - FIRE Calculator -
  • -
  • - - Ghostfolio MCP -
  • -
  • - - Data Import and Export - - - -
  • -
  • - - and more Features... -
  • -
  • - - Community Support -
  • -
-
-

Self-hosted, update manually.

-

Free

- @if (user?.subscription?.type === 'Basic') { - - } - @if (durationExtension) { - - } -
-
-
+
  • -   + Fully managed cloud offering, with:
  • @@ -151,7 +62,6 @@
-

Fully managed Ghostfolio cloud offering.

Free

@if (user?.subscription?.type === 'Basic') { -

Fully managed Ghostfolio cloud offering.

@if (price) {

@@ -314,65 +223,210 @@

+ @if (priceMax) { +
+ + +
+

Max

+

For investors who manage several portfolios.

+
    +
  • + Everything in Premium, + plus: +
  • +
  • + + Up to 5 User Accounts +
  • +
  • + + Centralized User Management +
  • +
+
+

+ + {{ baseCurrency }} {{ priceMax }} +   + per year +

+ @if ( + hasPermissionToUpdateUserSettings && + user?.subscription?.type === 'Basic' + ) { +
+ + Get Max + + +
+ } + @if (durationExtension) { + + } +
+
+
+ }
- - - @if (user?.referralPartners?.length) { -
-
-

- If you plan to open an account at -   - @for ( - partner of user.referralPartners; - track partner.name; - let i = $index; - let last = $last - ) { - {{ partner.name }} - @if (last) { - , - } @else { - @if (i === user.referralPartners.length - 2) { -   - or -   - } @else { - , + @if (user?.referralPartners?.length) { +

+
+

+ If you plan to open an account at +   + @for ( + partner of user.referralPartners; + track partner.name; + let i = $index; + let last = $last + ) { + {{ partner.name }} + @if (last) { + , + } @else { + @if (i === user.referralPartners.length - 2) { +   + or +   + } @else { + , + } + } } - } - } - please -   - contact us -   - to use our referral link and get a Ghostfolio Premium membership - for one year. Looking for a student discount? -   - Request it -   - here -   - with your university e-mail address. -

-
-
- } @else if (hasPermissionToCreateUser && !user) { -
-
- Get Started -

It’s free.

+ please +   + contact us +   + to use our referral link and get a Ghostfolio Premium + membership for one year. + Looking for a student discount? +   + Request it +   + here +   + with your university e-mail address. +

+
+
+ } @else if (hasPermissionToCreateUser && !user) { +
+
+ Get Started +

+ It’s free. +

+
+
+ } +
+
+ + +

Open Source

+

+ For tech-savvy investors who prefer to run Ghostfolio on their + own infrastructure. +

+
    +
  • + + Unlimited Transactions +
  • +
  • + + Unlimited Accounts +
  • +
  • + + Portfolio Performance +
  • +
  • + + Portfolio Summary +
  • +
  • + + Portfolio Allocations +
  • +
  • + + Performance Benchmarks +
  • +
  • + + FIRE Calculator +
  • +
  • + + Ghostfolio MCP +
  • +
  • + + Data Import and Export + + + +
  • +
  • + + and more Features... +
  • +
  • + + Community Support +
  • +
+
+

Self-hosted, update manually.

+

Free

+
+
+

+ Please find the source code and further instructions on + GitHub. +

+
+
+
- } +
diff --git a/libs/common/src/lib/config.ts b/libs/common/src/lib/config.ts index 31caf6d3a1..7ce34c4eee 100644 --- a/libs/common/src/lib/config.ts +++ b/libs/common/src/lib/config.ts @@ -291,6 +291,25 @@ export const HEADER_KEY_SKIP_INTERCEPTOR = 'X-Skip-Interceptor'; export const HTTP_RESPONSE_MESSAGE_IMPERSONATION_UNRESOLVED = 'The impersonation identifier cannot be resolved'; +export const MAX_PLAN_MAIL_HREF = `mailto:hi@ghostfol.io?subject=${encodeURIComponent( + 'Ghostfolio Max' +)}&body=${[ + 'Hello,', + '', + 'I am interested in Ghostfolio Max.', + '', + 'Who will use it (me, family, friends, family office, wealth manager, other):', + 'Number of persons or clients:', + 'How I track their portfolios today:', + 'Main reason to choose this plan:', + '', + 'Kind regards' +] + .map((line) => { + return encodeURIComponent(line); + }) + .join(E_MAIL_LINE_BREAK)}`; + export const MAX_TOP_HOLDINGS = 50; export const MCP_ENDPOINT = '/mcp'; diff --git a/libs/common/src/lib/interfaces/info-item.interface.ts b/libs/common/src/lib/interfaces/info-item.interface.ts index 355b19203c..a681f923f4 100644 --- a/libs/common/src/lib/interfaces/info-item.interface.ts +++ b/libs/common/src/lib/interfaces/info-item.interface.ts @@ -15,4 +15,5 @@ export interface InfoItem { isReadOnlyMode?: boolean; statistics: Statistics; subscriptionOffer?: SubscriptionOffer; + subscriptionOfferMax?: SubscriptionOffer; } diff --git a/libs/common/src/lib/types/subscription-offer-key.type.ts b/libs/common/src/lib/types/subscription-offer-key.type.ts index 89322a4002..e95ec93b57 100644 --- a/libs/common/src/lib/types/subscription-offer-key.type.ts +++ b/libs/common/src/lib/types/subscription-offer-key.type.ts @@ -1,5 +1,6 @@ export type SubscriptionOfferKey = | 'default' + | 'max' | 'renewal' | 'renewal-early-bird-2023' | 'renewal-early-bird-2024'