Skip to Content

React Integration

⚡ 15 min read

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

Client-side rendering only. The Transcodes SDK runs exclusively in the browser. Never use it in server-side rendering (SSR) or server components.


Prerequisites

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

Choose how you load the SDK

See Quick Integration — Two Ways to Integrate for the full comparison.

ApproachBest for
Script in index.htmlcopy-paste Dashboard snippets
npm @bigstrider/transcodes-sdkVite SPA, bundler-based apps

Installation

Option A: Script in index.html (default — Dashboard flow)

Add SDK Script

Choose your integration mode:

Authentication Toolkit Cluster only — passkey login and step-up MFA:

index.html
<script type="module" src="https://cdn.transcodes.link/%VITE_TRANSCODES_PROJECT_ID%/webworker.js" ></script>

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

Set Environment Variables

.env
VITE_TRANSCODES_PROJECT_ID=proj_abc123xyz

Add TypeScript Type Definitions

Download transcodes.d.ts from the Transcodes Dashboard  and save as types/transcodes.d.ts

Then update your tsconfig.json:

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

If you prefer the npm SDK (no script tag), continue to Option B below.


Option B: npm SDK

Install the package

npm install @bigstrider/transcodes-sdk

Call init in main.tsx

Remove the Transcodes <script> from index.html if you added it earlier. Vite supports top-level await. Call init once before createRoot:

src/main.tsx
import { StrictMode } from 'react'; import { createRoot } from 'react-dom/client'; import { init } from '@bigstrider/transcodes-sdk'; import App from './App'; import './index.css'; await init({ projectId: import.meta.env.VITE_TRANSCODES_PROJECT_ID }); // Optional: await init({ projectId: '...', customUserId: 'uid_xxx', debug: true }); createRoot(document.getElementById('root')!).render( <StrictMode> <App /> </StrictMode>, );

Then import named exports in your components:

import { openAuthLoginModal, isAuthenticated, getCurrentMember, signOut, on, } from '@bigstrider/transcodes-sdk';

TypeScript

Types ship with the npm package (lib/types/). You do not need a separate transcodes.d.ts.

Skip Option A when you use npm only (no webworker.js in HTML).


Auth Context

You can use the same AuthContext shape with CDN or npm. Below is an npm SDK example.

src/context/AuthContext.tsx
'use client'; // Next.js only import { createContext, useEffect, useState, type ReactNode } from 'react'; import { isAuthenticated as sdkIsAuthenticated, openAuthLoginModal as sdkLogin, openAuthConsoleModal as sdkConsole, openAuthIdpModal as sdkIdp, signOut as sdkSignOut, on, } from '@bigstrider/transcodes-sdk'; interface AuthContextValue { isAuthenticated: boolean; isLoading: boolean; memberId: string | null; openAuthLoginModal: () => Promise<void>; openAuthConsoleModal: () => Promise<void>; openAuthIdpModal: (params: { resource: string; action: 'create' | 'read' | 'update' | 'delete'; }) => Promise<void>; signOut: () => Promise<void>; } export const AuthContext = createContext<AuthContextValue>({ isAuthenticated: false, isLoading: true, memberId: null, openAuthLoginModal: async () => {}, openAuthConsoleModal: async () => {}, openAuthIdpModal: async () => {}, signOut: async () => {}, }); export function AuthProvider({ children }: { children: ReactNode }) { const [isAuth, setIsAuth] = useState(false); const [isLoading, setIsLoading] = useState(true); const [memberId, setMemberId] = useState<string | null>(null); useEffect(() => { sdkIsAuthenticated().then((auth) => { setIsAuth(auth); setIsLoading(false); }); const unsubscribe = on('AUTH_STATE_CHANGED', ({ isAuthenticated, member }) => { setIsAuth(isAuthenticated); setMemberId(member?.id ?? null); }); return () => unsubscribe(); }, []); const value: AuthContextValue = { isAuthenticated: isAuth, isLoading, memberId, openAuthLoginModal: async () => { const result = await sdkLogin({ webhookNotification: false }); if (result.success) { setMemberId(result.payload[0]?.member?.id ?? null); setIsAuth(true); } }, openAuthConsoleModal: async () => { await sdkConsole(); }, openAuthIdpModal: async (params) => { await sdkIdp(params); }, signOut: async () => { await sdkSignOut({ webhookNotification: false }); setIsAuth(false); setMemberId(null); }, }; return <AuthContext.Provider value={value}>{children}</AuthContext.Provider>; }

With CDN, swap sdkIsAuthenticated() for transcodes.token.isAuthenticated(), sdkLogin() for transcodes.openAuthLoginModal(), and so on.


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].member?.email); } } catch (error) { console.error('Login error:', error); } finally { setLoading(false); } }; return ( <button onClick={handleLogin} disabled={loading}> {loading ? 'Loading...' : 'Login with Passkey'} </button> ); }

Member Profile

src/components/MemberProfile.tsx
import { use, useEffect, useState } from 'react'; import { getCurrentMember } from '@bigstrider/transcodes-sdk'; import { AuthContext } from '../context/AuthContext'; import type { Member } from '../../types/transcodes'; export function MemberProfile() { const { isAuthenticated, signOut } = use(AuthContext); const [member, setMember] = useState<Member | null>(null); useEffect(() => { if (!isAuthenticated) { setMember(null); return; } void getCurrentMember().then(setMember); }, [isAuthenticated]); if (!isAuthenticated || !member) { return null; } return ( <div className='member-profile'> <h3>{member.name || 'Member'}</h3> <p>{member.email}</p> <button onClick={signOut}>Sign Out</button> </div> ); }

CDN (Option A): use await transcodes.token.getCurrentMember() instead of getCurrentMember() from npm.

Protected Route

A route guard that shows the login modal when the visitor 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