diff --git a/apps/web/app/page.tsx b/apps/web/app/page.tsx index 6bf8cf6f..ad3815fd 100644 --- a/apps/web/app/page.tsx +++ b/apps/web/app/page.tsx @@ -21,6 +21,7 @@ import { AnimatePresence, motion } from "motion/react" import Link from "next/link" import { useRouter } from "next/navigation" import { useCallback, useEffect, useMemo, useState } from "react" +import { useReferralTracking } from "@lib/use-referral-tracking" import type { z } from "zod" import { ConnectAIModal } from "@/components/connect-ai-modal" import { InstallPrompt } from "@/components/install-prompt" @@ -744,6 +745,9 @@ export default function Page() { const router = useRouter() const { user } = useAuth() + // Track referral if present + useReferralTracking(user) + useEffect(() => { // save the token for chrome extension const url = new URL(window.location.href) diff --git a/apps/web/app/ref/[code]/page.tsx b/apps/web/app/ref/[code]/page.tsx index 98afcfe5..8d7bc6fe 100644 --- a/apps/web/app/ref/[code]/page.tsx +++ b/apps/web/app/ref/[code]/page.tsx @@ -1,156 +1,87 @@ -"use client"; +"use client" -import { $fetch } from "@lib/api"; -import { Button } from "@ui/components/button"; +import { $fetch } from "@repo/lib/api" +import { useQuery } from "@tanstack/react-query" +import { Button } from "@ui/components/button" import { Card, CardContent, CardDescription, CardHeader, CardTitle, -} from "@ui/components/card"; -import { CheckIcon, CopyIcon, LoaderIcon, ShareIcon } from "lucide-react"; -import Link from "next/link"; -import { useParams, useRouter } from "next/navigation"; -import { useEffect, useState } from "react"; -import { toast } from "sonner"; +} from "@ui/components/card" +import { Avatar, AvatarFallback, AvatarImage } from "@ui/components/avatar" +import { ShareIcon, LoaderIcon } from "lucide-react" +import Link from "next/link" +import { useParams } from "next/navigation" -export default function ReferralPage() { - const router = useRouter(); - const params = useParams(); - const referralCode = params.code as string; +export default function ReferralInvitePage() { + const params = useParams() + const code = params.code as string - const [isLoading, setIsLoading] = useState(true); - const [referralData, setReferralData] = useState<{ - referrerName?: string; - valid: boolean; - } | null>(null); - const [copiedLink, setCopiedLink] = useState(false); - - const referralLink = `https://supermemory.ai/ref/${referralCode}`; - - // Verify referral code and get referrer info - useEffect(() => { - async function checkReferral() { - if (!referralCode) { - setIsLoading(false); - return; + const { + data: referrerData, + isLoading, + error, + } = useQuery({ + queryKey: ["referral", code], + queryFn: async () => { + if (!code) { + throw new Error("Missing referral code") } - try { - // Check if referral code is valid - // For now, we'll assume it's valid - in the future this should call an API - setReferralData({ - valid: true, - referrerName: "A supermemory user", // Placeholder - should come from API - }); - } catch (error) { - console.error("Error checking referral:", error); - setReferralData({ valid: false }); - } finally { - setIsLoading(false); + const response = await $fetch("@get/referral/:code", { + params: { + code, + }, + }) + + if (response.error) { + throw new Error( + response.error?.message || "Failed to load referral information", + ) } - } - checkReferral(); - }, [referralCode]); + return response.data + }, + enabled: !!code, + retry: 1, + }) - - - const handleCopyLink = async () => { - try { - await navigator.clipboard.writeText(referralLink); - setCopiedLink(true); - toast.success("Referral link copied!"); - setTimeout(() => setCopiedLink(false), 2000); - } catch (error) { - toast.error("Failed to copy link"); - } - }; - - const handleShare = () => { - if (navigator.share) { - navigator.share({ - title: "Join supermemory", - text: "I'm excited about supermemory - it's going to change how we store and interact with our memories!", - url: referralLink, - }); - } else { - handleCopyLink(); - } - }; + const referrer = referrerData?.referrer if (isLoading) { return (
-

Checking invitation...

+

Loading invitation...

- ); + ) } - if (!referralData?.valid) { + if (error || !code) { return (
- - Invalid Referral - - - This referral link is not valid or has expired. - - - -
- -
-
-
-
- ); - } - - return ( -
-
- {/* Welcome Card */} - - -
- +
+
- You're invited to supermemory! + Invalid Referral Link - {referralData.referrerName} invited you to join the future of - memory management. + {error instanceof Error ? error.message : "Invalid referral code"}
-
-

- What is supermemory? -

-

- supermemory is an AI-powered personal knowledge base that - helps you store, organize, and interact with all your digital - memories - from documents and links to conversations and - ideas. -

-
- -
Learn more about supermemory @@ -158,51 +89,62 @@ export default function ReferralPage() {
- - {/* Share Card */} - - - - Share with friends - - - Help others discover supermemory and earn priority access. - - - -
-
-
-

- {referralLink} -

-
- -
- - -
-
-
+ ) + } + + return ( +
+ + +
+ + + + {referrer?.name?.charAt(0).toUpperCase()} + + +
+ + You've been invited! + + + + {referrer?.name} + {" "} + invited you to join supermemory + +
+ +
+
+

+ What is supermemory? +

+

+ supermemory is an AI-powered personal knowledge base that helps + you store, organize, and interact with all your digital + memories. +

+
+ + + + + +
+ + Learn more about supermemory + +
+
+
+
- ); + ) } diff --git a/apps/web/app/upgrade-mcp/page.tsx b/apps/web/app/upgrade-mcp/page.tsx index baffcbd2..fcc40d8b 100644 --- a/apps/web/app/upgrade-mcp/page.tsx +++ b/apps/web/app/upgrade-mcp/page.tsx @@ -105,9 +105,6 @@ export default function MigrateMCPPage() { return (
- {/* Background elements */} -
- {/* Top navigation */} -
-
-
-
- -
-
-

- Upgrade supermemory MCP + Upgrade MCP

-

- Migrate your documents to the new MCP server -

{/* Form */} @@ -177,8 +163,7 @@ export default function MigrateMCPPage() { htmlFor="mcpUrl" className="text-sm font-medium text-slate-200 flex items-center gap-2" > - - MCP URL + URL
- - {/* Success/Error States */} - - {migrateMutation.isSuccess && migrateMutation.data && ( - -
-
-
-
-
- -

- Migration completed successfully! -

-
-

- Migrated {migrateMutation.data.migratedCount}{" "} - documents -

- {migrateMutation.data.documentIds && - migrateMutation.data.documentIds.length > 0 && ( -
- - View migrated document IDs - -
- {migrateMutation.data.documentIds.map( - (id) => ( - - {id} - - ), - )} -
-
- )} -
-
-
- - )} - - {migrateMutation.isError && ( - -
-
-
-
-

Migration failed

-

- {migrateMutation.error?.message || - "An unexpected error occurred"} -

-
-
-
- - )} -
diff --git a/apps/web/components/views/profile.tsx b/apps/web/components/views/profile.tsx index b19706f7..c652bae4 100644 --- a/apps/web/components/views/profile.tsx +++ b/apps/web/components/views/profile.tsx @@ -8,6 +8,7 @@ import { fetchSubscriptionStatus, } from "@lib/queries" import { Button } from "@repo/ui/components/button" +import { ReferralLink } from "@repo/ui/components/referral-link" import { HeadingH3Bold } from "@repo/ui/text/heading/heading-h3-bold" import { useCustomer } from "autumn-js/react" import { @@ -273,6 +274,9 @@ export function ProfileView() {
)} + {/* Referral Section */} + + +
+ + + + )} + + + ); +} diff --git a/packages/ui/pages/login.tsx b/packages/ui/pages/login.tsx index 74765da1..4e93c372 100644 --- a/packages/ui/pages/login.tsx +++ b/packages/ui/pages/login.tsx @@ -43,28 +43,45 @@ export function LoginPage({ // Get redirect URL from query params const redirectUrl = params.get("redirect"); + // Get referral code from query params + const referralCode = params.get("ref"); // Create callback URL that includes redirect parameter if provided const getCallbackURL = () => { const origin = window.location.origin; + let callbackUrl = origin; + if (redirectUrl) { // Validate that the redirect URL is safe (same origin or allow external based on your security requirements) try { const url = new URL(redirectUrl, origin); - return url.toString(); + callbackUrl = url.toString(); } catch { // If redirect URL is invalid, fall back to origin - return origin; + callbackUrl = origin; } } - return origin; + + // Add referral code to callback URL if present + if (referralCode) { + const url = new URL(callbackUrl); + url.searchParams.set("ref", referralCode); + return url.toString(); + } + + return callbackUrl; }; - // Load last used method from localStorage on mount + // Load last used method from localStorage on mount and save referral code useEffect(() => { const savedMethod = localStorage.getItem("supermemory-last-login-method"); setLastUsedMethod(savedMethod); - }, []); + + // Store referral code in localStorage if present + if (referralCode) { + localStorage.setItem("supermemory-pending-referral", referralCode); + } + }, [referralCode]); // Record the pending login method (will be committed after successful auth) function setPendingLoginMethod(method: string) { diff --git a/packages/validation/api.ts b/packages/validation/api.ts index 91ac5e09..d26f81c6 100644 --- a/packages/validation/api.ts +++ b/packages/validation/api.ts @@ -1384,3 +1384,36 @@ export const BulkDeleteMemoriesResponseSchema = z .openapi({ description: "Response for bulk memory deletion", }) + +// Referral schemas +export const ReferrerSchema = z.object({ + id: z.string(), + name: z.string(), + image: z.string().optional(), +}) + +export const ReferralDetailsResponseSchema = z.object({ + referrer: ReferrerSchema, +}) + +export const ReferralCodeResponseSchema = z.object({ + referralCode: z.string(), +}) + +export const TrackReferralRequestSchema = z.object({ + referralCode: z.string(), +}) + +export const TrackReferralResponseSchema = z.object({ + success: z.boolean(), + referrerId: z.string(), +}) + +// Referral type exports +export type Referrer = z.infer +export type ReferralDetailsResponse = z.infer< + typeof ReferralDetailsResponseSchema +> +export type ReferralCodeResponse = z.infer +export type TrackReferralRequest = z.infer +export type TrackReferralResponse = z.infer