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. 26
      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. 171
      apps/client/src/app/pages/pricing/pricing-page.html
  20. 16
      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
### Added
- Added an interstitial for the subscription
- Added a quote to the blog post _Ghostfolio auf Sackgeld.com vorgestellt_
### Changed
- 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 `prisma` from version `4.8.0` to `4.9.0`
### 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
## 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
- [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)
- 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 { DataSource } from '@prisma/client';
import { StatusCodes, getReasonPhrase } from 'http-status-codes';
import { isEmpty } from 'lodash';
import { ImportDataDto } from './import-data.dto';
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 { PROPERTY_IS_USER_SIGNUP_ENABLED } from '@ghostfolio/common/config';
import { User, UserSettings } from '@ghostfolio/common/interfaces';
import { hasPermission, permissions } from '@ghostfolio/common/permissions';
import type { RequestWithUser } from '@ghostfolio/common/types';
@ -31,7 +29,6 @@ import { UserService } from './user.service';
@Controller('user')
export class UserController {
public constructor(
private readonly configurationService: ConfigurationService,
private readonly jwtService: JwtService,
private readonly propertyService: PropertyService,
@Inject(REQUEST) private readonly request: RequestWithUser,

26
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')) {

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 { 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,

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);
}
public onCancel(): void {
public onCancel() {
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() {}
public onCancel(): void {
public onCancel() {
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;
}
public onCancel(): void {
public onCancel() {
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>
<div class="mb-4 text-center">
<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
alt="Ghostfolio auf Sackgeld.com vorgestellt Teaser"
class="border rounded w-100"
@ -16,7 +16,26 @@
<p>
Wir freuen uns darüber, dass unsere Open Source Portfolio Tracking
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>
</section>
<section class="mb-4">
@ -24,23 +43,23 @@
Ghostfolio – Open Source Wealth Management Software
</h2>
<p>
Ghostfolio ermöglicht es dir, deine Portfolio-Performance einfach zu
verfolgen und zu analysieren. Es bietet dir detaillierte
Informationen über deine Positionen, historische Entwicklung und die
Ghostfolio ermöglicht es dir, dein Portfolio einfach zu verfolgen
und zu analysieren. Es bietet dir detaillierte Informationen über
deine Positionen, historische Entwicklung, Performance und die
Zusammenstellung deines Portfolios. Durch die Open Source-Lizenz (<a
href="https://github.com/ghostfolio/ghostfolio/blob/main/LICENSE"
target="_blank"
>GNU Affero General Public License v3.0</a
>) wird die Software ständig weiterentwickelt und verbessert und du
hast sogar die Möglichkeit, dich selbst daran zu beteiligen. Wir
sind davon überzeugt, mit dem Open-Source-Ansatz von Ghostfolio das
Finanzwissen und Investieren für alle zugänglicher zu machen.
>) wird die Software ständig weiterentwickelt, verbessert und du
hast sogar selbst die Möglichkeit, dich daran zu beteiligen. Wir
sind davon überzeugt, mit diesem Open-Source-Ansatz von Ghostfolio
das Finanzwissen und Investieren für alle zugänglicher zu machen.
</p>
</section>
<section class="mb-4">
<h2 class="h4">Sackgeld.com – App für ein höheres Sackgeld</h2>
<p>
Das Schweizer Fintech News Portal
Das Schweizer FinTech Nachrichtenportal
<a href="https://www.sackgeld.com" target="_blank">Sackgeld.com</a>
informiert über die neuesten Entwicklungen und Innovationen im
Bereich FinTech. Dazu gehören News, Artikel und persönliche
@ -51,15 +70,12 @@
<section class="mb-4">
<p>
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"
target="_blank"
>Was taugt Ghostfolio als Portfolio Performance Tracking-Tool?</a
>" nachlesen.
</p>
<p>
Wir freuen uns auf dein Feedback.<br />
Thomas von Ghostfolio
>
</p>
</section>
<section class="mb-4">
@ -79,6 +95,9 @@
<li class="list-inline-item">
<span class="badge badge-light">App</span>
</li>
<li class="list-inline-item">
<span class="badge badge-light">Asset</span>
</li>
<li class="list-inline-item">
<span class="badge badge-light">Feedback</span>
</li>
@ -103,6 +122,9 @@
<li class="list-inline-item">
<span class="badge badge-light">Lizenz</span>
</li>
<li class="list-inline-item">
<span class="badge badge-light">Media</span>
</li>
<li class="list-inline-item">
<span class="badge badge-light">Open Source</span>
</li>
@ -118,6 +140,9 @@
<li class="list-inline-item">
<span class="badge badge-light">Portfolio</span>
</li>
<li class="list-inline-item">
<span class="badge badge-light">Presse</span>
</li>
<li class="list-inline-item">
<span class="badge badge-light">Sackgeld</span>
</li>
@ -139,6 +164,9 @@
<li class="list-inline-item">
<span class="badge badge-light">Vermögen</span>
</li>
<li class="list-inline-item">
<span class="badge badge-light">Vorsorge</span>
</li>
<li class="list-inline-item">
<span class="badge badge-light">Wealth Management</span>
</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();
}

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) {
if (symbol) {
if (symbol && symbol !== UNKNOWN_KEY) {
this.router.navigate([], {
queryParams: { accountId: symbol, accountDetailDialog: true }
});

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

@ -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>It’s free.</small></p>
<p class="m-0 text-muted"><small i18n>It’s free.</small></p>
</div>
</div>
</div>

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

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

1
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?: {

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

4
package.json

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

36
yarn.lock

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

Loading…
Cancel
Save