diff --git a/apps/backend/src/api/routes/integrations.controller.ts b/apps/backend/src/api/routes/integrations.controller.ts index f0dcabcd..dd4bd8e1 100644 --- a/apps/backend/src/api/routes/integrations.controller.ts +++ b/apps/backend/src/api/routes/integrations.controller.ts @@ -1,4 +1,4 @@ -import { Body, Controller, Get, Param, Post } from '@nestjs/common'; +import { Body, Controller, Delete, Get, Param, Post } from '@nestjs/common'; import { ioRedis } from '@gitroom/nestjs-libraries/redis/redis.service'; import { ConnectIntegrationDto } from '@gitroom/nestjs-libraries/dtos/integrations/connect.integration.dto'; import { IntegrationManager } from '@gitroom/nestjs-libraries/integrations/integration.manager'; @@ -33,6 +33,7 @@ export class IntegrationsController { ).map((p) => ({ name: p.name, id: p.id, + disabled: p.disabled, picture: p.picture, identifier: p.providerIdentifier, type: p.type, @@ -186,4 +187,34 @@ export class IntegrationsController { expiresIn ); } + + @Post('/disable') + disableChannel( + @GetOrgFromRequest() org: Organization, + @Body('id') id: string + ) { + return this._integrationService.disableChannel(org.id, id); + } + + @Post('/enable') + enableChannel( + @GetOrgFromRequest() org: Organization, + @Body('id') id: string + ) { + return this._integrationService.enableChannel( + org.id, + // @ts-ignore + org.subscription.totalChannels, + id + ); + } + + @Delete('/') + deleteChannel( + @GetOrgFromRequest() org: Organization, + @Body('id') id: string + ) { + // @ts-ignore + return this._integrationService.deleteChannel(org.id, id); + } } diff --git a/apps/backend/src/api/routes/users.controller.ts b/apps/backend/src/api/routes/users.controller.ts index 79a60411..04493e6b 100644 --- a/apps/backend/src/api/routes/users.controller.ts +++ b/apps/backend/src/api/routes/users.controller.ts @@ -21,6 +21,7 @@ import { Sections, } from '@gitroom/backend/services/auth/permissions/permissions.service'; import {removeSubdomain} from "@gitroom/helpers/subdomain/subdomain.management"; +import {pricing} from "@gitroom/nestjs-libraries/database/prisma/subscriptions/pricing"; @Controller('/user') export class UsersController { @@ -43,6 +44,8 @@ export class UsersController { ...user, orgId: organization.id, // @ts-ignore + totalChannels: organization?.subscription?.totalChannels || pricing.FREE.channel, + // @ts-ignore tier: organization?.subscription?.subscriptionTier || 'FREE', // @ts-ignore role: organization?.users[0]?.role, diff --git a/apps/frontend/src/components/billing/billing.component.tsx b/apps/frontend/src/components/billing/billing.component.tsx index 9d428752..b1b3a824 100644 --- a/apps/frontend/src/components/billing/billing.component.tsx +++ b/apps/frontend/src/components/billing/billing.component.tsx @@ -1,13 +1,14 @@ -"use client"; +'use client'; -import { useCallback } from 'react'; +import {useCallback, useEffect, useState} from 'react'; import { NoBillingComponent } from '@gitroom/frontend/components/billing/no.billing.component'; import useSWR from 'swr'; import { LoadingComponent } from '@gitroom/frontend/components/layout/loading'; -import {useFetch} from "@gitroom/helpers/utils/custom.fetch"; +import { useFetch } from '@gitroom/helpers/utils/custom.fetch'; export const BillingComponent = () => { const fetch = useFetch(); + const load = useCallback(async (path: string) => { return await (await fetch(path)).json(); }, []); diff --git a/apps/frontend/src/components/billing/no.billing.component.tsx b/apps/frontend/src/components/billing/no.billing.component.tsx index 3b055f0a..d31d0451 100644 --- a/apps/frontend/src/components/billing/no.billing.component.tsx +++ b/apps/frontend/src/components/billing/no.billing.component.tsx @@ -3,7 +3,7 @@ import { Slider } from '@gitroom/react/form/slider'; import { FC, useCallback, useEffect, useMemo, useState } from 'react'; import { Button } from '@gitroom/react/form/button'; -import { sortBy } from 'lodash'; +import { isEqual, sortBy } from 'lodash'; import { Track } from '@gitroom/react/form/track'; import { useFetch } from '@gitroom/helpers/utils/custom.fetch'; import { Subscription } from '@prisma/client'; @@ -14,8 +14,10 @@ import { useToaster } from '@gitroom/react/toaster/toaster'; import dayjs from 'dayjs'; import clsx from 'clsx'; import { pricing } from '@gitroom/nestjs-libraries/database/prisma/subscriptions/pricing'; -import { useRouter } from 'next/navigation'; import { FAQComponent } from '@gitroom/frontend/components/billing/faq.component'; +import { useSWRConfig } from 'swr'; +import { useUser } from '@gitroom/frontend/components/layout/user.context'; +import { useRouter } from 'next/navigation'; export interface Tiers { month: Array<{ @@ -153,13 +155,15 @@ export const NoBillingComponent: FC<{ sub?: Subscription; }> = (props) => { const { tiers, sub } = props; + const { mutate } = useSWRConfig(); const fetch = useFetch(); - const router = useRouter(); const toast = useToaster(); + const user = useUser(); const [subscription, setSubscription] = useState( sub ); + const [loading, setLoading] = useState(false); const [period, setPeriod] = useState<'MONTHLY' | 'YEARLY'>( subscription?.period || 'MONTHLY' @@ -169,10 +173,24 @@ export const NoBillingComponent: FC<{ ); const [initialChannels, setInitialChannels] = useState( - subscription?.totalChannels || 1 + sub?.totalChannels || 1 ); const [totalChannels, setTotalChannels] = useState(initialChannels); + useEffect(() => { + if (initialChannels !== sub?.totalChannels) { + setTotalChannels(sub?.totalChannels || 1); + setInitialChannels(sub?.totalChannels || 1); + } + + if (period !== sub?.period) { + setPeriod(sub?.period || 'MONTHLY'); + setMonthlyOrYearly(sub?.period === 'MONTHLY' ? 'off' : 'on'); + } + + setSubscription(sub); + }, [sub]); + const currentPackage = useMemo(() => { if (!subscription) { return 'FREE'; @@ -265,7 +283,17 @@ export const NoBillingComponent: FC<{ subscriptionTier: billing, cancelAt: null, })); - router.refresh(); + mutate( + '/user/self', + { + ...user, + totalChannels, + tier: billing, + }, + { + revalidate: false, + } + ); toast.show('Subscription updated successfully'); } diff --git a/apps/frontend/src/components/launches/launches.component.tsx b/apps/frontend/src/components/launches/launches.component.tsx index 998d3948..c7d3218e 100644 --- a/apps/frontend/src/components/launches/launches.component.tsx +++ b/apps/frontend/src/components/launches/launches.component.tsx @@ -1,7 +1,7 @@ 'use client'; import { AddProviderButton } from '@gitroom/frontend/components/launches/add.provider.component'; -import { useCallback, useMemo } from 'react'; +import { useCallback, useMemo, useState } from 'react'; import Image from 'next/image'; import { orderBy } from 'lodash'; import { Calendar } from '@gitroom/frontend/components/launches/calendar'; @@ -10,22 +10,53 @@ import { Filters } from '@gitroom/frontend/components/launches/filters'; import { useFetch } from '@gitroom/helpers/utils/custom.fetch'; import useSWR from 'swr'; import { LoadingComponent } from '@gitroom/frontend/components/layout/loading'; +import clsx from 'clsx'; +import { useUser } from '../layout/user.context'; +import { Menu } from '@gitroom/frontend/components/launches/menu/menu'; export const LaunchesComponent = () => { const fetch = useFetch(); + const [reload, setReload] = useState(false); const load = useCallback(async (path: string) => { return (await (await fetch(path)).json()).integrations; }, []); + const user = useUser(); - const { isLoading, data: integrations } = useSWR('/integrations/list', load, { + const { + isLoading, + data: integrations, + mutate, + } = useSWR('/integrations/list', load, { fallbackData: [], }); - const sortedIntegrations = useMemo(() => { - return orderBy(integrations, ['type', 'identifier'], ['desc', 'asc']); + const totalNonDisabledChannels = useMemo(() => { + return ( + integrations?.filter((integration: any) => !integration.disabled) + ?.length || 0 + ); }, [integrations]); - if (isLoading) { + const sortedIntegrations = useMemo(() => { + return orderBy( + integrations, + ['type', 'disabled', 'identifier'], + ['desc', 'asc', 'asc'] + ); + }, [integrations]); + + const update = useCallback(async (shouldReload: boolean) => { + if (shouldReload) { + setReload(true); + } + await mutate(); + + if (shouldReload) { + setReload(false); + } + }, []); + + if (isLoading || reload) { return ; } @@ -45,7 +76,12 @@ export const LaunchesComponent = () => { key={integration.id} className="flex gap-[8px] items-center" > -
+
{ height={20} />
-
{integration.name}
+
+ {integration.name} +
+ totalNonDisabledChannels && + integration.disabled + } + canDisable={!integration.disabled} + />
))} diff --git a/apps/frontend/src/components/launches/menu/menu.tsx b/apps/frontend/src/components/launches/menu/menu.tsx new file mode 100644 index 00000000..085d71eb --- /dev/null +++ b/apps/frontend/src/components/launches/menu/menu.tsx @@ -0,0 +1,172 @@ +import { FC, useCallback, useState } from 'react'; +import { useClickOutside } from '@mantine/hooks'; +import { useFetch } from '@gitroom/helpers/utils/custom.fetch'; +import { deleteDialog } from '@gitroom/react/helpers/delete.dialog'; +import { useToaster } from '@gitroom/react/toaster/toaster'; + +export const Menu: FC<{ + canEnable: boolean; + canDisable: boolean; + id: string; + onChange: (shouldReload: boolean) => void; +}> = (props) => { + const { canEnable, canDisable, id, onChange } = props; + const fetch = useFetch(); + const toast = useToaster(); + const [show, setShow] = useState(false); + const ref = useClickOutside(() => { + setShow(false); + }); + + const changeShow = useCallback(() => { + setShow(!show); + }, [show]); + + const disableChannel = useCallback(async () => { + if ( + !(await deleteDialog( + 'Are you sure you want to disable this channel?', + 'Disable Channel' + )) + ) { + return; + } + await fetch('/integrations/disable', { + method: 'POST', + body: JSON.stringify({ id }), + }); + + toast.show('Channel Disabled', 'success'); + setShow(false); + onChange(false); + }, []); + + const deleteChannel = useCallback(async () => { + if ( + !(await deleteDialog( + 'Are you sure you want to delete this channel?', + 'Delete Channel' + )) + ) { + return; + } + const deleteIntegration = await fetch('/integrations', { + method: 'DELETE', + body: JSON.stringify({ id }), + }); + + if (deleteIntegration.status === 406) { + toast.show( + 'You have to delete all the posts associated with this channel before deleting it', + 'warning' + ); + return; + } + + toast.show('Channel Deleted', 'success'); + setShow(false); + onChange(true); + }, []); + + const enableChannel = useCallback(async () => { + await fetch('/integrations/enable', { + method: 'POST', + body: JSON.stringify({ id }), + }); + + toast.show('Channel Enabled', 'success'); + setShow(false); + onChange(false); + }, []); + + return ( +
+ + + + {show && ( +
e.stopPropagation()} + className="absolute top-[100%] left-0 p-[8px] px-[20px] bg-fifth flex flex-col gap-[16px] z-[100] rounded-[8px] border border-tableBorder font-['Inter'] text-nowrap" + > + {canEnable && ( +
+
+ + + +
+
Enable Channel
+
+ )} + + {canDisable && ( +
+
+ + + +
+
Disable Channel
+
+ )} + +
+
+ + + +
+
Delete
+
+
+ )} +
+ ); +}; diff --git a/apps/frontend/src/components/layout/layout.settings.tsx b/apps/frontend/src/components/layout/layout.settings.tsx index ea31a87b..ad26d731 100644 --- a/apps/frontend/src/components/layout/layout.settings.tsx +++ b/apps/frontend/src/components/layout/layout.settings.tsx @@ -33,7 +33,13 @@ export const LayoutSettings = ({ children }: { children: ReactNode }) => { return await (await fetch(path)).json(); }, []); - const { data: user } = useSWR('/user/self', load); + const { data: user } = useSWR('/user/self', load, { + revalidateOnFocus: false, + revalidateOnReconnect: false, + revalidateIfStale: false, + refreshWhenOffline: false, + refreshWhenHidden: false, + }); return ( diff --git a/apps/frontend/src/components/layout/top.menu.tsx b/apps/frontend/src/components/layout/top.menu.tsx index a880dc5c..892b794f 100644 --- a/apps/frontend/src/components/layout/top.menu.tsx +++ b/apps/frontend/src/components/layout/top.menu.tsx @@ -36,7 +36,7 @@ export const TopMenu: FC = () => { const user = useUser(); return ( -
+
    {menuItems .filter((f) => { @@ -48,7 +48,7 @@ export const TopMenu: FC = () => { .map((item, index) => (
  • (undefined); @@ -21,15 +22,12 @@ export const ContextWrapper: FC<{ orgId: string; tier: 'FREE' | 'STANDARD' | 'PRO'; role: 'USER' | 'ADMIN' | 'SUPERADMIN'; + totalChannels: number; }; children: ReactNode; }> = ({ user, children }) => { const values = user ? { ...user, tier: pricing[user.tier] } : ({} as any); - return ( - - {children} - - ); + return {children}; }; export const useUser = () => useContext(UserContext); diff --git a/apps/frontend/src/components/settings/settings.component.tsx b/apps/frontend/src/components/settings/settings.component.tsx index 7e5912a4..0f69fe1d 100644 --- a/apps/frontend/src/components/settings/settings.component.tsx +++ b/apps/frontend/src/components/settings/settings.component.tsx @@ -15,6 +15,8 @@ export const SettingsComponent = () => { const fetch = useFetch(); + console.log(user); + const load = useCallback(async (path: string) => { const { github } = await (await fetch('/settings/github')).json(); if (!github) { diff --git a/apps/frontend/tailwind.config.js b/apps/frontend/tailwind.config.js index 3a3d641f..8c114584 100644 --- a/apps/frontend/tailwind.config.js +++ b/apps/frontend/tailwind.config.js @@ -48,7 +48,8 @@ module.exports = { }, boxShadow: { yellow: '0 0 60px 20px #6b6237', - green: '0px 0px 50px rgba(60, 124, 90, 0.3)', + yellowToast: '0px 0px 50px rgba(252, 186, 3, 0.3)', + greenToast: '0px 0px 50px rgba(60, 124, 90, 0.3)', }, // that is actual animation keyframes: (theme) => ({ diff --git a/libraries/nestjs-libraries/src/database/prisma/integrations/integration.repository.ts b/libraries/nestjs-libraries/src/database/prisma/integrations/integration.repository.ts index 3b8ae102..60d2908a 100644 --- a/libraries/nestjs-libraries/src/database/prisma/integrations/integration.repository.ts +++ b/libraries/nestjs-libraries/src/database/prisma/integrations/integration.repository.ts @@ -4,7 +4,10 @@ import dayjs from 'dayjs'; @Injectable() export class IntegrationRepository { - constructor(private _integration: PrismaRepository<'integration'>) {} + constructor( + private _integration: PrismaRepository<'integration'>, + private _posts: PrismaRepository<'post'> + ) {} createOrUpdateIntegration( org: string, @@ -77,7 +80,82 @@ export class IntegrationRepository { return this._integration.model.integration.findMany({ where: { organizationId: org, + deletedAt: null, }, }); } + + async disableChannel(org: string, id: string) { + await this._integration.model.integration.update({ + where: { + id, + organizationId: org, + }, + data: { + disabled: true, + }, + }); + } + + async enableChannel(org: string, id: string) { + await this._integration.model.integration.update({ + where: { + id, + organizationId: org, + }, + data: { + disabled: false, + }, + }); + } + + countPostsForChannel(org: string, id: string) { + return this._posts.model.post.count({ + where: { + organizationId: org, + integrationId: id, + deletedAt: null, + state: { + in: ['QUEUE', 'DRAFT'], + }, + }, + }); + } + + deleteChannel(org: string, id: string) { + return this._integration.model.integration.update({ + where: { + id, + organizationId: org, + }, + data: { + deletedAt: new Date(), + }, + }); + } + + async disableIntegrations(org: string, totalChannels: number) { + const getChannels = await this._integration.model.integration.findMany({ + where: { + organizationId: org, + disabled: false, + deletedAt: null, + }, + take: totalChannels, + select: { + id: true, + }, + }); + + for (const channel of getChannels) { + await this._integration.model.integration.update({ + where: { + id: channel.id, + }, + data: { + disabled: true, + }, + }); + } + } } diff --git a/libraries/nestjs-libraries/src/database/prisma/integrations/integration.service.ts b/libraries/nestjs-libraries/src/database/prisma/integrations/integration.service.ts index 9e1c8bdd..54ea01a4 100644 --- a/libraries/nestjs-libraries/src/database/prisma/integrations/integration.service.ts +++ b/libraries/nestjs-libraries/src/database/prisma/integrations/integration.service.ts @@ -1,4 +1,4 @@ -import { Injectable } from '@nestjs/common'; +import {HttpException, HttpStatus, Injectable} from '@nestjs/common'; import { IntegrationRepository } from '@gitroom/nestjs-libraries/database/prisma/integrations/integration.repository'; import { IntegrationManager } from '@gitroom/nestjs-libraries/integrations/integration.manager'; @@ -63,4 +63,32 @@ export class IntegrationService { ); } } + + async disableChannel(org: string, id: string) { + return this._integrationRepository.disableChannel(org, id); + } + + async enableChannel(org: string, totalChannels: number, id: string) { + const integrations = ( + await this._integrationRepository.getIntegrationsList(org) + ).filter((f) => !f.disabled); + if (integrations.length >= totalChannels) { + throw new Error('You have reached the maximum number of channels'); + } + + return this._integrationRepository.enableChannel(org, id); + } + + async deleteChannel(org: string, id: string) { + const isTherePosts = await this._integrationRepository.countPostsForChannel(org, id); + if (isTherePosts) { + throw new HttpException('There are posts for this channel', HttpStatus.NOT_ACCEPTABLE); + } + + return this._integrationRepository.deleteChannel(org, id); + } + + async disableIntegrations(org: string, totalChannels: number) { + return this._integrationRepository.disableIntegrations(org, totalChannels); + } } diff --git a/libraries/nestjs-libraries/src/database/prisma/organizations/organization.repository.ts b/libraries/nestjs-libraries/src/database/prisma/organizations/organization.repository.ts index 1bb222dc..f3628e49 100644 --- a/libraries/nestjs-libraries/src/database/prisma/organizations/organization.repository.ts +++ b/libraries/nestjs-libraries/src/database/prisma/organizations/organization.repository.ts @@ -33,6 +33,7 @@ export class OrganizationRepository { subscription: { select: { subscriptionTier: true, + totalChannels: true, }, }, }, @@ -186,4 +187,18 @@ export class OrganizationRepository { }, }); } + + disableOrEnableNonSuperAdminUsers(orgId: string, disable: boolean) { + return this._userOrg.model.userOrganization.updateMany({ + where: { + organizationId: orgId, + role: { + not: Role.SUPERADMIN, + }, + }, + data: { + disabled: disable, + }, + }); + } } diff --git a/libraries/nestjs-libraries/src/database/prisma/organizations/organization.service.ts b/libraries/nestjs-libraries/src/database/prisma/organizations/organization.service.ts index adfdeb9a..315f3071 100644 --- a/libraries/nestjs-libraries/src/database/prisma/organizations/organization.service.ts +++ b/libraries/nestjs-libraries/src/database/prisma/organizations/organization.service.ts @@ -76,4 +76,8 @@ export class OrganizationService { return this._organizationRepository.deleteTeamMember(org.id, userId); } + + disableOrEnableNonSuperAdminUsers(orgId: string, disable: boolean) { + return this._organizationRepository.disableOrEnableNonSuperAdminUsers(orgId, disable); + } } diff --git a/libraries/nestjs-libraries/src/database/prisma/schema.prisma b/libraries/nestjs-libraries/src/database/prisma/schema.prisma index 1a032e31..3085a2d5 100644 --- a/libraries/nestjs-libraries/src/database/prisma/schema.prisma +++ b/libraries/nestjs-libraries/src/database/prisma/schema.prisma @@ -52,6 +52,7 @@ model UserOrganization { userId String organization Organization @relation(fields: [organizationId], references: [id]) organizationId String + disabled Boolean @default(false) role Role @default(USER) createdAt DateTime @default(now()) updatedAt DateTime @updatedAt @@ -144,6 +145,7 @@ model Integration { providerIdentifier String type String token String + disabled Boolean @default(false) tokenExpiration DateTime? refreshToken String? posts Post[] diff --git a/libraries/nestjs-libraries/src/database/prisma/subscriptions/subscription.service.ts b/libraries/nestjs-libraries/src/database/prisma/subscriptions/subscription.service.ts index 23a0d27d..0ec8a65c 100644 --- a/libraries/nestjs-libraries/src/database/prisma/subscriptions/subscription.service.ts +++ b/libraries/nestjs-libraries/src/database/prisma/subscriptions/subscription.service.ts @@ -1,34 +1,86 @@ -import {Injectable} from "@nestjs/common"; -import {pricing} from "@gitroom/nestjs-libraries/database/prisma/subscriptions/pricing"; -import {SubscriptionRepository} from "@gitroom/nestjs-libraries/database/prisma/subscriptions/subscription.repository"; +import { Injectable } from '@nestjs/common'; +import { pricing } from '@gitroom/nestjs-libraries/database/prisma/subscriptions/pricing'; +import { SubscriptionRepository } from '@gitroom/nestjs-libraries/database/prisma/subscriptions/subscription.repository'; +import { IntegrationService } from '@gitroom/nestjs-libraries/database/prisma/integrations/integration.service'; +import { OrganizationService } from '@gitroom/nestjs-libraries/database/prisma/organizations/organization.service'; @Injectable() export class SubscriptionService { constructor( - private readonly _subscriptionRepository: SubscriptionRepository + private readonly _subscriptionRepository: SubscriptionRepository, + private readonly _integrationService: IntegrationService, + private readonly _organizationService: OrganizationService ) {} getSubscriptionByOrganizationId(organizationId: string) { - return this._subscriptionRepository.getSubscriptionByOrganizationId(organizationId); + return this._subscriptionRepository.getSubscriptionByOrganizationId( + organizationId + ); } async deleteSubscription(customerId: string) { - await this.modifySubscription(customerId, 'FREE'); - return this._subscriptionRepository.deleteSubscriptionByCustomerId(customerId); + await this.modifySubscription( + customerId, + pricing.FREE.channel || 0, + 'FREE' + ); + return this._subscriptionRepository.deleteSubscriptionByCustomerId( + customerId + ); } updateCustomerId(organizationId: string, customerId: string) { - return this._subscriptionRepository.updateCustomerId(organizationId, customerId); + return this._subscriptionRepository.updateCustomerId( + organizationId, + customerId + ); } checkSubscription(organizationId: string, subscriptionId: string) { - return this._subscriptionRepository.checkSubscription(organizationId, subscriptionId); + return this._subscriptionRepository.checkSubscription( + organizationId, + subscriptionId + ); } - async modifySubscription(customerId: string, billing: 'FREE' | 'STANDARD' | 'PRO') { - const getCurrentSubscription = (await this._subscriptionRepository.getSubscriptionByCustomerId(customerId))!; + async modifySubscription( + customerId: string, + totalChannels: number, + billing: 'FREE' | 'STANDARD' | 'PRO' + ) { + const getCurrentSubscription = + (await this._subscriptionRepository.getSubscriptionByCustomerId( + customerId + ))!; const from = pricing[getCurrentSubscription?.subscriptionTier || 'FREE']; - const to = pricing[billing]; + const to = pricing[billing]; + + const currentTotalChannels = ( + await this._integrationService.getIntegrationsList( + getCurrentSubscription.organizationId + ) + ).filter((f) => !f.disabled); + + if (currentTotalChannels.length > totalChannels) { + await this._integrationService.disableIntegrations( + getCurrentSubscription.organizationId, + currentTotalChannels.length - totalChannels + ); + } + + if (from.team_members && !to.team_members) { + await this._organizationService.disableOrEnableNonSuperAdminUsers( + getCurrentSubscription.organizationId, + true + ); + } + + if (!from.team_members && to.team_members) { + await this._organizationService.disableOrEnableNonSuperAdminUsers( + getCurrentSubscription.organizationId, + false + ); + } // if (to.faq < from.faq) { // await this._faqRepository.deleteFAQs(getCurrentSubscription?.organizationId, from.faq - to.faq); @@ -48,9 +100,23 @@ export class SubscriptionService { // } } - async createOrUpdateSubscription(identifier: string, customerId: string, totalChannels: number, billing: 'STANDARD' | 'PRO', period: 'MONTHLY' | 'YEARLY', cancelAt: number | null) { - await this.modifySubscription(customerId, billing); - return this._subscriptionRepository.createOrUpdateSubscription(identifier, customerId, totalChannels, billing, period, cancelAt); + async createOrUpdateSubscription( + identifier: string, + customerId: string, + totalChannels: number, + billing: 'STANDARD' | 'PRO', + period: 'MONTHLY' | 'YEARLY', + cancelAt: number | null + ) { + await this.modifySubscription(customerId, totalChannels, billing); + return this._subscriptionRepository.createOrUpdateSubscription( + identifier, + customerId, + totalChannels, + billing, + period, + cancelAt + ); } async getSubscription(organizationId: string) { diff --git a/libraries/react-shared-libraries/src/toaster/toaster.tsx b/libraries/react-shared-libraries/src/toaster/toaster.tsx index 46e5921e..f637a2f2 100644 --- a/libraries/react-shared-libraries/src/toaster/toaster.tsx +++ b/libraries/react-shared-libraries/src/toaster/toaster.tsx @@ -1,19 +1,28 @@ 'use client'; import { useCallback, useEffect, useState } from 'react'; import EventEmitter from 'events'; +import clsx from 'clsx'; const toaster = new EventEmitter(); export const Toaster = () => { const [showToaster, setShowToaster] = useState(false); const [toasterText, setToasterText] = useState(''); + const [toasterType, setToasterType] = useState<'success' | 'warning' | ''>( + '' + ); useEffect(() => { - toaster.on('show', (text: string) => { - setToasterText(text); - setShowToaster(true); - setTimeout(() => { - setShowToaster(false); - }, 4200); - }); + toaster.on( + 'show', + (params: { text: string; type?: 'success' | 'warning' }) => { + const { text, type } = params; + setToasterText(text); + setToasterType(type || 'success'); + setShowToaster(true); + setTimeout(() => { + setShowToaster(false); + }, 4200); + } + ); return () => { toaster.removeAllListeners(); }; @@ -24,20 +33,40 @@ export const Toaster = () => { } return ( -
    +
    - - - + {toasterType === 'success' ? ( + + + + ) : ( + + + + )}
    {toasterText}
    { className="absolute top-0 left-0" > - + { export const useToaster = () => { return { - show: useCallback((text: string) => { - toaster.emit('show', text); + show: useCallback((text: string, type?: 'success' | 'warning') => { + toaster.emit('show', { text, type }); }, []), }; };