@infloapi/react
v0.3.0
Published
Drop-in React auth & identity layer for Inflo partners
Downloads
457
Maintainers
Readme
@infloapi/react
Drop-in React auth & identity layer for Inflo partner apps.
Covers the auth primitives every partner needs on day one: token state management, SSO redirect with PKCE, reconnect handling, scope management, OIDC state round-trips, and a complete callback handler.
Installation
npm install @infloapi/react
# peer deps
npm install react react-domQuickstart (10 lines)
import { InfloProvider, AuthGuard, useInfloAuth } from "@infloapi/react";
// 1. Wrap your app
function App() {
return (
<InfloProvider clientId="your_client_id" redirectUri="https://app.example.com/callback">
<AuthGuard redirectTo="/dashboard">
<Dashboard />
</AuthGuard>
</InfloProvider>
);
}
// 2. Use auth state anywhere below the provider
function Dashboard() {
const { user, scopes, reconnect } = useInfloAuth();
return <h1>Hello, {user?.displayName}</h1>;
}Full setup — four steps
Step 1: InfloProvider
Wraps your app and manages token state. Probes GET https://sso.infloapp.com/oauth/userinfo
every 5 minutes to keep user profile and scope information fresh.
import { InfloProvider } from "@infloapi/react";
<InfloProvider
clientId="your_client_id"
redirectUri="https://app.example.com/callback"
ssoBaseUrl="https://sso.infloapp.com" // optional, default shown
scopeProbeIntervalMs={300_000} // optional, 5 min default
storage={myStorageAdapter} // optional custom token storage
>
<App />
</InfloProvider>Custom storage adapter
Partners choose where tokens live. Supply a StorageAdapter:
import type { StorageAdapter } from "@infloapi/react";
const storage: StorageAdapter = {
getAccessToken() { return localStorage.getItem("inflo_token"); },
setAccessToken(t) { localStorage.setItem("inflo_token", t); },
clearTokens() { localStorage.removeItem("inflo_token"); localStorage.removeItem("inflo_scopes"); },
// Optional — store granted scopes from the token exchange
getGrantedScopes() { return localStorage.getItem("inflo_scopes"); },
setGrantedScopes(s) { localStorage.setItem("inflo_scopes", s); },
};Step 2: AuthGuard
Blocks unauthenticated access. Builds a PKCE + CSRF-state-protected SSO redirect automatically.
import { AuthGuard } from "@infloapi/react";
<AuthGuard
redirectTo="/dashboard" // returned to post-login
loadingFallback={<Spinner />} // optional
reconnectFallback={<ReconnectPrompt />} // optional; auto-reconnects if omitted
>
<ProtectedPage />
</AuthGuard>When status === "reconnect_required" (409 inflo_reconnect_required), AuthGuard
either shows your reconnectFallback or calls reconnect() automatically.
Step 3: Callback page
Handle the SSO callback, exchange the code for tokens, and store them:
// pages/callback.tsx
import { useEffect } from "react";
import { useNavigate } from "react-router-dom";
import { handleInfloCallback, useInfloAuth } from "@infloapi/react";
export default function CallbackPage() {
const navigate = useNavigate();
const { refresh } = useInfloAuth();
useEffect(() => {
const params = new URLSearchParams(window.location.search);
handleInfloCallback({
params,
clientId: "your_client_id",
redirectUri: "https://app.example.com/callback",
storage: myStorageAdapter, // same adapter you gave InfloProvider
})
.then(async ({ payload }) => {
// ⚠️ refresh() is required here. Without it the provider hasn't seen
// the newly stored token yet and will redirect to SSO again.
await refresh();
navigate((payload.returnPath as string) ?? "/");
})
.catch(console.error);
}, []);
return <div>Signing in…</div>;
}handleInfloCallback:
- Validates the
stateparameter (CSRF) via the nonce store insessionStorage - Retrieves the PKCE code verifier and sends it in the token exchange
- Calls
storage.setAccessTokenandstorage.setGrantedScopeswith the results - Returns
{ payload, tokenResponse }so you can recover the pre-auth route
Step 4: useInfloAuth()
Access auth state from any component inside <InfloProvider>:
const { user, status, scopes, scopeFallbackActive, reconnect } = useInfloAuth();| Field | Type | Description |
|---|---|---|
| user | InfloUser \| null | Authenticated user profile (from /oauth/userinfo) |
| status | "loading" \| "unauthenticated" \| "connected" \| "reconnect_required" | Current auth state |
| scopes | string[] | Granted scopes from the token exchange |
| scopeFallbackActive | boolean | true when only base scopes (openid profile) were granted |
| refresh | () => Promise<void> | Re-probe auth state immediately — call this from your callback page after handleInfloCallback returns, before navigating |
| logout | () => Promise<void> | Clear stored tokens and reset state to "unauthenticated" (local only — does not end the SSO session) |
| reconnect | () => void | Redirect the user to SSO with prompt=login (PKCE-protected) |
OIDC state round-trip
Carry arbitrary context through the SSO redirect and recover it in your callback.
import { buildAuthUrlAsync, decodeOidcState } from "@infloapi/react";
// Before redirect — encode payload, generate PKCE
const url = await buildAuthUrlAsync({
clientId: "your_client_id",
redirectUri: "https://app.example.com/callback",
scope: "openid profile email offline_access",
payload: { invitationId: "inv_123", returnPath: "/dashboard" },
generatePkce: true, // recommended for SPAs (public clients)
});
window.location.href = url;
// In the callback — handleInfloCallback decodes state automatically
// and recovers the payload in its return value.
// You can also decode manually:
const { payload, codeVerifier } = decodeOidcState(
new URLSearchParams(window.location.search).get("state") ?? ""
);
console.log(payload.returnPath); // "/dashboard"Up to 3 concurrent in-flight states are tracked in sessionStorage, supporting
multi-tab auth flows. Each nonce is consumed on first use (replay-proof).
Error classes
import {
InfloAuthError,
InfloUnknownUserError,
InfloInsufficientScopeError,
} from "@infloapi/react";| Class | code | shouldClearTokens | When |
|---|---|---|---|
| InfloAuthError | varies | varies | Base class for all Inflo auth errors |
| InfloUnknownUserError | unknown_user | true | User linked to SSO but not registered in your app |
| InfloInsufficientScopeError | insufficient_scope | false | Token lacks required scopes — re-auth with broader scope |
useInfloStatus()
Lightweight polling hook for degraded-mode UI (outage banners, reconnect prompts in dialogs).
Polls GET /api/v1/users/me with exponential-backoff retry.
function ReconnectBanner() {
const { needsReconnect } = useInfloStatus({ getToken: () => myToken });
if (!needsReconnect) return null;
return <Banner>Please reconnect your Inflo account.</Banner>;
}const { linked, connected, needsReconnect, loading, error } = useInfloStatus({
getToken: () => accessToken, // token with appropriate resource-server scopes
baseUrl: "https://infloapp.com", // optional
pollIntervalMs: 30_000, // optional, 30 s default
maxRetries: 4, // optional
baseDelayMs: 1_000, // optional, exponential backoff base
});Related packages
| Package | Purpose |
|---------|---------|
| @infloapi/node | Node.js SDK for server-side API calls — search, invitations, connections, webhooks. |
| @infloapi/react-social | React UI components for connection & group selection (CirclesPickerDialog, ConnectionCard). Pair with this package for a complete social layer. |
Hosted invite landing page
When a partner app sends an invitation via POST /api/v1/invitations, recipients
follow a branded landing page at /i/<token> that drives them through your
InfloSSO authorization_code flow. The OIDC state carries { invitationId } so
your callback page can call POST /api/v1/invitations/:id/accept after exchanging
the code. Use handleInfloCallback to recover the payload:
const { payload } = await handleInfloCallback({ params, clientId, redirectUri, storage });
if (payload?.invitationId) {
await fetch(`/api/invitations/${payload.invitationId}/accept`, { method: "POST" });
}