Skip to content

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:

  1. "Get Started" buttons redirect to last1.app/auth?brand=radiocheck&redirect=...
  2. User sees RadioCheck-branded login/signup on Last1
  3. After auth, user is redirected back to /auth/callback
  4. Callback validates tokens and checks for organization
  5. User is routed to onboarding or checkout as appropriate
  6. 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:

NEXT_PUBLIC_SUPABASE_URL=https://your-radiocheck-project.supabase.co
NEXT_PUBLIC_SUPABASE_ANON_KEY=your-anon-key
NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY=pk_live_xxx