Thomas Kaul 7 days ago
committed by GitHub
parent
commit
f705a2aeac
No known key found for this signature in database GPG Key ID: B5690EEEBB952194
  1. 6
      CHANGELOG.md
  2. 28
      apps/api/src/app/info/info.service.ts
  3. 36
      apps/api/src/app/subscription/subscription.service.ts
  4. 2
      apps/api/src/app/user/user.service.ts
  5. 12
      apps/client/src/app/app.component.ts
  6. 19
      apps/client/src/app/components/user-account-membership/user-account-membership.component.ts
  7. 26
      apps/client/src/app/pages/pricing/pricing-page.component.ts
  8. 30
      apps/client/src/app/pages/resources/personal-finance-tools/product-page.component.ts
  9. 4
      libs/common/src/lib/interfaces/info-item.interface.ts
  10. 4
      libs/common/src/lib/interfaces/user.interface.ts
  11. 2
      libs/common/src/lib/types/index.ts
  12. 7
      libs/common/src/lib/types/user-with-settings.type.ts

6
CHANGELOG.md

@ -5,6 +5,12 @@ All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
## Unreleased
### Changed
- Moved the subscription offer from the info to the user service
## 2.151.0 - 2025-04-11 ## 2.151.0 - 2025-04-11
### Added ### Added

28
apps/api/src/app/info/info.service.ts

@ -13,7 +13,6 @@ import {
PROPERTY_DEMO_USER_ID, PROPERTY_DEMO_USER_ID,
PROPERTY_IS_READ_ONLY_MODE, PROPERTY_IS_READ_ONLY_MODE,
PROPERTY_SLACK_COMMUNITY_USERS, PROPERTY_SLACK_COMMUNITY_USERS,
PROPERTY_STRIPE_CONFIG,
ghostfolioFearAndGreedIndexDataSource ghostfolioFearAndGreedIndexDataSource
} from '@ghostfolio/common/config'; } from '@ghostfolio/common/config';
import { import {
@ -21,13 +20,8 @@ import {
encodeDataSource, encodeDataSource,
extractNumberFromString extractNumberFromString
} from '@ghostfolio/common/helper'; } from '@ghostfolio/common/helper';
import { import { InfoItem, Statistics } from '@ghostfolio/common/interfaces';
InfoItem,
Statistics,
SubscriptionOffer
} from '@ghostfolio/common/interfaces';
import { permissions } from '@ghostfolio/common/permissions'; import { permissions } from '@ghostfolio/common/permissions';
import { SubscriptionOfferKey } from '@ghostfolio/common/types';
import { Injectable, Logger } from '@nestjs/common'; import { Injectable, Logger } from '@nestjs/common';
import { JwtService } from '@nestjs/jwt'; import { JwtService } from '@nestjs/jwt';
@ -100,8 +94,7 @@ export class InfoService {
demoAuthToken, demoAuthToken,
isUserSignupEnabled, isUserSignupEnabled,
platforms, platforms,
statistics, statistics
subscriptionOffers
] = await Promise.all([ ] = await Promise.all([
this.benchmarkService.getBenchmarkAssetProfiles(), this.benchmarkService.getBenchmarkAssetProfiles(),
this.getDemoAuthToken(), this.getDemoAuthToken(),
@ -109,8 +102,7 @@ export class InfoService {
this.platformService.getPlatforms({ this.platformService.getPlatforms({
orderBy: { name: 'asc' } orderBy: { name: 'asc' }
}), }),
this.getStatistics(), this.getStatistics()
this.getSubscriptionOffers()
]); ]);
if (isUserSignupEnabled) { if (isUserSignupEnabled) {
@ -125,7 +117,6 @@ export class InfoService {
isReadOnlyMode, isReadOnlyMode,
platforms, platforms,
statistics, statistics,
subscriptionOffers,
baseCurrency: DEFAULT_CURRENCY, baseCurrency: DEFAULT_CURRENCY,
currencies: this.exchangeRateDataService.getCurrencies() currencies: this.exchangeRateDataService.getCurrencies()
}; };
@ -299,19 +290,6 @@ export class InfoService {
return statistics; return statistics;
} }
private async getSubscriptionOffers(): Promise<{
[offer in SubscriptionOfferKey]: SubscriptionOffer;
}> {
if (!this.configurationService.get('ENABLE_FEATURE_SUBSCRIPTION')) {
return undefined;
}
return (
((await this.propertyService.getByKey(PROPERTY_STRIPE_CONFIG)) as any) ??
{}
);
}
private async getUptime(): Promise<number> { private async getUptime(): Promise<number> {
{ {
try { try {

36
apps/api/src/app/subscription/subscription.service.ts

@ -158,26 +158,30 @@ export class SubscriptionService {
} }
} }
public getSubscription({ public async getSubscription({
createdAt, createdAt,
subscriptions subscriptions
}: { }: {
createdAt: UserWithSettings['createdAt']; createdAt: UserWithSettings['createdAt'];
subscriptions: Subscription[]; subscriptions: Subscription[];
}): UserWithSettings['subscription'] { }): Promise<UserWithSettings['subscription']> {
if (subscriptions.length > 0) { if (subscriptions.length > 0) {
const { expiresAt, price } = subscriptions.reduce((a, b) => { const { expiresAt, price } = subscriptions.reduce((a, b) => {
return new Date(a.expiresAt) > new Date(b.expiresAt) ? a : b; return new Date(a.expiresAt) > new Date(b.expiresAt) ? a : b;
}); });
let offer: SubscriptionOfferKey = price ? 'renewal' : 'default'; let offerKey: SubscriptionOfferKey = price ? 'renewal' : 'default';
if (isBefore(createdAt, parseDate('2023-01-01'))) { if (isBefore(createdAt, parseDate('2023-01-01'))) {
offer = 'renewal-early-bird-2023'; offerKey = 'renewal-early-bird-2023';
} else if (isBefore(createdAt, parseDate('2024-01-01'))) { } else if (isBefore(createdAt, parseDate('2024-01-01'))) {
offer = 'renewal-early-bird-2024'; offerKey = 'renewal-early-bird-2024';
} }
const offer = await this.getSubscriptionOffer({
key: offerKey
});
return { return {
expiresAt, expiresAt,
offer, offer,
@ -186,10 +190,30 @@ export class SubscriptionService {
: SubscriptionType.Basic : SubscriptionType.Basic
}; };
} else { } else {
const offer = await this.getSubscriptionOffer({
key: 'default'
});
return { return {
offer: 'default', offer,
type: SubscriptionType.Basic type: SubscriptionType.Basic
}; };
} }
} }
private async getSubscriptionOffer({
key
}: {
key: SubscriptionOfferKey;
}): Promise<SubscriptionOffer> {
if (!this.configurationService.get('ENABLE_FEATURE_SUBSCRIPTION')) {
return undefined;
}
const offers =
((await this.propertyService.getByKey(PROPERTY_STRIPE_CONFIG)) as any) ??
{};
return offers[key];
}
} }

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

@ -339,7 +339,7 @@ export class UserService {
} }
if (this.configurationService.get('ENABLE_FEATURE_SUBSCRIPTION')) { if (this.configurationService.get('ENABLE_FEATURE_SUBSCRIPTION')) {
user.subscription = this.subscriptionService.getSubscription({ user.subscription = await this.subscriptionService.getSubscription({
createdAt: user.createdAt, createdAt: user.createdAt,
subscriptions: Subscription subscriptions: Subscription
}); });

12
apps/client/src/app/app.component.ts

@ -142,10 +142,6 @@ export class AppComponent implements OnDestroy, OnInit {
permissions.enableFearAndGreedIndex permissions.enableFearAndGreedIndex
); );
this.hasPromotion =
!!this.info?.subscriptionOffers?.default?.coupon ||
!!this.info?.subscriptionOffers?.default?.durationExtension;
this.impersonationStorageService this.impersonationStorageService
.onChangeHasImpersonation() .onChangeHasImpersonation()
.pipe(takeUntil(this.unsubscribeSubject)) .pipe(takeUntil(this.unsubscribeSubject))
@ -242,12 +238,8 @@ export class AppComponent implements OnDestroy, OnInit {
this.canCreateAccount || !!this.user?.systemMessage; this.canCreateAccount || !!this.user?.systemMessage;
this.hasPromotion = this.hasPromotion =
!!this.info?.subscriptionOffers?.[ !!this.user?.subscription?.offer?.coupon ||
this.user?.subscription?.offer ?? 'default' !!this.user?.subscription?.offer?.durationExtension;
]?.coupon ||
!!this.info?.subscriptionOffers?.[
this.user?.subscription?.offer ?? 'default'
]?.durationExtension;
this.initializeTheme(this.user?.settings.colorScheme); this.initializeTheme(this.user?.settings.colorScheme);

19
apps/client/src/app/components/user-account-membership/user-account-membership.component.ts

@ -51,8 +51,7 @@ export class UserAccountMembershipComponent implements OnDestroy {
private stripeService: StripeService, private stripeService: StripeService,
private userService: UserService private userService: UserService
) { ) {
const { baseCurrency, globalPermissions, subscriptionOffers } = const { baseCurrency, globalPermissions } = this.dataService.fetchInfo();
this.dataService.fetchInfo();
this.baseCurrency = baseCurrency; this.baseCurrency = baseCurrency;
@ -81,18 +80,12 @@ export class UserAccountMembershipComponent implements OnDestroy {
permissions.updateUserSettings permissions.updateUserSettings
); );
this.coupon = this.coupon = this.user?.subscription?.offer?.coupon;
subscriptionOffers?.[this.user.subscription.offer]?.coupon; this.couponId = this.user?.subscription?.offer?.couponId;
this.couponId =
subscriptionOffers?.[this.user.subscription.offer]?.couponId;
this.durationExtension = this.durationExtension =
subscriptionOffers?.[ this.user?.subscription?.offer?.durationExtension;
this.user.subscription.offer this.price = this.user?.subscription?.offer?.price;
]?.durationExtension; this.priceId = this.user?.subscription?.offer?.priceId;
this.price =
subscriptionOffers?.[this.user.subscription.offer]?.price;
this.priceId =
subscriptionOffers?.[this.user.subscription.offer]?.priceId;
this.changeDetectorRef.markForCheck(); this.changeDetectorRef.markForCheck();
} }

26
apps/client/src/app/pages/pricing/pricing-page.component.ts

@ -55,14 +55,9 @@ export class PricingPageComponent implements OnDestroy, OnInit {
) {} ) {}
public ngOnInit() { public ngOnInit() {
const { baseCurrency, subscriptionOffers } = this.dataService.fetchInfo(); const { baseCurrency } = this.dataService.fetchInfo();
this.baseCurrency = baseCurrency; this.baseCurrency = baseCurrency;
this.coupon = subscriptionOffers?.default?.coupon;
this.durationExtension = subscriptionOffers?.default?.durationExtension;
this.label = subscriptionOffers?.default?.label;
this.price = subscriptionOffers?.default?.price;
this.userService.stateChanged this.userService.stateChanged
.pipe(takeUntil(this.unsubscribeSubject)) .pipe(takeUntil(this.unsubscribeSubject))
.subscribe((state) => { .subscribe((state) => {
@ -74,20 +69,13 @@ export class PricingPageComponent implements OnDestroy, OnInit {
permissions.updateUserSettings permissions.updateUserSettings
); );
this.coupon = this.coupon = this.user?.subscription?.offer?.coupon;
subscriptionOffers?.[this.user?.subscription?.offer]?.coupon; this.couponId = this.user?.subscription?.offer?.couponId;
this.couponId =
subscriptionOffers?.[this.user.subscription.offer]?.couponId;
this.durationExtension = this.durationExtension =
subscriptionOffers?.[ this.user?.subscription?.offer?.durationExtension;
this.user?.subscription?.offer this.label = this.user?.subscription?.offer?.label;
]?.durationExtension; this.price = this.user?.subscription?.offer?.price;
this.label = this.priceId = this.user?.subscription?.offer?.priceId;
subscriptionOffers?.[this.user?.subscription?.offer]?.label;
this.price =
subscriptionOffers?.[this.user?.subscription?.offer]?.price;
this.priceId =
subscriptionOffers?.[this.user.subscription.offer]?.priceId;
this.changeDetectorRef.markForCheck(); this.changeDetectorRef.markForCheck();
} }

30
apps/client/src/app/pages/resources/personal-finance-tools/product-page.component.ts

@ -1,11 +1,13 @@
import { DataService } from '@ghostfolio/client/services/data.service'; import { UserService } from '@ghostfolio/client/services/user/user.service';
import { Product } from '@ghostfolio/common/interfaces'; import { Product } from '@ghostfolio/common/interfaces';
import { User } from '@ghostfolio/common/interfaces';
import { personalFinanceTools } from '@ghostfolio/common/personal-finance-tools'; import { personalFinanceTools } from '@ghostfolio/common/personal-finance-tools';
import { translate } from '@ghostfolio/ui/i18n'; import { translate } from '@ghostfolio/ui/i18n';
import { Component, OnInit } from '@angular/core'; import { Component, OnInit } from '@angular/core';
import { MatButtonModule } from '@angular/material/button'; import { MatButtonModule } from '@angular/material/button';
import { ActivatedRoute, RouterModule } from '@angular/router'; import { ActivatedRoute, RouterModule } from '@angular/router';
import { Subject, takeUntil } from 'rxjs';
@Component({ @Component({
host: { class: 'page' }, host: { class: 'page' },
@ -26,17 +28,16 @@ export class GfProductPageComponent implements OnInit {
'personal-finance-tools' 'personal-finance-tools'
]; ];
public tags: string[]; public tags: string[];
public user: User;
private unsubscribeSubject = new Subject<void>();
public constructor( public constructor(
private dataService: DataService, private route: ActivatedRoute,
private route: ActivatedRoute private userService: UserService
) {} ) {}
public ngOnInit() { public ngOnInit() {
const { subscriptionOffers } = this.dataService.fetchInfo();
this.price = subscriptionOffers?.default?.price;
this.product1 = { this.product1 = {
founded: 2021, founded: 2021,
hasFreePlan: true, hasFreePlan: true,
@ -100,5 +101,20 @@ export class GfProductPageComponent implements OnInit {
].sort((a, b) => { ].sort((a, b) => {
return a.localeCompare(b, undefined, { sensitivity: 'base' }); return a.localeCompare(b, undefined, { sensitivity: 'base' });
}); });
this.userService.stateChanged
.pipe(takeUntil(this.unsubscribeSubject))
.subscribe((state) => {
if (state?.user) {
this.user = state.user;
this.price = this.user?.subscription?.offer?.price;
}
});
}
public ngOnDestroy() {
this.unsubscribeSubject.next();
this.unsubscribeSubject.complete();
} }
} }

4
libs/common/src/lib/interfaces/info-item.interface.ts

@ -1,9 +1,6 @@
import { SubscriptionOfferKey } from '@ghostfolio/common/types';
import { Platform, SymbolProfile } from '@prisma/client'; import { Platform, SymbolProfile } from '@prisma/client';
import { Statistics } from './statistics.interface'; import { Statistics } from './statistics.interface';
import { SubscriptionOffer } from './subscription-offer.interface';
export interface InfoItem { export interface InfoItem {
baseCurrency: string; baseCurrency: string;
@ -18,5 +15,4 @@ export interface InfoItem {
platforms: Platform[]; platforms: Platform[];
statistics: Statistics; statistics: Statistics;
stripePublicKey?: string; stripePublicKey?: string;
subscriptionOffers: { [offer in SubscriptionOfferKey]: SubscriptionOffer };
} }

4
libs/common/src/lib/interfaces/user.interface.ts

@ -1,8 +1,8 @@
import { SubscriptionOfferKey } from '@ghostfolio/common/types';
import { SubscriptionType } from '@ghostfolio/common/types/subscription-type.type'; import { SubscriptionType } from '@ghostfolio/common/types/subscription-type.type';
import { Access, Account, Tag } from '@prisma/client'; import { Access, Account, Tag } from '@prisma/client';
import { SubscriptionOffer } from './subscription-offer.interface';
import { SystemMessage } from './system-message.interface'; import { SystemMessage } from './system-message.interface';
import { UserSettings } from './user-settings.interface'; import { UserSettings } from './user-settings.interface';
@ -18,7 +18,7 @@ export interface User {
systemMessage?: SystemMessage; systemMessage?: SystemMessage;
subscription: { subscription: {
expiresAt?: Date; expiresAt?: Date;
offer: SubscriptionOfferKey; offer: SubscriptionOffer;
type: SubscriptionType; type: SubscriptionType;
}; };
tags: (Tag & { isUsed: boolean })[]; tags: (Tag & { isUsed: boolean })[];

2
libs/common/src/lib/types/index.ts

@ -17,6 +17,7 @@ import type { Market } from './market.type';
import type { OrderWithAccount } from './order-with-account.type'; import type { OrderWithAccount } from './order-with-account.type';
import type { RequestWithUser } from './request-with-user.type'; import type { RequestWithUser } from './request-with-user.type';
import type { SubscriptionOfferKey } from './subscription-offer-key.type'; import type { SubscriptionOfferKey } from './subscription-offer-key.type';
import type { SubscriptionType } from './subscription-type.type';
import type { UserWithSettings } from './user-with-settings.type'; import type { UserWithSettings } from './user-with-settings.type';
import type { ViewMode } from './view-mode.type'; import type { ViewMode } from './view-mode.type';
@ -40,6 +41,7 @@ export type {
OrderWithAccount, OrderWithAccount,
RequestWithUser, RequestWithUser,
SubscriptionOfferKey, SubscriptionOfferKey,
SubscriptionType,
UserWithSettings, UserWithSettings,
ViewMode ViewMode
}; };

7
libs/common/src/lib/types/user-with-settings.type.ts

@ -1,6 +1,5 @@
import { UserSettings } from '@ghostfolio/common/interfaces'; import { SubscriptionOffer, UserSettings } from '@ghostfolio/common/interfaces';
import { SubscriptionOfferKey } from '@ghostfolio/common/types'; import { SubscriptionType } from '@ghostfolio/common/types';
import { SubscriptionType } from '@ghostfolio/common/types/subscription-type.type';
import { Access, Account, Settings, User } from '@prisma/client'; import { Access, Account, Settings, User } from '@prisma/client';
@ -14,7 +13,7 @@ export type UserWithSettings = User & {
Settings: Settings & { settings: UserSettings }; Settings: Settings & { settings: UserSettings };
subscription?: { subscription?: {
expiresAt?: Date; expiresAt?: Date;
offer: SubscriptionOfferKey; offer: SubscriptionOffer;
type: SubscriptionType; type: SubscriptionType;
}; };
}; };

Loading…
Cancel
Save