diff --git a/CHANGELOG.md b/CHANGELOG.md index f68699fc..788abe8a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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` diff --git a/apps/api/src/app/user/user.service.ts b/apps/api/src/app/user/user.service.ts index 13f82aa4..176de700 100644 --- a/apps/api/src/app/user/user.service.ts +++ b/apps/api/src/app/user/user.service.ts @@ -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')) { diff --git a/apps/client/src/app/app.module.ts b/apps/client/src/app/app.module.ts index 2d1f4cc5..75055e01 100644 --- a/apps/client/src/app/app.module.ts +++ b/apps/client/src/app/app.module.ts @@ -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, diff --git a/apps/client/src/app/components/admin-market-data-detail/market-data-detail-dialog/market-data-detail-dialog.component.ts b/apps/client/src/app/components/admin-market-data-detail/market-data-detail-dialog/market-data-detail-dialog.component.ts index 8c761ccd..dddef0c8 100644 --- a/apps/client/src/app/components/admin-market-data-detail/market-data-detail-dialog/market-data-detail-dialog.component.ts +++ b/apps/client/src/app/components/admin-market-data-detail/market-data-detail-dialog/market-data-detail-dialog.component.ts @@ -36,7 +36,7 @@ export class MarketDataDetailDialog implements OnDestroy { this.dateAdapter.setLocale(this.locale); } - public onCancel(): void { + public onCancel() { this.dialogRef.close({ withRefresh: false }); } diff --git a/apps/client/src/app/components/subscription-interstitial-dialog/interfaces/interfaces.ts b/apps/client/src/app/components/subscription-interstitial-dialog/interfaces/interfaces.ts new file mode 100644 index 00000000..d93de3c4 --- /dev/null +++ b/apps/client/src/app/components/subscription-interstitial-dialog/interfaces/interfaces.ts @@ -0,0 +1 @@ +export interface SubscriptionInterstitialDialogParams {} diff --git a/apps/client/src/app/components/subscription-interstitial-dialog/subscription-interstitial-dialog.component.ts b/apps/client/src/app/components/subscription-interstitial-dialog/subscription-interstitial-dialog.component.ts new file mode 100644 index 00000000..0014df35 --- /dev/null +++ b/apps/client/src/app/components/subscription-interstitial-dialog/subscription-interstitial-dialog.component.ts @@ -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 + ) {} + + public onCancel() { + this.dialogRef.close({}); + } +} diff --git a/apps/client/src/app/components/subscription-interstitial-dialog/subscription-interstitial-dialog.html b/apps/client/src/app/components/subscription-interstitial-dialog/subscription-interstitial-dialog.html new file mode 100644 index 00000000..a45bb42a --- /dev/null +++ b/apps/client/src/app/components/subscription-interstitial-dialog/subscription-interstitial-dialog.html @@ -0,0 +1,42 @@ +

+ Ghostfolio Premium + +

+
+

+ Are you an ambitious investor who needs the full picture? +

+

+ By upgrading to Ghostfolio Premium, you will get these additional features: +

+ +

Refine your personal investment strategy now.

+
+
+ + + Upgrade Plan + + +
diff --git a/apps/client/src/app/components/subscription-interstitial-dialog/subscription-interstitial-dialog.module.ts b/apps/client/src/app/components/subscription-interstitial-dialog/subscription-interstitial-dialog.module.ts new file mode 100644 index 00000000..d7a7cfcf --- /dev/null +++ b/apps/client/src/app/components/subscription-interstitial-dialog/subscription-interstitial-dialog.module.ts @@ -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 {} diff --git a/apps/client/src/app/components/subscription-interstitial-dialog/subscription-interstitial-dialog.scss b/apps/client/src/app/components/subscription-interstitial-dialog/subscription-interstitial-dialog.scss new file mode 100644 index 00000000..cc0da207 --- /dev/null +++ b/apps/client/src/app/components/subscription-interstitial-dialog/subscription-interstitial-dialog.scss @@ -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); + } + } +} diff --git a/apps/client/src/app/pages/account/create-or-update-access-dialog/create-or-update-access-dialog.component.ts b/apps/client/src/app/pages/account/create-or-update-access-dialog/create-or-update-access-dialog.component.ts index 1235caab..1727191e 100644 --- a/apps/client/src/app/pages/account/create-or-update-access-dialog/create-or-update-access-dialog.component.ts +++ b/apps/client/src/app/pages/account/create-or-update-access-dialog/create-or-update-access-dialog.component.ts @@ -26,7 +26,7 @@ export class CreateOrUpdateAccessDialog implements OnDestroy { ngOnInit() {} - public onCancel(): void { + public onCancel() { this.dialogRef.close(); } diff --git a/apps/client/src/app/pages/accounts/create-or-update-account-dialog/create-or-update-account-dialog.component.ts b/apps/client/src/app/pages/accounts/create-or-update-account-dialog/create-or-update-account-dialog.component.ts index 23904e62..63641644 100644 --- a/apps/client/src/app/pages/accounts/create-or-update-account-dialog/create-or-update-account-dialog.component.ts +++ b/apps/client/src/app/pages/accounts/create-or-update-account-dialog/create-or-update-account-dialog.component.ts @@ -36,7 +36,7 @@ export class CreateOrUpdateAccountDialog implements OnDestroy { this.platforms = platforms; } - public onCancel(): void { + public onCancel() { this.dialogRef.close(); } diff --git a/apps/client/src/app/pages/portfolio/activities/import-activities-dialog/import-activities-dialog.component.ts b/apps/client/src/app/pages/portfolio/activities/import-activities-dialog/import-activities-dialog.component.ts index e72b7cde..bf880632 100644 --- a/apps/client/src/app/pages/portfolio/activities/import-activities-dialog/import-activities-dialog.component.ts +++ b/apps/client/src/app/pages/portfolio/activities/import-activities-dialog/import-activities-dialog.component.ts @@ -80,7 +80,7 @@ export class ImportActivitiesDialog implements OnDestroy { } } - public onCancel(): void { + public onCancel() { this.dialogRef.close(); } diff --git a/apps/client/src/app/pages/pricing/pricing-page.html b/apps/client/src/app/pages/pricing/pricing-page.html index 9245a6ac..42cd5055 100644 --- a/apps/client/src/app/pages/pricing/pricing-page.html +++ b/apps/client/src/app/pages/pricing/pricing-page.html @@ -31,50 +31,71 @@

Open Source

-

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

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

  • - Unlimited Transactions + Unlimited Transactions
  • - Portfolio Performance + Unlimited Accounts
  • - Zen Mode + Portfolio Performance
  • - Portfolio Summary + Portfolio Summary
  • - Advanced Insights + Performance Benchmarks +
  • +
  • + + Allocations +
  • +
  • + + FIRE Calculator +
  • +
  • + + and more Features...
-

Self-hosted, update manually.

-

Free

+

Self-hosted, update manually.

+

Free

- + Get Started -

It’s free.

+

It’s free.

diff --git a/apps/client/src/app/pages/pricing/pricing-page.scss b/apps/client/src/app/pages/pricing/pricing-page.scss index 8edf75b4..74b8facb 100644 --- a/apps/client/src/app/pages/pricing/pricing-page.scss +++ b/apps/client/src/app/pages/pricing/pricing-page.scss @@ -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); + } } } diff --git a/apps/client/src/app/services/user/user.service.ts b/apps/client/src/app/services/user/user.service.ts index 79c15e08..c40d0488 100644 --- a/apps/client/src/app/services/user/user.service.ts +++ b/apps/client/src/app/services/user/user.service.ts @@ -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 { - public constructor(private http: HttpClient) { + private deviceType: string; + private unsubscribeSubject = new Subject(); + + 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 { return this.http.get('/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: {}, + 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) diff --git a/libs/common/src/lib/interfaces/user-with-settings.ts b/libs/common/src/lib/interfaces/user-with-settings.ts index 55324ff3..80330d80 100644 --- a/libs/common/src/lib/interfaces/user-with-settings.ts +++ b/libs/common/src/lib/interfaces/user-with-settings.ts @@ -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?: { diff --git a/libs/common/src/lib/permissions.ts b/libs/common/src/lib/permissions.ts index 6be7a340..b9dc6806 100644 --- a/libs/common/src/lib/permissions.ts +++ b/libs/common/src/lib/permissions.ts @@ -19,6 +19,7 @@ export const permissions = { enableSocialLogin: 'enableSocialLogin', enableStatistics: 'enableStatistics', enableSubscription: 'enableSubscription', + enableSubscriptionInterstitial: 'enableSubscriptionInterstitial', enableSystemMessage: 'enableSystemMessage', reportDataGlitch: 'reportDataGlitch', toggleReadOnlyMode: 'toggleReadOnlyMode',