diff --git a/packages/app/control/src/app/(app)/app/[id]/(overview)/_components/header/buttons/index.tsx b/packages/app/control/src/app/(app)/app/[id]/(overview)/_components/header/buttons/index.tsx index 7969e5491..e219e1d36 100644 --- a/packages/app/control/src/app/(app)/app/[id]/(overview)/_components/header/buttons/index.tsx +++ b/packages/app/control/src/app/(app)/app/[id]/(overview)/_components/header/buttons/index.tsx @@ -12,7 +12,11 @@ interface Props { } export const HeaderButtons: React.FC = ({ appId }) => { - const [isOwner] = api.apps.app.isOwner.useSuspenseQuery(appId); + const { data: isOwner, isLoading } = api.apps.app.isOwner.useQuery(appId); + + if (isLoading || isOwner === undefined) { + return ; + } return (
diff --git a/packages/app/control/src/app/(app)/app/[id]/(overview)/_components/referral-handler.tsx b/packages/app/control/src/app/(app)/app/[id]/(overview)/_components/referral-handler.tsx new file mode 100644 index 000000000..8be5b4fca --- /dev/null +++ b/packages/app/control/src/app/(app)/app/[id]/(overview)/_components/referral-handler.tsx @@ -0,0 +1,37 @@ +'use client'; + +import { useEffect, useState } from 'react'; +import { useSearchParams } from 'next/navigation'; +import { api } from '@/trpc/client'; + +interface Props { + appId: string; +} + +export const ReferralHandler: React.FC = ({ appId }) => { + const searchParams = useSearchParams(); + const referralCode = searchParams.get('referral_code'); + const [processed, setProcessed] = useState(false); + + const { mutateAsync: registerReferral } = + api.apps.app.registerReferral.useMutation(); + + useEffect(() => { + if (!referralCode || processed) return; + + const processReferralCode = async () => { + await registerReferral({ + appId, + code: referralCode, + }).catch(() => { + // Silently fail - referral code may be invalid, expired, or user may already have a referrer + }); + + setProcessed(true); + }; + + void processReferralCode(); + }, [referralCode, appId, registerReferral, processed]); + + return null; +}; diff --git a/packages/app/control/src/app/(app)/app/[id]/(overview)/page.tsx b/packages/app/control/src/app/(app)/app/[id]/(overview)/page.tsx index 01e292043..e15a12287 100644 --- a/packages/app/control/src/app/(app)/app/[id]/(overview)/page.tsx +++ b/packages/app/control/src/app/(app)/app/[id]/(overview)/page.tsx @@ -9,6 +9,7 @@ import { api, HydrateClient } from '@/trpc/server'; import { HeaderCard, LoadingHeaderCard } from './_components/header'; import { Setup } from './_components/setup'; import { Overview } from './_components/overview'; +import { ReferralHandler } from './_components/referral-handler'; import { userOrRedirect } from '@/auth/user-or-redirect'; export default async function AppPage(props: PageProps<'/app/[id]'>) { @@ -26,6 +27,7 @@ export default async function AppPage(props: PageProps<'/app/[id]'>) { return ( + }> diff --git a/packages/app/control/src/trpc/routers/apps/index.ts b/packages/app/control/src/trpc/routers/apps/index.ts index a4d58ff6d..841947ddf 100644 --- a/packages/app/control/src/trpc/routers/apps/index.ts +++ b/packages/app/control/src/trpc/routers/apps/index.ts @@ -23,6 +23,7 @@ import { createAppMembershipSchema, updateAppMembershipReferrer, updateAppMembershipReferrerSchema, + setAppMembershipReferrer, } from '@/services/db/apps/membership'; import { listAppsSchema, @@ -262,6 +263,26 @@ export const appsRouter = createTRPCRouter({ }), }, + registerReferral: protectedProcedure + .input(z.object({ appId: appIdSchema, code: z.string() })) + .mutation(async ({ input, ctx }) => { + const success = await setAppMembershipReferrer( + ctx.session.user.id, + input.appId, + input.code + ); + + if (!success) { + throw new TRPCError({ + code: 'BAD_REQUEST', + message: + 'Referral code could not be applied. It may be invalid, expired, or you may already have a referrer for this app.', + }); + } + + return { success: true }; + }), + transactions: { list: paginatedProcedure .concat(protectedProcedure)