mirror of
https://github.com/supermemoryai/supermemory.git
synced 2026-10-06 02:47:54 +00:00
feat: referrals
This commit is contained in:
parent
84fea4a698
commit
ebc8c1f60d
9 changed files with 380 additions and 259 deletions
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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 (
|
||||
<div className="min-h-screen flex items-center justify-center p-4 bg-[#0f1419]">
|
||||
<div className="flex flex-col items-center gap-4">
|
||||
<LoaderIcon className="w-8 h-8 text-orange-500 animate-spin" />
|
||||
<p className="text-white/60">Checking invitation...</p>
|
||||
<p className="text-white/60">Loading invitation...</p>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
)
|
||||
}
|
||||
|
||||
if (!referralData?.valid) {
|
||||
if (error || !code) {
|
||||
return (
|
||||
<div className="min-h-screen flex items-center justify-center p-4 bg-[#0f1419]">
|
||||
<Card className="max-w-md w-full bg-[#1a1f2a] border-white/10">
|
||||
<CardHeader className="text-center">
|
||||
<CardTitle className="text-2xl font-bold text-white">
|
||||
Invalid Referral
|
||||
</CardTitle>
|
||||
<CardDescription className="text-white/60 mt-2">
|
||||
This referral link is not valid or has expired.
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="text-center">
|
||||
<Button asChild className="w-full">
|
||||
<Link href="https://supermemory.ai">Go to supermemory</Link>
|
||||
</Button>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="min-h-screen flex items-center justify-center p-4 bg-[#0f1419]">
|
||||
<div className="max-w-lg w-full space-y-6">
|
||||
{/* Welcome Card */}
|
||||
<Card className="bg-[#1a1f2a] border-white/10">
|
||||
<CardHeader className="text-center">
|
||||
<div className="mx-auto mb-4 w-16 h-16 rounded-full bg-orange-500/10 flex items-center justify-center">
|
||||
<ShareIcon className="w-8 h-8 text-orange-500" />
|
||||
<div className="mx-auto mb-4 w-16 h-16 rounded-full bg-red-500/10 flex items-center justify-center">
|
||||
<ShareIcon className="w-8 h-8 text-red-500" />
|
||||
</div>
|
||||
<CardTitle className="text-2xl font-bold text-white">
|
||||
You're invited to supermemory!
|
||||
Invalid Referral Link
|
||||
</CardTitle>
|
||||
<CardDescription className="text-white/60 mt-2">
|
||||
{referralData.referrerName} invited you to join the future of
|
||||
memory management.
|
||||
{error instanceof Error ? error.message : "Invalid referral code"}
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="space-y-4">
|
||||
<div className="bg-[#0f1419] rounded-lg p-4 border border-white/10">
|
||||
<h3 className="text-white font-semibold mb-2">
|
||||
What is supermemory?
|
||||
</h3>
|
||||
<p className="text-white/70 text-sm leading-relaxed">
|
||||
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.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
|
||||
<div className="text-center">
|
||||
<Link
|
||||
href="https://supermemory.ai"
|
||||
className="text-orange-500 hover:text-orange-400 text-sm underline"
|
||||
href="https://supermemory.ai"
|
||||
>
|
||||
Learn more about supermemory
|
||||
</Link>
|
||||
|
|
@ -158,51 +89,62 @@ export default function ReferralPage() {
|
|||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
{/* Share Card */}
|
||||
<Card className="bg-[#1a1f2a] border-white/10">
|
||||
<CardHeader>
|
||||
<CardTitle className="text-lg text-white">
|
||||
Share with friends
|
||||
</CardTitle>
|
||||
<CardDescription className="text-white/60">
|
||||
Help others discover supermemory and earn priority access.
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="space-y-3">
|
||||
<div className="flex gap-2">
|
||||
<div className="flex-1 px-3 py-2 bg-[#0f1419] border border-white/10 rounded-md">
|
||||
<p className="text-white/80 text-sm font-mono truncate">
|
||||
{referralLink}
|
||||
</p>
|
||||
</div>
|
||||
<Button
|
||||
onClick={handleCopyLink}
|
||||
size="sm"
|
||||
variant="outline"
|
||||
className="shrink-0 border-white/10 hover:bg-white/5"
|
||||
>
|
||||
{copiedLink ? (
|
||||
<CheckIcon className="w-4 h-4" />
|
||||
) : (
|
||||
<CopyIcon className="w-4 h-4" />
|
||||
)}
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<Button
|
||||
onClick={handleShare}
|
||||
variant="outline"
|
||||
className="w-full border-white/10 text-white hover:bg-white/5"
|
||||
>
|
||||
<ShareIcon className="w-4 h-4" />
|
||||
Share this link
|
||||
</Button>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="min-h-screen flex items-center justify-center p-4 bg-[#0f1419]">
|
||||
<Card className="max-w-md w-full bg-[#1a1f2a] border-white/10">
|
||||
<CardHeader className="text-center">
|
||||
<div className="mx-auto mb-4">
|
||||
<Avatar className="w-20 h-20">
|
||||
<AvatarImage src={referrer?.image || ""} alt={referrer?.name} />
|
||||
<AvatarFallback className="bg-orange-500/10 text-orange-500 text-2xl">
|
||||
{referrer?.name?.charAt(0).toUpperCase()}
|
||||
</AvatarFallback>
|
||||
</Avatar>
|
||||
</div>
|
||||
<CardTitle className="text-2xl font-bold text-white">
|
||||
You've been invited!
|
||||
</CardTitle>
|
||||
<CardDescription className="text-white/60 mt-2">
|
||||
<span className="text-orange-400 font-semibold">
|
||||
{referrer?.name}
|
||||
</span>{" "}
|
||||
invited you to join supermemory
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="space-y-4">
|
||||
<div className="bg-[#0f1419] rounded-lg p-4 border border-white/10">
|
||||
<h3 className="text-white font-semibold mb-2">
|
||||
What is supermemory?
|
||||
</h3>
|
||||
<p className="text-white/70 text-sm leading-relaxed">
|
||||
supermemory is an AI-powered personal knowledge base that helps
|
||||
you store, organize, and interact with all your digital
|
||||
memories.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<Link href={`/login?ref=${code}`} className="block">
|
||||
<Button className="w-full bg-orange-500 hover:bg-orange-600 text-white">
|
||||
Get Started
|
||||
</Button>
|
||||
</Link>
|
||||
|
||||
<div className="text-center">
|
||||
<Link
|
||||
className="text-orange-500 hover:text-orange-400 text-sm underline"
|
||||
href="https://supermemory.ai"
|
||||
>
|
||||
Learn more about supermemory
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
);
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -105,9 +105,6 @@ export default function MigrateMCPPage() {
|
|||
|
||||
return (
|
||||
<div className="min-h-screen bg-[#0f1419] overflow-hidden relative">
|
||||
{/* Background elements */}
|
||||
<div className="absolute inset-0 bg-gradient-to-br from-blue-900/10 via-transparent to-purple-900/10" />
|
||||
|
||||
{/* Top navigation */}
|
||||
<motion.div
|
||||
initial={{ opacity: 0, y: -20 }}
|
||||
|
|
@ -148,20 +145,9 @@ export default function MigrateMCPPage() {
|
|||
transition={{ delay: 0.2 }}
|
||||
className="text-center mb-8"
|
||||
>
|
||||
<div className="flex items-center justify-center mb-4">
|
||||
<div className="relative">
|
||||
<div className="absolute inset-0 bg-blue-500/20 rounded-full blur-xl" />
|
||||
<div className="relative bg-blue-500/10 p-3 rounded-full border border-blue-500/20">
|
||||
<Zap className="w-6 h-6 text-blue-400" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<h1 className="text-2xl md:text-3xl font-bold text-white mb-2">
|
||||
Upgrade supermemory MCP
|
||||
Upgrade MCP
|
||||
</h1>
|
||||
<p className="text-slate-400 text-sm md:text-base">
|
||||
Migrate your documents to the new MCP server
|
||||
</p>
|
||||
</motion.div>
|
||||
|
||||
{/* Form */}
|
||||
|
|
@ -177,8 +163,7 @@ export default function MigrateMCPPage() {
|
|||
htmlFor="mcpUrl"
|
||||
className="text-sm font-medium text-slate-200 flex items-center gap-2"
|
||||
>
|
||||
<Upload className="w-4 h-4" />
|
||||
MCP URL
|
||||
URL
|
||||
</label>
|
||||
<div className="relative">
|
||||
<Input
|
||||
|
|
@ -219,7 +204,7 @@ export default function MigrateMCPPage() {
|
|||
>
|
||||
<Button
|
||||
type="submit"
|
||||
className="w-full bg-gradient-to-r from-blue-600 to-purple-600 hover:from-blue-700 hover:to-purple-700 text-white border-0 py-3 rounded-xl font-medium shadow-lg hover:shadow-blue-500/25 transition-all duration-200 disabled:opacity-50 disabled:cursor-not-allowed"
|
||||
className="w-full transition-all duration-200 disabled:opacity-50 disabled:cursor-not-allowed"
|
||||
disabled={
|
||||
migrateMutation.isPending || !getUserIdFromUrl(mcpUrl)
|
||||
}
|
||||
|
|
@ -233,88 +218,12 @@ export default function MigrateMCPPage() {
|
|||
) : (
|
||||
<>
|
||||
Start Upgrade
|
||||
<ArrowRight className="ml-2 w-4 h-4" />
|
||||
<ArrowRight className="w-4 h-4" />
|
||||
</>
|
||||
)}
|
||||
</Button>
|
||||
</motion.div>
|
||||
</motion.form>
|
||||
|
||||
{/* Success/Error States */}
|
||||
<AnimatePresence mode="wait">
|
||||
{migrateMutation.isSuccess && migrateMutation.data && (
|
||||
<motion.div
|
||||
key="success"
|
||||
initial={{ opacity: 0, y: 20, scale: 0.95 }}
|
||||
animate={{ opacity: 1, y: 0, scale: 1 }}
|
||||
exit={{ opacity: 0, y: -10, scale: 0.95 }}
|
||||
transition={{ type: "spring", stiffness: 300, damping: 25 }}
|
||||
className="mt-6"
|
||||
>
|
||||
<div className="relative rounded-xl overflow-hidden">
|
||||
<div className="absolute inset-0 bg-gradient-to-r from-green-500/10 to-emerald-500/10" />
|
||||
<div className="relative p-4 border border-green-500/20 rounded-xl">
|
||||
<div className="text-green-400">
|
||||
<div className="flex items-center gap-2 mb-2">
|
||||
<CheckCircle className="w-5 h-5" />
|
||||
<p className="font-medium">
|
||||
Migration completed successfully!
|
||||
</p>
|
||||
</div>
|
||||
<p className="text-sm text-green-300/80 mb-3">
|
||||
Migrated {migrateMutation.data.migratedCount}{" "}
|
||||
documents
|
||||
</p>
|
||||
{migrateMutation.data.documentIds &&
|
||||
migrateMutation.data.documentIds.length > 0 && (
|
||||
<details className="mt-3">
|
||||
<summary className="cursor-pointer hover:text-green-300 transition-colors text-sm font-medium">
|
||||
View migrated document IDs
|
||||
</summary>
|
||||
<div className="mt-3 space-y-2 max-h-40 overflow-y-auto">
|
||||
{migrateMutation.data.documentIds.map(
|
||||
(id) => (
|
||||
<code
|
||||
key={id}
|
||||
className="block text-xs bg-black/30 px-3 py-2 rounded-lg border border-green-500/10 text-green-200"
|
||||
>
|
||||
{id}
|
||||
</code>
|
||||
),
|
||||
)}
|
||||
</div>
|
||||
</details>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</motion.div>
|
||||
)}
|
||||
|
||||
{migrateMutation.isError && (
|
||||
<motion.div
|
||||
key="error"
|
||||
initial={{ opacity: 0, y: 20, scale: 0.95 }}
|
||||
animate={{ opacity: 1, y: 0, scale: 1 }}
|
||||
exit={{ opacity: 0, y: -10, scale: 0.95 }}
|
||||
transition={{ type: "spring", stiffness: 300, damping: 25 }}
|
||||
className="mt-6"
|
||||
>
|
||||
<div className="relative rounded-xl overflow-hidden">
|
||||
<div className="absolute inset-0 bg-gradient-to-r from-red-500/10 to-pink-500/10" />
|
||||
<div className="relative p-4 border border-red-500/20 rounded-xl">
|
||||
<div className="text-red-400">
|
||||
<p className="font-medium mb-1">Migration failed</p>
|
||||
<p className="text-sm text-red-300/80">
|
||||
{migrateMutation.error?.message ||
|
||||
"An unexpected error occurred"}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</motion.div>
|
||||
)}
|
||||
</AnimatePresence>
|
||||
</div>
|
||||
</div>
|
||||
</motion.div>
|
||||
|
|
|
|||
|
|
@ -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() {
|
|||
</div>
|
||||
)}
|
||||
|
||||
{/* Referral Section */}
|
||||
<ReferralLink className="mt-4" />
|
||||
|
||||
<Button
|
||||
className="w-full bg-red-500/20 hover:bg-red-500/30 text-red-200 border-red-500/30"
|
||||
onClick={handleLogout}
|
||||
|
|
|
|||
|
|
@ -21,6 +21,10 @@ import {
|
|||
SearchResponseSchema,
|
||||
type SearchResult,
|
||||
SettingsRequestSchema,
|
||||
ReferralDetailsResponseSchema,
|
||||
ReferralCodeResponseSchema,
|
||||
TrackReferralRequestSchema,
|
||||
TrackReferralResponseSchema,
|
||||
} from "../validation/api"
|
||||
|
||||
// Settings response schema - this is custom to console (not in shared validation)
|
||||
|
|
@ -191,6 +195,21 @@ export const apiSchema = createSchema({
|
|||
"@get/waitlist/status": {
|
||||
output: WaitlistStatusResponseSchema,
|
||||
},
|
||||
|
||||
// Referral operations
|
||||
"@get/referral/:code": {
|
||||
output: ReferralDetailsResponseSchema,
|
||||
params: z.object({
|
||||
code: z.string(),
|
||||
}),
|
||||
},
|
||||
"@get/referral/user/code": {
|
||||
output: ReferralCodeResponseSchema,
|
||||
},
|
||||
"@post/referral/track": {
|
||||
input: TrackReferralRequestSchema,
|
||||
output: TrackReferralResponseSchema,
|
||||
},
|
||||
})
|
||||
|
||||
export const $fetch = createFetch({
|
||||
|
|
|
|||
55
packages/lib/use-referral-tracking.ts
Normal file
55
packages/lib/use-referral-tracking.ts
Normal file
|
|
@ -0,0 +1,55 @@
|
|||
import { $fetch } from "@repo/lib/api"
|
||||
import { useEffect } from "react"
|
||||
|
||||
export function useReferralTracking(user: any) {
|
||||
useEffect(() => {
|
||||
if (!user) return
|
||||
|
||||
const trackReferral = async () => {
|
||||
try {
|
||||
// Check for pending referral code in localStorage
|
||||
const pendingReferral = localStorage.getItem(
|
||||
"supermemory-pending-referral",
|
||||
)
|
||||
|
||||
// Also check URL params as a fallback
|
||||
const urlParams = new URLSearchParams(window.location.search)
|
||||
const urlReferral = urlParams.get("ref")
|
||||
|
||||
const referralCode = pendingReferral || urlReferral
|
||||
|
||||
if (!referralCode) return
|
||||
|
||||
// Send referral tracking request
|
||||
const response = await $fetch("@post/referral/track", {
|
||||
body: { referralCode },
|
||||
})
|
||||
|
||||
if (response.data) {
|
||||
// Clear the pending referral from localStorage
|
||||
localStorage.removeItem("supermemory-pending-referral")
|
||||
|
||||
// Clear the ref param from URL
|
||||
if (urlReferral) {
|
||||
const url = new URL(window.location.href)
|
||||
url.searchParams.delete("ref")
|
||||
window.history.replaceState({}, "", url.toString())
|
||||
}
|
||||
|
||||
console.log("Referral tracked successfully")
|
||||
} else if (response.error) {
|
||||
console.error(
|
||||
"Failed to track referral:",
|
||||
response.error?.message || "Unknown error",
|
||||
)
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Error tracking referral:", error)
|
||||
}
|
||||
}
|
||||
|
||||
// Only track referral for new users (you might want to add a check here)
|
||||
// For now, we'll attempt to track for any user that has a pending referral
|
||||
trackReferral()
|
||||
}, [user])
|
||||
}
|
||||
138
packages/ui/components/referral-link.tsx
Normal file
138
packages/ui/components/referral-link.tsx
Normal file
|
|
@ -0,0 +1,138 @@
|
|||
"use client";
|
||||
|
||||
import { $fetch } from "@repo/lib/api";
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
import { useState } from "react";
|
||||
import { Button } from "./button";
|
||||
import {
|
||||
Card,
|
||||
CardContent,
|
||||
CardDescription,
|
||||
CardHeader,
|
||||
CardTitle,
|
||||
} from "./card";
|
||||
import { Input } from "./input";
|
||||
import { CheckIcon, CopyIcon, ShareIcon, LoaderIcon } from "lucide-react";
|
||||
|
||||
interface ReferralLinkProps {
|
||||
className?: string;
|
||||
}
|
||||
|
||||
export function ReferralLink({ className }: ReferralLinkProps) {
|
||||
const [copied, setCopied] = useState(false);
|
||||
|
||||
const {
|
||||
data: referralData,
|
||||
isLoading,
|
||||
error,
|
||||
} = useQuery({
|
||||
queryKey: ["referral-code"],
|
||||
queryFn: async () => {
|
||||
const response = await $fetch("@get/referral/user/code");
|
||||
|
||||
if (response.error) {
|
||||
throw new Error(
|
||||
response.error?.message || "Failed to fetch referral code",
|
||||
);
|
||||
}
|
||||
|
||||
return response.data;
|
||||
},
|
||||
staleTime: 5 * 60 * 1000, // 5 minutes
|
||||
});
|
||||
|
||||
const referralLink = referralData?.referralCode
|
||||
? `https://app.supermemory.ai/ref/${referralData.referralCode}`
|
||||
: "";
|
||||
|
||||
const copyToClipboard = async () => {
|
||||
if (!referralLink) return;
|
||||
|
||||
try {
|
||||
await navigator.clipboard.writeText(referralLink);
|
||||
setCopied(true);
|
||||
setTimeout(() => setCopied(false), 2000);
|
||||
} catch (err) {
|
||||
console.error("Failed to copy:", err);
|
||||
}
|
||||
};
|
||||
|
||||
const shareReferralLink = async () => {
|
||||
if (!referralLink) return;
|
||||
|
||||
if (navigator.share) {
|
||||
try {
|
||||
await navigator.share({
|
||||
title: "Join supermemory!",
|
||||
text: "I'm using supermemory to organize and interact with all my digital memories. Join me!",
|
||||
url: referralLink,
|
||||
});
|
||||
} catch (err) {
|
||||
// User cancelled or share failed
|
||||
console.log("Share cancelled or failed:", err);
|
||||
}
|
||||
} else {
|
||||
// Fallback to copy
|
||||
copyToClipboard();
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Card className={`bg-[#1a1f2a] border-white/10 ${className}`}>
|
||||
<CardHeader>
|
||||
<CardTitle className="text-white flex items-center gap-2">
|
||||
<ShareIcon className="w-5 h-5 text-orange-500" />
|
||||
Invite Friends
|
||||
</CardTitle>
|
||||
<CardDescription className="text-white/60">
|
||||
Share your unique referral link with friends to invite them to
|
||||
supermemory
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-4">
|
||||
{isLoading ? (
|
||||
<div className="flex items-center justify-center py-4">
|
||||
<LoaderIcon className="w-6 h-6 text-orange-500 animate-spin" />
|
||||
</div>
|
||||
) : error ? (
|
||||
<div className="text-red-400 text-sm">
|
||||
{error instanceof Error
|
||||
? error.message
|
||||
: "Failed to load referral code"}
|
||||
</div>
|
||||
) : (
|
||||
<>
|
||||
<div className="flex gap-2">
|
||||
<Input
|
||||
value={referralLink}
|
||||
readOnly
|
||||
className="bg-[#0f1419] border-white/10 text-white/80"
|
||||
onClick={(e) => e.currentTarget.select()}
|
||||
/>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="icon"
|
||||
onClick={copyToClipboard}
|
||||
className="border-white/10 hover:bg-white/10"
|
||||
>
|
||||
{copied ? (
|
||||
<CheckIcon className="w-4 h-4 text-green-500" />
|
||||
) : (
|
||||
<CopyIcon className="w-4 h-4 text-white/60" />
|
||||
)}
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<Button
|
||||
onClick={shareReferralLink}
|
||||
className="w-full bg-orange-500 hover:bg-orange-600 text-white"
|
||||
>
|
||||
<ShareIcon className="w-4 h-4 mr-2" />
|
||||
Share Referral Link
|
||||
</Button>
|
||||
</>
|
||||
)}
|
||||
</CardContent>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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<typeof ReferrerSchema>
|
||||
export type ReferralDetailsResponse = z.infer<
|
||||
typeof ReferralDetailsResponseSchema
|
||||
>
|
||||
export type ReferralCodeResponse = z.infer<typeof ReferralCodeResponseSchema>
|
||||
export type TrackReferralRequest = z.infer<typeof TrackReferralRequestSchema>
|
||||
export type TrackReferralResponse = z.infer<typeof TrackReferralResponseSchema>
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue