diff --git a/apps/api/src/app/user/user.service.ts b/apps/api/src/app/user/user.service.ts index 13f82aa47..9b7b5df8e 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,12 +161,19 @@ 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); diff --git a/apps/client/src/app/app.module.ts b/apps/client/src/app/app.module.ts index 2d1f4cc5a..570556576 100644 --- a/apps/client/src/app/app.module.ts +++ b/apps/client/src/app/app.module.ts @@ -9,6 +9,7 @@ import { MAT_DATE_LOCALE, MatNativeDateModule } from '@angular/material/core'; +import { MatDialog, MatDialogModule } from '@angular/material/dialog'; import { MatSnackBarModule } from '@angular/material/snack-bar'; import { MatTooltipModule } from '@angular/material/tooltip'; import { BrowserModule } from '@angular/platform-browser'; @@ -25,6 +26,8 @@ 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 { SubscriptionInterstitialDialog } from './components/subscription-interstitial-dialog/subscription-interstitial-dialog.component'; +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 +43,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 8c761ccdb..dddef0c8f 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 000000000..d93de3c4a --- /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 000000000..e021d33c4 --- /dev/null +++ b/apps/client/src/app/components/subscription-interstitial-dialog/subscription-interstitial-dialog.component.ts @@ -0,0 +1,37 @@ +import { + ChangeDetectionStrategy, + Component, + Inject, + OnDestroy +} from '@angular/core'; +import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog'; +import { Subject } from 'rxjs'; + +import { SubscriptionInterstitialDialogParams } from './interfaces/interfaces'; + +@Component({ + host: { class: 'd-flex flex-column flex-grow-1 h-100' }, + selector: 'gf-subscription-interstitial-dialog', + changeDetection: ChangeDetectionStrategy.OnPush, + styleUrls: ['./subscription-interstitial-dialog.scss'], + templateUrl: 'subscription-interstitial-dialog.html' +}) +export class SubscriptionInterstitialDialog implements OnDestroy { + private unsubscribeSubject = new Subject(); + + public constructor( + @Inject(MAT_DIALOG_DATA) public data: SubscriptionInterstitialDialogParams, + public dialogRef: MatDialogRef + ) {} + + public ngOnInit() {} + + public onCancel() { + this.dialogRef.close({}); + } + + public ngOnDestroy() { + this.unsubscribeSubject.next(); + this.unsubscribeSubject.complete(); + } +} 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 000000000..ebe46767f --- /dev/null +++ b/apps/client/src/app/components/subscription-interstitial-dialog/subscription-interstitial-dialog.html @@ -0,0 +1,38 @@ +

+ Ghostfolio Premium + +

+
+

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

+

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

+
    +
  • + + Portfolio Summary +
  • +
  • + + Performance Benchmarks +
  • +
  • + + Allocations +
  • +
  • + + FIRE Calculator +
  • +
+

Refine your personal investment strategy now.

+
+ 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 000000000..d7a7cfcf2 --- /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 000000000..cc0da2076 --- /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 1235caab6..1727191e8 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 23904e628..63641644a 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 e72b7cde3..bf880632e 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 9245a6ac1..5c4fab82c 100644 --- a/apps/client/src/app/pages/pricing/pricing-page.html +++ b/apps/client/src/app/pages/pricing/pricing-page.html @@ -38,38 +38,52 @@ @@ -99,24 +113,36 @@ diff --git a/apps/client/src/app/pages/pricing/pricing-page.scss b/apps/client/src/app/pages/pricing/pricing-page.scss index 8edf75b42..40c64b093 100644 --- a/apps/client/src/app/pages/pricing/pricing-page.scss +++ b/apps/client/src/app/pages/pricing/pricing-page.scss @@ -17,6 +17,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 79c15e085..c40d04880 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 55324ff3d..80330d80c 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 6be7a3401..b9dc6806a 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',