From 76671dd1001598065038a2385f708e865d0302f8 Mon Sep 17 00:00:00 2001 From: Ben Reilly Date: Tue, 25 Nov 2025 18:03:39 -0500 Subject: [PATCH 1/2] add referral handler --- .../_components/header/buttons/index.tsx | 6 +- .../_components/referral-handler.tsx | 66 +++++++++++++++++++ .../app/(app)/app/[id]/(overview)/page.tsx | 2 + 3 files changed, 73 insertions(+), 1 deletion(-) create mode 100644 packages/app/control/src/app/(app)/app/[id]/(overview)/_components/referral-handler.tsx 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..ed0746c07 --- /dev/null +++ b/packages/app/control/src/app/(app)/app/[id]/(overview)/_components/referral-handler.tsx @@ -0,0 +1,66 @@ +'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 utils = api.useUtils(); + const { mutateAsync: createMembership } = + api.apps.app.memberships.create.useMutation(); + const { mutateAsync: updateReferrer } = + api.apps.app.memberships.update.referrer.useMutation(); + + useEffect(() => { + if (!referralCode || processed) return; + + const processReferralCode = async () => { + const referralCodeData = await utils.apps.app.referralCode.get.byCode + .fetch(referralCode) + .catch(() => null); + + if (!referralCodeData) { + setProcessed(true); + return; + } + + const membership = await utils.apps.app.memberships.get + .fetch({ appId }) + .catch(() => null); + + // Only update if user has no referrer yet + if (membership?.referrerId === null) { + await updateReferrer({ + appId, + referrerId: referralCodeData.id, + }).catch(() => { + // Silently fail - user might already have a referrer from a race condition + }); + } + + // Create membership if it doesn't exist + if (!membership) { + await createMembership({ + appId, + referrerId: referralCodeData.id, + }).catch(() => { + // Silently fail - membership might have been created in the meantime + }); + } + + setProcessed(true); + }; + + void processReferralCode(); + }, [referralCode, appId, createMembership, updateReferrer, processed, utils]); + + 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 ( + }> From df12300cce84cfda6e39c3a2a71fa60f03837bbf Mon Sep 17 00:00:00 2001 From: Ben Reilly Date: Tue, 25 Nov 2025 18:26:02 -0500 Subject: [PATCH 2/2] cleanup --- .../_components/referral-handler.tsx | 47 ++++--------------- .../control/src/trpc/routers/apps/index.ts | 21 +++++++++ 2 files changed, 30 insertions(+), 38 deletions(-) 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 index ed0746c07..8be5b4fca 100644 --- 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 @@ -13,54 +13,25 @@ export const ReferralHandler: React.FC = ({ appId }) => { const referralCode = searchParams.get('referral_code'); const [processed, setProcessed] = useState(false); - const utils = api.useUtils(); - const { mutateAsync: createMembership } = - api.apps.app.memberships.create.useMutation(); - const { mutateAsync: updateReferrer } = - api.apps.app.memberships.update.referrer.useMutation(); + const { mutateAsync: registerReferral } = + api.apps.app.registerReferral.useMutation(); useEffect(() => { if (!referralCode || processed) return; const processReferralCode = async () => { - const referralCodeData = await utils.apps.app.referralCode.get.byCode - .fetch(referralCode) - .catch(() => null); - - if (!referralCodeData) { - setProcessed(true); - return; - } - - const membership = await utils.apps.app.memberships.get - .fetch({ appId }) - .catch(() => null); - - // Only update if user has no referrer yet - if (membership?.referrerId === null) { - await updateReferrer({ - appId, - referrerId: referralCodeData.id, - }).catch(() => { - // Silently fail - user might already have a referrer from a race condition - }); - } - - // Create membership if it doesn't exist - if (!membership) { - await createMembership({ - appId, - referrerId: referralCodeData.id, - }).catch(() => { - // Silently fail - membership might have been created in the meantime - }); - } + 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, createMembership, updateReferrer, processed, utils]); + }, [referralCode, appId, registerReferral, processed]); return null; }; 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)