Thomas Kaul 1 week 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/),
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
### Added

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

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

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

@ -158,26 +158,30 @@ export class SubscriptionService {
}
}
public getSubscription({
public async getSubscription({
createdAt,
subscriptions
}: {
createdAt: UserWithSettings['createdAt'];
subscriptions: Subscription[];
}): UserWithSettings['subscription'] {
}): Promise<UserWithSettings['subscription']> {
if (subscriptions.length > 0) {
const { expiresAt, price } = subscriptions.reduce((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'))) {
offer = 'renewal-early-bird-2023';
offerKey = 'renewal-early-bird-2023';
} 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 {
expiresAt,
offer,
@ -186,10 +190,30 @@ export class SubscriptionService {
: SubscriptionType.Basic
};
} else {
const offer = await this.getSubscriptionOffer({
key: 'default'
});
return {
offer: 'default',
offer,
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')) {
user.subscription = this.subscriptionService.getSubscription({
user.subscription = await this.subscriptionService.getSubscription({
createdAt: user.createdAt,
subscriptions: Subscription
});

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

@ -142,10 +142,6 @@ export class AppComponent implements OnDestroy, OnInit {
permissions.enableFearAndGreedIndex
);
this.hasPromotion =
!!this.info?.subscriptionOffers?.default?.coupon ||
!!this.info?.subscriptionOffers?.default?.durationExtension;
this.impersonationStorageService
.onChangeHasImpersonation()
.pipe(takeUntil(this.unsubscribeSubject))
@ -242,12 +238,8 @@ export class AppComponent implements OnDestroy, OnInit {
this.canCreateAccount || !!this.user?.systemMessage;
this.hasPromotion =
!!this.info?.subscriptionOffers?.[
this.user?.subscription?.offer ?? 'default'
]?.coupon ||
!!this.info?.subscriptionOffers?.[
this.user?.subscription?.offer ?? 'default'
]?.durationExtension;
!!this.user?.subscription?.offer?.coupon ||
!!this.user?.subscription?.offer?.durationExtension;
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 userService: UserService
) {
const { baseCurrency, globalPermissions, subscriptionOffers } =
this.dataService.fetchInfo();
const { baseCurrency, globalPermissions } = this.dataService.fetchInfo();
this.baseCurrency = baseCurrency;
@ -81,18 +80,12 @@ export class UserAccountMembershipComponent implements OnDestroy {
permissions.updateUserSettings
);
this.coupon =
subscriptionOffers?.[this.user.subscription.offer]?.coupon;
this.couponId =
subscriptionOffers?.[this.user.subscription.offer]?.couponId;
this.coupon = this.user?.subscription?.offer?.coupon;
this.couponId = this.user?.subscription?.offer?.couponId;
this.durationExtension =
subscriptionOffers?.[
this.user.subscription.offer
]?.durationExtension;
this.price =
subscriptionOffers?.[this.user.subscription.offer]?.price;
this.priceId =
subscriptionOffers?.[this.user.subscription.offer]?.priceId;
this.user?.subscription?.offer?.durationExtension;
this.price = this.user?.subscription?.offer?.price;
this.priceId = this.user?.subscription?.offer?.priceId;
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() {
const { baseCurrency, subscriptionOffers } = this.dataService.fetchInfo();
const { baseCurrency } = this.dataService.fetchInfo();
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
.pipe(takeUntil(this.unsubscribeSubject))
.subscribe((state) => {
@ -74,20 +69,13 @@ export class PricingPageComponent implements OnDestroy, OnInit {
permissions.updateUserSettings
);
this.coupon =
subscriptionOffers?.[this.user?.subscription?.offer]?.coupon;
this.couponId =
subscriptionOffers?.[this.user.subscription.offer]?.couponId;
this.coupon = this.user?.subscription?.offer?.coupon;
this.couponId = this.user?.subscription?.offer?.couponId;
this.durationExtension =
subscriptionOffers?.[
this.user?.subscription?.offer
]?.durationExtension;
this.label =
subscriptionOffers?.[this.user?.subscription?.offer]?.label;
this.price =
subscriptionOffers?.[this.user?.subscription?.offer]?.price;
this.priceId =
subscriptionOffers?.[this.user.subscription.offer]?.priceId;
this.user?.subscription?.offer?.durationExtension;
this.label = this.user?.subscription?.offer?.label;
this.price = this.user?.subscription?.offer?.price;
this.priceId = this.user?.subscription?.offer?.priceId;
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 { User } from '@ghostfolio/common/interfaces';
import { personalFinanceTools } from '@ghostfolio/common/personal-finance-tools';
import { translate } from '@ghostfolio/ui/i18n';
import { Component, OnInit } from '@angular/core';
import { MatButtonModule } from '@angular/material/button';
import { ActivatedRoute, RouterModule } from '@angular/router';
import { Subject, takeUntil } from 'rxjs';
@Component({
host: { class: 'page' },
@ -26,17 +28,16 @@ export class GfProductPageComponent implements OnInit {
'personal-finance-tools'
];
public tags: string[];
public user: User;
private unsubscribeSubject = new Subject<void>();
public constructor(
private dataService: DataService,
private route: ActivatedRoute
private route: ActivatedRoute,
private userService: UserService
) {}
public ngOnInit() {
const { subscriptionOffers } = this.dataService.fetchInfo();
this.price = subscriptionOffers?.default?.price;
this.product1 = {
founded: 2021,
hasFreePlan: true,
@ -100,5 +101,20 @@ export class GfProductPageComponent implements OnInit {
].sort((a, b) => {
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 { Statistics } from './statistics.interface';
import { SubscriptionOffer } from './subscription-offer.interface';
export interface InfoItem {
baseCurrency: string;
@ -18,5 +15,4 @@ export interface InfoItem {
platforms: Platform[];
statistics: Statistics;
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 { Access, Account, Tag } from '@prisma/client';
import { SubscriptionOffer } from './subscription-offer.interface';
import { SystemMessage } from './system-message.interface';
import { UserSettings } from './user-settings.interface';
@ -18,7 +18,7 @@ export interface User {
systemMessage?: SystemMessage;
subscription: {
expiresAt?: Date;
offer: SubscriptionOfferKey;
offer: SubscriptionOffer;
type: SubscriptionType;
};
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 { RequestWithUser } from './request-with-user.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 { ViewMode } from './view-mode.type';
@ -40,6 +41,7 @@ export type {
OrderWithAccount,
RequestWithUser,
SubscriptionOfferKey,
SubscriptionType,
UserWithSettings,
ViewMode
};

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

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

Loading…
Cancel
Save