feat: responsive

This commit is contained in:
Nevo David 2024-12-16 14:35:29 +07:00
parent 12022a9d85
commit 72523ed2c1
16 changed files with 535 additions and 79 deletions

View File

@ -1,4 +1,4 @@
import { Body, Controller, Get, Param, Post } from '@nestjs/common';
import { Body, Controller, Get, Param, Post, Req } 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';
@ -7,6 +7,7 @@ import { BillingSubscribeDto } from '@gitroom/nestjs-libraries/dtos/billing/bill
import { ApiTags } from '@nestjs/swagger';
import { GetUserFromRequest } from '@gitroom/nestjs-libraries/user/user.from.request';
import { NotificationService } from '@gitroom/nestjs-libraries/database/prisma/notifications/notification.service';
import { Request } from 'express';
@ApiTags('Billing')
@Controller('/billing')
@ -33,9 +34,12 @@ export class BillingController {
@Post('/subscribe')
subscribe(
@GetOrgFromRequest() org: Organization,
@Body() body: BillingSubscribeDto
@GetUserFromRequest() user: User,
@Body() body: BillingSubscribeDto,
@Req() req: Request
) {
return this._stripeService.subscribe(org.id, body);
const uniqueId = req?.cookies?.track;
return this._stripeService.subscribe(uniqueId, org.id, user.id, body);
}
@Get('/portal')

View File

@ -48,21 +48,14 @@ export class PublicController {
body: { fbclid?: string; tt: TrackEnum; additional: Record<string, any> }
) {
const uniqueId = req?.cookies?.track || makeId(10);
console.log(
req?.cookies?.track,
ip,
userAgent,
body.tt,
body.additional,
body.fbclid
);
const fbclid = req?.cookies?.fbclid || body.fbclid;
await this._trackService.track(
req?.cookies?.track,
uniqueId,
ip,
userAgent,
body.tt,
body.additional,
body.fbclid
fbclid
);
if (!req.cookies.track) {
res.cookie('track', uniqueId, {

View File

@ -54,11 +54,13 @@ export class StripeController {
// Maybe it comes from another stripe webhook
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
if (event?.data?.object?.metadata?.service !== 'gitroom') {
if (event?.data?.object?.metadata?.service !== 'gitroom' && event.type !== 'invoice.payment_succeeded') {
return { ok: true };
}
switch (event.type) {
case 'invoice.payment_succeeded':
return this._stripeService.paymentSucceeded(event);
case 'checkout.session.completed':
return this._stripeService.updateOrder(event);
case 'account.updated':

View File

@ -222,10 +222,11 @@ export class UsersController {
@GetUserFromRequest() user: User,
@RealIP() ip: string,
@UserAgent() userAgent: string,
@Body() body: { tt: TrackEnum; additional: Record<string, any> }
@Body() body: { tt: TrackEnum; fbclid: string, additional: Record<string, any> }
) {
const uniqueId = req?.cookies?.track || makeId(10);
await this._trackService.track(req?.cookies?.track, ip, userAgent, body.tt, body.additional, null, user);
const fbclid = req?.cookies?.fbclid || body.fbclid;
await this._trackService.track(uniqueId, ip, userAgent, body.tt, body.additional, fbclid, user);
if (!req.cookies.track) {
res.cookie('track', uniqueId, {
domain: getCookieUrlFromDomain(process.env.FRONTEND_URL!),
@ -236,6 +237,7 @@ export class UsersController {
});
}
console.log('hello');
res.status(200).send();
}
}

324
apps/frontend/public/f.js Normal file

File diff suppressed because one or more lines are too long

View File

@ -14,6 +14,7 @@ import { Fragment } from 'react';
import { PHProvider } from '@gitroom/react/helpers/posthog';
import UtmSaver from '@gitroom/helpers/utils/utm.saver';
import { ToltScript } from '@gitroom/frontend/components/layout/tolt.script';
import { FacebookComponent } from '@gitroom/frontend/components/layout/facebook.component';
const chakra = Chakra_Petch({ weight: '400', subsets: ['latin'] });
@ -25,11 +26,7 @@ export default async function AppLayout({ children }: { children: ReactNode }) {
return (
<html className={interClass}>
<head>
<link
rel="icon"
href="/favicon.ico"
sizes="any"
/>
<link rel="icon" href="/favicon.ico" sizes="any" />
</head>
<body className={clsx(chakra.className, 'text-primary dark')}>
<VariableContextComponent
@ -46,6 +43,7 @@ export default async function AppLayout({ children }: { children: ReactNode }) {
tolt={process.env.NEXT_PUBLIC_TOLT!}
>
<ToltScript />
<FacebookComponent />
<Plausible
domain={!!process.env.IS_GENERAL ? 'postiz.com' : 'gitroom.com'}
>
@ -53,8 +51,10 @@ export default async function AppLayout({ children }: { children: ReactNode }) {
phkey={process.env.NEXT_PUBLIC_POSTHOG_KEY}
host={process.env.NEXT_PUBLIC_POSTHOG_HOST}
>
<UtmSaver />
<LayoutContext>{children}</LayoutContext>
<LayoutContext>
<UtmSaver />
{children}
</LayoutContext>
</PHProvider>
</Plausible>
</VariableContextComponent>

View File

@ -1,6 +1,6 @@
'use client';
import { useForm, SubmitHandler, FormProvider } from 'react-hook-form';
import { FormProvider, SubmitHandler, useForm } from 'react-hook-form';
import { useFetch } from '@gitroom/helpers/utils/custom.fetch';
import Link from 'next/link';
import { Button } from '@gitroom/react/form/button';
@ -16,6 +16,8 @@ import clsx from 'clsx';
import { GoogleProvider } from '@gitroom/frontend/components/auth/providers/google.provider';
import { useFireEvents } from '@gitroom/helpers/utils/use.fire.events';
import { useVariables } from '@gitroom/react/helpers/variable.context';
import { useTrack } from '@gitroom/react/helpers/use.track';
import { TrackEnum } from '@gitroom/nestjs-libraries/user/track.enum';
type Inputs = {
email: string;
@ -83,10 +85,11 @@ export function RegisterAfter({
token: string;
provider: string;
}) {
const {isGeneral} = useVariables();
const { isGeneral } = useVariables();
const [loading, setLoading] = useState(false);
const router = useRouter();
const fireEvents = useFireEvents();
const track = useTrack();
const isAfterProvider = useMemo(() => {
return !!token && !!provider;
@ -112,27 +115,33 @@ export function RegisterAfter({
await fetchData('/auth/register', {
method: 'POST',
body: JSON.stringify({ ...data }),
}).then((response) => {
setLoading(false);
if (response.status === 200) {
fireEvents('register')
if (response.headers.get('activate') === "true") {
router.push('/auth/activate');
} else {
router.push('/auth/login');
}
} else {
form.setError('email', {
message: getHelpfulReasonForRegistrationFailure(response.status),
});
}
}).catch(e => {
form.setError("email", {
message: 'General error: ' + e.toString() + '. Please check your browser console.',
});
})
.then((response) => {
setLoading(false);
if (response.status === 200) {
fireEvents('register');
return track(TrackEnum.CompleteRegistration).then(() => {
if (response.headers.get('activate') === 'true') {
router.push('/auth/activate');
} else {
router.push('/auth/login');
}
});
} else {
form.setError('email', {
message: getHelpfulReasonForRegistrationFailure(response.status),
});
}
})
.catch((e) => {
form.setError('email', {
message:
'General error: ' +
e.toString() +
'. Please check your browser console.',
});
});
};
return (
@ -143,7 +152,8 @@ export function RegisterAfter({
Sign Up
</h1>
</div>
{!isAfterProvider && (!isGeneral ? <GithubProvider /> : <GoogleProvider />)}
{!isAfterProvider &&
(!isGeneral ? <GithubProvider /> : <GoogleProvider />)}
{!isAfterProvider && (
<div className="h-[20px] mb-[24px] mt-[24px] relative">
<div className="absolute w-full h-[1px] bg-fifth top-[50%] -translate-y-[50%]" />
@ -198,7 +208,11 @@ export function RegisterAfter({
</div>
<div className="text-center mt-6">
<div className="w-full flex">
<Button type="submit" className="flex-1 rounded-[4px]" loading={loading}>
<Button
type="submit"
className="flex-1 rounded-[4px]"
loading={loading}
>
Create Account
</Button>
</div>

View File

@ -5,19 +5,9 @@ import useSWR from 'swr';
import { LoadingComponent } from '@gitroom/frontend/components/layout/loading';
import { useFetch } from '@gitroom/helpers/utils/custom.fetch';
import { MainBillingComponent } from './main.billing.component';
import { useSearchParams } from 'next/navigation';
import { useFireEvents } from '@gitroom/helpers/utils/use.fire.events';
export const BillingComponent = () => {
const fetch = useFetch();
const searchParams = useSearchParams();
const fireEvents = useFireEvents();
useEffect(() => {
if (searchParams.get('check')) {
fireEvents('purchase');
}
}, []);
const load = useCallback(async (path: string) => {
return await (await fetch(path)).json();
@ -36,7 +26,5 @@ export const BillingComponent = () => {
return <LoadingComponent />;
}
return (
<MainBillingComponent sub={subscription?.subscription} />
);
return <MainBillingComponent sub={subscription?.subscription} />;
};

View File

@ -24,6 +24,8 @@ import { Textarea } from '@gitroom/react/form/textarea';
import { useFireEvents } from '@gitroom/helpers/utils/use.fire.events';
import { useUtmUrl } from '@gitroom/helpers/utils/utm.saver';
import { useTolt } from '@gitroom/frontend/components/layout/tolt.script';
import { useTrack } from '@gitroom/react/helpers/use.track';
import { TrackEnum } from '@gitroom/nestjs-libraries/user/track.enum';
export interface Tiers {
month: Array<{
@ -223,6 +225,7 @@ export const MainBillingComponent: FC<{
const router = useRouter();
const utm = useUtmUrl();
const tolt = useTolt();
const track = useTrack();
const [subscription, setSubscription] = useState<Subscription | undefined>(
sub
@ -350,12 +353,18 @@ export const MainBillingComponent: FC<{
period: monthlyOrYearly === 'on' ? 'YEARLY' : 'MONTHLY',
utm,
billing,
tolt: tolt()
tolt: tolt(),
}),
})
).json();
if (url) {
await track(TrackEnum.InitiateCheckout, {
value:
pricing[billing][
monthlyOrYearly === 'on' ? 'year_price' : 'month_price'
],
});
window.location.href = url;
return;
}
@ -412,13 +421,13 @@ export const MainBillingComponent: FC<{
<div>YEARLY</div>
</div>
</div>
<div className="flex gap-[16px]">
<div className="flex gap-[16px] [@media(max-width:1024px)]:flex-col [@media(max-width:1024px)]:text-center">
{Object.entries(pricing)
.filter((f) => !isGeneral || f[0] !== 'FREE')
.map(([name, values]) => (
<div
key={name}
className="flex-1 bg-sixth border border-customColor6 rounded-[4px] p-[24px] gap-[16px] flex flex-col"
className="flex-1 bg-sixth border border-customColor6 rounded-[4px] p-[24px] gap-[16px] flex flex-col [@media(max-width:1024px)]:items-center"
>
<div className="text-[18px]">{name}</div>
<div className="text-[38px] flex gap-[2px] items-center">

View File

@ -0,0 +1,24 @@
'use client';
import Script from "next/script";
export const FacebookComponent = () => {
if (!process.env.NEXT_PUBLIC_FACEBOOK_PIXEL) {
return null;
}
return (
<Script strategy="afterInteractive" id="fb-pixel">
{`!function(f,b,e,v,n,t,s)
{if(f.fbq)return;n=f.fbq=function(){n.callMethod?
n.callMethod.apply(n,arguments):n.queue.push(arguments)};
if(!f._fbq)f._fbq=n;n.push=n;n.loaded=!0;n.version='2.0';
n.queue=[];t=b.createElement(e);t.async=!0;
t.src=v;s=b.getElementsByTagName(e)[0];
s.parentNode.insertBefore(t,s)}(window, document,'script',
'/f.js');
fbq('init', '${process.env.NEXT_PUBLIC_FACEBOOK_PIXEL}');
`}
</Script>
);
};

View File

@ -78,9 +78,9 @@ export const LayoutSettings = ({ children }: { children: ReactNode }) => {
{user.tier !== 'FREE' && <Onboarding />}
<Support />
<ContinueProvider />
<div className="min-h-[100vh] w-full max-w-[1440px] mx-auto bg-primary sm:px-6 px-0 text-textColor flex flex-col">
<div className="min-h-[100vh] w-full max-w-[1440px] mx-auto bg-primary px-6 text-textColor flex flex-col">
{user?.admin && <Impersonate />}
<nav className="px-0 md:px-[23px] gap-2 grid grid-rows-[repeat(2,_auto)] grid-cols-2 md:grid-rows-1 md:grid-cols-[repeat(3,_auto)] items-center justify-between z-[200] sticky top-0 bg-primary">
<nav className="flex items-center justify-between">
<Link
href="/"
className="text-2xl flex items-center gap-[10px] text-textColor order-1"
@ -130,7 +130,7 @@ export const LayoutSettings = ({ children }: { children: ReactNode }) => {
(user.tier !== 'FREE' || !isGeneral || !billingEnabled) ? (
<TopMenu />
) : (
<div />
<></>
)}
<div id = "systray-buttons" className="flex items-center justify-self-end gap-[8px] order-2 md:order-3">
<ModeComponent />
@ -140,11 +140,11 @@ export const LayoutSettings = ({ children }: { children: ReactNode }) => {
</div>
</nav>
<div className="flex-1 flex">
<div className="flex-1 rounded-3xl px-0 md:px-[23px] py-[17px] flex flex-col">
<div className="flex-1 rounded-3xl px-0 py-[17px] flex flex-col">
{user.tier === 'FREE' && isGeneral && billingEnabled ? (
<>
<div className="text-center mb-[20px] text-xl">
<h1 className="text-3xl">
<div className="text-center mb-[20px] text-xl [@media(max-width:1024px)]:text-xl">
<h1 className="text-3xl [@media(max-width:1024px)]:text-xl">
Join 1000+ Entrepreneurs Who Use Postiz
<br />
To Manage All Your Social Media Channels

View File

@ -3,10 +3,23 @@
import { FC, useCallback, useEffect } from 'react';
import { useSearchParams } from 'next/navigation';
import { useLocalStorage } from '@mantine/hooks';
import { TrackEnum } from '@gitroom/nestjs-libraries/user/track.enum';
import { useFireEvents } from '@gitroom/helpers/utils/use.fire.events';
import { useTrack } from '@gitroom/react/helpers/use.track';
const UtmSaver: FC = () => {
const query = useSearchParams();
const [value, setValue] = useLocalStorage({ key: 'utm', defaultValue: '' });
const searchParams = useSearchParams();
const fireEvents = useFireEvents();
const track = useTrack();
useEffect(() => {
if (searchParams.get('check')) {
fireEvents('purchase');
track(TrackEnum.StartTrial);
}
}, []);
useEffect(() => {
const landingUrl = localStorage.getItem('landingUrl');

View File

@ -29,6 +29,7 @@ import { ExtractContentService } from '@gitroom/nestjs-libraries/openai/extract.
import { OpenaiService } from '@gitroom/nestjs-libraries/openai/openai.service';
import { AgenciesService } from '@gitroom/nestjs-libraries/database/prisma/agencies/agencies.service';
import { AgenciesRepository } from '@gitroom/nestjs-libraries/database/prisma/agencies/agencies.repository';
import { TrackService } from '@gitroom/nestjs-libraries/track/track.service';
@Global()
@Module({
@ -66,6 +67,7 @@ import { AgenciesRepository } from '@gitroom/nestjs-libraries/database/prisma/ag
ExtractContentService,
OpenaiService,
EmailService,
TrackService,
],
get exports() {
return this.providers;

View File

@ -9,6 +9,9 @@ import { capitalize, groupBy } from 'lodash';
import { MessagesService } from '@gitroom/nestjs-libraries/database/prisma/marketplace/messages.service';
import { pricing } from '@gitroom/nestjs-libraries/database/prisma/subscriptions/pricing';
import { AuthService } from '@gitroom/helpers/auth/auth.service';
import { TrackService } from '@gitroom/nestjs-libraries/track/track.service';
import { UsersService } from '@gitroom/nestjs-libraries/database/prisma/users/users.service';
import { TrackEnum } from '@gitroom/nestjs-libraries/user/track.enum';
const stripe = new Stripe(process.env.STRIPE_SECRET_KEY!, {
apiVersion: '2024-04-10',
@ -19,7 +22,9 @@ export class StripeService {
constructor(
private _subscriptionService: SubscriptionService,
private _organizationService: OrganizationService,
private _messagesService: MessagesService
private _userService: UsersService,
private _messagesService: MessagesService,
private _trackService: TrackService
) {}
validateRequest(rawBody: Buffer, signature: string, endpointSecret: string) {
return stripe.webhooks.constructEvent(rawBody, signature, endpointSecret);
@ -265,10 +270,12 @@ export class StripeService {
}
private async createCheckoutSession(
ud: string,
uniqueId: string,
customer: string,
body: BillingSubscribeDto,
price: string
price: string,
userId: string
) {
const isUtm = body.utm ? `&utm_source=${body.utm}` : '';
const { url } = await stripe.checkout.sessions.create({
@ -283,14 +290,18 @@ export class StripeService {
metadata: {
service: 'gitroom',
...body,
userId,
uniqueId,
ud,
},
},
...body.tolt ? {
metadata: {
tolt_referral: body.tolt,
}
} : {},
...(body.tolt
? {
metadata: {
tolt_referral: body.tolt,
},
}
: {}),
allow_promotion_codes: true,
line_items: [
{
@ -416,7 +427,12 @@ export class StripeService {
return { url };
}
async subscribe(organizationId: string, body: BillingSubscribeDto) {
async subscribe(
uniqueId: string,
organizationId: string,
userId: string,
body: BillingSubscribeDto
) {
const id = makeId(10);
const priceData = pricing[body.billing];
const org = await this._organizationService.getOrgById(organizationId);
@ -475,7 +491,14 @@ export class StripeService {
};
if (!currentUserSubscription.data.length) {
return this.createCheckoutSession(id, customer, body, findPrice!.id);
return this.createCheckoutSession(
uniqueId,
id,
customer,
body,
findPrice!.id,
userId
);
}
try {
@ -484,7 +507,9 @@ export class StripeService {
metadata: {
service: 'gitroom',
...body,
userId,
id,
ud: uniqueId,
},
proration_behavior: 'always_invoice',
items: [
@ -505,6 +530,23 @@ export class StripeService {
}
}
async paymentSucceeded(event: Stripe.InvoicePaymentSucceededEvent) {
// get subscription from payment
const subscription = await stripe.subscriptions.retrieve(
event.data.object.subscription as string
);
const { userId, ud } = subscription.metadata;
const user = await this._userService.getUserById(userId);
if (user && user.ip && user.agent) {
this._trackService.track(ud, user.ip, user.agent, TrackEnum.Purchase, {
value: event.data.object.amount_paid / 100,
});
}
return { ok: true };
}
async updateOrder(event: Stripe.CheckoutSessionCompletedEvent) {
if (event?.data?.object?.metadata?.type !== 'marketplace') {
return { ok: true };

View File

@ -45,7 +45,7 @@ export class TrackService {
}
if (user && user.email) {
userData.setEmails([this.hashValue(user.email)]);
userData.setEmail(this.hashValue(user.email));
}
let customData = null;

View File

@ -0,0 +1,39 @@
import { TrackEnum } from '@gitroom/nestjs-libraries/user/track.enum';
import { useUser } from '@gitroom/frontend/components/layout/user.context';
import { useFetch } from '@gitroom/helpers/utils/custom.fetch';
import { useCallback } from 'react';
export const useTrack = () => {
const user = useUser();
const fetch = useFetch();
return useCallback(
async (track: TrackEnum, additional?: Record<string, any>) => {
if (!process.env.NEXT_PUBLIC_FACEBOOK_PIXEL) {
return;
}
try {
if (window.fbq) {
// @ts-ignore
window.fbq('track', TrackEnum[track], additional);
}
await fetch(user ? `/user/t` : `/public/t`, {
method: 'POST',
credentials: 'include',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
tt: track,
...(additional ? { additional } : {}),
}),
});
} catch (e) {
console.log(e);
}
},
[user]
);
};