Portal Guide
UI Components

Frontend SDK: UI Components

While Optare uses OAuth for authentication (meaning users sign in on id.optare.one), you can build custom UI components in your application to initiate the sign-in flow and display user information.

Sign-In Button Component

A simple button that redirects users to Optare for authentication:

import { signIn } from "@optare/optareid-react";
 
export function SignInButton() {
  const handleSignIn = async () => {
    await signIn.social({
      provider: "optare",
      callbackURL: "/dashboard",
    });
  };
 
  return (
    <button
      onClick={handleSignIn}
      className="px-6 py-3 bg-black text-white rounded-lg hover:bg-gray-800 transition-colors"
    >
      Sign in with Optare
    </button>
  );
}

Sign-In Page

A full sign-in page with branding:

import { signIn } from "@optare/optareid-react";
import Link from "next/link"; // or @remix-run/react
 
export default function SignInPage() {
  const handleSignIn = async () => {
    await signIn.social({
      provider: "optare",
      callbackURL: "/dashboard",
    });
  };
 
  return (
    <div className="min-h-screen flex items-center justify-center bg-gray-50">
      <div className="max-w-md w-full space-y-8">
        <div className="text-center">
          <h1 className="text-4xl font-bold text-gray-900">Welcome back</h1>
          <p className="mt-2 text-gray-600">
            Sign in to your account to continue
          </p>
        </div>
 
        <div className="bg-white py-8 px-6 shadow rounded-lg">
          <button
            onClick={handleSignIn}
            className="w-full flex justify-center py-3 px-4 border border-transparent rounded-md shadow-sm text-sm font-medium text-white bg-black hover:bg-gray-800 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-black"
          >
            Sign in with Optare
          </button>
          
          <p className="mt-6 text-center text-sm text-gray-600">
            Don't have an account?{" "}
            <Link href="/sign-up" className="font-medium text-black hover:text-gray-800">
              Sign up
            </Link>
          </p>
        </div>
 
        <p className="text-center text-xs text-gray-500">
          By signing in, you agree to our{" "}
          <Link href="/terms" className="underline">Terms of Service</Link>
          {" "}and{" "}
          <Link href="/privacy" className="underline">Privacy Policy</Link>
        </p>
      </div>
    </div>
  );
}

User Menu Dropdown

A dropdown menu showing user info with sign-out option:

import { useSession, signOut } from "@optare/optareid-react";
import { useState, useRef, useEffect } from "react";
import { ChevronDown, LogOut, User, Building } from "lucide-react";
 
export function UserMenu() {
  const { data: session } = useSession();
  const [isOpen, setIsOpen] = useState(false);
  const menuRef = useRef<HTMLDivElement>(null);
 
  // Close dropdown when clicking outside
  useEffect(() => {
    function handleClickOutside(event: MouseEvent) {
      if (menuRef.current && !menuRef.current.contains(event.target as Node)) {
        setIsOpen(false);
      }
    }
 
    document.addEventListener("mousedown", handleClickOutside);
    return () => document.removeEventListener("mousedown", handleClickOutside);
  }, []);
 
  if (!session) {
    return null;
  }
 
  const handleSignOut = async () => {
    await signOut({ callbackURL: "/" });
  };
 
  return (
    <div className="relative" ref={menuRef}>
      <button
        onClick={() => setIsOpen(!isOpen)}
        className="flex items-center space-x-3 px-3 py-2 rounded-lg hover:bg-gray-100 transition-colors"
      >
        <div className="flex items-center space-x-2">
          {session.user.image ? (
            <img
              src={session.user.image}
              alt={session.user.name}
              className="w-8 h-8 rounded-full"
            />
          ) : (
            <div className="w-8 h-8 rounded-full bg-gray-900 text-white flex items-center justify-center text-sm font-medium">
              {session.user.name[0].toUpperCase()}
            </div>
          )}
          <span className="text-sm font-medium text-gray-700">
            {session.user.name}
          </span>
        </div>
        <ChevronDown className={`w-4 h-4 text-gray-500 transition-transform ${isOpen ? "rotate-180" : ""}`} />
      </button>
 
      {isOpen && (
        <div className="absolute right-0 mt-2 w-64 bg-white rounded-lg shadow-lg border border-gray-200 py-1 z-50">
          {/* User Info */}
          <div className="px-4 py-3 border-b border-gray-100">
            <p className="text-sm font-medium text-gray-900">{session.user.name}</p>
            <p className="text-xs text-gray-500">{session.user.email}</p>
          </div>
 
          {/* Organization Info */}
          {session.organization && (
            <div className="px-4 py-3 border-b border-gray-100">
              <div className="flex items-center space-x-2 text-sm">
                <Building className="w-4 h-4 text-gray-400" />
                <div>
                  <p className="font-medium text-gray-900">{session.organization.name}</p>
                  <p className="text-xs text-gray-500 capitalize">{session.role}</p>
                </div>
              </div>
            </div>
          )}
 
          {/* Actions */}
          <div className="py-1">
            <button
              onClick={handleSignOut}
              className="flex items-center space-x-2 w-full px-4 py-2 text-sm text-gray-700 hover:bg-gray-50"
            >
              <LogOut className="w-4 h-4" />
              <span>Sign out</span>
            </button>
          </div>
        </div>
      )}
    </div>
  );
}

User Profile Card

Display user and organization information in a card:

import { useSession } from "@optare/optareid-react";
import { Mail, Building, Shield } from "lucide-react";
 
export function UserProfileCard() {
  const { data: session } = useSession();
 
  if (!session) {
    return null;
  }
 
  return (
    <div className="bg-white rounded-lg shadow p-6">
      <div className="flex items-center space-x-4 mb-6">
        {session.user.image ? (
          <img
            src={session.user.image}
            alt={session.user.name}
            className="w-16 h-16 rounded-full"
          />
        ) : (
          <div className="w-16 h-16 rounded-full bg-gray-900 text-white flex items-center justify-center text-2xl font-bold">
            {session.user.name[0].toUpperCase()}
          </div>
        )}
        <div>
          <h2 className="text-xl font-bold text-gray-900">{session.user.name}</h2>
          {session.user.emailVerified && (
            <span className="inline-flex items-center px-2 py-1 text-xs font-medium text-green-700 bg-green-100 rounded">
              ✓ Verified
            </span>
          )}
        </div>
      </div>
 
      <div className="space-y-3">
        <div className="flex items-center space-x-3 text-gray-600">
          <Mail className="w-5 h-5" />
          <span>{session.user.email}</span>
        </div>
 
        {session.organization && (
          <>
            <div className="flex items-center space-x-3 text-gray-600">
              <Building className="w-5 h-5" />
              <span>{session.organization.name}</span>
            </div>
 
            {session.role && (
              <div className="flex items-center space-x-3 text-gray-600">
                <Shield className="w-5 h-5" />
                <span className="capitalize">{session.role}</span>
              </div>
            )}
          </>
        )}
      </div>
    </div>
  );
}

Organization Switcher

Allow users to switch between multiple organizations:

import { useSession } from "@optare/optareid-react";
import { useState } from "react";
import { Check, ChevronDown } from "lucide-react";
 
interface Organization {
  id: string;
  name: string;
  slug: string;
}
 
interface OrganizationSwitcherProps {
  organizations: Organization[];
  onSwitch: (orgId: string) => void;
}
 
export function OrganizationSwitcher({ organizations, onSwitch }: OrganizationSwitcherProps) {
  const { data: session } = useSession();
  const [isOpen, setIsOpen] = useState(false);
 
  if (!session?.organization) {
    return null;
  }
 
  const currentOrg = session.organization;
 
  return (
    <div className="relative">
      <button
        onClick={() => setIsOpen(!isOpen)}
        className="flex items-center justify-between w-full px-4 py-2 text-sm bg-white border border-gray-300 rounded-lg hover:bg-gray-50"
      >
        <span className="font-medium">{currentOrg.name}</span>
        <ChevronDown className="w-4 h-4 text-gray-500" />
      </button>
 
      {isOpen && (
        <div className="absolute z-10 w-full mt-2 bg-white border border-gray-200 rounded-lg shadow-lg">
          {organizations.map((org) => (
            <button
              key={org.id}
              onClick={() => {
                onSwitch(org.id);
                setIsOpen(false);
              }}
              className="flex items-center justify-between w-full px-4 py-2 text-sm hover:bg-gray-50 first:rounded-t-lg last:rounded-b-lg"
            >
              <span>{org.name}</span>
              {org.id === currentOrg.id && (
                <Check className="w-4 h-4 text-green-600" />
              )}
            </button>
          ))}
        </div>
      )}
    </div>
  );
}

Protected Content Wrapper

Show content only to specific roles:

import { useSession } from "@optare/optareid-react";
import { ReactNode } from "react";
 
interface ProtectedContentProps {
  children: ReactNode;
  allowedRoles?: Array<"owner" | "admin" | "member">;
  fallback?: ReactNode;
}
 
export function ProtectedContent({ 
  children, 
  allowedRoles = ["owner", "admin", "member"],
  fallback = null 
}: ProtectedContentProps) {
  const { data: session } = useSession();
 
  if (!session) {
    return <>{fallback}</>;
  }
 
  if (!allowedRoles.includes(session.role || "member")) {
    return <>{fallback}</>;
  }
 
  return <>{children}</>;
}

Usage:

export function SettingsPage() {
  return (
    <div>
      <h1>Settings</h1>
 
      {/* Everyone can see */}
      <div>
        <h2>Profile Settings</h2>
        <ProfileForm />
      </div>
 
      {/* Only admins and owners */}
      <ProtectedContent allowedRoles={["owner", "admin"]}>
        <div>
          <h2>Team Settings</h2>
          <TeamForm />
        </div>
      </ProtectedContent>
 
      {/* Only owners */}
      <ProtectedContent 
        allowedRoles={["owner"]}
        fallback={<p className="text-gray-500">Only organization owners can access this section.</p>}
      >
        <div>
          <h2>Billing</h2>
          <BillingForm />
        </div>
      </ProtectedContent>
    </div>
  );
}

Loading Skeleton

Show a skeleton while session loads:

export function SessionLoadingSkeleton() {
  return (
    <div className="animate-pulse">
      <div className="flex items-center space-x-4">
        <div className="w-12 h-12 bg-gray-300 rounded-full"></div>
        <div className="flex-1 space-y-2">
          <div className="h-4 bg-gray-300 rounded w-3/4"></div>
          <div className="h-3 bg-gray-300 rounded w-1/2"></div>
        </div>
      </div>
    </div>
  );
}

Usage:

import { useSession } from "@optare/optareid-react";
import { SessionLoadingSkeleton } from "~/components/SessionLoadingSkeleton";
 
export function Header() {
  const { data: session, isPending } = useSession();
 
  return (
    <header>
      {isPending ? (
        <SessionLoadingSkeleton />
      ) : session ? (
        <UserMenu />
      ) : (
        <SignInButton />
      )}
    </header>
  );
}

Styling Guidelines

All examples use Tailwind CSS for styling. You can customize these components to match your design system:

  • Replace Tailwind classes with your own CSS modules or styled-components
  • Adjust colors, spacing, and typography to match your brand
  • Use your own icon library instead of Lucide React

Best Practices

1. Always Handle Loading States

Show skeletons or loading indicators while session data loads:

const { data: session, isPending } = useSession();
 
if (isPending) {
  return <LoadingSkeleton />;
}

2. Provide Fallbacks

For protected content, show helpful messages instead of hiding elements:

<ProtectedContent 
  allowedRoles={["owner"]}
  fallback={<p>Only owners can access this feature.</p>}
>
  {/* Protected content */}
</ProtectedContent>

3. Use Semantic HTML

Ensure your components are accessible:

<button
  onClick={handleSignIn}
  aria-label="Sign in with Optare"
>
  Sign in
</button>

Next Steps