diff --git a/apps/backend/src/api/routes/billing.controller.ts b/apps/backend/src/api/routes/billing.controller.ts index 5added0d..039513fb 100644 --- a/apps/backend/src/api/routes/billing.controller.ts +++ b/apps/backend/src/api/routes/billing.controller.ts @@ -30,6 +30,23 @@ export class BillingController { }; } + @Post('/finish-trial') + async finishTrial(@GetOrgFromRequest() org: Organization) { + try { + await this._stripeService.finishTrial(org.paymentId); + } catch (err) {} + return { + finish: true, + }; + } + + @Get('/is-trial-finished') + async isTrialFinished(@GetOrgFromRequest() org: Organization) { + return { + finished: !org.isTrailing, + }; + } + @Post('/subscribe') subscribe( @GetOrgFromRequest() org: Organization, diff --git a/apps/backend/src/api/routes/media.controller.ts b/apps/backend/src/api/routes/media.controller.ts index 480464ab..bb3a5c2f 100644 --- a/apps/backend/src/api/routes/media.controller.ts +++ b/apps/backend/src/api/routes/media.controller.ts @@ -180,6 +180,14 @@ export class MediaController { return this._mediaService.videoFunction(identifier, functionName, body); } + @Get('/generate-video/:type/allowed') + generateVideoAllowed( + @GetOrgFromRequest() org: Organization, + @Param('type') type: string + ) { + return this._mediaService.generateVideoAllowed(org, type); + } + @Post('/generate-video/:type') generateVideo( @GetOrgFromRequest() org: Organization, diff --git a/apps/frontend/src/components/billing/finish.trial.tsx b/apps/frontend/src/components/billing/finish.trial.tsx new file mode 100644 index 00000000..73ae1b74 --- /dev/null +++ b/apps/frontend/src/components/billing/finish.trial.tsx @@ -0,0 +1,84 @@ +import React, { FC, useCallback, useEffect, useState } from 'react'; +import { TopTitle } from '@gitroom/frontend/components/launches/helpers/top.title.component'; +import { LoadingComponent } from '@gitroom/frontend/components/layout/loading'; +import { useFetch } from '@gitroom/helpers/utils/custom.fetch'; +import { timer } from '@gitroom/helpers/utils/timer'; +import { Button } from '@gitroom/react/form/button'; + +export const FinishTrial: FC<{ close: () => void }> = (props) => { + const [finished, setFinished] = useState(false); + const fetch = useFetch(); + + const finishSubscription = useCallback(async () => { + await fetch('/billing/finish-trial', { + method: 'POST', + }); + checkFinished(); + }, []); + + const checkFinished = useCallback(async () => { + const {finished} = await (await fetch('/billing/is-trial-finished')).json(); + if (!finished) { + await timer(2000); + return checkFinished(); + } + + setFinished(true); + }, []); + + useEffect(() => { + finishSubscription(); + }, []); + + return ( +
+
+
+
+
+ +
+ +
+
+
+
+ {!finished && } + {finished && ( +
+
+ You trial has been successfully finished and you have been charged. +
+
+ + +
+
+ )} +
+
+
+
+
+
+ ); +}; diff --git a/apps/frontend/src/components/billing/main.billing.component.tsx b/apps/frontend/src/components/billing/main.billing.component.tsx index 3cca7aaf..6077fdbb 100644 --- a/apps/frontend/src/components/billing/main.billing.component.tsx +++ b/apps/frontend/src/components/billing/main.billing.component.tsx @@ -16,7 +16,7 @@ import { FAQComponent } from '@gitroom/frontend/components/billing/faq.component 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 { useParams, useRouter, useSearchParams } from 'next/navigation'; import { useVariables } from '@gitroom/react/helpers/variable.context'; import { useModals } from '@mantine/modals'; import { TopTitle } from '@gitroom/frontend/components/launches/helpers/top.title.component'; @@ -28,6 +28,7 @@ import { useTrack } from '@gitroom/react/helpers/use.track'; import { TrackEnum } from '@gitroom/nestjs-libraries/user/track.enum'; import { PurchaseCrypto } from '@gitroom/frontend/components/billing/purchase.crypto'; import { useT } from '@gitroom/react/translation/get.transation.service.client'; +import { FinishTrial } from '@gitroom/frontend/components/billing/finish.trial'; export interface Tiers { month: Array<{ name: 'Pro' | 'Standard'; @@ -224,6 +225,8 @@ export const MainBillingComponent: FC<{ const tolt = useTolt(); const track = useTrack(); const t = useT(); + const queryParams = useSearchParams(); + const [finishTrial, setFinishTrial] = useState(!!queryParams.get('finishTrial')); const [subscription, setSubscription] = useState( sub @@ -399,6 +402,10 @@ export const MainBillingComponent: FC<{
{t('yearly', 'YEARLY')}
+ + {finishTrial && ( + setFinishTrial(false)} /> + )}
{Object.entries(pricing) .filter((f) => !isGeneral || f[0] !== 'FREE') diff --git a/apps/frontend/src/components/launches/ai.video.tsx b/apps/frontend/src/components/launches/ai.video.tsx index b2e757b8..de75b4a5 100644 --- a/apps/frontend/src/components/launches/ai.video.tsx +++ b/apps/frontend/src/components/launches/ai.video.tsx @@ -9,6 +9,9 @@ import useSWR from 'swr'; import { TopTitle } from '@gitroom/frontend/components/launches/helpers/top.title.component'; import { VideoWrapper } from '@gitroom/frontend/components/videos/video.render.component'; import { FormProvider, useForm } from 'react-hook-form'; +import { useUser } from '@gitroom/frontend/components/layout/user.context'; +import { VideoContextWrapper } from '@gitroom/frontend/components/videos/video.context.wrapper'; +import { useToaster } from '@gitroom/react/toaster/toaster'; export const Modal: FC<{ close: () => void; @@ -22,6 +25,7 @@ export const Modal: FC<{ const setLocked = useLaunchStore((state) => state.setLocked); const form = useForm(); const [position, setPosition] = useState('vertical'); + const toaster = useToaster(); const loadCredits = useCallback(async () => { return ( @@ -34,12 +38,16 @@ export const Modal: FC<{ const { data, mutate } = useSWR('copilot-credits', loadCredits); const generate = useCallback(async () => { + await fetch(`/media/generate-video/${type.identifier}/allowed`); setLoading(true); close(); setLocked(true); - console.log('lock'); const customParams = form.getValues(); + if (!await form.trigger()) { + toaster.show('Please fill all required fields', 'warning'); + return ; + } try { const image = await fetch(`/media/generate-video/${type.identifier}`, { method: 'POST', @@ -50,91 +58,90 @@ export const Modal: FC<{ }), }); - console.log(image); - if (image.status == 200 || image.status == 201) { onChange(await image.json()); } } catch (e) {} - console.log('remove lock'); setLocked(false); setLoading(false); }, [type, value, position]); return ( -
- -
-
-
-
-
- -
- {data?.credits || 0} credits left -
-
-
- -
-
-
-
-
- -
-
- -
+ + + +
+
+
+
+
+ +
+ {data?.credits || 0} credits left +
+
- + +
+
+
+
+
+ +
+
+ +
+
+ +
+
+
+
-
-
-
-
- - + + + ); }; @@ -148,6 +155,7 @@ export const AiVideo: FC<{ const [type, setType] = useState(null); const [modal, setModal] = useState(false); const fetch = useFetch(); + const { isTrailing } = useUser(); const loadVideoList = useCallback(async () => { return (await (await fetch('/media/video-options')).json()).filter( diff --git a/apps/frontend/src/components/layout/layout.context.tsx b/apps/frontend/src/components/layout/layout.context.tsx index ea990517..2a920346 100644 --- a/apps/frontend/src/components/layout/layout.context.tsx +++ b/apps/frontend/src/components/layout/layout.context.tsx @@ -85,6 +85,21 @@ function LayoutContextInner(params: { children: ReactNode }) { } window.location.href = '/'; } + if (response.status === 406) { + if ( + await deleteDialog( + 'You are currently on trial, in order to use the feature you must finish the trial', + 'Finish the trial, charge me now', + 'Trial', + + ) + ) { + window.open('/billing?finishTrial=true', '_blank'); + return false; + } + return false; + } + if (response.status === 402) { if ( await deleteDialog( diff --git a/apps/frontend/src/components/layout/user.context.tsx b/apps/frontend/src/components/layout/user.context.tsx index 54a50db0..9e5ec48d 100644 --- a/apps/frontend/src/components/layout/user.context.tsx +++ b/apps/frontend/src/components/layout/user.context.tsx @@ -17,6 +17,7 @@ export const UserContext = createContext< isLifetime?: boolean; impersonate: boolean; allowTrial: boolean; + isTrailing: boolean; }) >(undefined); export const ContextWrapper: FC<{ diff --git a/apps/frontend/src/components/videos/providers/veo3.provider.tsx b/apps/frontend/src/components/videos/providers/veo3.provider.tsx new file mode 100644 index 00000000..e8d475b9 --- /dev/null +++ b/apps/frontend/src/components/videos/providers/veo3.provider.tsx @@ -0,0 +1,64 @@ +import { videoWrapper } from '@gitroom/frontend/components/videos/video.wrapper'; +import { FC, useState } from 'react'; +import { useFormContext } from 'react-hook-form'; +import { useVideo } from '@gitroom/frontend/components/videos/video.context.wrapper'; +import { Textarea } from '@gitroom/react/form/textarea'; +import { MultiMediaComponent } from '@gitroom/frontend/components/media/media.component'; + +export interface Voice { + id: string; + name: string; + preview_url: string; +} + +const VEO3Settings: FC = () => { + const { register, watch, setValue, formState } = useFormContext(); + const { value } = useVideo(); + const [videoValue, setVideoValue] = useState(value); + + const media = register('media', { + value: [], + }); + + const mediaValue = watch('media'); + + return ( +
+