Browse Source

Feature/Max plan on pricing page (#8376)

* Setup Ghostfolio Max plan

* Update changelog
pull/8368/head
Thomas Kaul 2 days ago
committed by GitHub
parent
commit
82fdf826f8
No known key found for this signature in database GPG Key ID: B5690EEEBB952194
  1. 1
      CHANGELOG.md
  2. 7
      apps/api/src/app/endpoints/info/info.service.ts
  3. 12
      apps/client/src/app/pages/pricing/pricing-page.component.ts
  4. 262
      apps/client/src/app/pages/pricing/pricing-page.html
  5. 19
      libs/common/src/lib/config.ts
  6. 1
      libs/common/src/lib/interfaces/info-item.interface.ts
  7. 1
      libs/common/src/lib/types/subscription-offer-key.type.ts

1
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

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

12
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))

262
apps/client/src/app/pages/pricing/pricing-page.html

@ -10,99 +10,8 @@
hosting infrastructure and professional data providers, and to fund
ongoing development.
</p>
<p i18n>
If you prefer to run Ghostfolio on your own infrastructure, please
find the source code and further instructions on
<a href="https://github.com/ghostfolio/ghostfolio">GitHub</a>.
</p>
</div>
<div class="row">
<div class="col-lg-4 mb-3">
<mat-card appearance="outlined" class="h-100">
<mat-card-content class="d-flex flex-column h-100">
<div class="flex-grow-1">
<h4>Open Source</h4>
<p i18n>
For tech-savvy investors who prefer to run Ghostfolio on their
own infrastructure.
</p>
<ul class="list-unstyled mb-3">
<li class="mb-2">
<ng-container>&nbsp;</ng-container>
</li>
<li class="align-items-center d-flex mb-1">
<ion-icon class="me-1" name="checkmark-circle-outline" />
<span i18n>Unlimited Transactions</span>
</li>
<li class="align-items-center d-flex mb-1">
<ion-icon class="me-1" name="checkmark-circle-outline" />
<span i18n>Unlimited Accounts</span>
</li>
<li class="align-items-center d-flex mb-1">
<ion-icon class="me-1" name="checkmark-circle-outline" />
<span i18n>Portfolio Performance</span>
</li>
<li class="align-items-center d-flex mb-1">
<ion-icon class="me-1" name="checkmark-circle-outline" />
<span i18n>Portfolio Summary</span>
</li>
<li class="align-items-center d-flex mb-1">
<ion-icon class="me-1" name="checkmark-circle-outline" />
<span i18n>Portfolio Allocations</span>
</li>
<li class="align-items-center d-flex mb-1">
<ion-icon class="me-1" name="checkmark-circle-outline" />
<span i18n>Performance Benchmarks</span>
</li>
<li class="align-items-center d-flex mb-1">
<ion-icon class="me-1" name="checkmark-circle-outline" />
<span i18n>FIRE Calculator</span>
</li>
<li class="align-items-center d-flex mb-1">
<ion-icon class="me-1" name="checkmark-circle-outline" />
<span>Ghostfolio MCP</span>
</li>
<li class="align-items-center d-flex mb-1">
<ion-icon class="me-1" name="checkmark-circle-outline" />
<span i18n>Data Import and Export</span>
<span
class="align-items-center d-flex ms-1"
matTooltipPosition="above"
[matTooltip]="importAndExportTooltipOSS"
>
<ion-icon name="information-circle-outline" />
</span>
</li>
<li class="align-items-center d-flex mb-1">
<ion-icon class="me-1" name="checkmark-circle-outline" />
<a i18n [routerLink]="routerLinkFeatures"
>and more Features...</a
>
</li>
<li class="align-items-center d-flex mb-1">
<ion-icon class="me-1" name="checkmark-circle-outline" />
<span i18n>Community Support</span>
</li>
</ul>
</div>
<p i18n>Self-hosted, update manually.</p>
<p class="h5 text-end" i18n>Free</p>
@if (user?.subscription?.type === 'Basic') {
<div class="d-none d-lg-block invisible mt-3 text-center">
<a color="primary" mat-flat-button>&nbsp;</a>
<p class="m-0 text-body-secondary">
<small>&nbsp;</small>
</p>
</div>
}
@if (durationExtension) {
<div class="d-none d-lg-block invisible mt-3">
<div class="badge p-3 w-100">&nbsp;</div>
</div>
}
</mat-card-content>
</mat-card>
</div>
<div class="justify-content-center row">
<div class="col-lg-4 mb-3">
<mat-card
appearance="outlined"
@ -124,7 +33,9 @@
</p>
<ul class="list-unstyled mb-3">
<li class="mb-2">
<ng-container>&nbsp;</ng-container>
<ng-container i18n
>Fully managed cloud offering, with</ng-container
>:
</li>
<li class="align-items-center d-flex mb-1">
<ion-icon class="me-1" name="checkmark-circle-outline" />
@ -151,7 +62,6 @@
</li>
</ul>
</div>
<p i18n>Fully managed Ghostfolio cloud offering.</p>
<p class="h5 text-end" i18n>Free</p>
@if (user?.subscription?.type === 'Basic') {
<div class="d-none d-lg-block invisible mt-3 text-center">
@ -259,7 +169,6 @@
</li>
</ul>
</div>
<p i18n>Fully managed Ghostfolio cloud offering.</p>
@if (price) {
<p class="h5 text-end">
<span class="fw-normal">
@ -314,14 +223,71 @@
</mat-card-content>
</mat-card>
</div>
@if (priceMax) {
<div class="col-lg-4 mb-3">
<mat-card appearance="outlined" class="h-100">
<mat-card-content class="d-flex flex-column h-100">
<div class="flex-grow-1">
<h4 class="mb-2">Max</h4>
<p i18n>For investors who manage several portfolios.</p>
<ul class="list-unstyled mb-3">
<li class="mb-2">
<ng-container i18n
>Everything in <strong>Premium</strong>,
plus</ng-container
>:
</li>
<li class="align-items-center d-flex mb-1">
<ion-icon class="me-1" name="checkmark-circle-outline" />
<span i18n>Up to 5 User Accounts</span>
</li>
<li class="align-items-center d-flex mb-1">
<ion-icon class="me-1" name="checkmark-circle-outline" />
<span i18n>Centralized User Management</span>
</li>
</ul>
</div>
<p class="h5 text-end">
<span class="fw-normal">
{{ baseCurrency }}&nbsp;<strong>{{ priceMax }}</strong>
<span>&nbsp;</span>
<span i18n>per year</span></span
>
</p>
@if (
hasPermissionToUpdateUserSettings &&
user?.subscription?.type === 'Basic'
) {
<div class="mt-3 text-center">
<a
color="primary"
mat-stroked-button
[href]="maxPlanMailHref"
>
<ng-container i18n>Get Max</ng-container>
</a>
<p class="invisible m-0">
<small>&nbsp;</small>
</p>
</div>
}
@if (durationExtension) {
<div class="d-none d-lg-block invisible mt-3">
<div class="badge p-3 w-100">&nbsp;</div>
</div>
}
</mat-card-content>
</mat-card>
</div>
}
</div>
@if (user?.referralPartners?.length) {
<div class="row">
<div class="col mt-3">
<div class="col mb-2 mt-2">
<p>
<ng-container i18n>If you plan to open an account at</ng-container>
<ng-container i18n
>If you plan to open an account at</ng-container
>
<ng-container>&nbsp;</ng-container>
@for (
partner of user.referralPartners;
@ -349,21 +315,26 @@
>
<ng-container>&nbsp;</ng-container>
<ng-container i18n
>to use our referral link and get a Ghostfolio Premium membership
for one year</ng-container
>. <ng-container i18n>Looking for a student discount?</ng-container>
>to use our referral link and get a Ghostfolio Premium
membership for one year</ng-container
>.
<ng-container i18n>Looking for a student discount?</ng-container>
<ng-container>&nbsp;</ng-container>
<ng-container i18n>Request it</ng-container>
<ng-container>&nbsp;</ng-container>
<a href="mailto:hi@ghostfol.io?subject=Student Discount" i18n>here</a>
<a href="mailto:hi@ghostfol.io?subject=Student Discount" i18n
>here</a
>
<ng-container>&nbsp;</ng-container>
<ng-container i18n>with your university e-mail address</ng-container>.
<ng-container i18n
>with your university e-mail address</ng-container
>.
</p>
</div>
</div>
} @else if (hasPermissionToCreateUser && !user) {
<div class="row">
<div class="col mt-3 text-center">
<div class="col mb-3 mt-2 text-center">
<a
color="primary"
i18n
@ -371,8 +342,91 @@
[routerLink]="routerLinkRegister"
>Get Started</a
>
<p class="m-0 text-body-secondary"><small i18n>It’s free.</small></p>
<p class="m-0 text-body-secondary">
<small i18n>It’s free.</small>
</p>
</div>
</div>
}
<div class="row">
<div class="col mb-3">
<mat-card appearance="outlined">
<mat-card-content>
<h4>Open Source</h4>
<p i18n>
For tech-savvy investors who prefer to run Ghostfolio on their
own infrastructure.
</p>
<ul
class="list-unstyled mb-3 row row-cols-1 row-cols-lg-3 row-cols-md-2"
>
<li class="align-items-center d-flex mb-1">
<ion-icon class="me-1" name="checkmark-circle-outline" />
<span i18n>Unlimited Transactions</span>
</li>
<li class="align-items-center d-flex mb-1">
<ion-icon class="me-1" name="checkmark-circle-outline" />
<span i18n>Unlimited Accounts</span>
</li>
<li class="align-items-center d-flex mb-1">
<ion-icon class="me-1" name="checkmark-circle-outline" />
<span i18n>Portfolio Performance</span>
</li>
<li class="align-items-center d-flex mb-1">
<ion-icon class="me-1" name="checkmark-circle-outline" />
<span i18n>Portfolio Summary</span>
</li>
<li class="align-items-center d-flex mb-1">
<ion-icon class="me-1" name="checkmark-circle-outline" />
<span i18n>Portfolio Allocations</span>
</li>
<li class="align-items-center d-flex mb-1">
<ion-icon class="me-1" name="checkmark-circle-outline" />
<span i18n>Performance Benchmarks</span>
</li>
<li class="align-items-center d-flex mb-1">
<ion-icon class="me-1" name="checkmark-circle-outline" />
<span i18n>FIRE Calculator</span>
</li>
<li class="align-items-center d-flex mb-1">
<ion-icon class="me-1" name="checkmark-circle-outline" />
<span>Ghostfolio MCP</span>
</li>
<li class="align-items-center d-flex mb-1">
<ion-icon class="me-1" name="checkmark-circle-outline" />
<span i18n>Data Import and Export</span>
<span
class="align-items-center d-flex ms-1"
matTooltipPosition="above"
[matTooltip]="importAndExportTooltipOSS"
>
<ion-icon name="information-circle-outline" />
</span>
</li>
<li class="align-items-center d-flex mb-1">
<ion-icon class="me-1" name="checkmark-circle-outline" />
<a i18n [routerLink]="routerLinkFeatures"
>and more Features...</a
>
</li>
<li class="align-items-center d-flex mb-1">
<ion-icon class="me-1" name="checkmark-circle-outline" />
<span i18n>Community Support</span>
</li>
</ul>
<div class="align-items-center d-flex justify-content-between">
<p class="m-0" i18n>Self-hosted, update manually.</p>
<p class="h5 m-0" i18n>Free</p>
</div>
<hr />
<p class="m-0" i18n>
Please find the source code and further instructions on
<a href="https://github.com/ghostfolio/ghostfolio">GitHub</a>.
</p>
</mat-card-content>
</mat-card>
</div>
</div>
</div>
</div>
</div>

19
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';

1
libs/common/src/lib/interfaces/info-item.interface.ts

@ -15,4 +15,5 @@ export interface InfoItem {
isReadOnlyMode?: boolean;
statistics: Statistics;
subscriptionOffer?: SubscriptionOffer;
subscriptionOfferMax?: SubscriptionOffer;
}

1
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'

Loading…
Cancel
Save