Skip to content
Closed
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
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
'use client';

import { useActionState } from 'react';
import { useActionState, useCallback } from 'react';
import { Button } from '@/components/ui/button';
import { Input } from '@/components/ui/input';
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
Expand All @@ -10,6 +10,7 @@ import { updateAccount } from '@/app/(login)/actions';
import { User } from '@/lib/db/schema';
import useSWR from 'swr';
import { Suspense } from 'react';
import posthog from 'posthog-js';

const fetcher = (url: string) => fetch(url).then((res) => res.json());

Expand Down Expand Up @@ -78,6 +79,16 @@ export default function GeneralPage() {
{}
);

const handleSubmit = useCallback(
(formData: FormData) => {
const name = formData.get('name') as string;
const email = formData.get('email') as string;
posthog.capture('account_update_submitted', { name, email });
return formAction(formData);
},
[formAction]
);

return (
<section className="flex-1 p-4 lg:p-8">
<h1 className="text-lg lg:text-2xl font-medium text-gray-900 mb-6">
Expand All @@ -89,7 +100,7 @@ export default function GeneralPage() {
<CardTitle>Account Information</CardTitle>
</CardHeader>
<CardContent>
<form className="space-y-4" action={formAction}>
<form className="space-y-4" action={handleSubmit}>
<Suspense fallback={<AccountForm state={state} />}>
<AccountFormWithData state={state} />
</Suspense>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,9 @@ import { Input } from '@/components/ui/input';
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
import { Label } from '@/components/ui/label';
import { Lock, Trash2, Loader2 } from 'lucide-react';
import { useActionState } from 'react';
import { useActionState, useCallback } from 'react';
import { updatePassword, deleteAccount } from '@/app/(login)/actions';
import posthog from 'posthog-js';

type PasswordState = {
currentPassword?: string;
Expand All @@ -33,6 +34,22 @@ export default function SecurityPage() {
FormData
>(deleteAccount, {});

const handlePasswordSubmit = useCallback(
(formData: FormData) => {
posthog.capture('password_update_submitted');
return passwordAction(formData);
},
[passwordAction]
);

const handleDeleteSubmit = useCallback(
(formData: FormData) => {
posthog.capture('account_delete_submitted');
return deleteAction(formData);
},
[deleteAction]
);

return (
<section className="flex-1 p-4 lg:p-8">
<h1 className="text-lg lg:text-2xl font-medium bold text-gray-900 mb-6">
Expand All @@ -43,7 +60,7 @@ export default function SecurityPage() {
<CardTitle>Password</CardTitle>
</CardHeader>
<CardContent>
<form className="space-y-4" action={passwordAction}>
<form className="space-y-4" action={handlePasswordSubmit}>
<div>
<Label htmlFor="current-password" className="mb-2">
Current Password
Expand Down Expand Up @@ -123,7 +140,7 @@ export default function SecurityPage() {
<p className="text-sm text-gray-500 mb-4">
Account deletion is non-reversable. Please proceed with caution.
</p>
<form action={deleteAction} className="space-y-4">
<form action={handleDeleteSubmit} className="space-y-4">
<div>
<Label htmlFor="delete-password" className="mb-2">
Confirm Password
Expand Down
29 changes: 4 additions & 25 deletions apps/next-js/15-app-router-saas/app/(dashboard)/page.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { Button } from '@/components/ui/button';
import { ArrowRight, CreditCard, Database } from 'lucide-react';
import { CreditCard, Database } from 'lucide-react';
import { Terminal } from './terminal';
import { DeployButton, ViewCodeButton } from './tracked-buttons';

export default function HomePage() {
return (
Expand All @@ -19,19 +19,7 @@ export default function HomePage() {
essential integrations.
</p>
<div className="mt-8 sm:max-w-lg sm:mx-auto sm:text-center lg:text-left lg:mx-0">
<a
href="https://vercel.com/templates/next.js/next-js-saas-starter"
target="_blank"
>
<Button
size="lg"
variant="outline"
className="text-lg rounded-full"
>
Deploy your own
<ArrowRight className="ml-2 h-5 w-5" />
</Button>
</a>
<DeployButton />
</div>
</div>
<div className="mt-12 relative sm:max-w-lg sm:mx-auto lg:mt-0 lg:max-w-none lg:mx-0 lg:col-span-6 lg:flex lg:items-center">
Expand Down Expand Up @@ -111,16 +99,7 @@ export default function HomePage() {
</p>
</div>
<div className="mt-8 lg:mt-0 flex justify-center lg:justify-end">
<a href="https://github.com/nextjs/saas-starter" target="_blank">
<Button
size="lg"
variant="outline"
className="text-lg rounded-full"
>
View the code
<ArrowRight className="ml-3 h-6 w-6" />
</Button>
</a>
<ViewCodeButton />
</div>
</div>
</div>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { checkoutAction } from '@/lib/payments/actions';
import { Check } from 'lucide-react';
import { getStripePrices, getStripeProducts } from '@/lib/payments/stripe';
import { SubmitButton } from './submit-button';
import { PricingPageTracker } from './pricing-page-tracker';

// Prices are fresh for one hour max
export const revalidate = 3600;
Expand All @@ -20,6 +21,7 @@ export default async function PricingPage() {

return (
<main className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-12">
<PricingPageTracker />
<div className="grid md:grid-cols-2 gap-8 max-w-xl mx-auto">
<PricingCard
name={basePlan?.name || 'Base'}
Expand Down Expand Up @@ -87,7 +89,7 @@ function PricingCard({
</ul>
<form action={checkoutAction}>
<input type="hidden" name="priceId" value={priceId} />
<SubmitButton />
<SubmitButton planName={name} priceId={priceId} />
</form>
</div>
);
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
'use client';

import { useEffect } from 'react';
import posthog from 'posthog-js';

export function PricingPageTracker() {
useEffect(() => {
posthog.capture('pricing_page_viewed');
}, []);

return null;
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,16 +3,25 @@
import { Button } from '@/components/ui/button';
import { ArrowRight, Loader2 } from 'lucide-react';
import { useFormStatus } from 'react-dom';
import posthog from 'posthog-js';

export function SubmitButton() {
export function SubmitButton({ planName, priceId }: { planName?: string; priceId?: string }) {
const { pending } = useFormStatus();

const handleClick = () => {
posthog.capture('checkout_started', {
plan_name: planName,
price_id: priceId,
});
};

return (
<Button
type="submit"
disabled={pending}
variant="outline"
className="w-full rounded-full"
onClick={handleClick}
>
{pending ? (
<>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
'use client';

import { Button } from '@/components/ui/button';
import { ArrowRight } from 'lucide-react';
import posthog from 'posthog-js';

export function DeployButton() {
const handleClick = () => {
posthog.capture('deploy_button_clicked');
};

return (
<a
href="https://vercel.com/templates/next.js/next-js-saas-starter"
target="_blank"
onClick={handleClick}
>
<Button size="lg" variant="outline" className="text-lg rounded-full">
Deploy your own
<ArrowRight className="ml-2 h-5 w-5" />
</Button>
</a>
);
}

export function ViewCodeButton() {
const handleClick = () => {
posthog.capture('view_code_clicked');
};

return (
<a
href="https://github.com/nextjs/saas-starter"
target="_blank"
onClick={handleClick}
>
<Button size="lg" variant="outline" className="text-lg rounded-full">
View the code
<ArrowRight className="ml-3 h-6 w-6" />
</Button>
</a>
);
}
37 changes: 35 additions & 2 deletions apps/next-js/15-app-router-saas/app/(login)/login.tsx
Original file line number Diff line number Diff line change
@@ -1,25 +1,58 @@
'use client';

import Link from 'next/link';
import { useActionState } from 'react';
import { useActionState, useRef, useEffect, useCallback } from 'react';
import { useSearchParams } from 'next/navigation';
import { Button } from '@/components/ui/button';
import { Input } from '@/components/ui/input';
import { Label } from '@/components/ui/label';
import { CircleIcon, Loader2 } from 'lucide-react';
import { signIn, signUp } from './actions';
import { ActionState } from '@/lib/auth/middleware';
import posthog from 'posthog-js';

export function Login({ mode = 'signin' }: { mode?: 'signin' | 'signup' }) {
const searchParams = useSearchParams();
const redirect = searchParams.get('redirect');
const priceId = searchParams.get('priceId');
const inviteId = searchParams.get('inviteId');
const formRef = useRef<HTMLFormElement>(null);
const lastEmailRef = useRef<string>('');
const [state, formAction, pending] = useActionState<ActionState, FormData>(
mode === 'signin' ? signIn : signUp,
{ error: '' }
);

// Track successful sign in/up and identify user
useEffect(() => {
// If we had a pending submission and now there's no error, the auth succeeded
// The page will redirect, but we capture the event first
if (lastEmailRef.current && !state.error && !pending) {
const email = lastEmailRef.current;
if (mode === 'signin') {
posthog.identify(email, { email });
posthog.capture('sign_in_submitted', { email, success: true });
} else {
posthog.identify(email, { email });
posthog.capture('sign_up_submitted', { email, success: true, has_invite: !!inviteId });
}
}
}, [state.error, pending, mode, inviteId]);

const handleSubmit = useCallback((formData: FormData) => {
const email = formData.get('email') as string;
lastEmailRef.current = email;

// Capture form submission attempt
if (mode === 'signin') {
posthog.capture('sign_in_submitted', { email });
} else {
posthog.capture('sign_up_submitted', { email, has_invite: !!inviteId });
}

return formAction(formData);
}, [mode, inviteId, formAction]);

return (
<div className="min-h-[100dvh] flex flex-col justify-center py-12 px-4 sm:px-6 lg:px-8 bg-gray-50">
<div className="sm:mx-auto sm:w-full sm:max-w-md">
Expand All @@ -34,7 +67,7 @@ export function Login({ mode = 'signin' }: { mode?: 'signin' | 'signup' }) {
</div>

<div className="mt-8 sm:mx-auto sm:w-full sm:max-w-md">
<form className="space-y-6" action={formAction}>
<form className="space-y-6" action={handleSubmit} ref={formRef}>
<input type="hidden" name="redirect" value={redirect || ''} />
<input type="hidden" name="priceId" value={priceId || ''} />
<input type="hidden" name="inviteId" value={inviteId || ''} />
Expand Down
27 changes: 27 additions & 0 deletions apps/next-js/15-app-router-saas/app/api/stripe/checkout/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import { setSession } from '@/lib/auth/session';
import { NextRequest, NextResponse } from 'next/server';
import { stripe } from '@/lib/payments/stripe';
import Stripe from 'stripe';
import { getPostHogClient } from '@/lib/posthog-server';

export async function GET(request: NextRequest) {
const searchParams = request.nextUrl.searchParams;
Expand Down Expand Up @@ -89,9 +90,35 @@ export async function GET(request: NextRequest) {
.where(eq(teams.id, userTeam[0].teamId));

await setSession(user[0]);

// Track successful checkout on server-side
const posthog = getPostHogClient();
posthog.capture({
distinctId: user[0].email,
event: 'stripe_checkout_completed',
properties: {
user_id: userId,
plan_name: (plan.product as Stripe.Product).name,
subscription_id: subscriptionId,
customer_id: customerId,
},
});

return NextResponse.redirect(new URL('/dashboard', request.url));
} catch (error) {
console.error('Error handling successful checkout:', error);

// Track failed checkout on server-side
const posthog = getPostHogClient();
posthog.capture({
distinctId: 'anonymous',
event: 'stripe_checkout_failed',
properties: {
error: error instanceof Error ? error.message : 'Unknown error',
session_id: searchParams.get('session_id'),
},
});

return NextResponse.redirect(new URL('/error', request.url));
}
}
Loading