feat: from free to trial

This commit is contained in:
Nevo David 2024-08-10 23:18:30 +07:00
parent e721a79132
commit 1bc0e0ad1f
8 changed files with 196 additions and 97 deletions

View File

@ -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 />

View File

@ -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>

View File

@ -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>

View File

@ -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;
};

View File

@ -417,6 +417,8 @@ enum State {
enum SubscriptionTier {
STANDARD
PRO
TEAM
ULTIMATE
}
enum Period {

View File

@ -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,

View File

@ -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';
}

View File

@ -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 {