@safercity/sdk-react 0.1.2 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -2,6 +2,18 @@
2
2
 
3
3
  React hooks and components for SaferCity SDK with TanStack Query integration.
4
4
 
5
+ ## What's New in v0.2.0
6
+
7
+ - **Panic Information Hooks** - New hooks for managing user panic profiles and emergency contacts.
8
+ - **User Scoping** - `SaferCityProvider` now supports `userId` for automatic request scoping.
9
+ - **Enhanced Crime Hooks** - Added `useBanner` and `useCrimeCategoriesWithTypes`.
10
+ - **Security Hardening** - Removed admin-only hooks (`useUsers`, `useDeleteUser`, `useSubscriptionStats`).
11
+ - **Path Alignment** - Hooks now use the latest singular API paths.
12
+
13
+ ## What's New in v0.1.3
14
+
15
+ - **Security hardening** - Removed `usePanics` and `useSubscriptionStats` hooks (client-side listing/stats are now restricted)
16
+
5
17
  ## Installation
6
18
 
7
19
  ```bash
@@ -45,6 +57,7 @@ function App() {
45
57
  mode="direct"
46
58
  baseUrl="https://api.safercity.com"
47
59
  tenantId="tenant-123"
60
+ userId="user-123" // optional, for auto-scoping
48
61
  getAccessToken={() => session?.accessToken}
49
62
  >
50
63
  <YourApp />
@@ -53,6 +66,27 @@ function App() {
53
66
  }
54
67
  ```
55
68
 
69
+ ### Cookie Mode
70
+
71
+ Browser with `credentials: include`. For first-party web apps using session cookies.
72
+
73
+ ```tsx
74
+ import { SaferCityProvider } from '@safercity/sdk-react';
75
+
76
+ function App() {
77
+ return (
78
+ <SaferCityProvider
79
+ mode="cookie"
80
+ baseUrl="https://api.safercity.com"
81
+ userId="user-123" // optional
82
+ >
83
+ <YourApp />
84
+ </SaferCityProvider>
85
+ );
86
+ }
87
+ ```
88
+
89
+
56
90
  The provider automatically refreshes the token every 30 seconds by calling `getAccessToken`.
57
91
 
58
92
  ### Cookie Mode
@@ -133,18 +167,16 @@ interface SessionState {
133
167
  ## Using Hooks
134
168
 
135
169
  ```tsx
136
- import { useUsers, usePanics, useCreatePanic } from '@safercity/sdk-react';
170
+ import { useUser, useCreatePanic } from '@safercity/sdk-react';
137
171
 
138
- function Dashboard() {
139
- const { data: users, isLoading } = useUsers();
140
- const { data: panics } = usePanics({ status: 'active' });
172
+ function Dashboard({ userId }: { userId: string }) {
173
+ const { data: user, isLoading } = useUser(userId);
141
174
 
142
175
  if (isLoading) return <div>Loading...</div>;
143
176
 
144
177
  return (
145
178
  <div>
146
- <h1>Users: {users?.data.users.length}</h1>
147
- <h1>Active Panics: {panics?.data.panics.length}</h1>
179
+ <h1>Welcome, {user?.data.firstName}</h1>
148
180
  </div>
149
181
  );
150
182
  }
@@ -182,33 +214,46 @@ function PanicButton({ userId }: { userId: string }) {
182
214
  - `useWhoAmI()` - Current auth context
183
215
 
184
216
  ### Users
185
- - `useUsers(filters?)` - List users
186
- - `useUser(userId)` - Get user by ID
217
+ - `useUser(userId?)` - Get user by ID (defaults to client's `userId`)
187
218
  - `useCreateUser()` - Create user mutation
188
219
  - `useUpdateUser()` - Update user mutation
189
- - `useDeleteUser()` - Delete user mutation
190
220
 
191
221
  ### Panics
192
- - `usePanics(filters?)` - List panics
193
222
  - `usePanic(panicId, query?)` - Get panic by ID
194
223
  - `useCreatePanic()` - Create panic mutation
195
224
  - `useUpdatePanicLocation()` - Update location mutation
196
225
  - `useCancelPanic()` - Cancel panic mutation
226
+ - `usePanicTypes(userId?)` - Get available panic types for a user
197
227
  - `usePanicStream(panicId, options?)` - Stream panic updates
198
228
 
229
+ ### Panic Information
230
+ - `usePanicInformation(id)` - Get profile by ID
231
+ - `usePanicInformationByUser(userId?)` - Get profile by user ID
232
+ - `usePanicEligibility(userId?)` - Check user eligibility
233
+ - `useCreatePanicInformation()` - Create profile mutation
234
+ - `useUpdatePanicInformation()` - Update profile mutation
235
+ - `useDeletePanicInformation()` - Delete profile mutation
236
+
199
237
  ### Subscriptions
200
238
  - `useSubscriptionTypes()` - List subscription types
201
- - `useSubscriptions(filters?)` - List subscriptions
202
- - `useSubscriptionStats()` - Get statistics
239
+ - `useSubscriptions(filters?)` - List subscriptions for a user
203
240
  - `useCreateSubscription()` - Create subscription mutation
241
+ - `useSubscribeUser()` - Subscribe user mutation
242
+
243
+ ### Notifications
244
+ - `useBulkTriggerNotifications()` - Bulk trigger notifications mutation
204
245
 
205
246
  ### Location Safety
206
247
  - `useLocationSafety(lat, lng, radius?)` - Check location safety
207
248
 
249
+ ### Banner
250
+ - `useBanner(body)` - Get crime banner data for a location
251
+
208
252
  ### Crimes
209
253
  - `useCrimes(filters?)` - List crimes
210
254
  - `useCrimeCategories()` - Get crime categories
211
255
  - `useCrimeTypes()` - Get crime types
256
+ - `useCrimeCategoriesWithTypes()` - Get categories with nested types
212
257
 
213
258
  ## Streaming Hook
214
259
 
package/dist/index.cjs CHANGED
@@ -49,13 +49,15 @@ function SaferCityProvider(props) {
49
49
  if (isDirectMode(props)) {
50
50
  return sdk.createSaferCityClient({
51
51
  baseUrl: props.baseUrl,
52
- tenantId: props.tenantId
52
+ tenantId: props.tenantId,
53
+ userId: props.userId
53
54
  });
54
55
  }
55
56
  if (isCookieMode(props)) {
56
57
  return sdk.createSaferCityClient({
57
58
  baseUrl: props.baseUrl,
58
59
  tenantId: props.tenantId,
60
+ userId: props.userId,
59
61
  headers: {
60
62
  // Note: cookies are sent automatically with fetch credentials: 'include'
61
63
  }
@@ -92,6 +94,7 @@ function SaferCityProvider(props) {
92
94
  if (!isMounted) return;
93
95
  if (response.ok) {
94
96
  const data = await response.json();
97
+ client.setUserId(data.userId);
95
98
  setSession({
96
99
  isAuthenticated: data.authenticated,
97
100
  isLoading: false,
@@ -118,7 +121,7 @@ function SaferCityProvider(props) {
118
121
  return () => {
119
122
  isMounted = false;
120
123
  };
121
- }, [mode, props]);
124
+ }, [mode, props, client]);
122
125
  const createSession = react.useCallback(async (token, tenantId) => {
123
126
  if (mode !== "cookie" || !isCookieMode(props)) {
124
127
  throw new Error("createSession is only available in cookie mode");
@@ -253,12 +256,17 @@ var saferCityKeys = {
253
256
  authWhoami: () => [...saferCityKeys.auth(), "whoami"],
254
257
  // Users
255
258
  users: () => [...saferCityKeys.all, "users"],
256
- usersList: (filters) => [...saferCityKeys.users(), "list", filters],
257
259
  usersDetail: (userId) => [...saferCityKeys.users(), "detail", userId],
258
260
  // Panics
259
261
  panics: () => [...saferCityKeys.all, "panics"],
260
262
  panicsList: (filters) => [...saferCityKeys.panics(), "list", filters],
261
263
  panicsDetail: (panicId) => [...saferCityKeys.panics(), "detail", panicId],
264
+ panicTypes: (userId) => [...saferCityKeys.panics(), "types", userId],
265
+ // Panic Information
266
+ panicInformation: () => [...saferCityKeys.all, "panicInformation"],
267
+ panicInformationDetail: (id) => [...saferCityKeys.panicInformation(), "detail", id],
268
+ panicInformationByUser: (userId) => [...saferCityKeys.panicInformation(), "byUser", userId],
269
+ panicEligibility: (userId) => [...saferCityKeys.panicInformation(), "eligibility", userId],
262
270
  // Subscriptions
263
271
  subscriptions: () => [...saferCityKeys.all, "subscriptions"],
264
272
  subscriptionsList: (filters) => [...saferCityKeys.subscriptions(), "list", filters],
@@ -266,11 +274,14 @@ var saferCityKeys = {
266
274
  subscriptionsStats: () => [...saferCityKeys.subscriptions(), "stats"],
267
275
  // Location Safety
268
276
  locationSafety: (lat, lng) => [...saferCityKeys.all, "location-safety", lat, lng],
277
+ // Banner
278
+ banner: (body) => [...saferCityKeys.all, "banner", body],
269
279
  // Crimes
270
280
  crimes: () => [...saferCityKeys.all, "crimes"],
271
281
  crimesList: (filters) => [...saferCityKeys.crimes(), "list", filters],
272
282
  crimesCategories: () => [...saferCityKeys.crimes(), "categories"],
273
- crimesTypes: () => [...saferCityKeys.crimes(), "types"]
283
+ crimesTypes: () => [...saferCityKeys.crimes(), "types"],
284
+ crimeCategoriesWithTypes: () => [...saferCityKeys.crimes(), "categoriesWithTypes"]
274
285
  };
275
286
  function useHealthCheck(options) {
276
287
  const client = useSaferCityClient();
@@ -288,14 +299,6 @@ function useWhoAmI(options) {
288
299
  ...options
289
300
  });
290
301
  }
291
- function useUsers(filters, options) {
292
- const client = useSaferCityClient();
293
- return reactQuery.useQuery({
294
- queryKey: saferCityKeys.usersList(filters),
295
- queryFn: () => client.users.list(filters),
296
- ...options
297
- });
298
- }
299
302
  function useUser(userId, options) {
300
303
  const client = useSaferCityClient();
301
304
  return reactQuery.useQuery({
@@ -323,30 +326,10 @@ function useUpdateUser(options) {
323
326
  mutationFn: ({ userId, data }) => client.users.update(userId, data),
324
327
  onSuccess: (_, { userId }) => {
325
328
  queryClient.invalidateQueries({ queryKey: saferCityKeys.usersDetail(userId) });
326
- queryClient.invalidateQueries({ queryKey: saferCityKeys.usersList() });
327
329
  },
328
330
  ...options
329
331
  });
330
332
  }
331
- function useDeleteUser(options) {
332
- const client = useSaferCityClient();
333
- const queryClient = reactQuery.useQueryClient();
334
- return reactQuery.useMutation({
335
- mutationFn: (userId) => client.users.delete(userId),
336
- onSuccess: () => {
337
- queryClient.invalidateQueries({ queryKey: saferCityKeys.users() });
338
- },
339
- ...options
340
- });
341
- }
342
- function usePanics(filters, options) {
343
- const client = useSaferCityClient();
344
- return reactQuery.useQuery({
345
- queryKey: saferCityKeys.panicsList(filters),
346
- queryFn: () => client.panics.list(filters),
347
- ...options
348
- });
349
- }
350
333
  function usePanic(panicId, query, options) {
351
334
  const client = useSaferCityClient();
352
335
  return reactQuery.useQuery({
@@ -390,6 +373,77 @@ function useCancelPanic(options) {
390
373
  ...options
391
374
  });
392
375
  }
376
+ function usePanicTypes(userId, options) {
377
+ const client = useSaferCityClient();
378
+ return reactQuery.useQuery({
379
+ queryKey: saferCityKeys.panicTypes(userId),
380
+ queryFn: () => client.panics.types(userId),
381
+ enabled: !!userId,
382
+ staleTime: 1e3 * 60 * 10,
383
+ // 10 minutes - types don't change often
384
+ ...options
385
+ });
386
+ }
387
+ function usePanicInformation(id, options) {
388
+ const client = useSaferCityClient();
389
+ return reactQuery.useQuery({
390
+ queryKey: saferCityKeys.panicInformationDetail(id),
391
+ queryFn: () => client.panicInformation.get(id),
392
+ enabled: !!id,
393
+ ...options
394
+ });
395
+ }
396
+ function usePanicInformationByUser(userId, options) {
397
+ const client = useSaferCityClient();
398
+ return reactQuery.useQuery({
399
+ queryKey: saferCityKeys.panicInformationByUser(userId),
400
+ queryFn: () => client.panicInformation.getByUser(userId),
401
+ enabled: !!userId,
402
+ ...options
403
+ });
404
+ }
405
+ function usePanicEligibility(userId, options) {
406
+ const client = useSaferCityClient();
407
+ return reactQuery.useQuery({
408
+ queryKey: saferCityKeys.panicEligibility(userId),
409
+ queryFn: () => client.panicInformation.validateEligibility(userId),
410
+ enabled: !!userId,
411
+ ...options
412
+ });
413
+ }
414
+ function useCreatePanicInformation(options) {
415
+ const client = useSaferCityClient();
416
+ const queryClient = reactQuery.useQueryClient();
417
+ return reactQuery.useMutation({
418
+ mutationFn: (data) => client.panicInformation.create(data),
419
+ onSuccess: () => {
420
+ queryClient.invalidateQueries({ queryKey: saferCityKeys.panicInformation() });
421
+ },
422
+ ...options
423
+ });
424
+ }
425
+ function useUpdatePanicInformation(options) {
426
+ const client = useSaferCityClient();
427
+ const queryClient = reactQuery.useQueryClient();
428
+ return reactQuery.useMutation({
429
+ mutationFn: ({ id, data }) => client.panicInformation.update(id, data),
430
+ onSuccess: () => {
431
+ queryClient.invalidateQueries({ queryKey: saferCityKeys.panicInformation() });
432
+ },
433
+ ...options
434
+ });
435
+ }
436
+ function useDeletePanicInformation(options) {
437
+ const client = useSaferCityClient();
438
+ const queryClient = reactQuery.useQueryClient();
439
+ return reactQuery.useMutation({
440
+ mutationFn: (id) => client.panicInformation.delete(id),
441
+ onSuccess: () => {
442
+ queryClient.invalidateQueries({ queryKey: saferCityKeys.panicInformation() });
443
+ },
444
+ ...options
445
+ });
446
+ }
393
447
  function useSubscriptionTypes(options) {
394
448
  const client = useSaferCityClient();
395
449
  return reactQuery.useQuery({
@@ -408,25 +462,35 @@ function useSubscriptions(filters, options) {
408
462
  ...options
409
463
  });
410
464
  }
411
- function useSubscriptionStats(options) {
465
+ function useCreateSubscription(options) {
412
466
  const client = useSaferCityClient();
413
- return reactQuery.useQuery({
414
- queryKey: saferCityKeys.subscriptionsStats(),
415
- queryFn: () => client.subscriptions.stats(),
467
+ const queryClient = reactQuery.useQueryClient();
468
+ return reactQuery.useMutation({
469
+ mutationFn: (data) => client.subscriptions.create(data),
470
+ onSuccess: () => {
471
+ queryClient.invalidateQueries({ queryKey: saferCityKeys.subscriptions() });
472
+ },
416
473
  ...options
417
474
  });
418
475
  }
419
- function useCreateSubscription(options) {
476
+ function useSubscribeUser(options) {
420
477
  const client = useSaferCityClient();
421
478
  const queryClient = reactQuery.useQueryClient();
422
479
  return reactQuery.useMutation({
423
- mutationFn: (data) => client.subscriptions.create(data),
480
+ mutationFn: (body) => client.subscriptions.subscribeUser(body),
424
481
  onSuccess: () => {
425
482
  queryClient.invalidateQueries({ queryKey: saferCityKeys.subscriptions() });
426
483
  },
427
484
  ...options
428
485
  });
429
486
  }
487
+ function useBulkTriggerNotifications(options) {
488
+ const client = useSaferCityClient();
489
+ return reactQuery.useMutation({
490
+ mutationFn: (body) => client.notifications.bulkTrigger(body),
491
+ ...options
492
+ });
493
+ }
430
494
  function useLocationSafety(latitude, longitude, radius, options) {
431
495
  const client = useSaferCityClient();
432
496
  return reactQuery.useQuery({
@@ -438,6 +502,17 @@ function useLocationSafety(latitude, longitude, radius, options) {
438
502
  ...options
439
503
  });
440
504
  }
505
+ function useBanner(body, options) {
506
+ const client = useSaferCityClient();
507
+ return reactQuery.useQuery({
508
+ queryKey: saferCityKeys.banner(body),
509
+ queryFn: () => client.banner.get(body),
510
+ enabled: body.latitude !== void 0 && body.longitude !== void 0,
511
+ staleTime: 1e3 * 60 * 5,
512
+ // 5 minutes - location-based, changes moderately
513
+ ...options
514
+ });
515
+ }
441
516
  function useCrimes(filters, options) {
442
517
  const client = useSaferCityClient();
443
518
  return reactQuery.useQuery({
@@ -466,6 +541,16 @@ function useCrimeTypes(options) {
466
541
  ...options
467
542
  });
468
543
  }
544
+ function useCrimeCategoriesWithTypes(options) {
545
+ const client = useSaferCityClient();
546
+ return reactQuery.useQuery({
547
+ queryKey: saferCityKeys.crimeCategoriesWithTypes(),
548
+ queryFn: () => client.crimes.categoriesWithTypes(),
549
+ staleTime: 1e3 * 60 * 30,
550
+ // 30 minutes - categories rarely change
551
+ ...options
552
+ });
553
+ }
469
554
  function usePanicStream(panicId, options = {}) {
470
555
  const client = useSaferCityClient();
471
556
  const {
@@ -663,31 +748,38 @@ function useStream(createStream, options = {}) {
663
748
  exports.SaferCityProvider = SaferCityProvider;
664
749
  exports.saferCityKeys = saferCityKeys;
665
750
  exports.useAuthMode = useAuthMode;
751
+ exports.useBanner = useBanner;
752
+ exports.useBulkTriggerNotifications = useBulkTriggerNotifications;
666
753
  exports.useCancelPanic = useCancelPanic;
667
754
  exports.useCreatePanic = useCreatePanic;
755
+ exports.useCreatePanicInformation = useCreatePanicInformation;
668
756
  exports.useCreateSubscription = useCreateSubscription;
669
757
  exports.useCreateUser = useCreateUser;
670
758
  exports.useCrimeCategories = useCrimeCategories;
759
+ exports.useCrimeCategoriesWithTypes = useCrimeCategoriesWithTypes;
671
760
  exports.useCrimeTypes = useCrimeTypes;
672
761
  exports.useCrimes = useCrimes;
673
- exports.useDeleteUser = useDeleteUser;
762
+ exports.useDeletePanicInformation = useDeletePanicInformation;
674
763
  exports.useHealthCheck = useHealthCheck;
675
764
  exports.useLocationSafety = useLocationSafety;
676
765
  exports.usePanic = usePanic;
766
+ exports.usePanicEligibility = usePanicEligibility;
767
+ exports.usePanicInformation = usePanicInformation;
768
+ exports.usePanicInformationByUser = usePanicInformationByUser;
677
769
  exports.usePanicStream = usePanicStream;
678
- exports.usePanics = usePanics;
770
+ exports.usePanicTypes = usePanicTypes;
679
771
  exports.useSaferCity = useSaferCity;
680
772
  exports.useSaferCityClient = useSaferCityClient;
681
773
  exports.useSession = useSession;
682
774
  exports.useSessionManager = useSessionManager;
683
775
  exports.useStream = useStream;
684
- exports.useSubscriptionStats = useSubscriptionStats;
776
+ exports.useSubscribeUser = useSubscribeUser;
685
777
  exports.useSubscriptionTypes = useSubscriptionTypes;
686
778
  exports.useSubscriptions = useSubscriptions;
779
+ exports.useUpdatePanicInformation = useUpdatePanicInformation;
687
780
  exports.useUpdatePanicLocation = useUpdatePanicLocation;
688
781
  exports.useUpdateUser = useUpdateUser;
689
782
  exports.useUser = useUser;
690
- exports.useUsers = useUsers;
691
783
  exports.useWhoAmI = useWhoAmI;
692
784
  Object.keys(sdk).forEach(function (k) {
693
785
  if (k !== 'default' && !Object.prototype.hasOwnProperty.call(exports, k)) Object.defineProperty(exports, k, {