Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Make omi personas on web smart and paid #1751 #1939

Open
wants to merge 8 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 16 additions & 0 deletions personas-open-source/Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,14 @@ ARG NEXT_PUBLIC_MIXPANEL_TOKEN
ARG NEXT_PUBLIC_EXTRA_PROMPT_RULES
ARG NEXT_PUBLIC_LINKEDIN_API_KEY
ARG NEXT_PUBLIC_LINKEDIN_API_HOST
ARG NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY
ARG STRIPE_SECRET_KEY
ARG STRIPE_WEBHOOK_SECRET
ARG STRIPE_PRICE_PRO_MONTHLY
ARG NEXT_PUBLIC_SITE_URL
ARG FIREBASE_PROJECT_ID
ARG FIREBASE_CLIENT_EMAIL
ARG FIREBASE_PRIVATE_KEY

ENV NEXT_PUBLIC_FIREBASE_API_KEY=$NEXT_PUBLIC_FIREBASE_API_KEY
ENV NEXT_PUBLIC_FIREBASE_AUTH_DOMAIN=$NEXT_PUBLIC_FIREBASE_AUTH_DOMAIN
Expand All @@ -40,6 +48,14 @@ ENV NEXT_PUBLIC_MIXPANEL_TOKEN=$NEXT_PUBLIC_MIXPANEL_TOKEN
ENV NEXT_PUBLIC_EXTRA_PROMPT_RULES=$NEXT_PUBLIC_EXTRA_PROMPT_RULES
ENV NEXT_PUBLIC_LINKEDIN_API_KEY=$NEXT_PUBLIC_LINKEDIN_API_KEY
ENV NEXT_PUBLIC_LINKEDIN_API_HOST=$NEXT_PUBLIC_LINKEDIN_API_HOST
ENV NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY=$NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY
ENV STRIPE_SECRET_KEY=$STRIPE_SECRET_KEY
ENV STRIPE_WEBHOOK_SECRET=$STRIPE_WEBHOOK_SECRET
ENV STRIPE_PRICE_PRO_MONTHLY=$STRIPE_PRICE_PRO_MONTHLY
ENV NEXT_PUBLIC_SITE_URL=$NEXT_PUBLIC_SITE_URL
ENV FIREBASE_PROJECT_ID=$FIREBASE_PROJECT_ID
ENV FIREBASE_CLIENT_EMAIL=$FIREBASE_CLIENT_EMAIL
ENV FIREBASE_PRIVATE_KEY=$FIREBASE_PRIVATE_KEY

# Build the Next.js application
RUN npm run build
Expand Down
14 changes: 14 additions & 0 deletions personas-open-source/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,20 @@ You'll need to obtain the following API keys and credentials:
NEXT_PUBLIC_FIREBASE_APP_ID=your_firebase_app_id
NEXT_PUBLIC_FIREBASE_VAPID_KEY=your_firebase_vapid_key

# Firebase Admin
FIREBASE_PROJECT_ID=your_firebase_project_id
FIREBASE_CLIENT_EMAIL=firebase_client_email
FIREBASE_PRIVATE_KEY=your_firebase_admin_private_key


# Stripe
NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY=stripe_publishable_key
STRIPE_SECRET_KEY=stripe_secret_key
STRIPE_WEBHOOK_SECRET=webhook_secret
STRIPE_PRICE_PRO_MONTHLY=pro_price_id

NEXT_PUBLIC_SITE_URL=deployment_url

# API Keys
NEXT_PUBLIC_RAPIDAPI_KEY=your_rapidapi_key
OPENROUTER_API_KEY=your_openrouter_api_key
Expand Down
3 changes: 3 additions & 0 deletions personas-open-source/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -14,10 +14,12 @@
"@radix-ui/react-dialog": "^1.1.2",
"@radix-ui/react-scroll-area": "^1.2.1",
"@radix-ui/react-slot": "^1.1.0",
"@stripe/stripe-js": "^5.7.0",
"class-variance-authority": "^0.7.0",
"clsx": "^2.1.1",
"eventsource-parser": "^3.0.0",
"firebase": "^11.0.2",
"firebase-admin": "^13.1.0",
"lucide-react": "^0.460.0",
"mixpanel-browser": "^2.56.0",
"next": "15.0.3",
Expand All @@ -28,6 +30,7 @@
"react-infinite-scroll-component": "^6.1.0",
"react-intersection-observer": "^9.13.1",
"sonner": "^1.7.0",
"stripe": "^17.7.0",
"tailwind-merge": "^2.5.4",
"tailwindcss-animate": "^1.0.7",
"ulid": "^2.3.0",
Expand Down
16 changes: 16 additions & 0 deletions personas-open-source/src/app/(auth)/login/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,22 @@ export default function SignInPage() {
const result = await signInWithPopup(auth, googleProvider);
const user = result.user;

// Get the ID token
const idToken = await user.getIdToken();

// Create session cookie
const response = await fetch('/api/auth/session', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({ idToken }),
});

if (!response.ok) {
throw new Error('Failed to create session');
}

const userRef = doc(db, 'users', user.uid);
const userSnap = await getDoc(userRef);

Expand Down
159 changes: 159 additions & 0 deletions personas-open-source/src/app/account/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,159 @@
'use client';

import { useState, useEffect } from 'react';
import { useRouter } from 'next/navigation';
import { auth, db } from '@/lib/firebase';
import { doc, getDoc } from 'firebase/firestore';
import { signOut } from 'firebase/auth';
import { Header } from '@/components/Header';
import { Footer } from '@/components/Footer';
import { useSubscription } from '@/lib/subscription-context';
import Link from 'next/link';
import { toast } from 'sonner';

export default function AccountPage() {
const router = useRouter();
const [user, setUser] = useState<any>(null);
const [loading, setLoading] = useState(true);
const { isSubscribed, currentPlan, subscriptionEndsAt, isLoading } = useSubscription();

useEffect(() => {
const unsubscribe = auth.onAuthStateChanged(async (authUser) => {
if (authUser) {
try {
const userDoc = await getDoc(doc(db, 'users', authUser.uid));
if (userDoc.exists()) {
setUser({
...authUser,
...userDoc.data(),
});
} else {
setUser(authUser);
}
} catch (error) {
console.error('Error fetching user data:', error);
setUser(authUser);
}
} else {
router.push('/');
}
setLoading(false);
});

return () => unsubscribe();
}, [router]);

const handleSignOut = async () => {
try {
// First sign out from Firebase Auth (client-side)
await signOut(auth);

// Then call our API to clear the session cookie (server-side)
await fetch('/api/auth/sign-out', {
method: 'POST',
});

router.push('/');
} catch (error) {
console.error('Error signing out:', error);
}
};

const handleManageSubscription = async () => {
try {
const response = await fetch('/api/create-portal-session', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
});

if (!response.ok) {
throw new Error('Failed to create portal session');
}

const { url } = await response.json();
window.location.href = url;
} catch (error) {
console.error('Error accessing customer portal:', error);
toast.error('Failed to access customer portal');
}
};

if (loading || isLoading) {
return (
<div className="min-h-screen bg-black text-white">
<Header />
<div className="flex flex-col items-center justify-center min-h-[60vh]">
<div className="animate-spin rounded-full h-12 w-12 border-t-2 border-b-2 border-white"></div>
<p className="mt-4">Loading...</p>
</div>
<Footer />
</div>
);
}

return (
<div className="min-h-screen bg-black text-white">
<Header />
<div className="max-w-3xl mx-auto px-4 py-12">
<h1 className="text-3xl font-bold mb-8">Account Settings</h1>

<div className="bg-zinc-900 rounded-lg p-6 mb-6">
<h2 className="text-xl font-semibold mb-4">Profile Information</h2>
{user && (
<div className="space-y-4">
<div>
<p className="text-lg font-medium">{user.displayName || 'User'}</p>
<p className="text-zinc-400">{user.email}</p>
</div>
</div>
)}
</div>

<div className="bg-zinc-900 rounded-lg p-6">
<h2 className="text-xl font-semibold mb-4">Subscription</h2>
<div className="space-y-4">
<div className="flex justify-between items-center">
<div>
<p className="font-medium">Current Plan</p>
<p className="text-zinc-400">{currentPlan === 'pro' ? 'Pro Plan' : 'Free Plan'}</p>
</div>
{isSubscribed && (
<button
onClick={handleManageSubscription}
className="bg-white text-black px-4 py-2 rounded-full text-sm font-medium hover:bg-gray-200 transition-colors"
>
Manage Subscription
</button>
)}
{!isSubscribed && (
<Link
href="/pricing"
className="bg-white text-black px-4 py-2 rounded-full text-sm font-medium hover:bg-gray-200 transition-colors"
>
Upgrade to Pro
</Link>
)}
</div>
{isSubscribed && subscriptionEndsAt && (
<p className="text-sm text-zinc-400">
Next billing date: {new Date(subscriptionEndsAt).toLocaleDateString()}
</p>
)}
</div>
</div>

<div className="mt-8">
<button
onClick={handleSignOut}
className="text-red-500 hover:text-red-400 transition-colors"
>
Sign Out
</button>
</div>
</div>
<Footer />
</div>
);
}
45 changes: 45 additions & 0 deletions personas-open-source/src/app/api/auth/session/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
import { NextRequest, NextResponse } from 'next/server';
import { auth } from '@/lib/firebase-admin';
import { cookies } from 'next/headers';

// Session duration: 5 days
const SESSION_DURATION = 60 * 60 * 24 * 5 * 1000;

export async function POST(req: NextRequest) {
try {
const { idToken } = await req.json();

if (!idToken) {
return NextResponse.json(
{ error: 'No ID token provided' },
{ status: 400 }
);
}

// Create session cookie
const sessionCookie = await auth.createSessionCookie(idToken, {
expiresIn: SESSION_DURATION,
});

// Create response with session cookie
const response = NextResponse.json({ status: 'success' });

// Set the cookie in the response
response.cookies.set({
name: 'session',
value: sessionCookie,
maxAge: SESSION_DURATION / 1000, // Convert to seconds
httpOnly: true,
secure: process.env.NODE_ENV === 'production',
path: '/',
});

return response;
} catch (error) {
console.error('Error creating session:', error);
return NextResponse.json(
{ error: 'Failed to create session' },
{ status: 401 }
);
}
}
31 changes: 31 additions & 0 deletions personas-open-source/src/app/api/auth/sign-out/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import { NextResponse } from 'next/server';
import { auth } from '@/lib/firebase-admin';
import { cookies } from 'next/headers';

export async function POST() {
try {
const cookieStore = await cookies();
const sessionCookie = cookieStore.get('session')?.value;

// If session cookie exists
if (sessionCookie) {
// Verify and get user info
try {
const decodedClaims = await auth.verifySessionCookie(sessionCookie);
// Revoke all user sessions
await auth.revokeRefreshTokens(decodedClaims.uid);
} catch (error) {
console.log('Invalid session cookie or already revoked');
// Continue to delete the cookie even if verification fails
}
}

// Clear the session cookie
const response = NextResponse.json({ success: true });
response.cookies.delete('session');
return response;
} catch (error: any) {
console.error('Error signing out:', error);
return NextResponse.json({ error: 'Failed to sign out' }, { status: 500 });
}
}
Loading