From f51b2b500ade9f474d07c8e8e93221ce52ac62c8 Mon Sep 17 00:00:00 2001 From: Nevo David Date: Fri, 9 Aug 2024 21:51:48 +0700 Subject: [PATCH 01/11] Feat: remove on complete --- .../bull-mq-transport/server/bull-mq.server.ts | 18 ++++++++---------- 1 file changed, 8 insertions(+), 10 deletions(-) diff --git a/libraries/nestjs-libraries/src/bull-mq-transport/server/bull-mq.server.ts b/libraries/nestjs-libraries/src/bull-mq-transport/server/bull-mq.server.ts index 4573dfd2..39c6803e 100644 --- a/libraries/nestjs-libraries/src/bull-mq-transport/server/bull-mq.server.ts +++ b/libraries/nestjs-libraries/src/bull-mq-transport/server/bull-mq.server.ts @@ -7,7 +7,7 @@ import { import { Job, Worker } from 'bullmq'; import { BULLMQ_MODULE_OPTIONS } from '../constants/bull-mq.constants'; import { WorkerFactory } from '../factories/worker.factory'; -import {IBullMqModuleOptions} from "@gitroom/nestjs-libraries/bull-mq-transport/interfaces/bull-mq-module-options.interface"; +import { IBullMqModuleOptions } from '@gitroom/nestjs-libraries/bull-mq-transport/interfaces/bull-mq-module-options.interface'; @Injectable() export class BullMqServer extends Server implements CustomTransportStrategy { @@ -19,7 +19,7 @@ export class BullMqServer extends Server implements CustomTransportStrategy { constructor( @Inject(BULLMQ_MODULE_OPTIONS) private readonly options: IBullMqModuleOptions, - private readonly workerFactory: WorkerFactory, + private readonly workerFactory: WorkerFactory ) { super(); @@ -29,18 +29,14 @@ export class BullMqServer extends Server implements CustomTransportStrategy { listen(callback: (...optionalParams: unknown[]) => void) { for (const [pattern, handler] of this.messageHandlers) { - if ( - pattern && - handler && - !this.workers.has(pattern) - ) { + if (pattern && handler && !this.workers.has(pattern)) { const worker = this.workerFactory.create( pattern, (job: Job) => { // eslint-disable-next-line no-async-promise-executor return new Promise(async (resolve, reject) => { const stream$ = this.transformToObservable( - await handler(job.data.payload, job), + await handler(job.data.payload, job) ); this.send(stream$, (packet) => { if (packet.err) { @@ -50,10 +46,12 @@ export class BullMqServer extends Server implements CustomTransportStrategy { }); }); }, + // @ts-ignore { ...this.options, - ...handler?.extras - }, + ...{ removeOnComplete: true, removeOnFail: true }, + ...handler?.extras, + } ); this.workers.set(pattern, worker); this.logger.log(`Registered queue "${pattern}"`); From e130717951994633beed0cb738ae464eec1eb8d8 Mon Sep 17 00:00:00 2001 From: Nevo David Date: Fri, 9 Aug 2024 22:01:39 +0700 Subject: [PATCH 02/11] feat: remove job on complete and failed --- .../src/bull-mq-transport/client/bull-mq.client.ts | 2 ++ .../src/bull-mq-transport/server/bull-mq.server.ts | 3 +-- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/libraries/nestjs-libraries/src/bull-mq-transport/client/bull-mq.client.ts b/libraries/nestjs-libraries/src/bull-mq-transport/client/bull-mq.client.ts index 2ea68379..356db032 100644 --- a/libraries/nestjs-libraries/src/bull-mq-transport/client/bull-mq.client.ts +++ b/libraries/nestjs-libraries/src/bull-mq-transport/client/bull-mq.client.ts @@ -58,6 +58,8 @@ export class BullMqClient extends ClientProxy { queue .add(packet.pattern, packet.data, { jobId: packet.data.id ?? v4(), + removeOnComplete: true, + removeOnFail: true, ...packet.data.options, }) .then(async (job) => { diff --git a/libraries/nestjs-libraries/src/bull-mq-transport/server/bull-mq.server.ts b/libraries/nestjs-libraries/src/bull-mq-transport/server/bull-mq.server.ts index 39c6803e..3aef1bc5 100644 --- a/libraries/nestjs-libraries/src/bull-mq-transport/server/bull-mq.server.ts +++ b/libraries/nestjs-libraries/src/bull-mq-transport/server/bull-mq.server.ts @@ -46,10 +46,9 @@ export class BullMqServer extends Server implements CustomTransportStrategy { }); }); }, - // @ts-ignore { ...this.options, - ...{ removeOnComplete: true, removeOnFail: true }, + ...{ removeOnComplete: { count: 0 }, removeOnFail: { count: 0 } }, ...handler?.extras, } ); From e721a791325cc1ef6b9e4139b92e2bf3e40ea49c Mon Sep 17 00:00:00 2001 From: Nevo David Date: Sat, 10 Aug 2024 12:35:10 +0700 Subject: [PATCH 03/11] feat: cheaper model --- apps/backend/src/api/routes/copilot.controller.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/backend/src/api/routes/copilot.controller.ts b/apps/backend/src/api/routes/copilot.controller.ts index 2f46b48a..e66a13fd 100644 --- a/apps/backend/src/api/routes/copilot.controller.ts +++ b/apps/backend/src/api/routes/copilot.controller.ts @@ -22,7 +22,7 @@ export class CopilotController { req?.body?.variables?.data?.metadata?.requestType === 'TextareaCompletion' ? 'gpt-4o-mini' - : 'gpt-4o', + : 'gpt-4o-2024-08-06', }), }); From 1bc0e0ad1fe55fb395a4c013a67c75d5bd1f0ca7 Mon Sep 17 00:00:00 2001 From: Nevo David Date: Sat, 10 Aug 2024 23:18:30 +0700 Subject: [PATCH 04/11] feat: from free to trial --- .../billing/main.billing.component.tsx | 164 ++++++++++-------- .../src/components/layout/layout.settings.tsx | 32 +++- .../components/layout/settings.component.tsx | 6 +- .../src/components/layout/user.context.tsx | 2 +- .../src/database/prisma/schema.prisma | 2 + .../database/prisma/subscriptions/pricing.ts | 34 +++- .../src/dtos/billing/billing.subscribe.dto.ts | 4 +- .../src/services/stripe.service.ts | 49 ++++-- 8 files changed, 196 insertions(+), 97 deletions(-) diff --git a/apps/frontend/src/components/billing/main.billing.component.tsx b/apps/frontend/src/components/billing/main.billing.component.tsx index 5931980c..78889abc 100644 --- a/apps/frontend/src/components/billing/main.billing.component.tsx +++ b/apps/frontend/src/components/billing/main.billing.component.tsx @@ -19,6 +19,7 @@ import { useSWRConfig } from 'swr'; import { useUser } from '@gitroom/frontend/components/layout/user.context'; import interClass from '@gitroom/react/helpers/inter.font'; import { useRouter } from 'next/navigation'; +import { isGeneral } from '@gitroom/react/helpers/is.general'; export interface Tiers { month: Array<{ @@ -79,7 +80,7 @@ export const Prorate: FC<{ return (
- (Pay Today ${(price < 0 ? 0 : price).toFixed(1)}) + (Pay Today ${(price < 0 ? 0 : price)?.toFixed(1)})
); }; @@ -315,81 +316,104 @@ export const MainBillingComponent: FC<{
- {Object.entries(pricing).map(([name, values]) => ( -
-
{name}
-
-
- $ - {monthlyOrYearly === 'on' - ? values.year_price - : values.month_price} -
-
- {monthlyOrYearly === 'on' ? '/year' : '/month'} -
-
-
- {currentPackage === name.toUpperCase() && - subscription?.cancelAt ? ( -
-
- -
+ {Object.entries(pricing) + .filter((f) => !isGeneral() || f[0] !== 'FREE') + .map(([name, values]) => ( +
+
{name}
+
+
+ $ + {monthlyOrYearly === 'on' + ? values.year_price + : values.month_price}
- ) : ( - - )} - {subscription && - currentPackage !== name.toUpperCase() && - name !== 'FREE' && - !!name && ( - +
+ {monthlyOrYearly === 'on' ? '/year' : '/month'} +
+
+
+ {currentPackage === name.toUpperCase() && + subscription?.cancelAt ? ( +
+
+ +
+
+ ) : ( + )} + {subscription && + currentPackage !== name.toUpperCase() && + name !== 'FREE' && + !!name && ( + + )} +
+
- -
- ))} + ))}
{!!subscription?.id && ( -
+
+ {isGeneral() && !subscription?.cancelAt && ( + + )} +
+ )} + {subscription?.cancelAt && isGeneral() && ( +
+ Your subscription will be cancel at {dayjs(subscription.cancelAt).local().format('D MMM, YYYY')}
+ You will never be charged again
)} diff --git a/apps/frontend/src/components/layout/layout.settings.tsx b/apps/frontend/src/components/layout/layout.settings.tsx index 8569bd32..582d2b15 100644 --- a/apps/frontend/src/components/layout/layout.settings.tsx +++ b/apps/frontend/src/components/layout/layout.settings.tsx @@ -29,6 +29,7 @@ import { isGeneral } from '@gitroom/react/helpers/is.general'; import { CopilotKit } from '@copilotkit/react-core'; import { Impersonate } from '@gitroom/frontend/components/layout/impersonate'; import clsx from 'clsx'; +import { BillingComponent } from '@gitroom/frontend/components/billing/billing.component'; dayjs.extend(utc); dayjs.extend(weekOfYear); @@ -49,6 +50,8 @@ export const LayoutSettings = ({ children }: { children: ReactNode }) => { refreshWhenHidden: false, }); + if (!user) return null; + return ( { - + {(user.tier !== 'FREE' || !isGeneral()) && }
@@ -92,7 +95,11 @@ export const LayoutSettings = ({ children }: { children: ReactNode }) => { )}
- {user?.orgId ? :
} + {user?.orgId && (user.tier !== 'FREE' || !isGeneral()) ? ( + + ) : ( +
+ )}
@@ -101,8 +108,25 @@ export const LayoutSettings = ({ children }: { children: ReactNode }) => {
- - <div className="flex flex-1 flex-col">{children}</div> + {user.tier === 'FREE' && isGeneral() ? ( + <> + <div className="text-center mb-[20px] text-xl"> + <h1 className="text-3xl"> + PLEASE SELECT A PLAN TO CONTINUE + </h1> + <br /> + You will not be charged today. + <br /> + You can cancel anytime. + </div> + <BillingComponent /> + </> + ) : ( + <> + <Title /> + <div className="flex flex-1 flex-col">{children}</div> + </> + )} </div> </div> </div> diff --git a/apps/frontend/src/components/layout/settings.component.tsx b/apps/frontend/src/components/layout/settings.component.tsx index eb694f27..a8854ae2 100644 --- a/apps/frontend/src/components/layout/settings.component.tsx +++ b/apps/frontend/src/components/layout/settings.component.tsx @@ -15,6 +15,7 @@ import { TeamsComponent } from '@gitroom/frontend/components/settings/teams.comp import { isGeneral } from '@gitroom/react/helpers/is.general'; import { useUser } from '@gitroom/frontend/components/layout/user.context'; import { LogoutComponent } from '@gitroom/frontend/components/layout/logout.component'; +import { useSearchParams } from 'next/navigation'; export const SettingsPopup: FC<{ getRef?: Ref<any> }> = (props) => { const { getRef } = props; @@ -33,6 +34,9 @@ export const SettingsPopup: FC<{ getRef?: Ref<any> }> = (props) => { return modal.closeAll(); }, []); + const url = useSearchParams(); + const showLogout = !url.get('onboarding'); + const loadProfile = useCallback(async () => { const personal = await (await fetch('/user/personal')).json(); form.setValue('fullname', personal.name || ''); @@ -188,7 +192,7 @@ export const SettingsPopup: FC<{ getRef?: Ref<any> }> = (props) => { </div> )} {!!user?.tier?.team_members && isGeneral() && <TeamsComponent />} - <LogoutComponent /> + {showLogout && <LogoutComponent />} </div> </form> </FormProvider> diff --git a/apps/frontend/src/components/layout/user.context.tsx b/apps/frontend/src/components/layout/user.context.tsx index 6dd4ff8b..1be1698f 100644 --- a/apps/frontend/src/components/layout/user.context.tsx +++ b/apps/frontend/src/components/layout/user.context.tsx @@ -22,7 +22,7 @@ export const UserContext = createContext< export const ContextWrapper: FC<{ user: User & { orgId: string; - tier: 'FREE' | 'STANDARD' | 'PRO'; + tier: 'FREE' | 'STANDARD' | 'PRO' | 'ULTIMATE' | 'TEAM'; role: 'USER' | 'ADMIN' | 'SUPERADMIN'; totalChannels: number; }; diff --git a/libraries/nestjs-libraries/src/database/prisma/schema.prisma b/libraries/nestjs-libraries/src/database/prisma/schema.prisma index 19321234..1b87a0c8 100644 --- a/libraries/nestjs-libraries/src/database/prisma/schema.prisma +++ b/libraries/nestjs-libraries/src/database/prisma/schema.prisma @@ -417,6 +417,8 @@ enum State { enum SubscriptionTier { STANDARD PRO + TEAM + ULTIMATE } enum Period { diff --git a/libraries/nestjs-libraries/src/database/prisma/subscriptions/pricing.ts b/libraries/nestjs-libraries/src/database/prisma/subscriptions/pricing.ts index cc8ac74c..37ea6180 100644 --- a/libraries/nestjs-libraries/src/database/prisma/subscriptions/pricing.ts +++ b/libraries/nestjs-libraries/src/database/prisma/subscriptions/pricing.ts @@ -44,11 +44,39 @@ export const pricing: PricingInterface = { import_from_channels: true, image_generator: false, }, - PRO: { - current: 'PRO', + TEAM: { + current: 'TEAM', month_price: 40, year_price: 384, - channel: 8, + channel: 10, + posts_per_month: 1000000, + image_generation_count: 100, + community_features: true, + team_members: true, + featured_by_gitroom: true, + ai: true, + import_from_channels: true, + image_generator: true, + }, + PRO: { + current: 'PRO', + month_price: 50, + year_price: 480, + channel: 30, + posts_per_month: 1000000, + image_generation_count: 100, + community_features: true, + team_members: true, + featured_by_gitroom: true, + ai: true, + import_from_channels: true, + image_generator: true, + }, + ULTIMATE: { + current: 'ULTIMATE', + month_price: 70, + year_price: 672, + channel: 100, posts_per_month: 1000000, image_generation_count: 100, community_features: true, diff --git a/libraries/nestjs-libraries/src/dtos/billing/billing.subscribe.dto.ts b/libraries/nestjs-libraries/src/dtos/billing/billing.subscribe.dto.ts index eed1b49d..7544d722 100644 --- a/libraries/nestjs-libraries/src/dtos/billing/billing.subscribe.dto.ts +++ b/libraries/nestjs-libraries/src/dtos/billing/billing.subscribe.dto.ts @@ -4,6 +4,6 @@ export class BillingSubscribeDto { @IsIn(['MONTHLY', 'YEARLY']) period: 'MONTHLY' | 'YEARLY'; - @IsIn(['STANDARD', 'PRO']) - billing: 'STANDARD' | 'PRO'; + @IsIn(['STANDARD', 'PRO', 'TEAM', 'ULTIMATE']) + billing: 'STANDARD' | 'PRO' | 'TEAM' | 'ULTIMATE'; } diff --git a/libraries/nestjs-libraries/src/services/stripe.service.ts b/libraries/nestjs-libraries/src/services/stripe.service.ts index 6791ea78..c32e4408 100644 --- a/libraries/nestjs-libraries/src/services/stripe.service.ts +++ b/libraries/nestjs-libraries/src/services/stripe.service.ts @@ -155,6 +155,7 @@ export class StripeService { (p) => p?.recurring?.interval?.toLowerCase() === (body.period === 'MONTHLY' ? 'month' : 'year') && + p?.nickname === body.billing + ' ' + body.period && p?.unit_amount === (body.period === 'MONTHLY' ? priceData.month_price @@ -177,10 +178,14 @@ export class StripeService { const proration_date = Math.floor(Date.now() / 1000); - const currentUserSubscription = await stripe.subscriptions.list({ - customer, - status: 'active', - }); + const currentUserSubscription = { + data: ( + await stripe.subscriptions.list({ + customer, + status: 'all', + }) + ).data.filter((f) => f.status === 'active' || f.status === 'trialing'), + }; try { const price = await stripe.invoices.retrieveUpcoming({ @@ -210,10 +215,14 @@ export class StripeService { const id = makeId(10); const org = await this._organizationService.getOrgById(organizationId); const customer = await this.createOrGetCustomer(org!); - const currentUserSubscription = await stripe.subscriptions.list({ - customer, - status: 'active', - }); + const currentUserSubscription = { + data: ( + await stripe.subscriptions.list({ + customer, + status: 'all', + }) + ).data.filter((f) => f.status === 'active' || f.status === 'trialing'), + }; const { cancel_at } = await stripe.subscriptions.update( currentUserSubscription.data[0].id, @@ -261,9 +270,12 @@ export class StripeService { ) { const { url } = await stripe.checkout.sessions.create({ customer, - success_url: process.env['FRONTEND_URL'] + `/billing?check=${uniqueId}`, + success_url: + process.env['FRONTEND_URL'] + + `/launches?onboarding=true&check=${uniqueId}`, mode: 'subscription', subscription_data: { + trial_period_days: 7, metadata: { service: 'gitroom', ...body, @@ -307,7 +319,7 @@ export class StripeService { }, card_payments: { requested: true, - } + }, }, tos_acceptance: { service_agreement: 'full', @@ -444,10 +456,14 @@ export class StripeService { }, })); - const currentUserSubscription = await stripe.subscriptions.list({ - customer, - status: 'active', - }); + const currentUserSubscription = { + data: ( + await stripe.subscriptions.list({ + customer, + status: 'all', + }) + ).data.filter((f) => f.status === 'active' || f.status === 'trialing'), + }; if (!currentUserSubscription.data.length) { return this.createCheckoutSession(id, customer, body, findPrice!.id); @@ -540,7 +556,9 @@ export class StripeService { await this._subscriptionService.createOrUpdateSubscription( makeId(10), organizationId, - getCurrentSubscription?.subscriptionTier === 'PRO' ? (getCurrentSubscription.totalChannels + 5) : findPricing.channel!, + getCurrentSubscription?.subscriptionTier === 'PRO' + ? getCurrentSubscription.totalChannels + 5 + : findPricing.channel!, nextPackage, 'MONTHLY', null, @@ -550,7 +568,6 @@ export class StripeService { return { success: true, }; - } catch (err) { console.log(err); return { From 69c4800693e5089267f5bf6ddf350b02e31d554e Mon Sep 17 00:00:00 2001 From: Nevo David <nevo@notu.co> Date: Sat, 10 Aug 2024 23:30:23 +0700 Subject: [PATCH 05/11] feat: fix pricing --- .../src/database/prisma/subscriptions/pricing.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/libraries/nestjs-libraries/src/database/prisma/subscriptions/pricing.ts b/libraries/nestjs-libraries/src/database/prisma/subscriptions/pricing.ts index 37ea6180..8cd31ecb 100644 --- a/libraries/nestjs-libraries/src/database/prisma/subscriptions/pricing.ts +++ b/libraries/nestjs-libraries/src/database/prisma/subscriptions/pricing.ts @@ -64,7 +64,7 @@ export const pricing: PricingInterface = { year_price: 480, channel: 30, posts_per_month: 1000000, - image_generation_count: 100, + image_generation_count: 300, community_features: true, team_members: true, featured_by_gitroom: true, @@ -78,7 +78,7 @@ export const pricing: PricingInterface = { year_price: 672, channel: 100, posts_per_month: 1000000, - image_generation_count: 100, + image_generation_count: 500, community_features: true, team_members: true, featured_by_gitroom: true, From 31be4350bbe382eaec53daae8040ca0dbffc0414 Mon Sep 17 00:00:00 2001 From: Nevo David <nevo@notu.co> Date: Sat, 10 Aug 2024 23:39:27 +0700 Subject: [PATCH 06/11] feat: change pricing --- .../src/database/prisma/subscriptions/pricing.ts | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/libraries/nestjs-libraries/src/database/prisma/subscriptions/pricing.ts b/libraries/nestjs-libraries/src/database/prisma/subscriptions/pricing.ts index 8cd31ecb..4a5f690a 100644 --- a/libraries/nestjs-libraries/src/database/prisma/subscriptions/pricing.ts +++ b/libraries/nestjs-libraries/src/database/prisma/subscriptions/pricing.ts @@ -32,8 +32,8 @@ export const pricing: PricingInterface = { }, STANDARD: { current: 'STANDARD', - month_price: 30, - year_price: 288, + month_price: 29, + year_price: 278, channel: 5, posts_per_month: 400, image_generation_count: 20, @@ -46,8 +46,8 @@ export const pricing: PricingInterface = { }, TEAM: { current: 'TEAM', - month_price: 40, - year_price: 384, + month_price: 39, + year_price: 374, channel: 10, posts_per_month: 1000000, image_generation_count: 100, @@ -60,8 +60,8 @@ export const pricing: PricingInterface = { }, PRO: { current: 'PRO', - month_price: 50, - year_price: 480, + month_price: 49, + year_price: 470, channel: 30, posts_per_month: 1000000, image_generation_count: 300, @@ -74,8 +74,8 @@ export const pricing: PricingInterface = { }, ULTIMATE: { current: 'ULTIMATE', - month_price: 70, - year_price: 672, + month_price: 69, + year_price: 662, channel: 100, posts_per_month: 1000000, image_generation_count: 500, From eae76e5deb65b2ecb7b265ca14f24c1e0555e183 Mon Sep 17 00:00:00 2001 From: Nevo David <nevo@notu.co> Date: Sun, 11 Aug 2024 12:25:36 +0700 Subject: [PATCH 07/11] feat: main billing --- .../billing/main.billing.component.tsx | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) diff --git a/apps/frontend/src/components/billing/main.billing.component.tsx b/apps/frontend/src/components/billing/main.billing.component.tsx index 78889abc..48ffd033 100644 --- a/apps/frontend/src/components/billing/main.billing.component.tsx +++ b/apps/frontend/src/components/billing/main.billing.component.tsx @@ -107,8 +107,20 @@ export const Features: FC<{ if (currentPricing?.ai) { list.push(`AI auto-complete`); + list.push(`AI copilots`); + list.push(`AI Autocomplete`); } + list.push(`Advanced Picture Editor`); + + if (currentPricing?.image_generator) { + list.push( + `${currentPricing?.image_generation_count} AI Images per month` + ); + } + + list.push(`Marketplace full access`); + return list; }, [pack]); @@ -412,7 +424,9 @@ export const MainBillingComponent: FC<{ )} {subscription?.cancelAt && isGeneral() && ( <div className="text-center"> - Your subscription will be cancel at {dayjs(subscription.cancelAt).local().format('D MMM, YYYY')}<br /> + Your subscription will be cancel at{' '} + {dayjs(subscription.cancelAt).local().format('D MMM, YYYY')} + <br /> You will never be charged again </div> )} From 789dce6d97c76da8fd688e476ea7a361883d8339 Mon Sep 17 00:00:00 2001 From: Nevo David <nevo@notu.co> Date: Sun, 11 Aug 2024 12:37:31 +0700 Subject: [PATCH 08/11] feat: return url --- libraries/nestjs-libraries/src/services/stripe.service.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/libraries/nestjs-libraries/src/services/stripe.service.ts b/libraries/nestjs-libraries/src/services/stripe.service.ts index c32e4408..61aa6d65 100644 --- a/libraries/nestjs-libraries/src/services/stripe.service.ts +++ b/libraries/nestjs-libraries/src/services/stripe.service.ts @@ -270,6 +270,7 @@ export class StripeService { ) { const { url } = await stripe.checkout.sessions.create({ customer, + return_url: process.env['FRONTEND_URL'] + `/billing`, success_url: process.env['FRONTEND_URL'] + `/launches?onboarding=true&check=${uniqueId}`, From 5aa1b9fcd03e29aef0f3d028adfe27b20cc17185 Mon Sep 17 00:00:00 2001 From: Nevo David <nevo@notu.co> Date: Sun, 11 Aug 2024 13:04:40 +0700 Subject: [PATCH 09/11] feat: changes --- .../src/components/billing/faq.component.tsx | 12 +++- .../src/components/layout/layout.settings.tsx | 60 +++++++++++++++++-- .../database/prisma/subscriptions/pricing.ts | 4 +- 3 files changed, 68 insertions(+), 8 deletions(-) diff --git a/apps/frontend/src/components/billing/faq.component.tsx b/apps/frontend/src/components/billing/faq.component.tsx index dc82aa97..34c4026f 100644 --- a/apps/frontend/src/components/billing/faq.component.tsx +++ b/apps/frontend/src/components/billing/faq.component.tsx @@ -4,9 +4,15 @@ import interClass from '@gitroom/react/helpers/inter.font'; import { isGeneral } from '@gitroom/react/helpers/is.general'; const list = [ + { + title: `Can I trust ${isGeneral() ? 'Postiz' : 'Gitroom'}?`, + description: `${isGeneral() ? 'Postiz' : 'Gitroom'} is proudly open-source! We believe in an ethical and transparent culture, meaning Postiz will live forever. You can check the entire code / or use it for your personal use. You can check the open-source repository click here.`, + }, { title: 'What are channels?', - description: `${isGeneral() ? 'Postiz' : 'Gitroom'} allows you to schedule your posts between different channels. + description: `${ + isGeneral() ? 'Postiz' : 'Gitroom' + } allows you to schedule your posts between different channels. A channel is a publishing platform where you can schedule your posts. For example, you can schedule your posts on Twitter, Linkedin, DEV and Hashnode`, }, @@ -35,7 +41,9 @@ export const FAQSection: FC<{ title: string; description: string }> = ( className="bg-sixth p-[24px] border border-tableBorder rounded-[4px] flex flex-col" onClick={changeShow} > - <div className={`text-[20px] ${interClass} cursor-pointer flex justify-center`}> + <div + className={`text-[20px] ${interClass} cursor-pointer flex justify-center`} + > <div className="flex-1">{title}</div> <div className="flex items-center justify-center w-[32px]"> {!show ? ( diff --git a/apps/frontend/src/components/layout/layout.settings.tsx b/apps/frontend/src/components/layout/layout.settings.tsx index 582d2b15..7c954ed9 100644 --- a/apps/frontend/src/components/layout/layout.settings.tsx +++ b/apps/frontend/src/components/layout/layout.settings.tsx @@ -112,12 +112,64 @@ export const LayoutSettings = ({ children }: { children: ReactNode }) => { <> <div className="text-center mb-[20px] text-xl"> <h1 className="text-3xl"> - PLEASE SELECT A PLAN TO CONTINUE + Join 1000+ Entrepreneurs Who Use Postiz + <br /> + To Manage All Your Social Media Channels </h1> <br /> - You will not be charged today. - <br /> - You can cancel anytime. + <div className="table mx-auto"> + <div className="flex gap-[5px] items-center"> + <div> + <svg + xmlns="http://www.w3.org/2000/svg" + width="24" + height="24" + viewBox="0 0 24 24" + fill="none" + > + <path + d="M16.2806 9.21937C16.3504 9.28903 16.4057 9.37175 16.4434 9.46279C16.4812 9.55384 16.5006 9.65144 16.5006 9.75C16.5006 9.84856 16.4812 9.94616 16.4434 10.0372C16.4057 10.1283 16.3504 10.211 16.2806 10.2806L11.0306 15.5306C10.961 15.6004 10.8783 15.6557 10.7872 15.6934C10.6962 15.7312 10.5986 15.7506 10.5 15.7506C10.4014 15.7506 10.3038 15.7312 10.2128 15.6934C10.1218 15.6557 10.039 15.6004 9.96938 15.5306L7.71938 13.2806C7.57865 13.1399 7.49959 12.949 7.49959 12.75C7.49959 12.551 7.57865 12.3601 7.71938 12.2194C7.86011 12.0786 8.05098 11.9996 8.25 11.9996C8.44903 11.9996 8.6399 12.0786 8.78063 12.2194L10.5 13.9397L15.2194 9.21937C15.289 9.14964 15.3718 9.09432 15.4628 9.05658C15.5538 9.01884 15.6514 8.99941 15.75 8.99941C15.8486 8.99941 15.9462 9.01884 16.0372 9.05658C16.1283 9.09432 16.211 9.14964 16.2806 9.21937ZM21.75 12C21.75 13.9284 21.1782 15.8134 20.1068 17.4168C19.0355 19.0202 17.5127 20.2699 15.7312 21.0078C13.9496 21.7458 11.9892 21.9389 10.0979 21.5627C8.20656 21.1865 6.46928 20.2579 5.10571 18.8943C3.74215 17.5307 2.81355 15.7934 2.43735 13.9021C2.06114 12.0108 2.25422 10.0504 2.99218 8.26884C3.73013 6.48726 4.97982 4.96451 6.58319 3.89317C8.18657 2.82183 10.0716 2.25 12 2.25C14.585 2.25273 17.0634 3.28084 18.8913 5.10872C20.7192 6.93661 21.7473 9.41498 21.75 12ZM20.25 12C20.25 10.3683 19.7661 8.77325 18.8596 7.41655C17.9531 6.05984 16.6646 5.00242 15.1571 4.37799C13.6497 3.75357 11.9909 3.59019 10.3905 3.90852C8.79017 4.22685 7.32016 5.01259 6.16637 6.16637C5.01259 7.32015 4.22685 8.79016 3.90853 10.3905C3.5902 11.9908 3.75358 13.6496 4.378 15.1571C5.00242 16.6646 6.05984 17.9531 7.41655 18.8596C8.77326 19.7661 10.3683 20.25 12 20.25C14.1873 20.2475 16.2843 19.3775 17.8309 17.8309C19.3775 16.2843 20.2475 14.1873 20.25 12Z" + fill="#06ff00" + /> + </svg> + </div> + <div>100% no-risk trial</div> + </div> + <div className="flex gap-[5px] items-center"> + <div> + <svg + xmlns="http://www.w3.org/2000/svg" + width="24" + height="24" + viewBox="0 0 24 24" + fill="none" + > + <path + d="M16.2806 9.21937C16.3504 9.28903 16.4057 9.37175 16.4434 9.46279C16.4812 9.55384 16.5006 9.65144 16.5006 9.75C16.5006 9.84856 16.4812 9.94616 16.4434 10.0372C16.4057 10.1283 16.3504 10.211 16.2806 10.2806L11.0306 15.5306C10.961 15.6004 10.8783 15.6557 10.7872 15.6934C10.6962 15.7312 10.5986 15.7506 10.5 15.7506C10.4014 15.7506 10.3038 15.7312 10.2128 15.6934C10.1218 15.6557 10.039 15.6004 9.96938 15.5306L7.71938 13.2806C7.57865 13.1399 7.49959 12.949 7.49959 12.75C7.49959 12.551 7.57865 12.3601 7.71938 12.2194C7.86011 12.0786 8.05098 11.9996 8.25 11.9996C8.44903 11.9996 8.6399 12.0786 8.78063 12.2194L10.5 13.9397L15.2194 9.21937C15.289 9.14964 15.3718 9.09432 15.4628 9.05658C15.5538 9.01884 15.6514 8.99941 15.75 8.99941C15.8486 8.99941 15.9462 9.01884 16.0372 9.05658C16.1283 9.09432 16.211 9.14964 16.2806 9.21937ZM21.75 12C21.75 13.9284 21.1782 15.8134 20.1068 17.4168C19.0355 19.0202 17.5127 20.2699 15.7312 21.0078C13.9496 21.7458 11.9892 21.9389 10.0979 21.5627C8.20656 21.1865 6.46928 20.2579 5.10571 18.8943C3.74215 17.5307 2.81355 15.7934 2.43735 13.9021C2.06114 12.0108 2.25422 10.0504 2.99218 8.26884C3.73013 6.48726 4.97982 4.96451 6.58319 3.89317C8.18657 2.82183 10.0716 2.25 12 2.25C14.585 2.25273 17.0634 3.28084 18.8913 5.10872C20.7192 6.93661 21.7473 9.41498 21.75 12ZM20.25 12C20.25 10.3683 19.7661 8.77325 18.8596 7.41655C17.9531 6.05984 16.6646 5.00242 15.1571 4.37799C13.6497 3.75357 11.9909 3.59019 10.3905 3.90852C8.79017 4.22685 7.32016 5.01259 6.16637 6.16637C5.01259 7.32015 4.22685 8.79016 3.90853 10.3905C3.5902 11.9908 3.75358 13.6496 4.378 15.1571C5.00242 16.6646 6.05984 17.9531 7.41655 18.8596C8.77326 19.7661 10.3683 20.25 12 20.25C14.1873 20.2475 16.2843 19.3775 17.8309 17.8309C19.3775 16.2843 20.2475 14.1873 20.25 12Z" + fill="#06ff00" + /> + </svg> + </div> + <div>Pay nothing for the first 7 days</div> + </div> + <div className="flex gap-[5px] items-center"> + <div> + <svg + xmlns="http://www.w3.org/2000/svg" + width="24" + height="24" + viewBox="0 0 24 24" + fill="none" + > + <path + d="M16.2806 9.21937C16.3504 9.28903 16.4057 9.37175 16.4434 9.46279C16.4812 9.55384 16.5006 9.65144 16.5006 9.75C16.5006 9.84856 16.4812 9.94616 16.4434 10.0372C16.4057 10.1283 16.3504 10.211 16.2806 10.2806L11.0306 15.5306C10.961 15.6004 10.8783 15.6557 10.7872 15.6934C10.6962 15.7312 10.5986 15.7506 10.5 15.7506C10.4014 15.7506 10.3038 15.7312 10.2128 15.6934C10.1218 15.6557 10.039 15.6004 9.96938 15.5306L7.71938 13.2806C7.57865 13.1399 7.49959 12.949 7.49959 12.75C7.49959 12.551 7.57865 12.3601 7.71938 12.2194C7.86011 12.0786 8.05098 11.9996 8.25 11.9996C8.44903 11.9996 8.6399 12.0786 8.78063 12.2194L10.5 13.9397L15.2194 9.21937C15.289 9.14964 15.3718 9.09432 15.4628 9.05658C15.5538 9.01884 15.6514 8.99941 15.75 8.99941C15.8486 8.99941 15.9462 9.01884 16.0372 9.05658C16.1283 9.09432 16.211 9.14964 16.2806 9.21937ZM21.75 12C21.75 13.9284 21.1782 15.8134 20.1068 17.4168C19.0355 19.0202 17.5127 20.2699 15.7312 21.0078C13.9496 21.7458 11.9892 21.9389 10.0979 21.5627C8.20656 21.1865 6.46928 20.2579 5.10571 18.8943C3.74215 17.5307 2.81355 15.7934 2.43735 13.9021C2.06114 12.0108 2.25422 10.0504 2.99218 8.26884C3.73013 6.48726 4.97982 4.96451 6.58319 3.89317C8.18657 2.82183 10.0716 2.25 12 2.25C14.585 2.25273 17.0634 3.28084 18.8913 5.10872C20.7192 6.93661 21.7473 9.41498 21.75 12ZM20.25 12C20.25 10.3683 19.7661 8.77325 18.8596 7.41655C17.9531 6.05984 16.6646 5.00242 15.1571 4.37799C13.6497 3.75357 11.9909 3.59019 10.3905 3.90852C8.79017 4.22685 7.32016 5.01259 6.16637 6.16637C5.01259 7.32015 4.22685 8.79016 3.90853 10.3905C3.5902 11.9908 3.75358 13.6496 4.378 15.1571C5.00242 16.6646 6.05984 17.9531 7.41655 18.8596C8.77326 19.7661 10.3683 20.25 12 20.25C14.1873 20.2475 16.2843 19.3775 17.8309 17.8309C19.3775 16.2843 20.2475 14.1873 20.25 12Z" + fill="#06ff00" + /> + </svg> + </div> + <div>Cancel anytime, hassle-free</div> + </div> + </div> </div> <BillingComponent /> </> diff --git a/libraries/nestjs-libraries/src/database/prisma/subscriptions/pricing.ts b/libraries/nestjs-libraries/src/database/prisma/subscriptions/pricing.ts index 4a5f690a..67dc2f15 100644 --- a/libraries/nestjs-libraries/src/database/prisma/subscriptions/pricing.ts +++ b/libraries/nestjs-libraries/src/database/prisma/subscriptions/pricing.ts @@ -74,8 +74,8 @@ export const pricing: PricingInterface = { }, ULTIMATE: { current: 'ULTIMATE', - month_price: 69, - year_price: 662, + month_price: 99, + year_price: 950, channel: 100, posts_per_month: 1000000, image_generation_count: 500, From 52d93d8a5143d975f2d44233513707dfc1842a6b Mon Sep 17 00:00:00 2001 From: Nevo David <nevo@notu.co> Date: Tue, 13 Aug 2024 11:17:06 +0700 Subject: [PATCH 10/11] feat: add edit modal --- apps/frontend/src/components/launches/add.edit.model.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/frontend/src/components/launches/add.edit.model.tsx b/apps/frontend/src/components/launches/add.edit.model.tsx index 328720c7..1e41e7c9 100644 --- a/apps/frontend/src/components/launches/add.edit.model.tsx +++ b/apps/frontend/src/components/launches/add.edit.model.tsx @@ -381,7 +381,7 @@ export const AddEditModal: FC<{ title: 'AI Content Assistant', }} className="!z-[499]" - instructions="You are an assistant that help the user to schedule their social media posts, do not answers to questions that don't trigger a function call" + instructions="You are an assistant that help the user to schedule their social media posts, everytime somebody write something, try to use a function call, if not prompt the user that the request is invalid and you are here to assists with social media posts" /> )} <div className={clsx('flex gap-[20px] bg-black')}> From cb30d5e324ee0b19f24b8325526b79daf7f7ce72 Mon Sep 17 00:00:00 2001 From: Nevo David <nevo@notu.co> Date: Tue, 13 Aug 2024 12:42:19 +0700 Subject: [PATCH 11/11] feat: add subscription --- .../src/api/routes/billing.controller.ts | 16 +++++- .../src/components/layout/impersonate.tsx | 49 ++++++++++++++++++- .../subscriptions/subscription.repository.ts | 11 +++++ .../subscriptions/subscription.service.ts | 26 ++++++++-- 4 files changed, 97 insertions(+), 5 deletions(-) diff --git a/apps/backend/src/api/routes/billing.controller.ts b/apps/backend/src/api/routes/billing.controller.ts index 32afd36b..988eabc1 100644 --- a/apps/backend/src/api/routes/billing.controller.ts +++ b/apps/backend/src/api/routes/billing.controller.ts @@ -2,9 +2,10 @@ import { Body, Controller, Get, Param, Post } from '@nestjs/common'; import { SubscriptionService } from '@gitroom/nestjs-libraries/database/prisma/subscriptions/subscription.service'; import { StripeService } from '@gitroom/nestjs-libraries/services/stripe.service'; import { GetOrgFromRequest } from '@gitroom/nestjs-libraries/user/org.from.request'; -import { Organization } from '@prisma/client'; +import { Organization, User } from '@prisma/client'; import { BillingSubscribeDto } from '@gitroom/nestjs-libraries/dtos/billing/billing.subscribe.dto'; import { ApiTags } from '@nestjs/swagger'; +import { GetUserFromRequest } from '@gitroom/nestjs-libraries/user/user.from.request'; @ApiTags('Billing') @Controller('/billing') @@ -71,4 +72,17 @@ export class BillingController { ) { return this._stripeService.lifetimeDeal(org.id, body.code); } + + @Post('/add-subscription') + async addSubscription( + @Body() body: { subscription: string }, + @GetUserFromRequest() user: User, + @GetOrgFromRequest() org: Organization + ) { + if (!user.isSuperAdmin) { + throw new Error('Unauthorized'); + } + + await this._subscriptionService.addSubscription(org.id, user.id, body.subscription); + } } diff --git a/apps/frontend/src/components/layout/impersonate.tsx b/apps/frontend/src/components/layout/impersonate.tsx index b8252b78..f13ad7b0 100644 --- a/apps/frontend/src/components/layout/impersonate.tsx +++ b/apps/frontend/src/components/layout/impersonate.tsx @@ -1,9 +1,55 @@ import { Input } from '@gitroom/react/form/input'; -import { useCallback, useMemo, useState } from 'react'; +import { ChangeEventHandler, useCallback, useMemo, useState } from 'react'; import useSWR from 'swr'; import { useFetch } from '@gitroom/helpers/utils/custom.fetch'; import { useUser } from '@gitroom/frontend/components/layout/user.context'; +import { Select } from '@gitroom/react/form/select'; +import { pricing } from '@gitroom/nestjs-libraries/database/prisma/subscriptions/pricing'; +import { deleteDialog } from '@gitroom/react/helpers/delete.dialog'; +export const Subscription = () => { + const fetch = useFetch(); + const addSubscription: ChangeEventHandler<HTMLSelectElement> = useCallback( + async (e) => { + const value = e.target.value; + if ( + await deleteDialog( + 'Are you sure you want to add a user subscription?', + 'Add' + ) + ) { + await fetch('/billing/add-subscription', { + method: 'POST', + body: JSON.stringify({ subscription: value }), + }); + + window.location.reload(); + } + }, + [] + ); + + return ( + <Select + onChange={addSubscription} + hideErrors={true} + disableForm={true} + name="sub" + label="" + value="" + className="text-black" + > + <option>-- ADD FREE SUBSCRIPTION --</option> + {Object.keys(pricing) + .filter((f) => !f.includes('FREE')) + .map((key) => ( + <option key={key} value={key}> + {key} + </option> + ))} + </Select> + ); +}; export const Impersonate = () => { const fetch = useFetch(); const [name, setName] = useState(''); @@ -76,6 +122,7 @@ export const Impersonate = () => { X </div> </div> + {user?.tier?.current === 'FREE' && <Subscription />} </div> ) : ( <Input diff --git a/libraries/nestjs-libraries/src/database/prisma/subscriptions/subscription.repository.ts b/libraries/nestjs-libraries/src/database/prisma/subscriptions/subscription.repository.ts index fb415a8f..04ccb431 100644 --- a/libraries/nestjs-libraries/src/database/prisma/subscriptions/subscription.repository.ts +++ b/libraries/nestjs-libraries/src/database/prisma/subscriptions/subscription.repository.ts @@ -202,4 +202,15 @@ export class SubscriptionRepository { }, }); } + + setCustomerId(orgId: string, customerId: string) { + return this._organization.model.organization.update({ + where: { + id: orgId, + }, + data: { + paymentId: customerId, + }, + }); + } } 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 839268cb..4f11c0f9 100644 --- a/libraries/nestjs-libraries/src/database/prisma/subscriptions/subscription.service.ts +++ b/libraries/nestjs-libraries/src/database/prisma/subscriptions/subscription.service.ts @@ -5,6 +5,7 @@ import { IntegrationService } from '@gitroom/nestjs-libraries/database/prisma/in import { OrganizationService } from '@gitroom/nestjs-libraries/database/prisma/organizations/organization.service'; import { Organization } from '@prisma/client'; import dayjs from 'dayjs'; +import { makeId } from '@gitroom/nestjs-libraries/services/make.is'; @Injectable() export class SubscriptionService { @@ -164,7 +165,7 @@ export class SubscriptionService { const type = organization?.subscription?.subscriptionTier || 'FREE'; if (type === 'FREE') { - return {credits: 0}; + return { credits: 0 }; } // @ts-ignore @@ -176,10 +177,29 @@ export class SubscriptionService { const checkFromMonth = date.subtract(1, 'month'); const imageGenerationCount = pricing[type].image_generation_count; - const totalUse = await this._subscriptionRepository.getCreditsFrom(organization.id, checkFromMonth); + const totalUse = await this._subscriptionRepository.getCreditsFrom( + organization.id, + checkFromMonth + ); return { credits: imageGenerationCount - totalUse, - } + }; } + + async addSubscription(orgId: string, userId: string, subscription: any) { + await this._subscriptionRepository.setCustomerId(orgId, orgId); + return this.createOrUpdateSubscription( + makeId(5), + userId, + pricing[subscription].channel!, + subscription, + 'MONTHLY', + null, + undefined, + orgId + ); + } + + }