Browse Source

Merge branch 'main' into bugfix/fix-issue-with-value-in-value-redaction-interceptor

pull/1627/head
Thomas Kaul 3 years ago
committed by GitHub
parent
commit
dcb4b046ba
No known key found for this signature in database GPG Key ID: 4AEE18F83AFDEB23
  1. 2
      .nvmrc
  2. 8
      CHANGELOG.md
  3. 2
      README.md
  4. 1
      apps/api/src/app/import/import.controller.ts
  5. 3
      apps/api/src/app/user/user.controller.ts
  6. 22
      apps/api/src/app/user/user.service.ts
  7. 2
      apps/client/src/app/app.module.ts
  8. 2
      apps/client/src/app/components/admin-market-data-detail/market-data-detail-dialog/market-data-detail-dialog.component.ts
  9. 1
      apps/client/src/app/components/subscription-interstitial-dialog/interfaces/interfaces.ts
  10. 22
      apps/client/src/app/components/subscription-interstitial-dialog/subscription-interstitial-dialog.component.ts
  11. 42
      apps/client/src/app/components/subscription-interstitial-dialog/subscription-interstitial-dialog.html
  12. 21
      apps/client/src/app/components/subscription-interstitial-dialog/subscription-interstitial-dialog.module.ts
  13. 11
      apps/client/src/app/components/subscription-interstitial-dialog/subscription-interstitial-dialog.scss
  14. 2
      apps/client/src/app/pages/account/create-or-update-access-dialog/create-or-update-access-dialog.component.ts
  15. 2
      apps/client/src/app/pages/accounts/create-or-update-account-dialog/create-or-update-account-dialog.component.ts
  16. 60
      apps/client/src/app/pages/blog/2023/01/ghostfolio-auf-sackgeld-vorgestellt/ghostfolio-auf-sackgeld-vorgestellt-page.html
  17. 2
      apps/client/src/app/pages/portfolio/activities/import-activities-dialog/import-activities-dialog.component.ts
  18. 2
      apps/client/src/app/pages/portfolio/allocations/allocations-page.component.ts
  19. 159
      apps/client/src/app/pages/pricing/pricing-page.html
  20. 6
      apps/client/src/app/pages/pricing/pricing-page.scss
  21. 40
      apps/client/src/app/services/user/user.service.ts
  22. 1
      libs/common/src/lib/interfaces/user-with-settings.ts
  23. 1
      libs/common/src/lib/permissions.ts
  24. 4
      package.json
  25. 36
      yarn.lock

2
.nvmrc

@ -1 +1 @@
v18 v16

8
CHANGELOG.md

@ -7,13 +7,21 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
## Unreleased ## Unreleased
### Added
- Added an interstitial for the subscription
- Added a quote to the blog post _Ghostfolio auf Sackgeld.com vorgestellt_
### Changed ### Changed
- Improved the unit format (`%`) in the global heat map component of the public page - Improved the unit format (`%`) in the global heat map component of the public page
- Improved the pricing page
- Upgraded `Node.js` from version `16` to `18` (`Dockerfile`) - Upgraded `Node.js` from version `16` to `18` (`Dockerfile`)
- Upgraded `prisma` from version `4.8.0` to `4.9.0`
### Fixed ### Fixed
- Fixed the click of unknown accounts in the portfolio proportion chart component
- Fixed an issue with `value` in the value redaction interceptor for the impersonation mode - Fixed an issue with `value` in the value redaction interceptor for the impersonation mode
## 1.229.0 - 2023-01-21 ## 1.229.0 - 2023-01-21

2
README.md

@ -148,7 +148,7 @@ Please follow the instructions of the Ghostfolio [Unraid Community App](https://
### Prerequisites ### Prerequisites
- [Docker](https://www.docker.com/products/docker-desktop) - [Docker](https://www.docker.com/products/docker-desktop)
- [Node.js](https://nodejs.org/en/download) (version 18+) - [Node.js](https://nodejs.org/en/download) (version 16)
- [Yarn](https://yarnpkg.com/en/docs/install) - [Yarn](https://yarnpkg.com/en/docs/install)
- A local copy of this Git repository (clone) - A local copy of this Git repository (clone)

1
apps/api/src/app/import/import.controller.ts

@ -20,7 +20,6 @@ import { REQUEST } from '@nestjs/core';
import { AuthGuard } from '@nestjs/passport'; import { AuthGuard } from '@nestjs/passport';
import { DataSource } from '@prisma/client'; import { DataSource } from '@prisma/client';
import { StatusCodes, getReasonPhrase } from 'http-status-codes'; import { StatusCodes, getReasonPhrase } from 'http-status-codes';
import { isEmpty } from 'lodash';
import { ImportDataDto } from './import-data.dto'; import { ImportDataDto } from './import-data.dto';
import { ImportService } from './import.service'; import { ImportService } from './import.service';

3
apps/api/src/app/user/user.controller.ts

@ -1,6 +1,4 @@
import { ConfigurationService } from '@ghostfolio/api/services/configuration.service';
import { PropertyService } from '@ghostfolio/api/services/property/property.service'; import { PropertyService } from '@ghostfolio/api/services/property/property.service';
import { PROPERTY_IS_USER_SIGNUP_ENABLED } from '@ghostfolio/common/config';
import { User, UserSettings } from '@ghostfolio/common/interfaces'; import { User, UserSettings } from '@ghostfolio/common/interfaces';
import { hasPermission, permissions } from '@ghostfolio/common/permissions'; import { hasPermission, permissions } from '@ghostfolio/common/permissions';
import type { RequestWithUser } from '@ghostfolio/common/types'; import type { RequestWithUser } from '@ghostfolio/common/types';
@ -31,7 +29,6 @@ import { UserService } from './user.service';
@Controller('user') @Controller('user')
export class UserController { export class UserController {
public constructor( public constructor(
private readonly configurationService: ConfigurationService,
private readonly jwtService: JwtService, private readonly jwtService: JwtService,
private readonly propertyService: PropertyService, private readonly propertyService: PropertyService,
@Inject(REQUEST) private readonly request: RequestWithUser, @Inject(REQUEST) private readonly request: RequestWithUser,

22
apps/api/src/app/user/user.service.ts

@ -97,6 +97,7 @@ export class UserService {
const { const {
accessToken, accessToken,
Account, Account,
Analytics,
authChallenge, authChallenge,
createdAt, createdAt,
id, id,
@ -107,7 +108,12 @@ export class UserService {
thirdPartyId, thirdPartyId,
updatedAt updatedAt
} = await this.prismaService.user.findUnique({ } = await this.prismaService.user.findUnique({
include: { Account: true, Settings: true, Subscription: true }, include: {
Account: true,
Analytics: true,
Settings: true,
Subscription: true
},
where: userWhereUniqueInput where: userWhereUniqueInput
}); });
@ -121,7 +127,8 @@ export class UserService {
role, role,
Settings, Settings,
thirdPartyId, thirdPartyId,
updatedAt updatedAt,
activityCount: Analytics?.activityCount
}; };
if (user?.Settings) { if (user?.Settings) {
@ -154,16 +161,23 @@ export class UserService {
(user.Settings.settings as UserSettings).viewMode = 'DEFAULT'; (user.Settings.settings as UserSettings).viewMode = 'DEFAULT';
} }
let currentPermissions = getPermissions(user.role);
if (this.configurationService.get('ENABLE_FEATURE_SUBSCRIPTION')) { if (this.configurationService.get('ENABLE_FEATURE_SUBSCRIPTION')) {
user.subscription = user.subscription =
this.subscriptionService.getSubscription(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') { if (user.subscription?.type === 'Premium') {
currentPermissions.push(permissions.reportDataGlitch); currentPermissions.push(permissions.reportDataGlitch);
} }
}
if (this.configurationService.get('ENABLE_FEATURE_READ_ONLY_MODE')) { if (this.configurationService.get('ENABLE_FEATURE_READ_ONLY_MODE')) {
if (hasRole(user, Role.ADMIN)) { if (hasRole(user, Role.ADMIN)) {

2
apps/client/src/app/app.module.ts

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

2
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); this.dateAdapter.setLocale(this.locale);
} }
public onCancel(): void { public onCancel() {
this.dialogRef.close({ withRefresh: false }); this.dialogRef.close({ withRefresh: false });
} }

1
apps/client/src/app/components/subscription-interstitial-dialog/interfaces/interfaces.ts

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

22
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<SubscriptionInterstitialDialog>
) {}
public onCancel() {
this.dialogRef.close({});
}
}

42
apps/client/src/app/components/subscription-interstitial-dialog/subscription-interstitial-dialog.html

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

21
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 {}

11
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);
}
}
}

2
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() {} ngOnInit() {}
public onCancel(): void { public onCancel() {
this.dialogRef.close(); this.dialogRef.close();
} }

2
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; this.platforms = platforms;
} }
public onCancel(): void { public onCancel() {
this.dialogRef.close(); this.dialogRef.close();
} }

60
apps/client/src/app/pages/blog/2023/01/ghostfolio-auf-sackgeld-vorgestellt/ghostfolio-auf-sackgeld-vorgestellt-page.html

@ -4,7 +4,7 @@
<article> <article>
<div class="mb-4 text-center"> <div class="mb-4 text-center">
<h1 class="mb-1">Ghostfolio auf Sackgeld.com vorgestellt</h1> <h1 class="mb-1">Ghostfolio auf Sackgeld.com vorgestellt</h1>
<div class="text-muted"><small>2023-01-21</small></div> <div class="mb-3 text-muted"><small>2023-01-21</small></div>
<img <img
alt="Ghostfolio auf Sackgeld.com vorgestellt Teaser" alt="Ghostfolio auf Sackgeld.com vorgestellt Teaser"
class="border rounded w-100" class="border rounded w-100"
@ -16,7 +16,26 @@
<p> <p>
Wir freuen uns darüber, dass unsere Open Source Portfolio Tracking Wir freuen uns darüber, dass unsere Open Source Portfolio Tracking
Software <a href="https://ghostfol.io">Ghostfolio</a> auf dem Software <a href="https://ghostfol.io">Ghostfolio</a> auf dem
Fintech News Portal <i>Sackgeld.com</i> vorgestellt wurde. FinTech Newsportal <i>Sackgeld.com</i> vorgestellt wurde.
</p>
<div class="container my-4">
<div class="row">
<div class="col-md-10 offset-md-1">
<blockquote class="blockquote m-0">
<p class="mb-0">
«Ghostfolio ist ein umfassender Portfolio Performance
Tracker der einfach zu bedienen ist, mit einigen sehr
innovativen Features aufwartet und echten Mehrwert für den
Investor bringt.»
</p>
</blockquote>
</div>
</div>
</div>
<p>
Im ausführlichen Bericht wird die Funktionsweise von Ghostfolio
erläutert, die unterstützten Assets aufgeführt sowie die
Preisstruktur im Vergleich zu anderen Anbietern dargelegt.
</p> </p>
</section> </section>
<section class="mb-4"> <section class="mb-4">
@ -24,23 +43,23 @@
Ghostfolio – Open Source Wealth Management Software Ghostfolio – Open Source Wealth Management Software
</h2> </h2>
<p> <p>
Ghostfolio ermöglicht es dir, deine Portfolio-Performance einfach zu Ghostfolio ermöglicht es dir, dein Portfolio einfach zu verfolgen
verfolgen und zu analysieren. Es bietet dir detaillierte und zu analysieren. Es bietet dir detaillierte Informationen über
Informationen über deine Positionen, historische Entwicklung und die deine Positionen, historische Entwicklung, Performance und die
Zusammenstellung deines Portfolios. Durch die Open Source-Lizenz (<a Zusammenstellung deines Portfolios. Durch die Open Source-Lizenz (<a
href="https://github.com/ghostfolio/ghostfolio/blob/main/LICENSE" href="https://github.com/ghostfolio/ghostfolio/blob/main/LICENSE"
target="_blank" target="_blank"
>GNU Affero General Public License v3.0</a >GNU Affero General Public License v3.0</a
>) wird die Software ständig weiterentwickelt und verbessert und du >) wird die Software ständig weiterentwickelt, verbessert und du
hast sogar die Möglichkeit, dich selbst daran zu beteiligen. Wir hast sogar selbst die Möglichkeit, dich daran zu beteiligen. Wir
sind davon überzeugt, mit dem Open-Source-Ansatz von Ghostfolio das sind davon überzeugt, mit diesem Open-Source-Ansatz von Ghostfolio
Finanzwissen und Investieren für alle zugänglicher zu machen. das Finanzwissen und Investieren für alle zugänglicher zu machen.
</p> </p>
</section> </section>
<section class="mb-4"> <section class="mb-4">
<h2 class="h4">Sackgeld.com – App für ein höheres Sackgeld</h2> <h2 class="h4">Sackgeld.com – App für ein höheres Sackgeld</h2>
<p> <p>
Das Schweizer Fintech News Portal Das Schweizer FinTech Nachrichtenportal
<a href="https://www.sackgeld.com" target="_blank">Sackgeld.com</a> <a href="https://www.sackgeld.com" target="_blank">Sackgeld.com</a>
informiert über die neuesten Entwicklungen und Innovationen im informiert über die neuesten Entwicklungen und Innovationen im
Bereich FinTech. Dazu gehören News, Artikel und persönliche Bereich FinTech. Dazu gehören News, Artikel und persönliche
@ -51,15 +70,12 @@
<section class="mb-4"> <section class="mb-4">
<p> <p>
Wenn du mehr über Ghostfolio erfahren möchtest, kannst du hier den Wenn du mehr über Ghostfolio erfahren möchtest, kannst du hier den
ganzen Artikel "<a ganzen Artikel nachlesen:
<a
href="https://www.sackgeld.com/was-taugt-ghostfolio-als-portfolio-performance-tracking-tool" href="https://www.sackgeld.com/was-taugt-ghostfolio-als-portfolio-performance-tracking-tool"
target="_blank" target="_blank"
>Was taugt Ghostfolio als Portfolio Performance Tracking-Tool?</a >Was taugt Ghostfolio als Portfolio Performance Tracking-Tool?</a
>" nachlesen. >
</p>
<p>
Wir freuen uns auf dein Feedback.<br />
Thomas von Ghostfolio
</p> </p>
</section> </section>
<section class="mb-4"> <section class="mb-4">
@ -79,6 +95,9 @@
<li class="list-inline-item"> <li class="list-inline-item">
<span class="badge badge-light">App</span> <span class="badge badge-light">App</span>
</li> </li>
<li class="list-inline-item">
<span class="badge badge-light">Asset</span>
</li>
<li class="list-inline-item"> <li class="list-inline-item">
<span class="badge badge-light">Feedback</span> <span class="badge badge-light">Feedback</span>
</li> </li>
@ -103,6 +122,9 @@
<li class="list-inline-item"> <li class="list-inline-item">
<span class="badge badge-light">Lizenz</span> <span class="badge badge-light">Lizenz</span>
</li> </li>
<li class="list-inline-item">
<span class="badge badge-light">Media</span>
</li>
<li class="list-inline-item"> <li class="list-inline-item">
<span class="badge badge-light">Open Source</span> <span class="badge badge-light">Open Source</span>
</li> </li>
@ -118,6 +140,9 @@
<li class="list-inline-item"> <li class="list-inline-item">
<span class="badge badge-light">Portfolio</span> <span class="badge badge-light">Portfolio</span>
</li> </li>
<li class="list-inline-item">
<span class="badge badge-light">Presse</span>
</li>
<li class="list-inline-item"> <li class="list-inline-item">
<span class="badge badge-light">Sackgeld</span> <span class="badge badge-light">Sackgeld</span>
</li> </li>
@ -139,6 +164,9 @@
<li class="list-inline-item"> <li class="list-inline-item">
<span class="badge badge-light">Vermögen</span> <span class="badge badge-light">Vermögen</span>
</li> </li>
<li class="list-inline-item">
<span class="badge badge-light">Vorsorge</span>
</li>
<li class="list-inline-item"> <li class="list-inline-item">
<span class="badge badge-light">Wealth Management</span> <span class="badge badge-light">Wealth Management</span>
</li> </li>

2
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(); this.dialogRef.close();
} }

2
apps/client/src/app/pages/portfolio/allocations/allocations-page.component.ts

@ -360,7 +360,7 @@ export class AllocationsPageComponent implements OnDestroy, OnInit {
} }
public onAccountChartClicked({ symbol }: UniqueAsset) { public onAccountChartClicked({ symbol }: UniqueAsset) {
if (symbol) { if (symbol && symbol !== UNKNOWN_KEY) {
this.router.navigate([], { this.router.navigate([], {
queryParams: { accountId: symbol, accountDetailDialog: true } queryParams: { accountId: symbol, accountDetailDialog: true }
}); });

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

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

6
apps/client/src/app/pages/pricing/pricing-page.scss

@ -2,6 +2,7 @@
color: rgb(var(--dark-primary-text)); color: rgb(var(--dark-primary-text));
display: block; display: block;
p {
a { a {
color: rgba(var(--palette-primary-500), 1); color: rgba(var(--palette-primary-500), 1);
font-weight: 500; font-weight: 500;
@ -10,6 +11,7 @@
color: rgba(var(--palette-primary-300), 1); color: rgba(var(--palette-primary-300), 1);
} }
} }
}
.mat-card { .mat-card {
&:hover, &:hover,
@ -17,6 +19,10 @@
border-color: rgba(var(--palette-primary-500), 1); border-color: rgba(var(--palette-primary-500), 1);
box-shadow: 0 0 0 1px 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);
}
} }
} }

40
apps/client/src/app/services/user/user.service.ts

@ -1,10 +1,15 @@
import { HttpClient } from '@angular/common/http'; import { HttpClient } from '@angular/common/http';
import { Injectable } from '@angular/core'; import { Injectable } from '@angular/core';
import { MatDialog } from '@angular/material/dialog';
import { ObservableStore } from '@codewithdan/observable-store'; 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 { 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 { throwError } from 'rxjs';
import { catchError, map } from 'rxjs/operators'; import { catchError, map, takeUntil } from 'rxjs/operators';
import { UserStoreActions } from './user-store.actions'; import { UserStoreActions } from './user-store.actions';
import { UserStoreState } from './user-store.state'; import { UserStoreState } from './user-store.state';
@ -13,10 +18,19 @@ import { UserStoreState } from './user-store.state';
providedIn: 'root' providedIn: 'root'
}) })
export class UserService extends ObservableStore<UserStoreState> { 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 }); super({ trackStateHistory: true });
this.setState({ user: undefined }, UserStoreActions.Initialize); this.setState({ user: undefined }, UserStoreActions.Initialize);
this.deviceType = this.deviceService.getDeviceInfo().deviceType;
} }
public get(force = false) { public get(force = false) {
@ -39,6 +53,26 @@ export class UserService extends ObservableStore<UserStoreState> {
return this.http.get<User>('/api/v1/user').pipe( return this.http.get<User>('/api/v1/user').pipe(
map((user) => { map((user) => {
this.setState({ user }, UserStoreActions.GetUser); 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; return user;
}), }),
catchError(this.handleError) catchError(this.handleError)

1
libs/common/src/lib/interfaces/user-with-settings.ts

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

1
libs/common/src/lib/permissions.ts

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

4
package.json

@ -81,7 +81,7 @@
"@nestjs/schedule": "2.1.0", "@nestjs/schedule": "2.1.0",
"@nestjs/serve-static": "3.0.0", "@nestjs/serve-static": "3.0.0",
"@nrwl/angular": "15.0.13", "@nrwl/angular": "15.0.13",
"@prisma/client": "4.8.0", "@prisma/client": "4.9.0",
"@simplewebauthn/browser": "5.2.1", "@simplewebauthn/browser": "5.2.1",
"@simplewebauthn/server": "5.2.1", "@simplewebauthn/server": "5.2.1",
"@stripe/stripe-js": "1.22.0", "@stripe/stripe-js": "1.22.0",
@ -119,7 +119,7 @@
"passport": "0.6.0", "passport": "0.6.0",
"passport-google-oauth20": "2.0.0", "passport-google-oauth20": "2.0.0",
"passport-jwt": "4.0.0", "passport-jwt": "4.0.0",
"prisma": "4.8.0", "prisma": "4.9.0",
"reflect-metadata": "0.1.13", "reflect-metadata": "0.1.13",
"rxjs": "7.5.6", "rxjs": "7.5.6",
"stripe": "8.199.0", "stripe": "8.199.0",

36
yarn.lock

@ -3677,22 +3677,22 @@
dependencies: dependencies:
esquery "^1.0.1" esquery "^1.0.1"
"@prisma/client@4.8.0": "@prisma/client@4.9.0":
version "4.8.0" version "4.9.0"
resolved "https://registry.yarnpkg.com/@prisma/client/-/client-4.8.0.tgz#6ec7adaca6a2e233d7e41dbe7cc6d0fa6143a407" resolved "https://registry.yarnpkg.com/@prisma/client/-/client-4.9.0.tgz#4a4068f3540732ea5723c008d49ed684d20f9340"
integrity sha512-Y1riB0p2W52kh3zgssP/YAhln3RjBFcJy3uwEiyjmU+TQYh6QTZDRFBo3JtBWuq2FyMOl1Rye8jxzUP+n0l5Cg== integrity sha512-bz6QARw54sWcbyR1lLnF2QHvRW5R/Jxnbbmwh3u+969vUKXtBkXgSgjDA85nji31ZBlf7+FrHDy5x+5ydGyQDg==
dependencies: dependencies:
"@prisma/engines-version" "4.8.0-61.d6e67a83f971b175a593ccc12e15c4a757f93ffe" "@prisma/engines-version" "4.9.0-42.ceb5c99003b99c9ee2c1d2e618e359c14aef2ea5"
"@prisma/engines-version@4.8.0-61.d6e67a83f971b175a593ccc12e15c4a757f93ffe": "@prisma/engines-version@4.9.0-42.ceb5c99003b99c9ee2c1d2e618e359c14aef2ea5":
version "4.8.0-61.d6e67a83f971b175a593ccc12e15c4a757f93ffe" version "4.9.0-42.ceb5c99003b99c9ee2c1d2e618e359c14aef2ea5"
resolved "https://registry.yarnpkg.com/@prisma/engines-version/-/engines-version-4.8.0-61.d6e67a83f971b175a593ccc12e15c4a757f93ffe.tgz#30401aba1029e7d32e3cb717e705a7c92ccc211e" resolved "https://registry.yarnpkg.com/@prisma/engines-version/-/engines-version-4.9.0-42.ceb5c99003b99c9ee2c1d2e618e359c14aef2ea5.tgz#9d817a5779fc05b107eb02f63d197ad296d60b3c"
integrity sha512-MHSOSexomRMom8QN4t7bu87wPPD+pa+hW9+71JnVcF3DqyyO/ycCLhRL1we3EojRpZxKvuyGho2REQsMCvxcJw== integrity sha512-M16aibbxi/FhW7z1sJCX8u+0DriyQYY5AyeTH7plQm9MLnURoiyn3CZBqAyIoQ+Z1pS77usCIibYJWSgleBMBA==
"@prisma/engines@4.8.0": "@prisma/engines@4.9.0":
version "4.8.0" version "4.9.0"
resolved "https://registry.yarnpkg.com/@prisma/engines/-/engines-4.8.0.tgz#5123c67dc0d2caa008268fc63081ca2d68b2ed7e" resolved "https://registry.yarnpkg.com/@prisma/engines/-/engines-4.9.0.tgz#05a1411964e047c1bc43f777c7a1c69f86a2a26c"
integrity sha512-A1Asn2rxZMlLAj1HTyfaCv0VQrLUv034jVay05QlqZg1qiHPeA3/pGTfNMijbsMYCsGVxfWEJuaZZuNxXGMCrA== integrity sha512-t1pt0Gsp+HcgPJrHFc+d/ZSAaKKWar2G/iakrE07yeKPNavDP3iVKPpfXP22OTCHZUWf7OelwKJxQgKAm5hkgw==
"@samverschueren/stream-to-observable@^0.3.0": "@samverschueren/stream-to-observable@^0.3.0":
version "0.3.1" version "0.3.1"
@ -16427,12 +16427,12 @@ pretty-hrtime@^1.0.3:
resolved "https://registry.npmjs.org/pretty-hrtime/-/pretty-hrtime-1.0.3.tgz" resolved "https://registry.npmjs.org/pretty-hrtime/-/pretty-hrtime-1.0.3.tgz"
integrity sha512-66hKPCr+72mlfiSjlEB1+45IjXSqvVAIy6mocupoww4tBFE9R9IhwwUGoI4G++Tc9Aq+2rxOt0RFU6gPcrte0A== integrity sha512-66hKPCr+72mlfiSjlEB1+45IjXSqvVAIy6mocupoww4tBFE9R9IhwwUGoI4G++Tc9Aq+2rxOt0RFU6gPcrte0A==
prisma@4.8.0: prisma@4.9.0:
version "4.8.0" version "4.9.0"
resolved "https://registry.yarnpkg.com/prisma/-/prisma-4.8.0.tgz#634dbbdc9d3f76c61604880251673d08ccb6f02b" resolved "https://registry.yarnpkg.com/prisma/-/prisma-4.9.0.tgz#295954b2a89cd35a0e6bcf66b2b036dbf80c75ee"
integrity sha512-DWIhxvxt8f4h6MDd35mz7BJff+fu7HItW3WPDIEpCR3RzcOWyiHBbLQW5/DOgmf+pRLTjwXQob7kuTZVYUAw5w== integrity sha512-bS96oZ5oDFXYgoF2l7PJ3Mp1wWWfLOo8B/jAfbA2Pn0Wm5Z/owBHzaMQKS3i1CzVBDWWPVnOohmbJmjvkcHS5w==
dependencies: dependencies:
"@prisma/engines" "4.8.0" "@prisma/engines" "4.9.0"
prismjs@^1.27.0, prismjs@^1.28.0: prismjs@^1.27.0, prismjs@^1.28.0:
version "1.28.0" version "1.28.0"

Loading…
Cancel
Save