React

Sign in with UNET in a React app with a small provider and hook, a callback route, and a protected page. Plain React, no router assumed.

The SDK has no React code in it, and does not need any: one client, one provider, one hook. This recipe uses React 18 or 19 and any router (the examples use a plain pathname check so you can adapt them).

The client

Create the client once, outside any component:

// src/unet.ts
import { UnetId } from "@unet/id-web";

export const unet = new UnetId({
  clientId: import.meta.env.VITE_UNET_CLIENT_ID,
  redirectUri: `${location.origin}/auth/callback`,
  scopes: ["openid", "profile", "offline_access"],
});

A provider and a hook

// src/UnetProvider.tsx
import { createContext, useContext, useEffect, useState, type ReactNode } from "react";
import type { UnetUser } from "@unet/id-web";
import { unet } from "./unet";

type State = { user: UnetUser | null; loading: boolean };
const Ctx = createContext<State>({ user: null, loading: true });

export function UnetProvider({ children }: { children: ReactNode }) {
  const [state, setState] = useState<State>({ user: unet.getUser(), loading: true });

  useEffect(() => {
    // follows sign-in, sign-out and a profile change, also made in another tab
    const off = unet.onSessionChange((user) => setState({ user, loading: false }));
    unet.restore().then((user) => setState({ user, loading: false }), () => setState({ user: null, loading: false }));
    return off;
  }, []);

  return <Ctx.Provider value={state}>{children}</Ctx.Provider>;
}

export const useUnet = () => useContext(Ctx);

restore() runs once at startup and returns null when nobody is signed in. If the network is down it rejects, and the example treats that as "signed out" for the moment; the tokens are kept, so the next call tries again.

The callback route

The page at /auth/callback finishes the sign-in. Guard it so that React's strict mode, which runs effects twice in development, does not use the one-time code twice:

// src/AuthCallback.tsx
import { useEffect, useRef, useState } from "react";
import { AccessDenied } from "@unet/id-web";
import { unet } from "./unet";

export function AuthCallback() {
  const started = useRef(false);
  const [error, setError] = useState<string | null>(null);

  useEffect(() => {
    if (started.current) return;
    started.current = true;
    unet.handleRedirectCallback().then(
      ({ returnTo }) => location.replace(returnTo),
      (e) => setError(e instanceof AccessDenied ? "Sign-in was cancelled." : "Sign-in failed. Please try again."),
    );
  }, []);

  return error ? <p role="alert">{error}</p> : <p>Signing you in…</p>;
}

Using it

import { unet } from "./unet";
import { useUnet } from "./UnetProvider";

export function Header() {
  const { user, loading } = useUnet();
  if (loading) return null;
  return user ? (
    <button onClick={() => void unet.signOut()}>Sign out @{user.username}</button>
  ) : (
    <button onClick={() => void unet.signIn({ returnTo: location.pathname })}>Sign in with UNET</button>
  );
}

returnTo is where the person lands after the callback; the SDK keeps it for you while they are away on unet.social.

A protected page

import { useUnet } from "./UnetProvider";
import { unet } from "./unet";

export function Dashboard() {
  const { user, loading } = useUnet();
  if (loading) return <p>Loading…</p>;
  if (!user) {
    void unet.signIn({ returnTo: "/dashboard" });
    return null;
  }
  return <h1>Hello, {user.displayName ?? user.username}</h1>;
}

Server-side rendering

The SDK needs a browser. In Next.js or Remix, create the client and call its methods only in client components or effects ("use client"), never while rendering on the server.