Skip to Content

React Integration

⚡ 15 min read

Integrate Transcodes WebAuthn/Passkey authentication into your Vite React application with TypeScript.


Prerequisites

  • Vite React project with TypeScript
  • Transcodes project ID from Dashboard 
  • HTTPS environment (or localhost for development)

Installation

Add SDK Script

Add the Transcodes SDK script to your index.html:

index.html
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" /> <!-- Transcodes SDK --> <script type="module" src="https://cdn.transcodes.link/%VITE_TRANSCODES_PROJECT_ID%/webworker.js" ></script> <title>My App</title> </head> <body> <div id="root"></div> <script type="module" src="/src/main.tsx"></script> </body> </html>

Vite automatically replaces %VITE_*% placeholders in index.html with environment variables.

Set Environment Variables

.env
VITE_TRANSCODES_PROJECT_ID=proj_abc123xyz

Add TypeScript Type Definitions

Download the complete type definitions from the API Reference and save as types/transcodes.d.ts.

Then update your tsconfig.json:

tsconfig.json
{ "compilerOptions": { "typeRoots": ["./node_modules/@types", "./types"] }, "include": ["src", "types"] }

Auth Context

Create an AuthContext to manage authentication state across your application:

src/context/AuthContext.tsx
import { createContext, useEffect, useState, type ReactNode } from 'react'; import type { ApiResponse, AuthResult, AuthStateChangedPayload, User, } from '../../types/transcodes'; interface AuthContextValue { isAuthenticated: boolean; isLoading: boolean; userId: string | null; openAuthLoginModal: () => Promise<ApiResponse<AuthResult[]>>; openAuthModal: () => Promise<ApiResponse<null>>; openAuthMfaModal: () => Promise<ApiResponse<AuthResult[]>>; getUser: () => Promise<ApiResponse<User[]>>; signOut: () => Promise<void>; } const projectId = import.meta.env.VITE_TRANSCODES_PROJECT_ID; const initialValue: AuthContextValue = { isAuthenticated: false, isLoading: true, userId: null, signOut: () => transcodes.token.signOut(), openAuthLoginModal: () => transcodes.openAuthLoginModal({ projectId, }), openAuthModal: () => Promise.reject(new Error('Not authenticated')), openAuthMfaModal: () => Promise.reject(new Error('Not authenticated')), getUser: () => Promise.reject(new Error('Not authenticated')), }; export const AuthContext = createContext<AuthContextValue>(initialValue); export function AuthProvider({ children }: { children: ReactNode }) { const [isAuthenticated, setIsAuthenticated] = useState(false); const [isLoading, setIsLoading] = useState(true); const [userId, setUserId] = useState<string | null>(null); // Check initial auth state and subscribe to changes useEffect(() => { transcodes.token.isAuthenticated().then((isAuth) => { setIsAuthenticated(isAuth); setIsLoading(false); }); const handleAuthChange = (e: AuthStateChangedPayload) => { setIsAuthenticated(e.isAuthenticated); if (!e.isAuthenticated) { setUserId(null); } }; const unsubscribe = transcodes.on('AUTH_STATE_CHANGED', handleAuthChange); return () => unsubscribe(); }, []); const openAuthLoginModal = async () => { const result = await transcodes.openAuthLoginModal({ projectId, }); if (result.success && result.payload.length > 0) { setUserId(result.payload[0].user.id); } return result; }; const value: AuthContextValue = { isAuthenticated, isLoading, userId, openAuthLoginModal, openAuthModal: () => { if (!userId) return Promise.reject(new Error('Not authenticated')); return transcodes.openAuthModal({ projectId, userId, }); }, openAuthMfaModal: () => { if (!userId) return Promise.reject(new Error('Not authenticated')); return transcodes.openAuthMfaModal({ projectId, userId, }); }, getUser: () => { if (!userId) return Promise.reject(new Error('Not authenticated')); return transcodes.user.get({ projectId, userId }); }, signOut: () => transcodes.token.signOut(), }; return <AuthContext.Provider value={value}>{children}</AuthContext.Provider>; }

Components

Login Button

src/components/LoginButton.tsx
import { use, useState } from 'react'; import { AuthContext } from '../context/AuthContext'; export function LoginButton() { const { openAuthLoginModal } = use(AuthContext); const [loading, setLoading] = useState(false); const handleLogin = async () => { setLoading(true); try { const result = await openAuthLoginModal(); if (result.success) { console.log('Login successful:', result.payload[0].user.email); } } catch (error) { console.error('Login error:', error); } finally { setLoading(false); } }; return ( <button onClick={handleLogin} disabled={loading}> {loading ? 'Loading...' : 'Login with Passkey'} </button> ); }

User Profile

src/components/UserProfile.tsx
import { use, useEffect, useState } from 'react'; import { AuthContext } from '../context/AuthContext'; import type { User } from '../../types/transcodes'; export function UserProfile() { const { isAuthenticated, getUser, signOut } = use(AuthContext); const [user, setUser] = useState<User | null>(null); useEffect(() => { if (isAuthenticated) { getUser().then((result) => { if (result.success && result.payload.length > 0) { setUser(result.payload[0]); } }); } }, [isAuthenticated, getUser]); if (!isAuthenticated || !user) { return null; } return ( <div className='user-profile'> <h3>{user.name || 'User'}</h3> <p>{user.email}</p> <button onClick={signOut}>Sign Out</button> </div> ); }

Protected Route

A route guard that shows login modal when user is not authenticated:

src/components/ProtectedRoute.tsx
import { use, useEffect, useRef } from 'react'; import { useNavigate } from 'react-router-dom'; import { AuthContext } from '../context/AuthContext'; interface ProtectedRouteProps { children: React.ReactNode; redirectTo?: string; } export function ProtectedRoute({ children, redirectTo = '/', }: ProtectedRouteProps) { const { isAuthenticated, isLoading, openAuthLoginModal } = use(AuthContext); const navigate = useNavigate(); const hasAttemptedLogin = useRef(false); useEffect(() => { if (isLoading || isAuthenticated || hasAttemptedLogin.current) return; hasAttemptedLogin.current = true; openAuthLoginModal() .then((result) => { if (!result.success) { navigate(redirectTo, { replace: true }); } }) .catch(() => { navigate(redirectTo, { replace: true }); }); }, [isLoading, isAuthenticated, openAuthLoginModal, navigate, redirectTo]); if (isLoading) { return <div>Loading...</div>; } if (!isAuthenticated) { return null; } return <>{children}</>; }

App Setup

src/App.tsx
import { BrowserRouter, Routes, Route } from 'react-router-dom'; import { AuthProvider } from './context/AuthContext'; import { ProtectedRoute } from './components/ProtectedRoute'; import { HomePage } from './pages/HomePage'; import { DashboardPage } from './pages/DashboardPage'; function App() { return ( <AuthProvider> <BrowserRouter> <Routes> <Route path='/' element={<HomePage />} /> <Route path='/dashboard' element={ <ProtectedRoute> <DashboardPage /> </ProtectedRoute> } /> </Routes> </BrowserRouter> </AuthProvider> ); } export default App;

Custom Hook

A simpler hook for basic authentication needs:

src/hooks/useAuth.ts
import { useState, useEffect } from 'react'; import type { AuthStateChangedPayload } from '../../types/transcodes'; const projectId = import.meta.env.VITE_TRANSCODES_PROJECT_ID; export function useAuth() { const [isAuthenticated, setIsAuthenticated] = useState(false); const [loading, setLoading] = useState(true); useEffect(() => { // Check initial auth state transcodes.token.isAuthenticated().then((isAuth) => { setIsAuthenticated(isAuth); setLoading(false); }); // Subscribe to auth changes const unsubscribe = transcodes.on( 'AUTH_STATE_CHANGED', (payload: AuthStateChangedPayload) => { setIsAuthenticated(payload.isAuthenticated); } ); return () => unsubscribe(); }, []); const login = async () => { const result = await transcodes.openAuthLoginModal({ projectId, }); return result; }; const signOut = () => transcodes.token.signOut(); return { isAuthenticated, loading, login, signOut, }; }

API Calls with Token

src/services/api.ts
const API_BASE_URL = import.meta.env.VITE_API_URL || 'https://api.example.com'; export async function fetchWithAuth( endpoint: string, options: RequestInit = {} ) { const token = await transcodes.token.getAccessToken(); if (!token) { throw new Error('Not authenticated'); } const response = await fetch(`${API_BASE_URL}${endpoint}`, { ...options, headers: { ...options.headers, Authorization: `Bearer ${token}`, 'Content-Type': 'application/json', }, }); if (!response.ok) { throw new Error(`API error: ${response.status}`); } return response.json(); }

Best Practices

Recommended patterns for using Transcodes in React applications.

  1. Use Context for Global State: Manage auth state with React Context
  2. Subscribe to Events: Use AUTH_STATE_CHANGED to sync React state
  3. Always Cleanup: Unsubscribe from events in useEffect cleanup
  4. Async Methods: Remember isAuthenticated() and getAccessToken() are async
  5. Type Safety: Use the provided TypeScript definitions

Common Mistakes

// WRONG: isAuthenticated() is async if (transcodes.token.isAuthenticated()) { // This always runs! (Promise is truthy) } // CORRECT: use await if (await transcodes.token.isAuthenticated()) { // This correctly checks auth status }

Next Steps

Last updated on