Merge branch 'main' into feat/agencies
This commit is contained in:
commit
84e22203d9
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -22,7 +22,7 @@ export class CopilotController {
|
|||
req?.body?.variables?.data?.metadata?.requestType ===
|
||||
'TextareaCompletion'
|
||||
? 'gpt-4o-mini'
|
||||
: 'gpt-4o',
|
||||
: 'gpt-4o-2024-08-06',
|
||||
}),
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -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 ? (
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
);
|
||||
};
|
||||
|
|
@ -106,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]);
|
||||
|
||||
|
|
@ -315,81 +328,106 @@ 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 />
|
||||
|
|
|
|||
|
|
@ -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')}>
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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,77 @@ 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">
|
||||
Join 1000+ Entrepreneurs Who Use Postiz
|
||||
<br />
|
||||
To Manage All Your Social Media Channels
|
||||
</h1>
|
||||
<br />
|
||||
<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 />
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<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;
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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) => {
|
||||
|
|
|
|||
|
|
@ -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<unknown>(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) {
|
||||
|
|
@ -52,8 +48,9 @@ export class BullMqServer extends Server implements CustomTransportStrategy {
|
|||
},
|
||||
{
|
||||
...this.options,
|
||||
...handler?.extras
|
||||
},
|
||||
...{ removeOnComplete: { count: 0 }, removeOnFail: { count: 0 } },
|
||||
...handler?.extras,
|
||||
}
|
||||
);
|
||||
this.workers.set(pattern, worker);
|
||||
this.logger.log(`Registered queue "${pattern}"`);
|
||||
|
|
|
|||
|
|
@ -471,6 +471,8 @@ enum State {
|
|||
enum SubscriptionTier {
|
||||
STANDARD
|
||||
PRO
|
||||
TEAM
|
||||
ULTIMATE
|
||||
}
|
||||
|
||||
enum Period {
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
@ -44,11 +44,11 @@ export const pricing: PricingInterface = {
|
|||
import_from_channels: true,
|
||||
image_generator: false,
|
||||
},
|
||||
PRO: {
|
||||
current: 'PRO',
|
||||
month_price: 40,
|
||||
year_price: 384,
|
||||
channel: 8,
|
||||
TEAM: {
|
||||
current: 'TEAM',
|
||||
month_price: 39,
|
||||
year_price: 374,
|
||||
channel: 10,
|
||||
posts_per_month: 1000000,
|
||||
image_generation_count: 100,
|
||||
community_features: true,
|
||||
|
|
@ -58,4 +58,32 @@ export const pricing: PricingInterface = {
|
|||
import_from_channels: true,
|
||||
image_generator: true,
|
||||
},
|
||||
PRO: {
|
||||
current: 'PRO',
|
||||
month_price: 49,
|
||||
year_price: 470,
|
||||
channel: 30,
|
||||
posts_per_month: 1000000,
|
||||
image_generation_count: 300,
|
||||
community_features: true,
|
||||
team_members: true,
|
||||
featured_by_gitroom: true,
|
||||
ai: true,
|
||||
import_from_channels: true,
|
||||
image_generator: true,
|
||||
},
|
||||
ULTIMATE: {
|
||||
current: 'ULTIMATE',
|
||||
month_price: 99,
|
||||
year_price: 950,
|
||||
channel: 100,
|
||||
posts_per_month: 1000000,
|
||||
image_generation_count: 500,
|
||||
community_features: true,
|
||||
team_members: true,
|
||||
featured_by_gitroom: true,
|
||||
ai: true,
|
||||
import_from_channels: true,
|
||||
image_generator: true,
|
||||
},
|
||||
};
|
||||
|
|
|
|||
|
|
@ -202,4 +202,15 @@ export class SubscriptionRepository {
|
|||
},
|
||||
});
|
||||
}
|
||||
|
||||
setCustomerId(orgId: string, customerId: string) {
|
||||
return this._organization.model.organization.update({
|
||||
where: {
|
||||
id: orgId,
|
||||
},
|
||||
data: {
|
||||
paymentId: customerId,
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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,13 @@ export class StripeService {
|
|||
) {
|
||||
const { url } = await stripe.checkout.sessions.create({
|
||||
customer,
|
||||
success_url: process.env['FRONTEND_URL'] + `/billing?check=${uniqueId}`,
|
||||
return_url: process.env['FRONTEND_URL'] + `/billing`,
|
||||
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 +320,7 @@ export class StripeService {
|
|||
},
|
||||
card_payments: {
|
||||
requested: true,
|
||||
}
|
||||
},
|
||||
},
|
||||
tos_acceptance: {
|
||||
service_agreement: 'full',
|
||||
|
|
@ -444,10 +457,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 +557,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 +569,6 @@ export class StripeService {
|
|||
return {
|
||||
success: true,
|
||||
};
|
||||
|
||||
} catch (err) {
|
||||
console.log(err);
|
||||
return {
|
||||
|
|
|
|||
Loading…
Reference in New Issue