feat: from free to trial
This commit is contained in:
parent
e721a79132
commit
1bc0e0ad1f
|
|
@ -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 (
|
||||
<div className="text-[12px] flex pt-[12px]">
|
||||
(Pay Today ${(price < 0 ? 0 : price).toFixed(1)})
|
||||
(Pay Today ${(price < 0 ? 0 : price)?.toFixed(1)})
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
|
@ -315,81 +316,104 @@ export const MainBillingComponent: FC<{
|
|||
</div>
|
||||
</div>
|
||||
<div className="flex gap-[16px]">
|
||||
{Object.entries(pricing).map(([name, values]) => (
|
||||
<div
|
||||
key={name}
|
||||
className="flex-1 bg-sixth border border-[#172034] rounded-[4px] p-[24px] gap-[16px] flex flex-col"
|
||||
>
|
||||
<div className="text-[18px]">{name}</div>
|
||||
<div className="text-[38px] flex gap-[2px] items-center">
|
||||
<div>
|
||||
$
|
||||
{monthlyOrYearly === 'on'
|
||||
? values.year_price
|
||||
: values.month_price}
|
||||
</div>
|
||||
<div className={`text-[14px] ${interClass} text-[#AAA]`}>
|
||||
{monthlyOrYearly === 'on' ? '/year' : '/month'}
|
||||
</div>
|
||||
</div>
|
||||
<div className="text-[14px] flex gap-[10px]">
|
||||
{currentPackage === name.toUpperCase() &&
|
||||
subscription?.cancelAt ? (
|
||||
<div className="gap-[3px] flex flex-col">
|
||||
<div>
|
||||
<Button onClick={moveToCheckout('FREE')} loading={loading}>
|
||||
Reactivate subscription
|
||||
</Button>
|
||||
</div>
|
||||
{Object.entries(pricing)
|
||||
.filter((f) => !isGeneral() || f[0] !== 'FREE')
|
||||
.map(([name, values]) => (
|
||||
<div
|
||||
key={name}
|
||||
className="flex-1 bg-sixth border border-[#172034] rounded-[4px] p-[24px] gap-[16px] flex flex-col"
|
||||
>
|
||||
<div className="text-[18px]">{name}</div>
|
||||
<div className="text-[38px] flex gap-[2px] items-center">
|
||||
<div>
|
||||
$
|
||||
{monthlyOrYearly === 'on'
|
||||
? values.year_price
|
||||
: values.month_price}
|
||||
</div>
|
||||
) : (
|
||||
<Button
|
||||
loading={loading}
|
||||
disabled={
|
||||
(!!subscription?.cancelAt &&
|
||||
name.toUpperCase() === 'FREE') ||
|
||||
currentPackage === name.toUpperCase()
|
||||
}
|
||||
className={clsx(
|
||||
subscription &&
|
||||
name.toUpperCase() === 'FREE' &&
|
||||
'!bg-red-500'
|
||||
)}
|
||||
onClick={moveToCheckout(
|
||||
name.toUpperCase() as 'STANDARD' | 'PRO'
|
||||
)}
|
||||
>
|
||||
{currentPackage === name.toUpperCase()
|
||||
? 'Current Plan'
|
||||
: name.toUpperCase() === 'FREE'
|
||||
? subscription?.cancelAt
|
||||
? `Downgrade on ${dayjs
|
||||
.utc(subscription?.cancelAt)
|
||||
.local()
|
||||
.format('D MMM, YYYY')}`
|
||||
: 'Cancel subscription'
|
||||
: 'Purchase Plan'}
|
||||
</Button>
|
||||
)}
|
||||
{subscription &&
|
||||
currentPackage !== name.toUpperCase() &&
|
||||
name !== 'FREE' &&
|
||||
!!name && (
|
||||
<Prorate
|
||||
period={monthlyOrYearly === 'on' ? 'YEARLY' : 'MONTHLY'}
|
||||
pack={name.toUpperCase() as 'STANDARD' | 'PRO'}
|
||||
/>
|
||||
<div className={`text-[14px] ${interClass} text-[#AAA]`}>
|
||||
{monthlyOrYearly === 'on' ? '/year' : '/month'}
|
||||
</div>
|
||||
</div>
|
||||
<div className="text-[14px] flex gap-[10px]">
|
||||
{currentPackage === name.toUpperCase() &&
|
||||
subscription?.cancelAt ? (
|
||||
<div className="gap-[3px] flex flex-col">
|
||||
<div>
|
||||
<Button
|
||||
onClick={moveToCheckout('FREE')}
|
||||
loading={loading}
|
||||
>
|
||||
Reactivate subscription
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<Button
|
||||
loading={loading}
|
||||
disabled={
|
||||
(!!subscription?.cancelAt &&
|
||||
name.toUpperCase() === 'FREE') ||
|
||||
currentPackage === name.toUpperCase()
|
||||
}
|
||||
className={clsx(
|
||||
subscription &&
|
||||
name.toUpperCase() === 'FREE' &&
|
||||
'!bg-red-500'
|
||||
)}
|
||||
onClick={moveToCheckout(
|
||||
name.toUpperCase() as 'STANDARD' | 'PRO'
|
||||
)}
|
||||
>
|
||||
{currentPackage === name.toUpperCase()
|
||||
? 'Current Plan'
|
||||
: name.toUpperCase() === 'FREE'
|
||||
? subscription?.cancelAt
|
||||
? `Downgrade on ${dayjs
|
||||
.utc(subscription?.cancelAt)
|
||||
.local()
|
||||
.format('D MMM, YYYY')}`
|
||||
: 'Cancel subscription'
|
||||
: // @ts-ignore
|
||||
user?.tier === 'FREE' || user?.tier?.current === 'FREE'
|
||||
? 'Start 7 days free trial'
|
||||
: 'Purchase'}
|
||||
</Button>
|
||||
)}
|
||||
{subscription &&
|
||||
currentPackage !== name.toUpperCase() &&
|
||||
name !== 'FREE' &&
|
||||
!!name && (
|
||||
<Prorate
|
||||
period={monthlyOrYearly === 'on' ? 'YEARLY' : 'MONTHLY'}
|
||||
pack={name.toUpperCase() as 'STANDARD' | 'PRO'}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
<Features
|
||||
pack={name.toUpperCase() as 'FREE' | 'STANDARD' | 'PRO'}
|
||||
/>
|
||||
</div>
|
||||
<Features
|
||||
pack={name.toUpperCase() as 'FREE' | 'STANDARD' | 'PRO'}
|
||||
/>
|
||||
</div>
|
||||
))}
|
||||
))}
|
||||
</div>
|
||||
{!!subscription?.id && (
|
||||
<div className="flex justify-center mt-[20px]">
|
||||
<div className="flex justify-center mt-[20px] gap-[10px]">
|
||||
<Button onClick={updatePayment}>Update Payment Method</Button>
|
||||
{isGeneral() && !subscription?.cancelAt && (
|
||||
<Button
|
||||
className="bg-red-500"
|
||||
loading={loading}
|
||||
onClick={moveToCheckout('FREE')}
|
||||
>
|
||||
Cancel subscription
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
{subscription?.cancelAt && isGeneral() && (
|
||||
<div className="text-center">
|
||||
Your subscription will be cancel at {dayjs(subscription.cancelAt).local().format('D MMM, YYYY')}<br />
|
||||
You will never be charged again
|
||||
</div>
|
||||
)}
|
||||
<FAQComponent />
|
||||
|
|
|
|||
|
|
@ -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 (
|
||||
<ContextWrapper user={user}>
|
||||
<CopilotKit
|
||||
|
|
@ -62,7 +65,7 @@ export const LayoutSettings = ({ children }: { children: ReactNode }) => {
|
|||
<ShowLinkedinCompany />
|
||||
<Toaster />
|
||||
<ShowPostSelector />
|
||||
<Onboarding />
|
||||
{(user.tier !== 'FREE' || !isGeneral()) && <Onboarding />}
|
||||
<Support />
|
||||
<ContinueProvider />
|
||||
<div className="min-h-[100vh] w-full max-w-[1440px] mx-auto bg-primary px-[12px] text-white flex flex-col">
|
||||
|
|
@ -92,7 +95,11 @@ export const LayoutSettings = ({ children }: { children: ReactNode }) => {
|
|||
)}
|
||||
</div>
|
||||
</Link>
|
||||
{user?.orgId ? <TopMenu /> : <div />}
|
||||
{user?.orgId && (user.tier !== 'FREE' || !isGeneral()) ? (
|
||||
<TopMenu />
|
||||
) : (
|
||||
<div />
|
||||
)}
|
||||
<div className="flex items-center gap-[8px]">
|
||||
<SettingsComponent />
|
||||
<NotificationComponent />
|
||||
|
|
@ -101,8 +108,25 @@ export const LayoutSettings = ({ children }: { children: ReactNode }) => {
|
|||
</div>
|
||||
<div className="flex-1 flex">
|
||||
<div className="flex-1 rounded-3xl px-[23px] py-[17px] flex flex-col">
|
||||
<Title />
|
||||
<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>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
};
|
||||
|
|
|
|||
|
|
@ -417,6 +417,8 @@ enum State {
|
|||
enum SubscriptionTier {
|
||||
STANDARD
|
||||
PRO
|
||||
TEAM
|
||||
ULTIMATE
|
||||
}
|
||||
|
||||
enum Period {
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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';
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
Loading…
Reference in New Issue