expo-supabase-email-confirm
v1.0.1
Published
Add email confirmation with success screen to Expo + Supabase apps
Maintainers
Readme
expo-supabase-email-confirm
Add email confirmation with a success screen to your Expo + Supabase app. Uses direct fetch calls to Supabase Auth API for maximum control and reliability.
The Problem
When users verify their email via deep link:
- They get immediately redirected to the dashboard without confirmation
- Global auth redirects interfere with showing success screens
- Supabase client methods can have timing issues on cold start
The Solution
This package provides:
- A dedicated confirmation screen that handles the deep link
- Direct fetch-based auth verification (no Supabase client dependency for critical paths)
- Support for multiple auth flows (PKCE, implicit, OTP)
- A success message displayed for a configurable duration
- Proper error handling with user-friendly messages
Installation
npx expo-supabase-email-confirmThe CLI will prompt you for:
- App scheme (from your app.json)
- Success/error message text
- Loading text
- Display duration
- Colors
- File paths
What Gets Created
pendingAuthUrl.ts- URL handoff utility between root layout and confirm screenconfirm.tsx- The confirmation screen with fetch-based auth
Auth Flows Supported
The generated code handles all Supabase email confirmation flows:
1. PKCE Flow (Recommended)
URL: yourscheme://auth/confirm?code=abc123Exchanges authorization code for session tokens via:
POST /auth/v1/token?grant_type=pkce2. Implicit Flow
URL: yourscheme://auth/confirm#access_token=...&refresh_token=...Tokens are in the URL fragment and set directly.
3. OTP/Token Hash Flow
URL: yourscheme://auth/confirm?token_hash=...&type=signupVerifies the token via:
POST /auth/v1/verifyManual Setup Required
After running the CLI, you need to:
1. Update app/_layout.tsx
Add the URL handoff in your deep link handler:
import { setPendingAuthUrl } from '@/utils/pendingAuthUrl';
// In your deep link handling useEffect:
if (url.includes('auth/confirm')) {
setPendingAuthUrl(url);
return; // Don't process here - let confirm screen handle it
}2. Bypass Auth Redirect for Confirm Screen
In your auth redirect hook, add an early return:
const onConfirmScreen = segments[0] === 'auth' && segments[1] === 'confirm';
// Let confirm screen handle its own navigation
if (onConfirmScreen) {
return;
}3. Add Route to Stack
<Stack.Screen name="auth/confirm" options={{ animation: 'none' }} />4. Configure Supabase
In Supabase Dashboard → Authentication → URL Configuration:
Redirect URLs: yourscheme://auth/confirm5. Implement Auth Store Integration
The generated confirm.tsx has placeholder functions you need to implement:
// getCurrentSession() - Check if session already exists
// setSession(accessToken, refreshToken) - Store tokens in your auth system
// isAuthStoreReady(userId) - Check if auth store has user loadedExample with Supabase client:
import { supabase } from '@/services/supabase';
import { useAuthStore } from '@/store';
async function getCurrentSession() {
const { data: { session } } = await supabase.auth.getSession();
return session;
}
async function setSession(accessToken: string, refreshToken: string) {
const { error } = await supabase.auth.setSession({
access_token: accessToken,
refresh_token: refreshToken,
});
return !error;
}
function isAuthStoreReady(userId: string) {
return useAuthStore.getState().user?.id === userId;
}How It Works
┌─────────────────────────────────────────────────────────────┐
│ 1. User taps email confirmation link │
└─────────────────────────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────┐
│ 2. App opens via deep link │
│ yourscheme://auth/confirm#access_token=... │
└─────────────────────────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────┐
│ 3. _layout.tsx detects auth/confirm URL │
│ → Stores URL via setPendingAuthUrl() │
│ → Does NOT process tokens (avoids duplicate sessions) │
└─────────────────────────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────┐
│ 4. confirm.tsx retrieves URL via getPendingAuthUrl() │
│ → Parses tokens from URL fragment/query │
│ → Calls Supabase Auth API directly via fetch │
└─────────────────────────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────┐
│ 5. Session established │
│ → Tokens stored via setSession() │
│ → Success screen shown for 2.5 seconds │
└─────────────────────────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────┐
│ 6. Navigate to dashboard │
│ → No skeleton overlay (doesn't call setTransitioning) │
└─────────────────────────────────────────────────────────────┘Key Implementation Details
- URL Handoff Pattern: Uses module-level state to pass URL from root layout to confirm screen, preventing duplicate token processing
- Direct Fetch Calls: Uses
fetch()to call Supabase Auth API instead of client methods for more control and better error handling - No Skeleton Overlay: Doesn't trigger
setTransitioning()to avoid covering the success screen - Minimum Display Time: Ensures success message is visible for the configured duration before navigation
- Auth Store Sync: Waits for auth store to update before navigating to prevent flicker
Dependencies
This package assumes you have:
expo-routerexpo-linkingreact-native-reanimated(optional, for animations)react-native-safe-area-context
Note: This package does NOT require @supabase/supabase-js for the confirmation flow itself - it uses direct fetch calls. However, you'll likely have it installed for other auth operations.
Environment Variables
The generated code expects these environment variables:
EXPO_PUBLIC_SUPABASE_URL=https://your-project.supabase.co
EXPO_PUBLIC_SUPABASE_ANON_KEY=your-anon-keyLicense
MIT
