From e7fbcd4fa0acf32d8afd5c0873c6488e8f45c2f1 Mon Sep 17 00:00:00 2001 From: Thomas <4159106+dtslvr@users.noreply.github.com> Date: Wed, 2 Jun 2021 20:15:53 +0200 Subject: [PATCH] Feature/extend pricing page (#130) * Extend pricing page * Feature/align pricing page with subscription model (#135) * Align pricing page with subscription model * Update changelog --- CHANGELOG.md | 1 + apps/api/src/app/user/user.service.ts | 52 ++-- .../pages/account/account-page.component.ts | 8 +- .../src/app/pages/account/account-page.html | 6 +- .../src/app/pages/pricing/pricing-page.html | 249 ++++++++++++------ .../app/pages/pricing/pricing-page.module.ts | 10 +- .../src/app/pages/pricing/pricing-page.scss | 13 + .../src/lib/interfaces/user-with-settings.ts | 5 + .../src/lib/interfaces/user.interface.ts | 2 +- .../common/src/lib/types/subscription.type.ts | 4 + prisma/schema.prisma | 24 +- 11 files changed, 262 insertions(+), 112 deletions(-) create mode 100644 libs/common/src/lib/types/subscription.type.ts diff --git a/CHANGELOG.md b/CHANGELOG.md index 77aea462b..7715e20c2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Changed - Moved the tools to a sub path (`/tools`) +- Extended the pricing page and aligned with the subscription model ## 1.9.0 - 01.06.2021 diff --git a/apps/api/src/app/user/user.service.ts b/apps/api/src/app/user/user.service.ts index fc5a461c4..ff018ed69 100644 --- a/apps/api/src/app/user/user.service.ts +++ b/apps/api/src/app/user/user.service.ts @@ -4,9 +4,10 @@ import { locale } from '@ghostfolio/common/config'; import { resetHours } from '@ghostfolio/common/helper'; import { User as IUser, UserWithSettings } from '@ghostfolio/common/interfaces'; import { getPermissions, permissions } from '@ghostfolio/common/permissions'; +import { SubscriptionType } from '@ghostfolio/common/types/subscription.type'; import { Injectable } from '@nestjs/common'; import { Currency, Prisma, Provider, User, ViewMode } from '@prisma/client'; -import { add } from 'date-fns'; +import { add, isBefore } from 'date-fns'; const crypto = require('crypto'); @@ -24,7 +25,8 @@ export class UserService { alias, id, role, - Settings + Settings, + subscription }: UserWithSettings): Promise { const access = await this.prisma.access.findMany({ include: { @@ -43,6 +45,7 @@ export class UserService { return { alias, id, + subscription, access: access.map((accessItem) => { return { alias: accessItem.User.alias, @@ -54,11 +57,7 @@ export class UserService { settings: { locale, baseCurrency: Settings?.currency ?? UserService.DEFAULT_CURRENCY, - viewMode: Settings.viewMode ?? ViewMode.DEFAULT - }, - subscription: { - expiresAt: resetHours(add(new Date(), { days: 7 })), - type: 'Trial' + viewMode: Settings?.viewMode ?? ViewMode.DEFAULT } }; } @@ -66,26 +65,49 @@ export class UserService { public async user( userWhereUniqueInput: Prisma.UserWhereUniqueInput ): Promise { - const user = await this.prisma.user.findUnique({ - include: { Account: true, Settings: true }, + const userFromDatabase = await this.prisma.user.findUnique({ + include: { Account: true, Settings: true, Subscription: true }, where: userWhereUniqueInput }); - if (user?.Settings) { - if (!user.Settings.currency) { + const user: UserWithSettings = userFromDatabase; + + if (userFromDatabase?.Settings) { + if (!userFromDatabase.Settings.currency) { // Set default currency if needed - user.Settings.currency = UserService.DEFAULT_CURRENCY; + userFromDatabase.Settings.currency = UserService.DEFAULT_CURRENCY; } - } else if (user) { + } else if (userFromDatabase) { // Set default settings if needed - user.Settings = { + userFromDatabase.Settings = { currency: UserService.DEFAULT_CURRENCY, updatedAt: new Date(), - userId: user?.id, + userId: userFromDatabase?.id, viewMode: ViewMode.DEFAULT }; } + if (this.configurationService.get('ENABLE_FEATURE_SUBSCRIPTION')) { + if (userFromDatabase?.Subscription?.length > 0) { + const latestSubscription = userFromDatabase.Subscription.reduce( + (a, b) => { + return new Date(a.expiresAt) > new Date(b.expiresAt) ? a : b; + } + ); + + user.subscription = { + expiresAt: latestSubscription.expiresAt, + type: isBefore(new Date(), latestSubscription.expiresAt) + ? SubscriptionType.Premium + : SubscriptionType.Basic + }; + } else { + user.subscription = { + type: SubscriptionType.Basic + }; + } + } + return user; } diff --git a/apps/client/src/app/pages/account/account-page.component.ts b/apps/client/src/app/pages/account/account-page.component.ts index ed70a2561..14264f246 100644 --- a/apps/client/src/app/pages/account/account-page.component.ts +++ b/apps/client/src/app/pages/account/account-page.component.ts @@ -18,7 +18,6 @@ export class AccountPageComponent implements OnDestroy, OnInit { public baseCurrency: Currency; public currencies: Currency[] = []; public defaultDateFormat = DEFAULT_DATE_FORMAT; - public hasPermissionForSubscription: boolean; public hasPermissionToUpdateUserSettings: boolean; public user: User; @@ -35,13 +34,8 @@ export class AccountPageComponent implements OnDestroy, OnInit { this.dataService .fetchInfo() .pipe(takeUntil(this.unsubscribeSubject)) - .subscribe(({ currencies, globalPermissions }) => { + .subscribe(({ currencies }) => { this.currencies = currencies; - - this.hasPermissionForSubscription = hasPermission( - globalPermissions, - permissions.enableSubscription - ); }); this.userService.stateChanged diff --git a/apps/client/src/app/pages/account/account-page.html b/apps/client/src/app/pages/account/account-page.html index abd1f0fa4..327ab5256 100644 --- a/apps/client/src/app/pages/account/account-page.html +++ b/apps/client/src/app/pages/account/account-page.html @@ -15,13 +15,13 @@
Alias
{{ user.alias }}
-
+
Membership
- {{ user?.subscription?.type }} + {{ user.subscription.type }}
-
+
Valid until {{ user.subscription.expiresAt | date: defaultDateFormat }}
diff --git a/apps/client/src/app/pages/pricing/pricing-page.html b/apps/client/src/app/pages/pricing/pricing-page.html index 2e580c61a..1ade326bb 100644 --- a/apps/client/src/app/pages/pricing/pricing-page.html +++ b/apps/client/src/app/pages/pricing/pricing-page.html @@ -2,95 +2,178 @@

Pricing Plans

+

+ Our official + Ghostfolio cloud offering is the easiest way to get + started. Due to the time it saves, this will be the best option for most + people. The revenue is used for covering the hosting costs. +

+

+ If you prefer to run Ghostfolio on your own + infrastructure, please find the source code and further instructions on + GitHub. +

-
- -

Open Source

-

Host your Ghostfolio instance by yourself.

-
    -
  • - - Portfolio Performance -
  • -
  • - - Portfolio Summary -
  • -
  • - - Unlimited Transactions -
  • -
  • - - Advanced Insights -
  • -
-

- Free -

+
+ +
+

Open Source

+

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

+
    +
  • + + Unlimited Transactions +
  • +
  • + + Portfolio Performance +
  • +
  • + + Zen Mode +
  • +
  • + + Portfolio Summary +
  • +
  • + + Advanced Insights +
  • +
+
+

Self-hosted.

+

Free

-
+
-

- Diamond - -

-

- Get a fully managed Ghostfolio cloud offering. -

-
    -
  • - - Portfolio Performance -
  • -
  • - - Portfolio Summary -
  • -
  • - - Unlimited Transactions -
  • -
  • +
    +

    Basic

    +

    + For new investors who are just getting started with trading. +

    +
      +
    • + + Unlimited Transactions +
    • +
    • + + Portfolio Performance +
    • +
    • + + Zen Mode +
    • +
    • + +
    • +
    • + +
    • +
    +
    +

    Fully managed Ghostfolio cloud offering.

    +

    Free

    + +
+
+ +
+

+ Premium - Advanced Insights - - +

+

+ For ambitious investors who need the full picture of their + financial assets. +

+
    +
  • + + Unlimited Transactions +
  • +
  • + + Portfolio Performance +
  • +
  • + + Zen Mode +
  • +
  • + + Portfolio Summary +
  • +
  • + + Advanced Insights +
  • +
+
+

Fully managed Ghostfolio cloud offering.

{{ user?.settings.baseCurrency || baseCurrency }} - 2.99 + 0.00 3.99 / Month

@@ -99,4 +182,12 @@
+
+
+ + Create Account + +

It's free

+
+
diff --git a/apps/client/src/app/pages/pricing/pricing-page.module.ts b/apps/client/src/app/pages/pricing/pricing-page.module.ts index 810d9c0bf..02d59a364 100644 --- a/apps/client/src/app/pages/pricing/pricing-page.module.ts +++ b/apps/client/src/app/pages/pricing/pricing-page.module.ts @@ -1,6 +1,8 @@ import { CommonModule } from '@angular/common'; import { CUSTOM_ELEMENTS_SCHEMA, NgModule } from '@angular/core'; +import { MatButtonModule } from '@angular/material/button'; import { MatCardModule } from '@angular/material/card'; +import { RouterModule } from '@angular/router'; import { PricingPageRoutingModule } from './pricing-page-routing.module'; import { PricingPageComponent } from './pricing-page.component'; @@ -8,7 +10,13 @@ import { PricingPageComponent } from './pricing-page.component'; @NgModule({ declarations: [PricingPageComponent], exports: [], - imports: [CommonModule, MatCardModule, PricingPageRoutingModule], + imports: [ + CommonModule, + MatButtonModule, + MatCardModule, + PricingPageRoutingModule, + RouterModule + ], providers: [], schemas: [CUSTOM_ELEMENTS_SCHEMA] }) diff --git a/apps/client/src/app/pages/pricing/pricing-page.scss b/apps/client/src/app/pages/pricing/pricing-page.scss index 23f5657f2..53f951997 100644 --- a/apps/client/src/app/pages/pricing/pricing-page.scss +++ b/apps/client/src/app/pages/pricing/pricing-page.scss @@ -2,6 +2,15 @@ color: rgb(var(--dark-primary-text)); display: block; + a { + color: rgba(var(--palette-primary-500), 1); + font-weight: bold; + + &:hover { + color: rgba(var(--palette-primary-300), 1); + } + } + .mat-card { &.active { border-color: rgba(var(--palette-primary-500), 1); @@ -11,4 +20,8 @@ :host-context(.is-dark-theme) { color: rgb(var(--light-primary-text)); + + a { + color: rgb(var(--light-primary-text)); + } } diff --git a/libs/common/src/lib/interfaces/user-with-settings.ts b/libs/common/src/lib/interfaces/user-with-settings.ts index 655322936..eae7789ad 100644 --- a/libs/common/src/lib/interfaces/user-with-settings.ts +++ b/libs/common/src/lib/interfaces/user-with-settings.ts @@ -1,6 +1,11 @@ +import { SubscriptionType } from '@ghostfolio/common/types/subscription.type'; import { Account, Settings, User } from '@prisma/client'; export type UserWithSettings = User & { Account: Account[]; Settings: Settings; + subscription?: { + expiresAt?: Date; + type: SubscriptionType; + }; }; diff --git a/libs/common/src/lib/interfaces/user.interface.ts b/libs/common/src/lib/interfaces/user.interface.ts index e480bcd27..6b98ad786 100644 --- a/libs/common/src/lib/interfaces/user.interface.ts +++ b/libs/common/src/lib/interfaces/user.interface.ts @@ -12,6 +12,6 @@ export interface User { settings: UserSettings; subscription: { expiresAt: Date; - type: 'Trial'; + type: 'Basic' | 'Premium'; }; } diff --git a/libs/common/src/lib/types/subscription.type.ts b/libs/common/src/lib/types/subscription.type.ts new file mode 100644 index 000000000..0f4cd2dc2 --- /dev/null +++ b/libs/common/src/lib/types/subscription.type.ts @@ -0,0 +1,4 @@ +export enum SubscriptionType { + Basic = 'Basic', + Premium = 'Premium' +} diff --git a/prisma/schema.prisma b/prisma/schema.prisma index 56f367929..0c5f756a4 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -99,21 +99,33 @@ model Settings { userId String @id } +model Subscription { + createdAt DateTime @default(now()) + expiresAt DateTime + id String @default(uuid()) + updatedAt DateTime @updatedAt + User User @relation(fields: [userId], references: [id]) + userId String + + @@id([id, userId]) +} + model User { - Access Access[] @relation("accessGet") - AccessGive Access[] @relation(name: "accessGive") + Access Access[] @relation("accessGet") + AccessGive Access[] @relation(name: "accessGive") accessToken String? Account Account[] alias String? Analytics Analytics? - createdAt DateTime @default(now()) - id String @id @default(uuid()) + createdAt DateTime @default(now()) + id String @id @default(uuid()) Order Order[] provider Provider? - role Role @default(USER) + role Role @default(USER) Settings Settings? + Subscription Subscription[] thirdPartyId String? - updatedAt DateTime @updatedAt + updatedAt DateTime @updatedAt } enum AccountType {