RadioCheck.app Auth Integration - Prompt for Lovable¶
Copy this prompt to implement the auth redirect flow on radiocheck.app (Lovable).
Task: Implement Last1 Auth Redirect Flow¶
RadioCheck.app uses Last1 as its authentication provider. When users click "Get Started" or "Subscribe", they should be redirected to Last1 for authentication, then back to complete their purchase.
1. Auth Redirect Flow¶
"Get Started" / "Sign Up" Button Handler:
function handleGetStarted() {
const redirectUrl = `${window.location.origin}/auth/callback`;
const authUrl = `https://last1.app/auth?brand=radiocheck&redirect=${encodeURIComponent(redirectUrl)}&mode=signup`;
window.location.href = authUrl;
}
function handleLogin() {
const redirectUrl = `${window.location.origin}/auth/callback`;
const authUrl = `https://last1.app/auth?brand=radiocheck&redirect=${encodeURIComponent(redirectUrl)}&mode=login`;
window.location.href = authUrl;
}
2. Auth Callback Page¶
Create a new route at /auth/callback to handle the redirect from Last1.
URL Parameters from Last1:
- token - Access token (JWT)
- refresh_token - Refresh token for session persistence
- user_id - User's UUID
- error - Error message (if auth failed)
Callback Handler:
// pages/auth/callback.tsx or equivalent
import { useEffect, useState } from 'react';
import { useRouter } from 'next/router'; // or your routing solution
import { createClient } from '@supabase/supabase-js';
// RadioCheck Supabase client (for app data)
const supabase = createClient(
process.env.NEXT_PUBLIC_SUPABASE_URL!,
process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!
);
export default function AuthCallback() {
const router = useRouter();
const [status, setStatus] = useState<'loading' | 'success' | 'error'>('loading');
const [error, setError] = useState<string | null>(null);
useEffect(() => {
async function handleCallback() {
const params = new URLSearchParams(window.location.search);
// Check for errors
const errorParam = params.get('error');
if (errorParam) {
setStatus('error');
setError(errorParam);
return;
}
const token = params.get('token');
const refreshToken = params.get('refresh_token');
const userId = params.get('user_id');
if (!token || !userId) {
setStatus('error');
setError('Missing authentication data');
return;
}
try {
// Store the session tokens (for future authenticated requests)
localStorage.setItem('last1_access_token', token);
localStorage.setItem('last1_refresh_token', refreshToken || '');
localStorage.setItem('last1_user_id', userId);
// Ensure user exists in RadioCheck database
const { data: user, error: userError } = await supabase
.from('users')
.select('*')
.eq('id', userId)
.single();
if (userError && userError.code !== 'PGRST116') {
throw new Error('Failed to fetch user');
}
// Check if user has an organization
const { data: orgMembership } = await supabase
.from('organization_members')
.select('organization_id, role')
.eq('user_id', userId)
.single();
if (orgMembership) {
// User has an org - redirect to checkout or dashboard
const pendingCheckout = sessionStorage.getItem('pending_checkout');
if (pendingCheckout) {
sessionStorage.removeItem('pending_checkout');
router.push(`/checkout?tier=${pendingCheckout}`);
} else {
router.push('/dashboard');
}
} else {
// User needs to create/join an organization
router.push('/onboarding/organization');
}
setStatus('success');
} catch (err) {
console.error('Auth callback error:', err);
setStatus('error');
setError(err instanceof Error ? err.message : 'Authentication failed');
}
}
handleCallback();
}, [router]);
if (status === 'loading') {
return (
<div className="flex items-center justify-center min-h-screen">
<div className="text-center">
<div className="animate-spin rounded-full h-12 w-12 border-b-2 border-gold mx-auto mb-4"></div>
<p>Completing sign in...</p>
</div>
</div>
);
}
if (status === 'error') {
return (
<div className="flex items-center justify-center min-h-screen">
<div className="text-center">
<h1 className="text-2xl font-bold text-red-600 mb-4">Authentication Failed</h1>
<p className="text-gray-600 mb-6">{error}</p>
<button
onClick={() => router.push('/')}
className="bg-gold text-black px-6 py-2 rounded-lg"
>
Return Home
</button>
</div>
</div>
);
}
return null; // Will redirect
}
3. Organization Onboarding Page¶
Create /onboarding/organization for users who need to create or join an organization.
// pages/onboarding/organization.tsx
export default function OrganizationOnboarding() {
const [mode, setMode] = useState<'create' | 'join'>('create');
return (
<div className="max-w-md mx-auto py-12 px-4">
<h1 className="text-2xl font-bold mb-6">Set Up Your Organization</h1>
<div className="flex gap-4 mb-8">
<button
onClick={() => setMode('create')}
className={`flex-1 py-2 rounded ${mode === 'create' ? 'bg-gold text-black' : 'bg-gray-200'}`}
>
Create New
</button>
<button
onClick={() => setMode('join')}
className={`flex-1 py-2 rounded ${mode === 'join' ? 'bg-gold text-black' : 'bg-gray-200'}`}
>
Join Existing
</button>
</div>
{mode === 'create' ? (
<CreateOrganizationForm />
) : (
<JoinOrganizationForm />
)}
</div>
);
}
4. Checkout Flow with Auth¶
Update the pricing page to handle auth state:
// components/PricingSection.tsx
function PricingCard({ tier, price, features }) {
const userId = localStorage.getItem('last1_user_id');
const handleSelectPlan = () => {
if (!userId) {
// Store the selected tier for after auth
sessionStorage.setItem('pending_checkout', tier);
// Redirect to Last1 auth
const redirectUrl = `${window.location.origin}/auth/callback`;
window.location.href = `https://last1.app/auth?brand=radiocheck&redirect=${encodeURIComponent(redirectUrl)}&mode=signup`;
} else {
// User is logged in - go to checkout
window.location.href = `/checkout?tier=${tier}`;
}
};
return (
<div className="pricing-card">
<h3>{tier}</h3>
<p className="price">${price}/mo</p>
<ul>{features.map(f => <li key={f}>{f}</li>)}</ul>
<button onClick={handleSelectPlan}>
{userId ? 'Subscribe' : 'Get Started'}
</button>
</div>
);
}
5. Checkout Page¶
Create /checkout to handle Stripe checkout:
// pages/checkout.tsx
import { useEffect, useState } from 'react';
import { loadStripe } from '@stripe/stripe-js';
const stripePromise = loadStripe(process.env.NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY!);
export default function CheckoutPage() {
const [loading, setLoading] = useState(false);
const tier = new URLSearchParams(window.location.search).get('tier');
const userId = localStorage.getItem('last1_user_id');
useEffect(() => {
if (!userId) {
// Not logged in - redirect to auth
sessionStorage.setItem('pending_checkout', tier || 'pro');
window.location.href = '/';
}
}, [userId, tier]);
const handleCheckout = async () => {
setLoading(true);
try {
// Call your Supabase Edge Function to create Stripe checkout
const response = await fetch(
`${process.env.NEXT_PUBLIC_SUPABASE_URL}/functions/v1/stripe-checkout`,
{
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${localStorage.getItem('last1_access_token')}`,
},
body: JSON.stringify({
tier,
userId,
successUrl: `${window.location.origin}/checkout/success`,
cancelUrl: `${window.location.origin}/pricing`,
}),
}
);
const { sessionId, error } = await response.json();
if (error) throw new Error(error);
// Redirect to Stripe Checkout
const stripe = await stripePromise;
await stripe?.redirectToCheckout({ sessionId });
} catch (err) {
console.error('Checkout error:', err);
alert('Failed to start checkout. Please try again.');
} finally {
setLoading(false);
}
};
return (
<div className="max-w-md mx-auto py-12 px-4">
<h1 className="text-2xl font-bold mb-6">Complete Your Subscription</h1>
<div className="bg-gray-100 rounded-lg p-6 mb-6">
<h2 className="font-bold">{tier?.toUpperCase()} Plan</h2>
{/* Show plan details */}
</div>
<button
onClick={handleCheckout}
disabled={loading}
className="w-full bg-gold text-black py-3 rounded-lg font-bold"
>
{loading ? 'Processing...' : 'Proceed to Payment'}
</button>
</div>
);
}
6. Auth State Management¶
Create a simple auth context to manage state across the app:
// context/AuthContext.tsx
import { createContext, useContext, useEffect, useState } from 'react';
interface AuthContextType {
userId: string | null;
isAuthenticated: boolean;
logout: () => void;
}
const AuthContext = createContext<AuthContextType>({
userId: null,
isAuthenticated: false,
logout: () => {},
});
export function AuthProvider({ children }) {
const [userId, setUserId] = useState<string | null>(null);
useEffect(() => {
// Check for stored auth on mount
const storedUserId = localStorage.getItem('last1_user_id');
if (storedUserId) {
setUserId(storedUserId);
}
}, []);
const logout = () => {
localStorage.removeItem('last1_access_token');
localStorage.removeItem('last1_refresh_token');
localStorage.removeItem('last1_user_id');
setUserId(null);
window.location.href = '/';
};
return (
<AuthContext.Provider value={{ userId, isAuthenticated: !!userId, logout }}>
{children}
</AuthContext.Provider>
);
}
export const useAuth = () => useContext(AuthContext);
7. Navigation Updates¶
Update the header/nav to show auth state:
// components/Header.tsx
import { useAuth } from '../context/AuthContext';
export function Header() {
const { isAuthenticated, logout } = useAuth();
return (
<header>
<nav>
<a href="/">Radio Check</a>
<div className="flex gap-4">
{isAuthenticated ? (
<>
<a href="/dashboard">Dashboard</a>
<button onClick={logout}>Sign Out</button>
</>
) : (
<>
<button onClick={() => {
const redirectUrl = `${window.location.origin}/auth/callback`;
window.location.href = `https://last1.app/auth?brand=radiocheck&redirect=${encodeURIComponent(redirectUrl)}&mode=login`;
}}>
Sign In
</button>
<button onClick={() => {
const redirectUrl = `${window.location.origin}/auth/callback`;
window.location.href = `https://last1.app/auth?brand=radiocheck&redirect=${encodeURIComponent(redirectUrl)}&mode=signup`;
}} className="bg-gold text-black px-4 py-2 rounded">
Get Started
</button>
</>
)}
</div>
</nav>
</header>
);
}
Summary¶
After implementing this:
- "Get Started" buttons redirect to
last1.app/auth?brand=radiocheck&redirect=... - User sees RadioCheck-branded login/signup on Last1
- After auth, user is redirected back to
/auth/callback - Callback validates tokens and checks for organization
- User is routed to onboarding or checkout as appropriate
- Stripe checkout completes the subscription
Routes to Create:
- /auth/callback - Handle Last1 redirect
- /onboarding/organization - Create/join org
- /checkout - Stripe checkout flow
- /checkout/success - Post-payment success page
- /dashboard - Authenticated user dashboard
Environment Variables: