/**
* React hooks and utilities for AT Protocol authentication
*
* @example
* ```tsx
* import { useAuthStore, AuthProvider } from 'atauth/react';
*
* function App() {
* const { isAuthenticated, user, login, logout } = useAuthStore();
*
* if (!isAuthenticated) {
* return ;
* }
*
* return (
*
*
Welcome, {user?.handle}!
*
*
* );
* }
* ```
*/
import { create } from 'zustand';
import { persist } from 'zustand/middleware';
import type { TokenPayload, AuthStore, AtAuthConfig } from './types';
import { decodeToken, isTokenExpired, shouldRefreshToken } from './token';
import { getStoredToken, removeStoredToken, storeToken } from './storage';
import { redirectToAuth, handleCallback, isOAuthCallback, buildLogoutUrl } from './oauth';
/**
* Create an auth store with the given configuration.
*
* @param config - Auth configuration
* @returns Zustand store hook
*/
export function createAuthStore(config: AtAuthConfig) {
const storageKey = config.storageKey || 'atauth_token';
return create()(
persist(
(set, _get) => ({
// Initial state
isAuthenticated: false,
isLoading: true,
user: null,
token: null,
error: null,
// Actions
setToken: (token: string) => {
const user = decodeToken(token);
if (user && !isTokenExpired(user)) {
storeToken(token, storageKey, config.persistSession ?? true);
set({
isAuthenticated: true,
isLoading: false,
user,
token,
error: null,
});
} else {
set({
isAuthenticated: false,
isLoading: false,
user: null,
token: null,
error: user ? 'Token expired' : 'Invalid token',
});
}
},
clearAuth: () => {
removeStoredToken(storageKey, config.persistSession ?? true);
set({
isAuthenticated: false,
isLoading: false,
user: null,
token: null,
error: null,
});
},
setLoading: (loading: boolean) => {
set({ isLoading: loading });
},
setError: (error: string | null) => {
set({ error });
},
refreshFromStorage: () => {
const token = getStoredToken(storageKey, config.persistSession ?? true);
if (token) {
const user = decodeToken(token);
if (user && !isTokenExpired(user)) {
set({
isAuthenticated: true,
isLoading: false,
user,
token,
error: null,
});
return;
}
}
set({
isAuthenticated: false,
isLoading: false,
user: null,
token: null,
error: null,
});
},
}),
{
name: storageKey,
partialize: (state) => ({
token: state.token,
}),
}
)
);
}
/**
* Default auth store instance.
*
* Initialize with your config before using:
* ```ts
* initAuthStore({ gatewayUrl: 'https://auth.example.com' });
* ```
*/
let defaultStore: ReturnType | null = null;
let defaultConfig: AtAuthConfig | null = null;
/**
* Initialize the default auth store.
*
* @param config - Auth configuration
*/
export function initAuthStore(config: AtAuthConfig): void {
defaultConfig = config;
defaultStore = createAuthStore(config);
// Auto-handle callback if on callback page
if (isOAuthCallback()) {
const result = handleCallback(config);
if (result.success && result.token) {
defaultStore.getState().setToken(result.token);
} else if (result.error) {
defaultStore.getState().setError(result.error);
}
} else {
// Load from storage
defaultStore.getState().refreshFromStorage();
}
}
/**
* Get the auth store hook.
*
* Must call `initAuthStore` first.
*/
export function useAuthStore(): AuthStore & {
login: (returnTo?: string) => void;
logout: () => void;
} {
if (!defaultStore || !defaultConfig) {
throw new Error(
'Auth store not initialized. Call initAuthStore(config) first.'
);
}
const state = defaultStore();
return {
...state,
login: (returnTo?: string) => {
redirectToAuth(defaultConfig!, { returnTo });
},
logout: () => {
state.clearAuth();
// Optionally redirect to gateway logout
if (defaultConfig?.gatewayUrl) {
// Build logout URL - app can redirect if needed
void buildLogoutUrl(defaultConfig, window.location.origin);
}
},
};
}
/**
* Check if user needs to re-authenticate (token expiring soon).
*
* @param thresholdSeconds - Seconds before expiry to trigger refresh
* @returns True if token should be refreshed
*/
export function useNeedsRefresh(thresholdSeconds = 300): boolean {
if (!defaultStore) return false;
const { user } = defaultStore();
if (!user) return false;
return shouldRefreshToken(user, thresholdSeconds);
}
/**
* Hook to get just the current user.
*/
export function useUser(): TokenPayload | null {
if (!defaultStore) return null;
return defaultStore().user;
}
/**
* Hook to check authentication status.
*/
export function useIsAuthenticated(): boolean {
if (!defaultStore) return false;
return defaultStore().isAuthenticated;
}
// Re-export types for convenience
export type { TokenPayload, AuthState, AuthStore, AtAuthConfig } from './types';