npm package: @fjordid/react
Description: React integration for FjordID — <FjordIDProvider> and useAuth() with StrictMode support and automatic redirectUri normalization.
Latest version: 0.2.1

# @fjordid/react

React integration for [FjordID](https://fjordid.eu) — EU-first OIDC/OAuth2 identity platform.

Provides `<FjordIDProvider>` and `useAuth()` with first-class support for:

- **React 18 / 19 StrictMode** — safe double-invoke guard, no "can only be initialized once" errors
- **Automatic `redirectUri` normalization** — defaults to `window.location.origin`, avoiding the keycloak-js footgun where `?code=…&state=…` callback params end up in the redirect URI
- **Silent token refresh** — handled automatically via `FjordAuth`
- **TypeScript** — full type inference, zero `any`s

---

## Install

```bash
npm install @fjordid/react
# or
pnpm add @fjordid/react
```

---

## Quick start

### 1. Wrap your app

```tsx
// main.tsx
import { StrictMode } from "react";
import { createRoot } from "react-dom/client";
import { FjordIDProvider } from "@fjordid/react";
import App from "./App";

createRoot(document.getElementById("root")!).render(
  <StrictMode>
    <FjordIDProvider
      options={{ clientId: import.meta.env.VITE_KEYCLOAK_CLIENT_ID }}
      loadingFallback={<div>Loading…</div>}
    >
      <App />
    </FjordIDProvider>
  </StrictMode>,
);
```

### 2. Add `silent-check-sso.html`

Create `public/silent-check-sso.html`:

```html
<html>
  <body>
    <script>
      parent.postMessage(location.href, location.origin);
    </script>
  </body>
</html>
```

Register it as a redirect URI in the [FjordID console](https://fjordid.eu/console):
`http://localhost:5173/silent-check-sso.html`

### 3. Use auth in any component

```tsx
import { useAuth } from "@fjordid/react";

export function UserMenu() {
  const { user, authenticated, loading, login, logout } = useAuth();

  if (loading) return null;

  if (!authenticated) {
    return <button onClick={() => login()}>Sign in with FjordID</button>;
  }

  return (
    <div>
      <span>Hello, {user?.email}</span>
      <button onClick={() => logout()}>Sign out</button>
    </div>
  );
}
```

---

## API

### `<FjordIDProvider>`

| Prop              | Type               | Default     | Description                                                                  |
| ----------------- | ------------------ | ----------- | ---------------------------------------------------------------------------- |
| `options`         | `FjordAuthOptions` | required    | FjordID configuration                                                        |
| `children`        | `ReactNode`        | required    | Your app tree                                                                |
| `loadingFallback` | `ReactNode`        | `undefined` | Shown while the initial SSO check runs (omit to use `loading` state instead) |

**`options.redirectUri` defaults to `window.location.origin`** — do not set it to `window.location.href`, which may include OIDC callback query params after a login redirect.

### `useAuth()`

Returns an `AuthContextValue` object:

| Property                 | Type                                 | Description                           |
| ------------------------ | ------------------------------------ | ------------------------------------- |
| `authenticated`          | `boolean`                            | Whether the user is logged in         |
| `loading`                | `boolean`                            | True during the initial SSO check     |
| `user`                   | `FjordUser \| null`                  | Authenticated user profile            |
| `login(redirectUri?)`    | `() => void`                         | Redirect to FjordID login             |
| `logout(redirectUri?)`   | `() => void`                         | Redirect to FjordID logout            |
| `register(redirectUri?)` | `() => void`                         | Redirect to FjordID registration      |
| `getToken()`             | `() => Promise<string \| undefined>` | Get a valid Bearer token              |
| `auth`                   | `FjordAuth`                          | Underlying `@fjordid/client` instance |

---

## Environment variables

```env
VITE_KEYCLOAK_URL=https://auth.fjordid.eu
VITE_KEYCLOAK_REALM=fjordid
VITE_KEYCLOAK_CLIENT_ID=fjid_your_client_id
```

---

## React StrictMode details

React 18 and 19 run `useEffect` twice in development to surface side-effect bugs. Raw `keycloak-js` throws `"can only be initialized once"` on the second call. `@fjordid/react` avoids this by:

1. Creating the `FjordAuth` instance in a `useRef` (not `useMemo` or `useState`) so it is never recreated.
2. Tracking the init call with a separate `useRef(false)` that persists across the StrictMode unmount → remount cycle, so `auth.init()` is called exactly once.
3. Re-subscribing to auth events on every mount/remount (cleanup is properly hooked up in the effect return).

---

## `redirectUri` mismatch — the callback URL footgun

A common integration mistake with raw `keycloak-js`:

```ts
// ❌ Wrong — window.location.href contains ?code=…&state=… after a login callback.
//    On a second login() call (StrictMode re-render, page reload, etc.) Keycloak
//    rejects it because the registered URI is just the origin.
keycloak.login({ redirectUri: window.location.href });

// ✅ Correct — always redirect back to the clean origin.
keycloak.login({ redirectUri: window.location.origin });
```

`@fjordid/react` (and `@fjordid/client`) default to `window.location.origin` for you. If you override `options.redirectUri`, use the clean form without query parameters.

---

## Protected route example

```tsx
import { Navigate, Outlet } from "react-router-dom";
import { useAuth } from "@fjordid/react";

export function ProtectedRoute() {
  const { authenticated, loading, login } = useAuth();

  if (loading) return <div>Loading…</div>;
  if (!authenticated) {
    login();
    return null;
  }
  return <Outlet />;
}
```

---

## License

MIT © FjordID