React Integration
⚡ 15 min readIntegrate 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
localhostfor 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_abc123xyzAdd 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.
- Use Context for Global State: Manage auth state with React Context
- Subscribe to Events: Use
AUTH_STATE_CHANGEDto sync React state - Always Cleanup: Unsubscribe from events in useEffect cleanup
- Async Methods: Remember
isAuthenticated()andgetAccessToken()are async - 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
- Next.js Integration - Server-side rendering with Next.js
- API Reference - Full API documentation
- Security Best Practices - Security guidelines
Last updated on