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 {