Feature/add interstitial for subscription (#1637)

* Add interstitial

* Improve pricing page

* Update changelog
pull/1627/head^2
Thomas Kaul 1 year ago committed by GitHub
parent 662231e830
commit 5d8a50a80d
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23

@ -9,10 +9,12 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
### Added
- Added an interstitial for the subscription
- Added a quote to the blog post _Ghostfolio auf Sackgeld.com vorgestellt_
### Changed
- Improved the pricing page
- Upgraded `Node.js` from version `16` to `18` (`Dockerfile`)
- Upgraded `prisma` from version `4.8.0` to `4.9.0`

@ -97,6 +97,7 @@ export class UserService {
const {
accessToken,
Account,
Analytics,
authChallenge,
createdAt,
id,
@ -107,7 +108,12 @@ export class UserService {
thirdPartyId,
updatedAt
} = await this.prismaService.user.findUnique({
include: { Account: true, Settings: true, Subscription: true },
include: {
Account: true,
Analytics: true,
Settings: true,
Subscription: true
},
where: userWhereUniqueInput
});
@ -121,7 +127,8 @@ export class UserService {
role,
Settings,
thirdPartyId,
updatedAt
updatedAt,
activityCount: Analytics?.activityCount
};
if (user?.Settings) {
@ -154,15 +161,22 @@ export class UserService {
(user.Settings.settings as UserSettings).viewMode = 'DEFAULT';
}
let currentPermissions = getPermissions(user.role);
if (this.configurationService.get('ENABLE_FEATURE_SUBSCRIPTION')) {
user.subscription =
this.subscriptionService.getSubscription(Subscription);
}
let currentPermissions = getPermissions(user.role);
if (
Analytics?.activityCount % 25 === 0 &&
user.subscription?.type === 'Basic'
) {
currentPermissions.push(permissions.enableSubscriptionInterstitial);
}
if (user.subscription?.type === 'Premium') {
currentPermissions.push(permissions.reportDataGlitch);
if (user.subscription?.type === 'Premium') {
currentPermissions.push(permissions.reportDataGlitch);
}
}
if (this.configurationService.get('ENABLE_FEATURE_READ_ONLY_MODE')) {

@ -25,6 +25,7 @@ import { DateFormats } from './adapter/date-formats';
import { AppRoutingModule } from './app-routing.module';
import { AppComponent } from './app.component';
import { GfHeaderModule } from './components/header/header.module';
import { GfSubscriptionInterstitialDialogModule } from './components/subscription-interstitial-dialog/subscription-interstitial-dialog.module';
import { authInterceptorProviders } from './core/auth.interceptor';
import { httpResponseInterceptorProviders } from './core/http-response.interceptor';
import { LanguageService } from './core/language.service';
@ -40,6 +41,7 @@ export function NgxStripeFactory(): string {
BrowserAnimationsModule,
BrowserModule,
GfHeaderModule,
GfSubscriptionInterstitialDialogModule,
HttpClientModule,
MarkdownModule.forRoot(),
MatAutocompleteModule,

@ -36,7 +36,7 @@ export class MarketDataDetailDialog implements OnDestroy {
this.dateAdapter.setLocale(this.locale);
}
public onCancel(): void {
public onCancel() {
this.dialogRef.close({ withRefresh: false });
}

@ -0,0 +1 @@
export interface SubscriptionInterstitialDialogParams {}

@ -0,0 +1,22 @@
import { ChangeDetectionStrategy, Component, Inject } from '@angular/core';
import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog';
import { SubscriptionInterstitialDialogParams } from './interfaces/interfaces';
@Component({
changeDetection: ChangeDetectionStrategy.OnPush,
host: { class: 'd-flex flex-column flex-grow-1 h-100' },
selector: 'gf-subscription-interstitial-dialog',
styleUrls: ['./subscription-interstitial-dialog.scss'],
templateUrl: 'subscription-interstitial-dialog.html'
})
export class SubscriptionInterstitialDialog {
public constructor(
@Inject(MAT_DIALOG_DATA) public data: SubscriptionInterstitialDialogParams,
public dialogRef: MatDialogRef<SubscriptionInterstitialDialog>
) {}
public onCancel() {
this.dialogRef.close({});
}
}

@ -0,0 +1,42 @@
<h1 class="align-items-center d-flex" mat-dialog-title>
<span>Ghostfolio Premium</span>
<gf-premium-indicator class="ml-1"></gf-premium-indicator>
</h1>
<div class="flex-grow-1" mat-dialog-content>
<p class="h5" i18n>
Are you an ambitious investor who needs the full picture?
</p>
<p i18n>
By upgrading to Ghostfolio Premium, you will get these additional features:
</p>
<ul class="list-unstyled mb-3">
<li class="align-items-center d-flex mb-1">
<ion-icon class="mr-1" name="checkmark-circle-outline"></ion-icon>
<span i18n>Portfolio Summary</span>
</li>
<li class="align-items-center d-flex mb-1">
<ion-icon class="mr-1" name="checkmark-circle-outline"></ion-icon>
<span i18n>Performance Benchmarks</span>
</li>
<li class="align-items-center d-flex mb-1">
<ion-icon class="mr-1" name="checkmark-circle-outline"></ion-icon>
<span i18n>Allocations</span>
</li>
<li class="align-items-center d-flex mb-1">
<ion-icon class="mr-1" name="checkmark-circle-outline"></ion-icon>
<span i18n>FIRE Calculator</span>
</li>
<li class="align-items-center d-flex mb-1">
<ion-icon class="mr-1" name="checkmark-circle-outline"></ion-icon>
<a i18n [routerLink]="['/features']">and more Features...</a>
</li>
</ul>
<p>Refine your personal investment strategy now.</p>
</div>
<div class="justify-content-end" mat-dialog-actions>
<button i18n mat-button (click)="onCancel()">Skip</button>
<a color="primary" mat-flat-button [routerLink]="['/pricing']">
<span i18n>Upgrade Plan</span>
<ion-icon class="ml-1" name="arrow-forward-outline"></ion-icon>
</a>
</div>

@ -0,0 +1,21 @@
import { CommonModule } from '@angular/common';
import { CUSTOM_ELEMENTS_SCHEMA, NgModule } from '@angular/core';
import { MatButtonModule } from '@angular/material/button';
import { MatDialogModule } from '@angular/material/dialog';
import { RouterModule } from '@angular/router';
import { GfPremiumIndicatorModule } from '@ghostfolio/ui/premium-indicator';
import { SubscriptionInterstitialDialog } from './subscription-interstitial-dialog.component';
@NgModule({
declarations: [SubscriptionInterstitialDialog],
imports: [
CommonModule,
GfPremiumIndicatorModule,
MatButtonModule,
MatDialogModule,
RouterModule
],
schemas: [CUSTOM_ELEMENTS_SCHEMA]
})
export class GfSubscriptionInterstitialDialogModule {}

@ -0,0 +1,11 @@
:host {
display: block;
.mat-dialog-content {
max-height: unset;
ion-icon[name='checkmark-circle-outline'] {
color: rgba(var(--palette-accent-500), 1);
}
}
}

@ -26,7 +26,7 @@ export class CreateOrUpdateAccessDialog implements OnDestroy {
ngOnInit() {}
public onCancel(): void {
public onCancel() {
this.dialogRef.close();
}

@ -36,7 +36,7 @@ export class CreateOrUpdateAccountDialog implements OnDestroy {
this.platforms = platforms;
}
public onCancel(): void {
public onCancel() {
this.dialogRef.close();
}

@ -80,7 +80,7 @@ export class ImportActivitiesDialog implements OnDestroy {
}
}
public onCancel(): void {
public onCancel() {
this.dialogRef.close();
}

@ -31,50 +31,71 @@
<mat-card class="d-flex flex-column h-100">
<div class="flex-grow-1">
<h4>Open Source</h4>
<p>
For tech-savvy investors who prefer to run
<strong>Ghostfolio</strong> on their own infrastructure.
<p i18n>
For tech-savvy investors who prefer to run Ghostfolio on their
own infrastructure.
</p>
<ul class="list-unstyled mb-3">
<li class="align-items-center d-flex mb-1">
<ion-icon
class="mr-1 text-muted"
class="mr-1"
name="checkmark-circle-outline"
></ion-icon>
<span>Unlimited Transactions</span>
<span i18n>Unlimited Transactions</span>
</li>
<li class="align-items-center d-flex mb-1">
<ion-icon
class="mr-1 text-muted"
class="mr-1"
name="checkmark-circle-outline"
></ion-icon>
<span>Portfolio Performance</span>
<span i18n>Unlimited Accounts</span>
</li>
<li class="align-items-center d-flex mb-1">
<ion-icon
class="mr-1 text-muted"
class="mr-1"
name="checkmark-circle-outline"
></ion-icon>
<span>Zen Mode</span>
<span i18n>Portfolio Performance</span>
</li>
<li class="align-items-center d-flex mb-1">
<ion-icon
class="mr-1 text-muted"
class="mr-1"
name="checkmark-circle-outline"
></ion-icon>
<span>Portfolio Summary</span>
<span i18n>Portfolio Summary</span>
</li>
<li class="align-items-center d-flex mb-1">
<ion-icon
class="mr-1 text-muted"
class="mr-1"
name="checkmark-circle-outline"
></ion-icon>
<span>Advanced Insights</span>
<span i18n>Performance Benchmarks</span>
</li>
<li class="align-items-center d-flex mb-1">
<ion-icon
class="mr-1"
name="checkmark-circle-outline"
></ion-icon>
<span i18n>Allocations</span>
</li>
<li class="align-items-center d-flex mb-1">
<ion-icon
class="mr-1"
name="checkmark-circle-outline"
></ion-icon>
<span i18n>FIRE Calculator</span>
</li>
<li class="align-items-center d-flex mb-1">
<ion-icon
class="mr-1"
name="checkmark-circle-outline"
></ion-icon>
<a i18n [routerLink]="['/features']">and more Features...</a>
</li>
</ul>
</div>
<p>Self-hosted, update manually.</p>
<p class="h5 text-right">Free</p>
<p i18n>Self-hosted, update manually.</p>
<p class="h5 text-right" i18n>Free</p>
<div
*ngIf="user?.subscription?.type === 'Basic'"
class="d-none d-lg-block hidden mt-3 text-center"
@ -92,31 +113,54 @@
[ngClass]="{ 'active': user?.subscription?.type === 'Basic' }"
>
<div class="flex-grow-1">
<h4 class="align-items-center d-flex">Basic</h4>
<p>
<div class="align-items-center d-flex mb-2">
<h4 class="flex-grow-1 m-0">Basic</h4>
<div *ngIf="user?.subscription?.type === 'Basic'">
<ion-icon class="mr-1" name="checkmark-outline"></ion-icon>
</div>
</div>
<p i18n>
For new investors who are just getting started with trading.
</p>
<ul class="list-unstyled mb-3">
<li class="align-items-center d-flex mb-1">
<ion-icon
class="mr-1 text-muted"
class="mr-1"
name="checkmark-circle-outline"
></ion-icon>
<span>Unlimited Transactions</span>
<span i18n>Unlimited Transactions</span>
</li>
<li class="align-items-center d-flex mb-1">
<ion-icon
class="mr-1 text-muted"
class="mr-1"
name="checkmark-circle-outline"
></ion-icon>
<span>Portfolio Performance</span>
<span i18n>Unlimited Accounts</span>
</li>
<li class="align-items-center d-flex mb-1">
<ion-icon
class="mr-1 text-muted"
class="mr-1"
name="checkmark-circle-outline"
></ion-icon>
<span i18n>Portfolio Performance</span>
</li>
<li>
<ion-icon
class="invisible"
name="checkmark-circle-outline"
></ion-icon>
</li>
<li>
<ion-icon
class="invisible"
name="checkmark-circle-outline"
></ion-icon>
</li>
<li>
<ion-icon
class="invisible"
name="checkmark-circle-outline"
></ion-icon>
<span>Zen Mode</span>
</li>
<li>
<ion-icon
@ -132,8 +176,8 @@
</li>
</ul>
</div>
<p>Fully managed <strong>Ghostfolio</strong> cloud offering.</p>
<p class="h5 text-right">Free</p>
<p i18n>Fully managed Ghostfolio cloud offering.</p>
<p class="h5 text-right" i18n>Free</p>
<div
*ngIf="user?.subscription?.type === 'Basic'"
class="d-none d-lg-block hidden mt-3 text-center"
@ -151,56 +195,82 @@
[ngClass]="{ 'active': user?.subscription?.type === 'Premium' }"
>
<div class="flex-grow-1">
<h4 class="align-items-center d-flex">
<span>Premium</span>
<gf-premium-indicator
class="ml-1"
[enableLink]="false"
></gf-premium-indicator>
</h4>
<p>
<div class="align-items-center d-flex mb-2">
<h4 class="align-items-center d-flex flex-grow-1 m-0">
<span>Premium</span>
<gf-premium-indicator
class="ml-1"
[enableLink]="false"
></gf-premium-indicator>
</h4>
<div *ngIf="user?.subscription?.type === 'Premium'">
<ion-icon class="mr-1" name="checkmark-outline"></ion-icon>
</div>
</div>
<p i18n>
For ambitious investors who need the full picture of their
financial assets.
</p>
<ul class="list-unstyled mb-3">
<li class="align-items-center d-flex mb-1">
<ion-icon
class="mr-1 text-muted"
class="mr-1"
name="checkmark-circle-outline"
></ion-icon>
<span i18n>Unlimited Transactions</span>
</li>
<li class="align-items-center d-flex mb-1">
<ion-icon
class="mr-1"
name="checkmark-circle-outline"
></ion-icon>
<span i18n>Unlimited Accounts</span>
</li>
<li class="align-items-center d-flex mb-1">
<ion-icon
class="mr-1"
name="checkmark-circle-outline"
></ion-icon>
<span>Unlimited Transactions</span>
<span i18n>Portfolio Performance</span>
</li>
<li class="align-items-center d-flex mb-1">
<ion-icon
class="mr-1 text-muted"
class="mr-1"
name="checkmark-circle-outline"
></ion-icon>
<span>Portfolio Performance</span>
<span i18n>Portfolio Summary</span>
</li>
<li class="align-items-center d-flex mb-1">
<ion-icon
class="mr-1 text-muted"
class="mr-1"
name="checkmark-circle-outline"
></ion-icon>
<span>Zen Mode</span>
<span i18n>Performance Benchmarks</span>
</li>
<li class="align-items-center d-flex mb-1">
<ion-icon
class="mr-1 text-muted"
class="mr-1"
name="checkmark-circle-outline"
></ion-icon>
<span>Portfolio Summary</span>
<span i18n>Allocations</span>
</li>
<li class="align-items-center d-flex mb-1">
<ion-icon
class="mr-1 text-muted"
class="mr-1"
name="checkmark-circle-outline"
></ion-icon>
<span>Advanced Insights</span>
<span i18n>FIRE Calculator</span>
</li>
<li class="align-items-center d-flex mb-1">
<ion-icon
class="mr-1"
name="checkmark-circle-outline"
></ion-icon>
<a i18n [routerLink]="['/features']">and more Features...</a>
</li>
</ul>
</div>
<p>Fully managed <strong>Ghostfolio</strong> cloud offering.</p>
<p i18n>Fully managed Ghostfolio cloud offering.</p>
<p class="h5 text-right" [hidden]="!price">
<span class="font-weight-normal">
<ng-container *ngIf="coupon"
@ -221,11 +291,16 @@
*ngIf="user?.subscription?.type === 'Basic'"
class="mt-3 text-center"
>
<a color="primary" mat-flat-button [routerLink]="['/account']">
<a
color="primary"
i18n
mat-flat-button
[routerLink]="['/account']"
>
Upgrade Plan
</a>
<p class="m-0 text-muted">
<small>One-time payment, no auto-renewal.</small>
<small i18n>One-time payment, no auto-renewal.</small>
</p>
</div>
</mat-card>
@ -235,10 +310,10 @@
</div>
<div *ngIf="!user" class="row">
<div class="col mt-3 text-center">
<a color="primary" mat-flat-button [routerLink]="['/register']">
<a color="primary" i18n mat-flat-button [routerLink]="['/register']">
Get Started
</a>
<p class="m-0 text-muted"><small>Its free.</small></p>
<p class="m-0 text-muted"><small i18n>Its free.</small></p>
</div>
</div>
</div>

@ -2,12 +2,14 @@
color: rgb(var(--dark-primary-text));
display: block;
a {
color: rgba(var(--palette-primary-500), 1);
font-weight: 500;
p {
a {
color: rgba(var(--palette-primary-500), 1);
font-weight: 500;
&:hover {
color: rgba(var(--palette-primary-300), 1);
&:hover {
color: rgba(var(--palette-primary-300), 1);
}
}
}
@ -17,6 +19,10 @@
border-color: rgba(var(--palette-primary-500), 1);
box-shadow: 0 0 0 1px rgba(var(--palette-primary-500), 1);
}
ion-icon[name='checkmark-circle-outline'] {
color: rgba(var(--palette-accent-500), 1);
}
}
}

@ -1,10 +1,15 @@
import { HttpClient } from '@angular/common/http';
import { Injectable } from '@angular/core';
import { MatDialog } from '@angular/material/dialog';
import { ObservableStore } from '@codewithdan/observable-store';
import { SubscriptionInterstitialDialogParams } from '@ghostfolio/client/components/subscription-interstitial-dialog/interfaces/interfaces';
import { SubscriptionInterstitialDialog } from '@ghostfolio/client/components/subscription-interstitial-dialog/subscription-interstitial-dialog.component';
import { User } from '@ghostfolio/common/interfaces';
import { of } from 'rxjs';
import { hasPermission, permissions } from '@ghostfolio/common/permissions';
import { DeviceDetectorService } from 'ngx-device-detector';
import { of, Subject } from 'rxjs';
import { throwError } from 'rxjs';
import { catchError, map } from 'rxjs/operators';
import { catchError, map, takeUntil } from 'rxjs/operators';
import { UserStoreActions } from './user-store.actions';
import { UserStoreState } from './user-store.state';
@ -13,10 +18,19 @@ import { UserStoreState } from './user-store.state';
providedIn: 'root'
})
export class UserService extends ObservableStore<UserStoreState> {
public constructor(private http: HttpClient) {
private deviceType: string;
private unsubscribeSubject = new Subject<void>();
public constructor(
private deviceService: DeviceDetectorService,
private dialog: MatDialog,
private http: HttpClient
) {
super({ trackStateHistory: true });
this.setState({ user: undefined }, UserStoreActions.Initialize);
this.deviceType = this.deviceService.getDeviceInfo().deviceType;
}
public get(force = false) {
@ -39,6 +53,26 @@ export class UserService extends ObservableStore<UserStoreState> {
return this.http.get<User>('/api/v1/user').pipe(
map((user) => {
this.setState({ user }, UserStoreActions.GetUser);
if (
hasPermission(
user.permissions,
permissions.enableSubscriptionInterstitial
)
) {
const dialogRef = this.dialog.open(SubscriptionInterstitialDialog, {
autoFocus: false,
data: <SubscriptionInterstitialDialogParams>{},
height: this.deviceType === 'mobile' ? '97.5vh' : '80vh',
width: this.deviceType === 'mobile' ? '100vw' : '50rem'
});
dialogRef
.afterClosed()
.pipe(takeUntil(this.unsubscribeSubject))
.subscribe(() => {});
}
return user;
}),
catchError(this.handleError)

@ -5,6 +5,7 @@ import { UserSettings } from './user-settings.interface';
export type UserWithSettings = User & {
Account: Account[];
activityCount: number;
permissions?: string[];
Settings: Settings & { settings: UserSettings };
subscription?: {

@ -19,6 +19,7 @@ export const permissions = {
enableSocialLogin: 'enableSocialLogin',
enableStatistics: 'enableStatistics',
enableSubscription: 'enableSubscription',
enableSubscriptionInterstitial: 'enableSubscriptionInterstitial',
enableSystemMessage: 'enableSystemMessage',
reportDataGlitch: 'reportDataGlitch',
toggleReadOnlyMode: 'toggleReadOnlyMode',

Loading…
Cancel
Save