npm package: @safercity/sdk-react
Description: React hooks and components for SaferCity SDK with TanStack Query integration
Author: SaferCity
Latest version: 0.4.0

# @safercity/sdk-react

React hooks and components for SaferCity SDK with TanStack Query integration.

## What's New in v0.4.0

- **100% Codegen Types** - All hook parameter and return types flow from the generated OpenAPI types. Type changes propagate automatically after `bun run generate:all`.
- **New Panic Hooks** - Added `useCreatePremiumPanic()` for CellFind provider integration.
- **Subscription Realignment** - Removed `useSubscribeUser()` hook (dead route). `useCreateSubscription()` body shape updated to use `isPremium` flag.
- **Enhanced Filtering** - `useSubscriptions()` now supports expanded query parameters (`status`, `subscriptionTypeId`, `sortBy`, `search`).

## What's New in v0.3.2

- **Proxy Mode Enhanced** - `SaferCityProvider` in proxy mode now supports `tenantId`, `userId`, and custom `headers`. Perfect for white-label React Native apps that need to pass tenant context through a proxy.
- **useBanner Fix** - Fixed `useBanner` hook parameter from `radius` to `days` to match API schema.

```tsx
// White-label app with proxy mode
<SaferCityProvider
  mode="proxy"
  proxyBaseUrl={`${API_URL}/api/safercity`}
  tenantId={TENANT_ID}
>
  <App />
</SaferCityProvider>
```

## What's New in v0.3.1

- **Simplified Hook Data** - Hook `data` now reflects the API response body directly (no `ApiResponse` wrapper). Access `data.status` for health, `data.data.firstName` for user responses with API envelopes.
- **Typed Hook Errors** - All hooks use `SaferCityApiError` as the error type. Access `error.status`, `error.message`, `error.error` directly in `isError` blocks or `onError` callbacks.

## What's New in v0.3.0

- **Typed SDK Alignment** - Hooks now use the auto-generated types from `@safercity/sdk`, ensuring 1:1 alignment with the API schema.
- **Correct Field Names** - Mutations now use the exact API field names (`emailAddress`, `panicType`, `phoneNumber`).
- **Simplified Return Types** - Domain methods now return the API body directly (no more `ApiResponse<T>` wrapper). Hook data reflects the API response structure directly (e.g., `user.data.firstName`).

## What's New in v0.2.0

- **Panic Information Hooks** - New hooks for managing user panic profiles and emergency contacts.
- **User Scoping** - `SaferCityProvider` now supports `userId` for automatic request scoping.
- **Enhanced Crime Hooks** - Added `useBanner` and `useCrimeCategoriesWithTypes`.
- **Security Hardening** - Removed admin-only hooks (`useUsers`, `useDeleteUser`, `useSubscriptionStats`).
- **Path Alignment** - Hooks now use the latest singular API paths.

## What's New in v0.1.3

- **Security hardening** - Removed `usePanics` and `useSubscriptionStats` hooks (client-side listing/stats are now restricted)

## Installation

```bash
npm install @safercity/sdk-react @tanstack/react-query
# or
bun add @safercity/sdk-react @tanstack/react-query
```

## Authentication Modes

The provider supports three authentication modes. Choose the one that fits your architecture:

### Proxy Mode (Default - Most Secure)

Client -> Your Backend -> SaferCity API. Your backend handles credentials.

```tsx
import { SaferCityProvider } from '@safercity/sdk-react';

function App() {
  return (
    <SaferCityProvider mode="proxy" proxyBaseUrl="/api/safercity">
      <YourApp />
    </SaferCityProvider>
  );
}
```

Set up the proxy on your backend with `createNextHandler` or `createExpressMiddleware` from `@safercity/sdk`.

### Direct Mode

Client -> SaferCity API with an external auth token. For white-label apps using Clerk, Auth0, better-auth, etc.

```tsx
import { SaferCityProvider } from '@safercity/sdk-react';

function App() {
  return (
    <SaferCityProvider
      mode="direct"
      baseUrl="https://api.safercity.com"
      tenantId="tenant-123"
      userId="user-123" // optional, for auto-scoping
      getAccessToken={() => session?.accessToken}
    >
      <YourApp />
    </SaferCityProvider>
  );
}
```

### Cookie Mode

Browser with `credentials: include`. For first-party web apps using session cookies.

```tsx
import { SaferCityProvider } from '@safercity/sdk-react';

function App() {
  return (
    <SaferCityProvider 
      mode="cookie" 
      baseUrl="https://api.safercity.com"
      userId="user-123" // optional
    >
      <YourApp />
    </SaferCityProvider>
  );
}
```


## Session Management (Cookie Mode)

When using cookie mode, the provider exposes session management hooks:

```tsx
import { useSession, useSessionManager, useAuthMode } from '@safercity/sdk-react';

function AuthStatus() {
  const session = useSession();
  const mode = useAuthMode(); // "proxy" | "direct" | "cookie"

  if (session.isLoading) return <div>Loading...</div>;
  if (session.error) return <div>Error: {session.error.message}</div>;

  return <div>Authenticated: {session.isAuthenticated ? 'Yes' : 'No'}</div>;
}

function LoginButton() {
  const { createSession, clearSession, refreshCsrf, isAuthenticated } = useSessionManager();

  const handleLogin = async (externalToken: string) => {
    await createSession(externalToken, 'tenant-123');
  };

  const handleLogout = async () => {
    await clearSession();
  };

  return isAuthenticated
    ? <button onClick={handleLogout}>Logout</button>
    : <button onClick={() => handleLogin('...')}>Login</button>;
}
```

### SessionState

```typescript
interface SessionState {
  isAuthenticated: boolean;
  isLoading: boolean;
  userId?: string;
  tenantId?: string;
  expiresAt?: number;
  error?: Error;
}
```

## Using Hooks

```tsx
import { useUser, useCreatePanic } from '@safercity/sdk-react';

function Dashboard({ userId }: { userId: string }) {
  const { data: user, isLoading } = useUser(userId);
  
  if (isLoading) return <div>Loading...</div>;
  
  return (
    <div>
      <h1>Welcome, {user?.data.firstName}</h1>
    </div>
  );
}

function PanicButton({ userId }: { userId: string }) {
  const createPanic = useCreatePanic();
  
  const handlePanic = () => {
    createPanic.mutate({
      userId,
      panicType: 'emergency',
      latitude: -26.2041,
      longitude: 28.0473,
    });
  };
  
  return (
    <button onClick={handlePanic} disabled={createPanic.isPending}>
      {createPanic.isPending ? 'Creating...' : 'Trigger Panic'}
    </button>
  );
}
```

## Available Hooks

### Provider Hooks
- `useSaferCity()` - Full context (client, mode, session, session management)
- `useSaferCityClient()` - SaferCity client instance
- `useSession()` - Session state (cookie mode)
- `useAuthMode()` - Current auth mode (`"proxy"` | `"direct"` | `"cookie"`)
- `useSessionManager()` - Session management functions (cookie mode)

### Health and Auth
- `useHealthCheck()` - API health status
- `useWhoAmI()` - Current auth context

### Users
- `useUser(userId?)` - Get user by ID (defaults to client's `userId`)
- `useCreateUser()` - Create user mutation
- `useUpdateUser()` - Update user mutation

### Panics
- `usePanic(panicId, query?)` - Get panic by ID
- `useCreatePanic()` - Create panic mutation
- `useCreatePremiumPanic()` - Create premium panic mutation
- `useUpdatePanicLocation()` - Update location mutation
- `useCancelPanic()` - Cancel panic mutation
- `usePanicTypes(userId?)` - Get available panic types for a user
- `usePanicStream(panicId, options?)` - Stream panic updates

### Panic Information
- `usePanicInformation(id)` - Get profile by ID
- `usePanicInformationByUser(userId?)` - Get profile by user ID
- `usePanicEligibility(userId?)` - Check user eligibility
- `useCreatePanicInformation()` - Create profile mutation
- `useUpdatePanicInformation()` - Update profile mutation
- `useDeletePanicInformation()` - Delete profile mutation

### Subscriptions
- `useSubscriptionTypes()` - List subscription types
- `useSubscriptions(filters?)` - List subscriptions for a user
- `useCreateSubscription()` - Create subscription mutation

### Notifications
- `useBulkTriggerNotifications()` - Bulk trigger notifications mutation

### Location Safety
- `useLocationSafety(lat, lng, radius?)` - Check location safety

### Banner
- `useBanner(body)` - Get crime banner data for a location

### Crimes
- `useCrimes(filters?)` - List crimes
- `useCrimeCategories()` - Get crime categories
- `useCrimeTypes()` - Get crime types
- `useCrimeCategoriesWithTypes()` - Get categories with nested types

## Streaming Hook

```tsx
import { usePanicStream } from '@safercity/sdk-react';

function PanicTracker({ panicId }: { panicId: string }) {
  const { data, isConnected, error, events } = usePanicStream(panicId, {
    keepHistory: true,
    onEvent: (event) => console.log('Update:', event),
  });
  
  if (error) return <div>Error: {error.message}</div>;
  if (!isConnected) return <div>Connecting...</div>;
  
  return (
    <div>
      <p>Latest: {data?.data}</p>
      <p>Total events: {events.length}</p>
    </div>
  );
}
```

## Query Keys

Use query keys for manual cache management:

```tsx
import { saferCityKeys } from '@safercity/sdk-react';
import { useQueryClient } from '@tanstack/react-query';

function RefreshButton() {
  const queryClient = useQueryClient();
  
  const refresh = () => {
    // Invalidate all user queries
    queryClient.invalidateQueries({ queryKey: saferCityKeys.users() });
    
    // Invalidate specific user
    queryClient.invalidateQueries({ queryKey: saferCityKeys.usersDetail('user-123') });
  };
  
  return <button onClick={refresh}>Refresh</button>;
}
```

## Custom QueryClient

```tsx
import { SaferCityProvider } from '@safercity/sdk-react';
import { QueryClient } from '@tanstack/react-query';

const queryClient = new QueryClient({
  defaultOptions: {
    queries: {
      staleTime: 1000 * 60 * 5, // 5 minutes
    },
  },
});

function App() {
  return (
    <SaferCityProvider mode="proxy" proxyBaseUrl="/api/safercity" queryClient={queryClient}>
      <YourApp />
    </SaferCityProvider>
  );
}
```

## Access Raw Client

```tsx
import { useSaferCityClient } from '@safercity/sdk-react';

function CustomComponent() {
  const client = useSaferCityClient();
  
  const customRequest = async () => {
    const response = await client._client.get('/custom-endpoint');
    return response.data;
  };
}
```

## License

MIT