Feature/setup subscription with stripe (#178)
* Set up stripe for subscriptions * Update permissions and add discount * Update changelog
This commit is contained in:
parent
373a2015c0
commit
ad00cd9d81
@ -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
|
||||||
|
|
||||||
|
### Added
|
||||||
|
|
||||||
|
- Set up _Stripe_ for subscriptions
|
||||||
|
|
||||||
## 1.19.0 - 17.06.2021
|
## 1.19.0 - 17.06.2021
|
||||||
|
|
||||||
### Added
|
### Added
|
||||||
|
@ -27,6 +27,7 @@ import { InfoModule } from './info/info.module';
|
|||||||
import { OrderModule } from './order/order.module';
|
import { OrderModule } from './order/order.module';
|
||||||
import { PortfolioModule } from './portfolio/portfolio.module';
|
import { PortfolioModule } from './portfolio/portfolio.module';
|
||||||
import { RedisCacheModule } from './redis-cache/redis-cache.module';
|
import { RedisCacheModule } from './redis-cache/redis-cache.module';
|
||||||
|
import { SubscriptionModule } from './subscription/subscription.module';
|
||||||
import { SymbolModule } from './symbol/symbol.module';
|
import { SymbolModule } from './symbol/symbol.module';
|
||||||
import { UserModule } from './user/user.module';
|
import { UserModule } from './user/user.module';
|
||||||
|
|
||||||
@ -59,6 +60,7 @@ import { UserModule } from './user/user.module';
|
|||||||
rootPath: join(__dirname, '..', 'client'),
|
rootPath: join(__dirname, '..', 'client'),
|
||||||
exclude: ['/api*']
|
exclude: ['/api*']
|
||||||
}),
|
}),
|
||||||
|
SubscriptionModule,
|
||||||
SymbolModule,
|
SymbolModule,
|
||||||
UserModule
|
UserModule
|
||||||
],
|
],
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
import { ConfigurationService } from '@ghostfolio/api/services/configuration.service';
|
import { ConfigurationService } from '@ghostfolio/api/services/configuration.service';
|
||||||
import { PrismaService } from '@ghostfolio/api/services/prisma.service';
|
import { PrismaService } from '@ghostfolio/api/services/prisma.service';
|
||||||
import { InfoItem } from '@ghostfolio/common/interfaces';
|
import { InfoItem } from '@ghostfolio/common/interfaces';
|
||||||
|
import { Subscription } from '@ghostfolio/common/interfaces/subscription.interface';
|
||||||
import { permissions } from '@ghostfolio/common/permissions';
|
import { permissions } from '@ghostfolio/common/permissions';
|
||||||
import { Injectable } from '@nestjs/common';
|
import { Injectable } from '@nestjs/common';
|
||||||
import { JwtService } from '@nestjs/jwt';
|
import { JwtService } from '@nestjs/jwt';
|
||||||
@ -44,37 +45,8 @@ export class InfoService {
|
|||||||
currencies: Object.values(Currency),
|
currencies: Object.values(Currency),
|
||||||
demoAuthToken: this.getDemoAuthToken(),
|
demoAuthToken: this.getDemoAuthToken(),
|
||||||
lastDataGathering: await this.getLastDataGathering(),
|
lastDataGathering: await this.getLastDataGathering(),
|
||||||
statistics: await this.getStatistics()
|
statistics: await this.getStatistics(),
|
||||||
};
|
subscriptions: await this.getSubscriptions()
|
||||||
}
|
|
||||||
|
|
||||||
private getDemoAuthToken() {
|
|
||||||
return this.jwtService.sign({
|
|
||||||
id: InfoService.DEMO_USER_ID
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
private async getLastDataGathering() {
|
|
||||||
const lastDataGathering = await this.prisma.property.findUnique({
|
|
||||||
where: { key: 'LAST_DATA_GATHERING' }
|
|
||||||
});
|
|
||||||
|
|
||||||
return lastDataGathering?.value ? new Date(lastDataGathering.value) : null;
|
|
||||||
}
|
|
||||||
|
|
||||||
private async getStatistics() {
|
|
||||||
if (!this.configurationService.get('ENABLE_FEATURE_STATISTICS')) {
|
|
||||||
return undefined;
|
|
||||||
}
|
|
||||||
|
|
||||||
const activeUsers1d = await this.countActiveUsers(1);
|
|
||||||
const activeUsers30d = await this.countActiveUsers(30);
|
|
||||||
const gitHubStargazers = await this.countGitHubStargazers();
|
|
||||||
|
|
||||||
return {
|
|
||||||
activeUsers1d,
|
|
||||||
activeUsers30d,
|
|
||||||
gitHubStargazers
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -124,4 +96,50 @@ export class InfoService {
|
|||||||
return undefined;
|
return undefined;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private getDemoAuthToken() {
|
||||||
|
return this.jwtService.sign({
|
||||||
|
id: InfoService.DEMO_USER_ID
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private async getLastDataGathering() {
|
||||||
|
const lastDataGathering = await this.prisma.property.findUnique({
|
||||||
|
where: { key: 'LAST_DATA_GATHERING' }
|
||||||
|
});
|
||||||
|
|
||||||
|
return lastDataGathering?.value ? new Date(lastDataGathering.value) : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
private async getStatistics() {
|
||||||
|
if (!this.configurationService.get('ENABLE_FEATURE_STATISTICS')) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
const activeUsers1d = await this.countActiveUsers(1);
|
||||||
|
const activeUsers30d = await this.countActiveUsers(30);
|
||||||
|
const gitHubStargazers = await this.countGitHubStargazers();
|
||||||
|
|
||||||
|
return {
|
||||||
|
activeUsers1d,
|
||||||
|
activeUsers30d,
|
||||||
|
gitHubStargazers
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private async getSubscriptions(): Promise<Subscription[]> {
|
||||||
|
if (!this.configurationService.get('ENABLE_FEATURE_SUBSCRIPTION')) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
const stripeConfig = await this.prisma.property.findUnique({
|
||||||
|
where: { key: 'STRIPE_CONFIG' }
|
||||||
|
});
|
||||||
|
|
||||||
|
if (stripeConfig) {
|
||||||
|
return [JSON.parse(stripeConfig.value)];
|
||||||
|
}
|
||||||
|
|
||||||
|
return [];
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
57
apps/api/src/app/subscription/subscription.controller.ts
Normal file
57
apps/api/src/app/subscription/subscription.controller.ts
Normal file
@ -0,0 +1,57 @@
|
|||||||
|
import { ConfigurationService } from '@ghostfolio/api/services/configuration.service';
|
||||||
|
import { RequestWithUser } from '@ghostfolio/common/types';
|
||||||
|
import {
|
||||||
|
Body,
|
||||||
|
Controller,
|
||||||
|
Get,
|
||||||
|
HttpException,
|
||||||
|
Inject,
|
||||||
|
Post,
|
||||||
|
Req,
|
||||||
|
Res,
|
||||||
|
UseGuards
|
||||||
|
} from '@nestjs/common';
|
||||||
|
import { REQUEST } from '@nestjs/core';
|
||||||
|
import { AuthGuard } from '@nestjs/passport';
|
||||||
|
import { StatusCodes, getReasonPhrase } from 'http-status-codes';
|
||||||
|
|
||||||
|
import { SubscriptionService } from './subscription.service';
|
||||||
|
|
||||||
|
@Controller('subscription')
|
||||||
|
export class SubscriptionController {
|
||||||
|
public constructor(
|
||||||
|
private readonly configurationService: ConfigurationService,
|
||||||
|
@Inject(REQUEST) private readonly request: RequestWithUser,
|
||||||
|
private readonly subscriptionService: SubscriptionService
|
||||||
|
) {}
|
||||||
|
|
||||||
|
@Get('stripe/callback')
|
||||||
|
public async stripeCallback(@Req() req, @Res() res) {
|
||||||
|
await this.subscriptionService.createSubscription(
|
||||||
|
req.query.checkoutSessionId
|
||||||
|
);
|
||||||
|
|
||||||
|
res.redirect(`${this.configurationService.get('ROOT_URL')}/account`);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Post('stripe/checkout-session')
|
||||||
|
@UseGuards(AuthGuard('jwt'))
|
||||||
|
public async createCheckoutSession(
|
||||||
|
@Body() { couponId, priceId }: { couponId: string; priceId: string }
|
||||||
|
) {
|
||||||
|
try {
|
||||||
|
return await this.subscriptionService.createCheckoutSession({
|
||||||
|
couponId,
|
||||||
|
priceId,
|
||||||
|
userId: this.request.user.id
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
console.error(error);
|
||||||
|
|
||||||
|
throw new HttpException(
|
||||||
|
getReasonPhrase(StatusCodes.BAD_REQUEST),
|
||||||
|
StatusCodes.BAD_REQUEST
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
13
apps/api/src/app/subscription/subscription.module.ts
Normal file
13
apps/api/src/app/subscription/subscription.module.ts
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
import { ConfigurationService } from '@ghostfolio/api/services/configuration.service';
|
||||||
|
import { PrismaService } from '@ghostfolio/api/services/prisma.service';
|
||||||
|
import { Module } from '@nestjs/common';
|
||||||
|
|
||||||
|
import { SubscriptionController } from './subscription.controller';
|
||||||
|
import { SubscriptionService } from './subscription.service';
|
||||||
|
|
||||||
|
@Module({
|
||||||
|
imports: [],
|
||||||
|
controllers: [SubscriptionController],
|
||||||
|
providers: [ConfigurationService, PrismaService, SubscriptionService]
|
||||||
|
})
|
||||||
|
export class SubscriptionModule {}
|
88
apps/api/src/app/subscription/subscription.service.ts
Normal file
88
apps/api/src/app/subscription/subscription.service.ts
Normal file
@ -0,0 +1,88 @@
|
|||||||
|
import { ConfigurationService } from '@ghostfolio/api/services/configuration.service';
|
||||||
|
import { PrismaService } from '@ghostfolio/api/services/prisma.service';
|
||||||
|
import { Injectable } from '@nestjs/common';
|
||||||
|
import { addDays } from 'date-fns';
|
||||||
|
import Stripe from 'stripe';
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class SubscriptionService {
|
||||||
|
private stripe: Stripe;
|
||||||
|
|
||||||
|
public constructor(
|
||||||
|
private readonly configurationService: ConfigurationService,
|
||||||
|
private prisma: PrismaService
|
||||||
|
) {
|
||||||
|
this.stripe = new Stripe(
|
||||||
|
this.configurationService.get('STRIPE_SECRET_KEY'),
|
||||||
|
{
|
||||||
|
apiVersion: '2020-08-27'
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async createCheckoutSession({
|
||||||
|
couponId,
|
||||||
|
priceId,
|
||||||
|
userId
|
||||||
|
}: {
|
||||||
|
couponId?: string;
|
||||||
|
priceId: string;
|
||||||
|
userId: string;
|
||||||
|
}) {
|
||||||
|
const checkoutSessionCreateParams: Stripe.Checkout.SessionCreateParams = {
|
||||||
|
cancel_url: `${this.configurationService.get('ROOT_URL')}/account`,
|
||||||
|
client_reference_id: userId,
|
||||||
|
line_items: [
|
||||||
|
{
|
||||||
|
price: priceId,
|
||||||
|
quantity: 1
|
||||||
|
}
|
||||||
|
],
|
||||||
|
metadata: {
|
||||||
|
user_id: userId
|
||||||
|
},
|
||||||
|
mode: 'subscription',
|
||||||
|
payment_method_types: ['card'],
|
||||||
|
success_url: `${this.configurationService.get(
|
||||||
|
'ROOT_URL'
|
||||||
|
)}/api/subscription/stripe/callback?checkoutSessionId={CHECKOUT_SESSION_ID}`
|
||||||
|
};
|
||||||
|
|
||||||
|
if (couponId) {
|
||||||
|
checkoutSessionCreateParams.discounts = [
|
||||||
|
{
|
||||||
|
coupon: couponId
|
||||||
|
}
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
const session = await this.stripe.checkout.sessions.create(
|
||||||
|
checkoutSessionCreateParams
|
||||||
|
);
|
||||||
|
|
||||||
|
return {
|
||||||
|
sessionId: session.id
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
public async createSubscription(aCheckoutSessionId: string) {
|
||||||
|
try {
|
||||||
|
const session = await this.stripe.checkout.sessions.retrieve(
|
||||||
|
aCheckoutSessionId
|
||||||
|
);
|
||||||
|
|
||||||
|
await this.prisma.subscription.create({
|
||||||
|
data: {
|
||||||
|
expiresAt: addDays(new Date(), 365),
|
||||||
|
User: {
|
||||||
|
connect: {
|
||||||
|
id: session.client_reference_id
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
console.error(error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,7 @@
|
|||||||
|
import { Currency, ViewMode } from '@prisma/client';
|
||||||
|
|
||||||
|
export interface UserSettingsParams {
|
||||||
|
currency?: Currency;
|
||||||
|
userId: string;
|
||||||
|
viewMode?: ViewMode;
|
||||||
|
}
|
@ -25,6 +25,7 @@ import { User as UserModel } from '@prisma/client';
|
|||||||
import { StatusCodes, getReasonPhrase } from 'http-status-codes';
|
import { StatusCodes, getReasonPhrase } from 'http-status-codes';
|
||||||
|
|
||||||
import { UserItem } from './interfaces/user-item.interface';
|
import { UserItem } from './interfaces/user-item.interface';
|
||||||
|
import { UserSettingsParams } from './interfaces/user-settings-params.interface';
|
||||||
import { UpdateUserSettingsDto } from './update-user-settings.dto';
|
import { UpdateUserSettingsDto } from './update-user-settings.dto';
|
||||||
import { UserService } from './user.service';
|
import { UserService } from './user.service';
|
||||||
|
|
||||||
@ -92,10 +93,20 @@ export class UserController {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
return await this.userService.updateUserSettings({
|
const userSettings: UserSettingsParams = {
|
||||||
currency: data.baseCurrency,
|
currency: data.baseCurrency,
|
||||||
userId: this.request.user.id,
|
userId: this.request.user.id
|
||||||
viewMode: data.viewMode
|
};
|
||||||
});
|
|
||||||
|
if (
|
||||||
|
hasPermission(
|
||||||
|
getPermissions(this.request.user.role),
|
||||||
|
permissions.updateViewMode
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
userSettings.viewMode = data.viewMode;
|
||||||
|
}
|
||||||
|
|
||||||
|
return await this.userService.updateUserSettings(userSettings);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,13 +1,14 @@
|
|||||||
import { ConfigurationService } from '@ghostfolio/api/services/configuration.service';
|
import { ConfigurationService } from '@ghostfolio/api/services/configuration.service';
|
||||||
import { PrismaService } from '@ghostfolio/api/services/prisma.service';
|
import { PrismaService } from '@ghostfolio/api/services/prisma.service';
|
||||||
import { locale } from '@ghostfolio/common/config';
|
import { locale } from '@ghostfolio/common/config';
|
||||||
import { resetHours } from '@ghostfolio/common/helper';
|
|
||||||
import { User as IUser, UserWithSettings } from '@ghostfolio/common/interfaces';
|
import { User as IUser, UserWithSettings } from '@ghostfolio/common/interfaces';
|
||||||
import { getPermissions, permissions } from '@ghostfolio/common/permissions';
|
import { getPermissions, permissions } from '@ghostfolio/common/permissions';
|
||||||
import { SubscriptionType } from '@ghostfolio/common/types/subscription.type';
|
import { SubscriptionType } from '@ghostfolio/common/types/subscription.type';
|
||||||
import { Injectable } from '@nestjs/common';
|
import { Injectable } from '@nestjs/common';
|
||||||
import { Currency, Prisma, Provider, User, ViewMode } from '@prisma/client';
|
import { Currency, Prisma, Provider, User, ViewMode } from '@prisma/client';
|
||||||
import { add, isBefore } from 'date-fns';
|
import { isBefore } from 'date-fns';
|
||||||
|
|
||||||
|
import { UserSettingsParams } from './interfaces/user-settings-params.interface';
|
||||||
|
|
||||||
const crypto = require('crypto');
|
const crypto = require('crypto');
|
||||||
|
|
||||||
@ -24,7 +25,7 @@ export class UserService {
|
|||||||
Account,
|
Account,
|
||||||
alias,
|
alias,
|
||||||
id,
|
id,
|
||||||
role,
|
permissions,
|
||||||
Settings,
|
Settings,
|
||||||
subscription
|
subscription
|
||||||
}: UserWithSettings): Promise<IUser> {
|
}: UserWithSettings): Promise<IUser> {
|
||||||
@ -36,15 +37,10 @@ export class UserService {
|
|||||||
where: { GranteeUser: { id } }
|
where: { GranteeUser: { id } }
|
||||||
});
|
});
|
||||||
|
|
||||||
const currentPermissions = getPermissions(role);
|
|
||||||
|
|
||||||
if (this.configurationService.get('ENABLE_FEATURE_FEAR_AND_GREED_INDEX')) {
|
|
||||||
currentPermissions.push(permissions.accessFearAndGreedIndex);
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
alias,
|
alias,
|
||||||
id,
|
id,
|
||||||
|
permissions,
|
||||||
subscription,
|
subscription,
|
||||||
access: access.map((accessItem) => {
|
access: access.map((accessItem) => {
|
||||||
return {
|
return {
|
||||||
@ -53,7 +49,6 @@ export class UserService {
|
|||||||
};
|
};
|
||||||
}),
|
}),
|
||||||
accounts: Account,
|
accounts: Account,
|
||||||
permissions: currentPermissions,
|
|
||||||
settings: {
|
settings: {
|
||||||
locale,
|
locale,
|
||||||
baseCurrency: Settings?.currency ?? UserService.DEFAULT_CURRENCY,
|
baseCurrency: Settings?.currency ?? UserService.DEFAULT_CURRENCY,
|
||||||
@ -72,6 +67,14 @@ export class UserService {
|
|||||||
|
|
||||||
const user: UserWithSettings = userFromDatabase;
|
const user: UserWithSettings = userFromDatabase;
|
||||||
|
|
||||||
|
const currentPermissions = getPermissions(userFromDatabase.role);
|
||||||
|
|
||||||
|
if (this.configurationService.get('ENABLE_FEATURE_FEAR_AND_GREED_INDEX')) {
|
||||||
|
currentPermissions.push(permissions.accessFearAndGreedIndex);
|
||||||
|
}
|
||||||
|
|
||||||
|
user.permissions = currentPermissions;
|
||||||
|
|
||||||
if (userFromDatabase?.Settings) {
|
if (userFromDatabase?.Settings) {
|
||||||
if (!userFromDatabase.Settings.currency) {
|
if (!userFromDatabase.Settings.currency) {
|
||||||
// Set default currency if needed
|
// Set default currency if needed
|
||||||
@ -106,6 +109,13 @@ export class UserService {
|
|||||||
type: SubscriptionType.Basic
|
type: SubscriptionType.Basic
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (user.subscription.type === SubscriptionType.Basic) {
|
||||||
|
user.permissions = user.permissions.filter((permission) => {
|
||||||
|
return permission !== permissions.updateViewMode;
|
||||||
|
});
|
||||||
|
user.Settings.viewMode = ViewMode.ZEN;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return user;
|
return user;
|
||||||
@ -213,11 +223,7 @@ export class UserService {
|
|||||||
currency,
|
currency,
|
||||||
userId,
|
userId,
|
||||||
viewMode
|
viewMode
|
||||||
}: {
|
}: UserSettingsParams) {
|
||||||
currency?: Currency;
|
|
||||||
userId: string;
|
|
||||||
viewMode?: ViewMode;
|
|
||||||
}) {
|
|
||||||
await this.prisma.settings.upsert({
|
await this.prisma.settings.upsert({
|
||||||
create: {
|
create: {
|
||||||
currency,
|
currency,
|
||||||
|
@ -28,6 +28,7 @@ export class ConfigurationService {
|
|||||||
REDIS_HOST: str({ default: 'localhost' }),
|
REDIS_HOST: str({ default: 'localhost' }),
|
||||||
REDIS_PORT: port({ default: 6379 }),
|
REDIS_PORT: port({ default: 6379 }),
|
||||||
ROOT_URL: str({ default: 'http://localhost:4200' }),
|
ROOT_URL: str({ default: 'http://localhost:4200' }),
|
||||||
|
STRIPE_SECRET_KEY: str({ default: '' }),
|
||||||
WEB_AUTH_RP_ID: host({ default: 'localhost' })
|
WEB_AUTH_RP_ID: host({ default: 'localhost' })
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
@ -19,5 +19,6 @@ export interface Environment extends CleanedEnvAccessors {
|
|||||||
REDIS_HOST: string;
|
REDIS_HOST: string;
|
||||||
REDIS_PORT: number;
|
REDIS_PORT: number;
|
||||||
ROOT_URL: string;
|
ROOT_URL: string;
|
||||||
|
STRIPE_SECRET_KEY: string;
|
||||||
WEB_AUTH_RP_ID: string;
|
WEB_AUTH_RP_ID: string;
|
||||||
}
|
}
|
||||||
|
@ -15,7 +15,9 @@ import { BrowserAnimationsModule } from '@angular/platform-browser/animations';
|
|||||||
import { MaterialCssVarsModule } from 'angular-material-css-vars';
|
import { MaterialCssVarsModule } from 'angular-material-css-vars';
|
||||||
import { MarkdownModule } from 'ngx-markdown';
|
import { MarkdownModule } from 'ngx-markdown';
|
||||||
import { NgxSkeletonLoaderModule } from 'ngx-skeleton-loader';
|
import { NgxSkeletonLoaderModule } from 'ngx-skeleton-loader';
|
||||||
|
import { NgxStripeModule } from 'ngx-stripe';
|
||||||
|
|
||||||
|
import { environment } from '../environments/environment';
|
||||||
import { CustomDateAdapter } from './adapter/custom-date-adapter';
|
import { CustomDateAdapter } from './adapter/custom-date-adapter';
|
||||||
import { DateFormats } from './adapter/date-formats';
|
import { DateFormats } from './adapter/date-formats';
|
||||||
import { AppRoutingModule } from './app-routing.module';
|
import { AppRoutingModule } from './app-routing.module';
|
||||||
@ -43,7 +45,8 @@ import { LanguageService } from './core/language.service';
|
|||||||
}),
|
}),
|
||||||
MatNativeDateModule,
|
MatNativeDateModule,
|
||||||
MatSnackBarModule,
|
MatSnackBarModule,
|
||||||
NgxSkeletonLoaderModule
|
NgxSkeletonLoaderModule,
|
||||||
|
NgxStripeModule.forRoot(environment.stripePublicKey)
|
||||||
],
|
],
|
||||||
providers: [
|
providers: [
|
||||||
authInterceptorProviders,
|
authInterceptorProviders,
|
||||||
|
@ -16,8 +16,9 @@ import { DEFAULT_DATE_FORMAT } from '@ghostfolio/common/config';
|
|||||||
import { Access, User } from '@ghostfolio/common/interfaces';
|
import { Access, User } from '@ghostfolio/common/interfaces';
|
||||||
import { hasPermission, permissions } from '@ghostfolio/common/permissions';
|
import { hasPermission, permissions } from '@ghostfolio/common/permissions';
|
||||||
import { Currency } from '@prisma/client';
|
import { Currency } from '@prisma/client';
|
||||||
|
import { StripeService } from 'ngx-stripe';
|
||||||
import { EMPTY, Subject } from 'rxjs';
|
import { EMPTY, Subject } from 'rxjs';
|
||||||
import { catchError, takeUntil } from 'rxjs/operators';
|
import { catchError, switchMap, takeUntil } from 'rxjs/operators';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'gf-account-page',
|
selector: 'gf-account-page',
|
||||||
@ -30,9 +31,14 @@ export class AccountPageComponent implements OnDestroy, OnInit {
|
|||||||
|
|
||||||
public accesses: Access[];
|
public accesses: Access[];
|
||||||
public baseCurrency: Currency;
|
public baseCurrency: Currency;
|
||||||
|
public coupon: number;
|
||||||
|
public couponId: string;
|
||||||
public currencies: Currency[] = [];
|
public currencies: Currency[] = [];
|
||||||
public defaultDateFormat = DEFAULT_DATE_FORMAT;
|
public defaultDateFormat = DEFAULT_DATE_FORMAT;
|
||||||
|
public hasPermissionToUpdateViewMode: boolean;
|
||||||
public hasPermissionToUpdateUserSettings: boolean;
|
public hasPermissionToUpdateUserSettings: boolean;
|
||||||
|
public price: number;
|
||||||
|
public priceId: string;
|
||||||
public user: User;
|
public user: User;
|
||||||
|
|
||||||
private unsubscribeSubject = new Subject<void>();
|
private unsubscribeSubject = new Subject<void>();
|
||||||
@ -43,14 +49,21 @@ export class AccountPageComponent implements OnDestroy, OnInit {
|
|||||||
public constructor(
|
public constructor(
|
||||||
private changeDetectorRef: ChangeDetectorRef,
|
private changeDetectorRef: ChangeDetectorRef,
|
||||||
private dataService: DataService,
|
private dataService: DataService,
|
||||||
|
private stripeService: StripeService,
|
||||||
private userService: UserService,
|
private userService: UserService,
|
||||||
public webAuthnService: WebAuthnService
|
public webAuthnService: WebAuthnService
|
||||||
) {
|
) {
|
||||||
this.dataService
|
this.dataService
|
||||||
.fetchInfo()
|
.fetchInfo()
|
||||||
.pipe(takeUntil(this.unsubscribeSubject))
|
.pipe(takeUntil(this.unsubscribeSubject))
|
||||||
.subscribe(({ currencies }) => {
|
.subscribe(({ currencies, subscriptions }) => {
|
||||||
|
this.coupon = subscriptions?.[0]?.coupon;
|
||||||
|
this.couponId = subscriptions?.[0]?.couponId;
|
||||||
this.currencies = currencies;
|
this.currencies = currencies;
|
||||||
|
this.price = subscriptions?.[0]?.price;
|
||||||
|
this.priceId = subscriptions?.[0]?.priceId;
|
||||||
|
|
||||||
|
this.changeDetectorRef.markForCheck();
|
||||||
});
|
});
|
||||||
|
|
||||||
this.userService.stateChanged
|
this.userService.stateChanged
|
||||||
@ -64,6 +77,11 @@ export class AccountPageComponent implements OnDestroy, OnInit {
|
|||||||
permissions.updateUserSettings
|
permissions.updateUserSettings
|
||||||
);
|
);
|
||||||
|
|
||||||
|
this.hasPermissionToUpdateViewMode = hasPermission(
|
||||||
|
this.user.permissions,
|
||||||
|
permissions.updateViewMode
|
||||||
|
);
|
||||||
|
|
||||||
this.changeDetectorRef.markForCheck();
|
this.changeDetectorRef.markForCheck();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@ -99,6 +117,23 @@ export class AccountPageComponent implements OnDestroy, OnInit {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public onCheckout() {
|
||||||
|
this.dataService
|
||||||
|
.createCheckoutSession({ couponId: this.couponId, priceId: this.priceId })
|
||||||
|
.pipe(
|
||||||
|
switchMap(({ sessionId }: { sessionId: string }) => {
|
||||||
|
return this.stripeService.redirectToCheckout({
|
||||||
|
sessionId
|
||||||
|
});
|
||||||
|
})
|
||||||
|
)
|
||||||
|
.subscribe((result) => {
|
||||||
|
if (result.error) {
|
||||||
|
alert(result.error.message);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
public onSignInWithFingerprintChange(aEvent: MatSlideToggleChange) {
|
public onSignInWithFingerprintChange(aEvent: MatSlideToggleChange) {
|
||||||
if (aEvent.checked) {
|
if (aEvent.checked) {
|
||||||
this.registerDevice();
|
this.registerDevice();
|
||||||
|
@ -1,10 +1,7 @@
|
|||||||
<div class="container">
|
<div class="container">
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="col">
|
<div class="col">
|
||||||
<h3 class="d-flex justify-content-center mb-3">
|
<h3 class="d-flex justify-content-center mb-3" i18n>Account</h3>
|
||||||
<ng-container *ngIf="user?.alias">{{ user.alias }}</ng-container>
|
|
||||||
<ng-container *ngIf="!user?.alias" i18n>Account</ng-container>
|
|
||||||
</h3>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div *ngIf="user?.settings" class="mb-5 row">
|
<div *ngIf="user?.settings" class="mb-5 row">
|
||||||
@ -26,9 +23,23 @@
|
|||||||
defaultDateFormat }}
|
defaultDateFormat }}
|
||||||
</div>
|
</div>
|
||||||
<div *ngIf="!user.subscription.expiresAt">
|
<div *ngIf="!user.subscription.expiresAt">
|
||||||
<button color="primary" disabled i18n mat-flat-button>
|
<button
|
||||||
|
color="primary"
|
||||||
|
i18n
|
||||||
|
mat-flat-button
|
||||||
|
(click)="onCheckout(priceId)"
|
||||||
|
>
|
||||||
Upgrade
|
Upgrade
|
||||||
</button>
|
</button>
|
||||||
|
<div *ngIf="price" class="mt-1">
|
||||||
|
{{ user.settings.baseCurrency }}
|
||||||
|
<ng-container *ngIf="coupon"
|
||||||
|
>{{ price - coupon }}
|
||||||
|
<del>{{ user.settings.baseCurrency }} {{ price }}</del>
|
||||||
|
</ng-container>
|
||||||
|
<ng-container *ngIf="!coupon">{{ price }}</ng-container>
|
||||||
|
<span i18n> per year</span>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -55,7 +66,7 @@
|
|||||||
<mat-label i18n>View Mode</mat-label>
|
<mat-label i18n>View Mode</mat-label>
|
||||||
<mat-select
|
<mat-select
|
||||||
name="viewMode"
|
name="viewMode"
|
||||||
[disabled]="!hasPermissionToUpdateUserSettings"
|
[disabled]="!hasPermissionToUpdateViewMode"
|
||||||
[value]="user.settings.viewMode"
|
[value]="user.settings.viewMode"
|
||||||
(selectionChange)="onChangeUserSettings('viewMode', $event.value)"
|
(selectionChange)="onChangeUserSettings('viewMode', $event.value)"
|
||||||
>
|
>
|
||||||
|
@ -6,7 +6,7 @@ import { GfAccountsTableModule } from '@ghostfolio/client/components/accounts-ta
|
|||||||
|
|
||||||
import { AccountsPageRoutingModule } from './accounts-page-routing.module';
|
import { AccountsPageRoutingModule } from './accounts-page-routing.module';
|
||||||
import { AccountsPageComponent } from './accounts-page.component';
|
import { AccountsPageComponent } from './accounts-page.component';
|
||||||
import { CreateOrUpdateAccountDialogModule } from './create-or-update-account-dialog/create-or-update-account-dialog.module';
|
import { GfCreateOrUpdateAccountDialogModule } from './create-or-update-account-dialog/create-or-update-account-dialog.module';
|
||||||
|
|
||||||
@NgModule({
|
@NgModule({
|
||||||
declarations: [AccountsPageComponent],
|
declarations: [AccountsPageComponent],
|
||||||
@ -14,8 +14,8 @@ import { CreateOrUpdateAccountDialogModule } from './create-or-update-account-di
|
|||||||
imports: [
|
imports: [
|
||||||
AccountsPageRoutingModule,
|
AccountsPageRoutingModule,
|
||||||
CommonModule,
|
CommonModule,
|
||||||
CreateOrUpdateAccountDialogModule,
|
|
||||||
GfAccountsTableModule,
|
GfAccountsTableModule,
|
||||||
|
GfCreateOrUpdateAccountDialogModule,
|
||||||
MatButtonModule,
|
MatButtonModule,
|
||||||
RouterModule
|
RouterModule
|
||||||
],
|
],
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
<form #addAccountForm="ngForm" class="d-flex flex-column h-100">
|
<form #addAccountForm="ngForm" class="d-flex flex-column h-100">
|
||||||
<h1 *ngIf="data.account.id" mat-dialog-title i18n>Update account</h1>
|
<h1 *ngIf="data.account.id" i18n mat-dialog-title>Update account</h1>
|
||||||
<h1 *ngIf="!data.account.id" mat-dialog-title i18n>Add account</h1>
|
<h1 *ngIf="!data.account.id" i18n mat-dialog-title>Add account</h1>
|
||||||
<div class="flex-grow-1" mat-dialog-content>
|
<div class="flex-grow-1" mat-dialog-content>
|
||||||
<div>
|
<div>
|
||||||
<mat-form-field appearance="outline" class="w-100">
|
<mat-form-field appearance="outline" class="w-100">
|
||||||
|
@ -24,4 +24,4 @@ import { CreateOrUpdateAccountDialog } from './create-or-update-account-dialog.c
|
|||||||
],
|
],
|
||||||
providers: []
|
providers: []
|
||||||
})
|
})
|
||||||
export class CreateOrUpdateAccountDialogModule {}
|
export class GfCreateOrUpdateAccountDialogModule {}
|
||||||
|
@ -1,4 +1,5 @@
|
|||||||
import { ChangeDetectorRef, Component, OnInit } from '@angular/core';
|
import { ChangeDetectorRef, Component, OnInit } from '@angular/core';
|
||||||
|
import { DataService } from '@ghostfolio/client/services/data.service';
|
||||||
import { UserService } from '@ghostfolio/client/services/user/user.service';
|
import { UserService } from '@ghostfolio/client/services/user/user.service';
|
||||||
import { baseCurrency } from '@ghostfolio/common/config';
|
import { baseCurrency } from '@ghostfolio/common/config';
|
||||||
import { User } from '@ghostfolio/common/interfaces';
|
import { User } from '@ghostfolio/common/interfaces';
|
||||||
@ -12,7 +13,9 @@ import { takeUntil } from 'rxjs/operators';
|
|||||||
})
|
})
|
||||||
export class PricingPageComponent implements OnInit {
|
export class PricingPageComponent implements OnInit {
|
||||||
public baseCurrency = baseCurrency;
|
public baseCurrency = baseCurrency;
|
||||||
|
public coupon: number;
|
||||||
public isLoggedIn: boolean;
|
public isLoggedIn: boolean;
|
||||||
|
public price: number;
|
||||||
public user: User;
|
public user: User;
|
||||||
|
|
||||||
private unsubscribeSubject = new Subject<void>();
|
private unsubscribeSubject = new Subject<void>();
|
||||||
@ -22,8 +25,19 @@ export class PricingPageComponent implements OnInit {
|
|||||||
*/
|
*/
|
||||||
public constructor(
|
public constructor(
|
||||||
private changeDetectorRef: ChangeDetectorRef,
|
private changeDetectorRef: ChangeDetectorRef,
|
||||||
|
private dataService: DataService,
|
||||||
private userService: UserService
|
private userService: UserService
|
||||||
) {}
|
) {
|
||||||
|
this.dataService
|
||||||
|
.fetchInfo()
|
||||||
|
.pipe(takeUntil(this.unsubscribeSubject))
|
||||||
|
.subscribe(({ subscriptions }) => {
|
||||||
|
this.coupon = this.price = subscriptions?.[0]?.coupon;
|
||||||
|
this.price = subscriptions?.[0]?.price;
|
||||||
|
|
||||||
|
this.changeDetectorRef.markForCheck();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Initializes the controller
|
* Initializes the controller
|
||||||
|
@ -176,11 +176,17 @@
|
|||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
<p>Fully managed <strong>Ghostfolio</strong> cloud offering.</p>
|
<p>Fully managed <strong>Ghostfolio</strong> cloud offering.</p>
|
||||||
<p class="h5 text-right">
|
<p class="h5 text-right" [hidden]="!price">
|
||||||
<span class="font-weight-normal"
|
<span class="font-weight-normal"
|
||||||
>{{ user?.settings.baseCurrency || baseCurrency }}
|
>{{ user?.settings.baseCurrency || baseCurrency }}
|
||||||
<strong>0.00</strong>
|
<ng-container *ngIf="coupon"
|
||||||
<del class="ml-1 text-muted">3.99</del> / Month</span
|
><strong>{{ price - coupon }} </strong>
|
||||||
|
<del>{{ user.settings.baseCurrency }} {{ price }}</del>
|
||||||
|
</ng-container>
|
||||||
|
<ng-container *ngIf="!coupon"
|
||||||
|
><strong>{{ price }}</strong></ng-container
|
||||||
|
>
|
||||||
|
<span i18n> per year</span></span
|
||||||
>
|
>
|
||||||
</p>
|
</p>
|
||||||
</mat-card>
|
</mat-card>
|
||||||
@ -188,6 +194,13 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<div *ngIf="user?.subscription?.type === 'Basic'" class="row">
|
||||||
|
<div class="col mt-3 text-center">
|
||||||
|
<a color="primary" i18n mat-flat-button [routerLink]="['/account']">
|
||||||
|
Upgrade Plan
|
||||||
|
</a>
|
||||||
|
</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" i18n mat-flat-button [routerLink]="['/register']">
|
<a color="primary" i18n mat-flat-button [routerLink]="['/register']">
|
||||||
|
@ -43,6 +43,19 @@ export class DataService {
|
|||||||
private settingsStorageService: SettingsStorageService
|
private settingsStorageService: SettingsStorageService
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
|
public createCheckoutSession({
|
||||||
|
couponId,
|
||||||
|
priceId
|
||||||
|
}: {
|
||||||
|
couponId?: string;
|
||||||
|
priceId: string;
|
||||||
|
}) {
|
||||||
|
return this.http.post('/api/subscription/stripe/checkout-session', {
|
||||||
|
couponId,
|
||||||
|
priceId
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
public fetchAccounts() {
|
public fetchAccounts() {
|
||||||
return this.http.get<AccountModel[]>('/api/account');
|
return this.http.get<AccountModel[]>('/api/account');
|
||||||
}
|
}
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
export const environment = {
|
export const environment = {
|
||||||
lastPublish: '{BUILD_TIMESTAMP}',
|
lastPublish: '{BUILD_TIMESTAMP}',
|
||||||
production: true,
|
production: true,
|
||||||
|
stripePublicKey: '{STRIPE_PUBLIC_KEY}',
|
||||||
version: `v${require('../../../../package.json').version}`
|
version: `v${require('../../../../package.json').version}`
|
||||||
};
|
};
|
||||||
|
@ -5,6 +5,7 @@
|
|||||||
export const environment = {
|
export const environment = {
|
||||||
lastPublish: null,
|
lastPublish: null,
|
||||||
production: false,
|
production: false,
|
||||||
|
stripePublicKey: '',
|
||||||
version: 'dev'
|
version: 'dev'
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
import { Currency } from '@prisma/client';
|
import { Currency } from '@prisma/client';
|
||||||
|
|
||||||
import { Statistics } from './statistics.interface';
|
import { Statistics } from './statistics.interface';
|
||||||
|
import { Subscription } from './subscription.interface';
|
||||||
|
|
||||||
export interface InfoItem {
|
export interface InfoItem {
|
||||||
currencies: Currency[];
|
currencies: Currency[];
|
||||||
@ -13,4 +14,5 @@ export interface InfoItem {
|
|||||||
};
|
};
|
||||||
platforms: { id: string; name: string }[];
|
platforms: { id: string; name: string }[];
|
||||||
statistics: Statistics;
|
statistics: Statistics;
|
||||||
|
subscriptions: Subscription[];
|
||||||
}
|
}
|
||||||
|
6
libs/common/src/lib/interfaces/subscription.interface.ts
Normal file
6
libs/common/src/lib/interfaces/subscription.interface.ts
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
export interface Subscription {
|
||||||
|
coupon?: number;
|
||||||
|
couponId?: string;
|
||||||
|
price: number;
|
||||||
|
priceId: string;
|
||||||
|
}
|
@ -1,7 +1,7 @@
|
|||||||
import { Currency, ViewMode } from '@prisma/client';
|
import { Currency, ViewMode } from '@prisma/client';
|
||||||
|
|
||||||
export interface UserSettings {
|
export interface UserSettings {
|
||||||
baseCurrency: Currency;
|
baseCurrency?: Currency;
|
||||||
locale: string;
|
locale: string;
|
||||||
viewMode: ViewMode;
|
viewMode?: ViewMode;
|
||||||
}
|
}
|
||||||
|
@ -3,6 +3,7 @@ import { Account, Settings, User } from '@prisma/client';
|
|||||||
|
|
||||||
export type UserWithSettings = User & {
|
export type UserWithSettings = User & {
|
||||||
Account: Account[];
|
Account: Account[];
|
||||||
|
permissions?: string[];
|
||||||
Settings: Settings;
|
Settings: Settings;
|
||||||
subscription?: {
|
subscription?: {
|
||||||
expiresAt?: Date;
|
expiresAt?: Date;
|
||||||
|
@ -21,7 +21,8 @@ export const permissions = {
|
|||||||
updateAccount: 'updateAccount',
|
updateAccount: 'updateAccount',
|
||||||
updateAuthDevice: 'updateAuthDevice',
|
updateAuthDevice: 'updateAuthDevice',
|
||||||
updateOrder: 'updateOrder',
|
updateOrder: 'updateOrder',
|
||||||
updateUserSettings: 'updateUserSettings'
|
updateUserSettings: 'updateUserSettings',
|
||||||
|
updateViewMode: 'updateViewMode'
|
||||||
};
|
};
|
||||||
|
|
||||||
export function hasPermission(
|
export function hasPermission(
|
||||||
@ -46,7 +47,8 @@ export function getPermissions(aRole: Role): string[] {
|
|||||||
permissions.updateAccount,
|
permissions.updateAccount,
|
||||||
permissions.updateAuthDevice,
|
permissions.updateAuthDevice,
|
||||||
permissions.updateOrder,
|
permissions.updateOrder,
|
||||||
permissions.updateUserSettings
|
permissions.updateUserSettings,
|
||||||
|
permissions.updateViewMode
|
||||||
];
|
];
|
||||||
|
|
||||||
case 'DEMO':
|
case 'DEMO':
|
||||||
@ -62,7 +64,8 @@ export function getPermissions(aRole: Role): string[] {
|
|||||||
permissions.updateAccount,
|
permissions.updateAccount,
|
||||||
permissions.updateAuthDevice,
|
permissions.updateAuthDevice,
|
||||||
permissions.updateOrder,
|
permissions.updateOrder,
|
||||||
permissions.updateUserSettings
|
permissions.updateUserSettings,
|
||||||
|
permissions.updateViewMode
|
||||||
];
|
];
|
||||||
|
|
||||||
default:
|
default:
|
||||||
|
@ -13,7 +13,7 @@
|
|||||||
"affected:lint": "nx affected:lint",
|
"affected:lint": "nx affected:lint",
|
||||||
"affected:test": "nx affected:test",
|
"affected:test": "nx affected:test",
|
||||||
"angular": "node --max_old_space_size=32768 ./node_modules/@angular/cli/bin/ng",
|
"angular": "node --max_old_space_size=32768 ./node_modules/@angular/cli/bin/ng",
|
||||||
"build:all": "ng build --prod api && ng build --prod client && yarn replace-placeholders-in-build",
|
"build:all": "ng build --configuration production api && ng build --configuration production client && yarn replace-placeholders-in-build",
|
||||||
"clean": "rimraf dist",
|
"clean": "rimraf dist",
|
||||||
"database:format-schema": "prisma format",
|
"database:format-schema": "prisma format",
|
||||||
"database:generate-typings": "prisma generate",
|
"database:generate-typings": "prisma generate",
|
||||||
@ -69,6 +69,7 @@
|
|||||||
"@simplewebauthn/browser": "3.0.0",
|
"@simplewebauthn/browser": "3.0.0",
|
||||||
"@simplewebauthn/server": "3.0.0",
|
"@simplewebauthn/server": "3.0.0",
|
||||||
"@simplewebauthn/typescript-types": "3.0.0",
|
"@simplewebauthn/typescript-types": "3.0.0",
|
||||||
|
"@stripe/stripe-js": "1.15.0",
|
||||||
"@types/lodash": "4.14.168",
|
"@types/lodash": "4.14.168",
|
||||||
"alphavantage": "2.2.0",
|
"alphavantage": "2.2.0",
|
||||||
"angular-material-css-vars": "1.2.0",
|
"angular-material-css-vars": "1.2.0",
|
||||||
@ -92,6 +93,7 @@
|
|||||||
"ngx-device-detector": "2.1.1",
|
"ngx-device-detector": "2.1.1",
|
||||||
"ngx-markdown": "12.0.1",
|
"ngx-markdown": "12.0.1",
|
||||||
"ngx-skeleton-loader": "2.9.1",
|
"ngx-skeleton-loader": "2.9.1",
|
||||||
|
"ngx-stripe": "12.0.2",
|
||||||
"passport": "0.4.1",
|
"passport": "0.4.1",
|
||||||
"passport-google-oauth20": "2.0.0",
|
"passport-google-oauth20": "2.0.0",
|
||||||
"passport-jwt": "4.0.0",
|
"passport-jwt": "4.0.0",
|
||||||
@ -99,6 +101,7 @@
|
|||||||
"reflect-metadata": "0.1.13",
|
"reflect-metadata": "0.1.13",
|
||||||
"round-to": "5.0.0",
|
"round-to": "5.0.0",
|
||||||
"rxjs": "6.6.7",
|
"rxjs": "6.6.7",
|
||||||
|
"stripe": "8.156.0",
|
||||||
"svgmap": "2.1.1",
|
"svgmap": "2.1.1",
|
||||||
"uuid": "8.3.2",
|
"uuid": "8.3.2",
|
||||||
"yahoo-finance": "0.3.6",
|
"yahoo-finance": "0.3.6",
|
||||||
@ -129,6 +132,7 @@
|
|||||||
"@typescript-eslint/parser": "4.27.0",
|
"@typescript-eslint/parser": "4.27.0",
|
||||||
"codelyzer": "6.0.1",
|
"codelyzer": "6.0.1",
|
||||||
"cypress": "6.2.1",
|
"cypress": "6.2.1",
|
||||||
|
"dotenv": "8.2.0",
|
||||||
"eslint": "7.28.0",
|
"eslint": "7.28.0",
|
||||||
"eslint-config-prettier": "8.3.0",
|
"eslint-config-prettier": "8.3.0",
|
||||||
"eslint-plugin-import": "2.23.4",
|
"eslint-plugin-import": "2.23.4",
|
||||||
|
@ -1,4 +1,11 @@
|
|||||||
|
const dotenv = require('dotenv');
|
||||||
|
const path = require('path');
|
||||||
const replace = require('replace-in-file');
|
const replace = require('replace-in-file');
|
||||||
|
|
||||||
|
dotenv.config({
|
||||||
|
path: path.resolve(__dirname, '.env')
|
||||||
|
});
|
||||||
|
|
||||||
const now = new Date();
|
const now = new Date();
|
||||||
const buildTimestamp = `${formatWithTwoDigits(
|
const buildTimestamp = `${formatWithTwoDigits(
|
||||||
now.getDate()
|
now.getDate()
|
||||||
@ -7,17 +14,24 @@ const buildTimestamp = `${formatWithTwoDigits(
|
|||||||
)}.${now.getFullYear()} ${formatWithTwoDigits(
|
)}.${now.getFullYear()} ${formatWithTwoDigits(
|
||||||
now.getHours()
|
now.getHours()
|
||||||
)}:${formatWithTwoDigits(now.getMinutes())}`;
|
)}:${formatWithTwoDigits(now.getMinutes())}`;
|
||||||
const options = {
|
|
||||||
files: './dist/apps/client/main.*.js',
|
|
||||||
from: /{BUILD_TIMESTAMP}/g,
|
|
||||||
to: buildTimestamp,
|
|
||||||
allowEmptyPaths: false
|
|
||||||
};
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const changedFiles = replace.sync(options);
|
let changedFiles = replace.sync({
|
||||||
|
files: './dist/apps/client/main.*.js',
|
||||||
|
from: /{BUILD_TIMESTAMP}/g,
|
||||||
|
to: buildTimestamp,
|
||||||
|
allowEmptyPaths: false
|
||||||
|
});
|
||||||
console.log('Build version set: ' + buildTimestamp);
|
console.log('Build version set: ' + buildTimestamp);
|
||||||
console.log(changedFiles);
|
console.log(changedFiles);
|
||||||
|
|
||||||
|
changedFiles = replace.sync({
|
||||||
|
files: './dist/apps/client/main.*.js',
|
||||||
|
from: /{STRIPE_PUBLIC_KEY}/g,
|
||||||
|
to: process.env.STRIPE_PUBLIC_KEY ?? '',
|
||||||
|
allowEmptyPaths: false
|
||||||
|
});
|
||||||
|
console.log(changedFiles);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error occurred:', error);
|
console.error('Error occurred:', error);
|
||||||
}
|
}
|
||||||
|
41
yarn.lock
41
yarn.lock
@ -2454,6 +2454,11 @@
|
|||||||
resolved "https://registry.yarnpkg.com/@stencil/core/-/core-2.5.2.tgz#ad00d495ee37bbed4044524d59c7f22de15ab4a7"
|
resolved "https://registry.yarnpkg.com/@stencil/core/-/core-2.5.2.tgz#ad00d495ee37bbed4044524d59c7f22de15ab4a7"
|
||||||
integrity sha512-bgjPXkSzzg1WnTgVUm6m5ZzpKt602WmA/QljODAW1xVN40OHJdbGblzF/F6MFzqv2c5Cy30CB41arc8qADIdcQ==
|
integrity sha512-bgjPXkSzzg1WnTgVUm6m5ZzpKt602WmA/QljODAW1xVN40OHJdbGblzF/F6MFzqv2c5Cy30CB41arc8qADIdcQ==
|
||||||
|
|
||||||
|
"@stripe/stripe-js@1.15.0":
|
||||||
|
version "1.15.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/@stripe/stripe-js/-/stripe-js-1.15.0.tgz#86178cfbe66151910b09b03595e60048ab4c698e"
|
||||||
|
integrity sha512-KQsNPc+uVQkc8dewwz1A6uHOWeU2cWoZyNIbsx5mtmperr5TPxw4u8M20WOa22n6zmIOh/zLdzEe8DYK/0IjBw==
|
||||||
|
|
||||||
"@tootallnate/once@1":
|
"@tootallnate/once@1":
|
||||||
version "1.1.2"
|
version "1.1.2"
|
||||||
resolved "https://registry.yarnpkg.com/@tootallnate/once/-/once-1.1.2.tgz#ccb91445360179a04e7fe6aff78c00ffc1eeaf82"
|
resolved "https://registry.yarnpkg.com/@tootallnate/once/-/once-1.1.2.tgz#ccb91445360179a04e7fe6aff78c00ffc1eeaf82"
|
||||||
@ -2656,6 +2661,11 @@
|
|||||||
resolved "https://registry.yarnpkg.com/@types/node/-/node-14.14.33.tgz#9e4f8c64345522e4e8ce77b334a8aaa64e2b6c78"
|
resolved "https://registry.yarnpkg.com/@types/node/-/node-14.14.33.tgz#9e4f8c64345522e4e8ce77b334a8aaa64e2b6c78"
|
||||||
integrity sha512-oJqcTrgPUF29oUP8AsUqbXGJNuPutsetaa9kTQAQce5Lx5dTYWV02ScBiT/k1BX/Z7pKeqedmvp39Wu4zR7N7g==
|
integrity sha512-oJqcTrgPUF29oUP8AsUqbXGJNuPutsetaa9kTQAQce5Lx5dTYWV02ScBiT/k1BX/Z7pKeqedmvp39Wu4zR7N7g==
|
||||||
|
|
||||||
|
"@types/node@>=8.1.0":
|
||||||
|
version "15.12.3"
|
||||||
|
resolved "https://registry.yarnpkg.com/@types/node/-/node-15.12.3.tgz#2817bf5f25bc82f56579018c53f7d41b1830b1af"
|
||||||
|
integrity sha512-SNt65CPCXvGNDZ3bvk1TQ0Qxoe3y1RKH88+wZ2Uf05dduBCqqFQ76ADP9pbT+Cpvj60SkRppMCh2Zo8tDixqjQ==
|
||||||
|
|
||||||
"@types/normalize-package-data@^2.4.0":
|
"@types/normalize-package-data@^2.4.0":
|
||||||
version "2.4.0"
|
version "2.4.0"
|
||||||
resolved "https://registry.yarnpkg.com/@types/normalize-package-data/-/normalize-package-data-2.4.0.tgz#e486d0d97396d79beedd0a6e33f4534ff6b4973e"
|
resolved "https://registry.yarnpkg.com/@types/normalize-package-data/-/normalize-package-data-2.4.0.tgz#e486d0d97396d79beedd0a6e33f4534ff6b4973e"
|
||||||
@ -9623,6 +9633,13 @@ ngx-skeleton-loader@2.9.1:
|
|||||||
perf-marks "^1.13.4"
|
perf-marks "^1.13.4"
|
||||||
tslib "^1.10.0"
|
tslib "^1.10.0"
|
||||||
|
|
||||||
|
ngx-stripe@12.0.2:
|
||||||
|
version "12.0.2"
|
||||||
|
resolved "https://registry.yarnpkg.com/ngx-stripe/-/ngx-stripe-12.0.2.tgz#b250acc2a08dc96dac035fc0a67b4a8cbeca3efb"
|
||||||
|
integrity sha512-/arfIi996yv3EpzqjYsb20TUdQ9t+GVMNVIx1mdsiWcpiNjL36tO3lG45T0hyiBJNAds87Ag40Fm8PfsuHFCUw==
|
||||||
|
dependencies:
|
||||||
|
tslib "^2.1.0"
|
||||||
|
|
||||||
nice-try@^1.0.4:
|
nice-try@^1.0.4:
|
||||||
version "1.0.5"
|
version "1.0.5"
|
||||||
resolved "https://registry.yarnpkg.com/nice-try/-/nice-try-1.0.5.tgz#a3378a7696ce7d223e88fc9b764bd7ef1089e366"
|
resolved "https://registry.yarnpkg.com/nice-try/-/nice-try-1.0.5.tgz#a3378a7696ce7d223e88fc9b764bd7ef1089e366"
|
||||||
@ -11329,6 +11346,13 @@ qs@6.7.0:
|
|||||||
resolved "https://registry.yarnpkg.com/qs/-/qs-6.7.0.tgz#41dc1a015e3d581f1621776be31afb2876a9b1bc"
|
resolved "https://registry.yarnpkg.com/qs/-/qs-6.7.0.tgz#41dc1a015e3d581f1621776be31afb2876a9b1bc"
|
||||||
integrity sha512-VCdBRNFTX1fyE7Nb6FYoURo/SPe62QCaAyzJvUjwRaIsc+NePBEniHlvxFmmX56+HZphIGtV0XeCirBtpDrTyQ==
|
integrity sha512-VCdBRNFTX1fyE7Nb6FYoURo/SPe62QCaAyzJvUjwRaIsc+NePBEniHlvxFmmX56+HZphIGtV0XeCirBtpDrTyQ==
|
||||||
|
|
||||||
|
qs@^6.6.0:
|
||||||
|
version "6.10.1"
|
||||||
|
resolved "https://registry.yarnpkg.com/qs/-/qs-6.10.1.tgz#4931482fa8d647a5aab799c5271d2133b981fb6a"
|
||||||
|
integrity sha512-M528Hph6wsSVOBiYUnGf+K/7w0hNshs/duGsNXPUCLH5XAqjEtiPGwNONLV0tBH8NoGb0mvD5JubnUTrujKDTg==
|
||||||
|
dependencies:
|
||||||
|
side-channel "^1.0.4"
|
||||||
|
|
||||||
qs@~6.5.2:
|
qs@~6.5.2:
|
||||||
version "6.5.2"
|
version "6.5.2"
|
||||||
resolved "https://registry.yarnpkg.com/qs/-/qs-6.5.2.tgz#cb3ae806e8740444584ef154ce8ee98d403f3e36"
|
resolved "https://registry.yarnpkg.com/qs/-/qs-6.5.2.tgz#cb3ae806e8740444584ef154ce8ee98d403f3e36"
|
||||||
@ -12185,6 +12209,15 @@ shellwords@^0.1.1:
|
|||||||
resolved "https://registry.yarnpkg.com/shellwords/-/shellwords-0.1.1.tgz#d6b9181c1a48d397324c84871efbcfc73fc0654b"
|
resolved "https://registry.yarnpkg.com/shellwords/-/shellwords-0.1.1.tgz#d6b9181c1a48d397324c84871efbcfc73fc0654b"
|
||||||
integrity sha512-vFwSUfQvqybiICwZY5+DAWIPLKsWO31Q91JSKl3UYv+K5c2QRPzn0qzec6QPu1Qc9eHYItiP3NdJqNVqetYAww==
|
integrity sha512-vFwSUfQvqybiICwZY5+DAWIPLKsWO31Q91JSKl3UYv+K5c2QRPzn0qzec6QPu1Qc9eHYItiP3NdJqNVqetYAww==
|
||||||
|
|
||||||
|
side-channel@^1.0.4:
|
||||||
|
version "1.0.4"
|
||||||
|
resolved "https://registry.yarnpkg.com/side-channel/-/side-channel-1.0.4.tgz#efce5c8fdc104ee751b25c58d4290011fa5ea2cf"
|
||||||
|
integrity sha512-q5XPytqFEIKHkGdiMIrY10mvLRvnQh42/+GoBlFW3b2LXLE2xxJpZFdm94we0BaoV3RwJyGqg5wS7epxTv0Zvw==
|
||||||
|
dependencies:
|
||||||
|
call-bind "^1.0.0"
|
||||||
|
get-intrinsic "^1.0.2"
|
||||||
|
object-inspect "^1.9.0"
|
||||||
|
|
||||||
signal-exit@^3.0.0, signal-exit@^3.0.2:
|
signal-exit@^3.0.0, signal-exit@^3.0.2:
|
||||||
version "3.0.3"
|
version "3.0.3"
|
||||||
resolved "https://registry.yarnpkg.com/signal-exit/-/signal-exit-3.0.3.tgz#a1410c2edd8f077b08b4e253c8eacfcaf057461c"
|
resolved "https://registry.yarnpkg.com/signal-exit/-/signal-exit-3.0.3.tgz#a1410c2edd8f077b08b4e253c8eacfcaf057461c"
|
||||||
@ -12692,6 +12725,14 @@ strip-json-comments@^3.1.0, strip-json-comments@^3.1.1:
|
|||||||
resolved "https://registry.yarnpkg.com/strip-json-comments/-/strip-json-comments-3.1.1.tgz#31f1281b3832630434831c310c01cccda8cbe006"
|
resolved "https://registry.yarnpkg.com/strip-json-comments/-/strip-json-comments-3.1.1.tgz#31f1281b3832630434831c310c01cccda8cbe006"
|
||||||
integrity sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==
|
integrity sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==
|
||||||
|
|
||||||
|
stripe@8.156.0:
|
||||||
|
version "8.156.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/stripe/-/stripe-8.156.0.tgz#040de551df88d71ef670a8c8d4df114c3fa6eb4b"
|
||||||
|
integrity sha512-q+bixlhaxnSI/Htk/iB1i5LhuZ557hL0pFgECBxQNhso1elxIsOsPOIXEuo3tSLJEb8CJSB7t/+Fyq6KP69tAQ==
|
||||||
|
dependencies:
|
||||||
|
"@types/node" ">=8.1.0"
|
||||||
|
qs "^6.6.0"
|
||||||
|
|
||||||
style-loader@2.0.0:
|
style-loader@2.0.0:
|
||||||
version "2.0.0"
|
version "2.0.0"
|
||||||
resolved "https://registry.yarnpkg.com/style-loader/-/style-loader-2.0.0.tgz#9669602fd4690740eaaec137799a03addbbc393c"
|
resolved "https://registry.yarnpkg.com/style-loader/-/style-loader-2.0.0.tgz#9669602fd4690740eaaec137799a03addbbc393c"
|
||||||
|
Loading…
x
Reference in New Issue
Block a user