@safercity/sdk-react
v0.4.0
Published
React hooks and components for SaferCity SDK with TanStack Query integration
Maintainers
Readme
@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 useisPremiumflag. - Enhanced Filtering -
useSubscriptions()now supports expanded query parameters (status,subscriptionTypeId,sortBy,search).
What's New in v0.3.2
- Proxy Mode Enhanced -
SaferCityProviderin proxy mode now supportstenantId,userId, and customheaders. Perfect for white-label React Native apps that need to pass tenant context through a proxy. - useBanner Fix - Fixed
useBannerhook parameter fromradiustodaysto match API schema.
// 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
datanow reflects the API response body directly (noApiResponsewrapper). Accessdata.statusfor health,data.data.firstNamefor user responses with API envelopes. - Typed Hook Errors - All hooks use
SaferCityApiErroras the error type. Accesserror.status,error.message,error.errordirectly inisErrorblocks oronErrorcallbacks.
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 -
SaferCityProvidernow supportsuserIdfor automatic request scoping. - Enhanced Crime Hooks - Added
useBanneranduseCrimeCategoriesWithTypes. - 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
usePanicsanduseSubscriptionStatshooks (client-side listing/stats are now restricted)
Installation
npm install @safercity/sdk-react @tanstack/react-query
# or
bun add @safercity/sdk-react @tanstack/react-queryAuthentication 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.
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.
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.
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:
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
interface SessionState {
isAuthenticated: boolean;
isLoading: boolean;
userId?: string;
tenantId?: string;
expiresAt?: number;
error?: Error;
}Using Hooks
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 instanceuseSession()- Session state (cookie mode)useAuthMode()- Current auth mode ("proxy"|"direct"|"cookie")useSessionManager()- Session management functions (cookie mode)
Health and Auth
useHealthCheck()- API health statususeWhoAmI()- Current auth context
Users
useUser(userId?)- Get user by ID (defaults to client'suserId)useCreateUser()- Create user mutationuseUpdateUser()- Update user mutation
Panics
usePanic(panicId, query?)- Get panic by IDuseCreatePanic()- Create panic mutationuseCreatePremiumPanic()- Create premium panic mutationuseUpdatePanicLocation()- Update location mutationuseCancelPanic()- Cancel panic mutationusePanicTypes(userId?)- Get available panic types for a userusePanicStream(panicId, options?)- Stream panic updates
Panic Information
usePanicInformation(id)- Get profile by IDusePanicInformationByUser(userId?)- Get profile by user IDusePanicEligibility(userId?)- Check user eligibilityuseCreatePanicInformation()- Create profile mutationuseUpdatePanicInformation()- Update profile mutationuseDeletePanicInformation()- Delete profile mutation
Subscriptions
useSubscriptionTypes()- List subscription typesuseSubscriptions(filters?)- List subscriptions for a useruseCreateSubscription()- 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 crimesuseCrimeCategories()- Get crime categoriesuseCrimeTypes()- Get crime typesuseCrimeCategoriesWithTypes()- Get categories with nested types
Streaming Hook
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:
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
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
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
