React Integration Guide

Complete guide to integrating HelioRim passkey authentication in React applications.

Installation

npm install @heliorim/sdk-react
# or
yarn add @heliorim/sdk-react
# or
pnpm add @heliorim/sdk-react

Setup

1. Wrap Your App with Provider

// App.tsx
import { HelioRimProvider } from '@heliorim/sdk-react';

function App() {
  const config = {
    tenantId: process.env.REACT_APP_HELIORIM_TENANT_ID,
    apiUrl: 'https://auth.api.heliorim.com',
    appId: 'your-app-id'
  };

  return (
    <HelioRimProvider config={config}>
      <YourAppContent />
    </HelioRimProvider>
  );
}

export default App;

2. Use Authentication Hooks

// components/LoginButton.tsx
import { useAuth, usePasskey } from '@heliorim/sdk-react';

function LoginButton() {
  const { isAuthenticated, user, logout } = useAuth();
  const { authenticate, isAuthenticating } = usePasskey();

  if (isAuthenticated) {
    return (
      <div>
        <span>Welcome, {user?.email}</span>
        <button onClick={logout}>Sign Out</button>
      </div>
    );
  }

  return (
    <button
      onClick={() => authenticate()}
      disabled={isAuthenticating}
    >
      {isAuthenticating ? 'Authenticating...' : 'Sign In with Passkey'}
    </button>
  );
}

Available Hooks

Complete Example: Authentication Flow

// pages/AuthPage.tsx
import React, { useState } from 'react';
import {
  useAuth,
  usePasskey,
  useRecovery,
  useHelioRim
} from '@heliorim/sdk-react';

function AuthPage() {
  const { isAuthenticated, user, logout } = useAuth();
  const { register, authenticate, credentials, deleteCredential } = usePasskey();
  const { generateBackupCodes } = useRecovery();
  const { isSupported, capabilities } = useHelioRim();

  const [email, setEmail] = useState('');
  const [displayName, setDisplayName] = useState('');
  const [mode, setMode] = useState<'login' | 'register'>('login');

  if (!isSupported) {
    return (
      <div className="alert alert-warning">
        Your browser doesn't support passkeys.
        Please use a modern browser.
      </div>
    );
  }

  const handleRegister = async (e: React.FormEvent) => {
    e.preventDefault();
    try {
      // Register new passkey
      const credential = await register({
        email: email,
        displayName: displayName,
        userVerification: 'required'
      });

      // Generate backup codes
      const codes = await generateBackupCodes();
      console.log('Save these backup codes:', codes);

      alert('Registration successful!');
    } catch (error) {
      console.error('Registration failed:', error);
    }
  };

  const handleLogin = async (e: React.FormEvent) => {
    e.preventDefault();
    try {
      await authenticate({
        email: email ?? undefined,
        userVerification: 'required'
      });
      alert('Login successful!');
    } catch (error) {
      console.error('Login failed:', error);
    }
  };

  if (isAuthenticated) {
    return (
      <div className="dashboard">
        <h2>Welcome, {user?.email}</h2>

        <div className="credentials-section">
          <h3>Your Passkeys</h3>
          {credentials.map(cred => (
            <div key={cred.id} className="credential-item">
              <span>{cred.name}</span>
              <span>{new Date(cred.createdAt).toLocaleDateString()}</span>
              <button onClick={() => deleteCredential(cred.id)}>
                Remove
              </button>
            </div>
          ))}
        </div>

        <button onClick={logout} className="btn-logout">
          Sign Out
        </button>
      </div>
    );
  }

  return (
    <div className="auth-container">
      <div className="auth-tabs">
        <button
          className={mode === 'login' ? 'active' : ''}
          onClick={() => setMode('login')}
        >
          Sign In
        </button>
        <button
          className={mode === 'register' ? 'active' : ''}
          onClick={() => setMode('register')}
        >
          Register
        </button>
      </div>

      {mode === 'login' ? (
        <form onSubmit={handleLogin}>
          <input
            type="email"
            value={email}
            onChange={(e) => setEmail(e.target.value)}
            placeholder="Email (optional for returning users)"
          />
          <button type="submit">Sign In with Passkey</button>
        </form>
      ) : (
        <form onSubmit={handleRegister}>
          <input
            type="email"
            value={email}
            onChange={(e) => setEmail(e.target.value)}
            placeholder="Email"
            required
          />
          <input
            type="text"
            value={displayName}
            onChange={(e) => setDisplayName(e.target.value)}
            placeholder="Display Name"
            required
          />
          <button type="submit">Create Account with Passkey</button>
        </form>
      )}
    </div>
  );
}

export default AuthPage;

TypeScript Support

// types/auth.ts
import type {
  UseAuthReturn,
  UsePasskeyReturn,
  Session,
  Credential,
  WebAuthnCapabilities
} from '@heliorim/sdk-react';

// All types are fully typed and exported
interface AuthContextValue {
  auth: UseAuthReturn;
  passkey: UsePasskeyReturn;
  session: Session | null;
}

// Custom hook with types
function useAuthContext(): AuthContextValue {
  const auth = useAuth();
  const passkey = usePasskey();

  return {
    auth,
    passkey,
    session: auth.session
  };
}

Protected Routes

// components/ProtectedRoute.tsx
import { Navigate } from 'react-router-dom';
import { useAuth } from '@heliorim/sdk-react';

interface ProtectedRouteProps {
  children: React.ReactNode;
  redirectTo?: string;
}

export function ProtectedRoute({
  children,
  redirectTo = '/login'
}: ProtectedRouteProps) {
  const { isAuthenticated, isLoading } = useAuth();

  if (isLoading) {
    return <div>Loading...</div>;
  }

  if (!isAuthenticated) {
    return <Navigate to={redirectTo} replace />;
  }

  return <>{children}</>;
}

// Usage in App.tsx
<Routes>
  <Route path="/login" element={<LoginPage />} />
  <Route
    path="/dashboard"
    element={
      <ProtectedRoute>
        <Dashboard />
      </ProtectedRoute>
    }
  />
</Routes>

Error Handling

import { useAuth, usePasskey } from '@heliorim/sdk-react';

function ErrorBoundaryAuth() {
  const { error: authError, clearError: clearAuthError } = useAuth();
  const { error: passkeyError, clearError: clearPasskeyError } = usePasskey();

  // Global error display
  if (authError ?? passkeyError) {
    return (
      <div className="error-banner">
        <p>{authError?.message ?? passkeyError?.message}</p>
        <button onClick={() => {
          clearAuthError();
          clearPasskeyError();
        }}>
          Dismiss
        </button>
      </div>
    );
  }

  return null;
}

// Specific error handling
async function handleAuthentication() {
  try {
    await authenticate();
  } catch (error) {
    if (error.name === 'NotAllowedError') {
      // User cancelled
      showToast('Authentication cancelled');
    } else if (error.name === 'InvalidStateError') {
      // Credential already exists
      showToast('This device is already registered');
    } else {
      // Generic error
      showToast('Authentication failed. Please try again.');
    }
  }
}

Best Practices

✓ Recommended

  • Always wrap your app with HelioRimProvider
  • Check isSupported before showing passkey UI
  • Handle loading states with isLoading flags
  • Provide clear error messages to users
  • Use TypeScript for better type safety
  • Implement proper error boundaries

✗ Avoid

  • Don't call hooks outside React components
  • Don't store sensitive data in localStorage
  • Don't skip user verification for sensitive ops
  • Don't assume passkeys are available

Next Steps