@safercity/sdk-react 0.1.3 → 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,14 @@
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
+
5
13
  ## What's New in v0.1.3
6
14
 
7
15
  - **Security hardening** - Removed `usePanics` and `useSubscriptionStats` hooks (client-side listing/stats are now restricted)
@@ -49,6 +57,7 @@ function App() {
49
57
  mode="direct"
50
58
  baseUrl="https://api.safercity.com"
51
59
  tenantId="tenant-123"
60
+ userId="user-123" // optional, for auto-scoping
52
61
  getAccessToken={() => session?.accessToken}
53
62
  >
54
63
  <YourApp />
@@ -57,6 +66,27 @@ function App() {
57
66
  }
58
67
  ```
59
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
+
60
90
  The provider automatically refreshes the token every 30 seconds by calling `getAccessToken`.
61
91
 
62
92
  ### Cookie Mode
@@ -137,16 +167,16 @@ interface SessionState {
137
167
  ## Using Hooks
138
168
 
139
169
  ```tsx
140
- import { useUsers, useCreatePanic } from '@safercity/sdk-react';
170
+ import { useUser, useCreatePanic } from '@safercity/sdk-react';
141
171
 
142
- function Dashboard() {
143
- const { data: users, isLoading } = useUsers();
172
+ function Dashboard({ userId }: { userId: string }) {
173
+ const { data: user, isLoading } = useUser(userId);
144
174
 
145
175
  if (isLoading) return <div>Loading...</div>;
146
176
 
147
177
  return (
148
178
  <div>
149
- <h1>Users: {users?.data.users.length}</h1>
179
+ <h1>Welcome, {user?.data.firstName}</h1>
150
180
  </div>
151
181
  );
152
182
  }
@@ -184,31 +214,46 @@ function PanicButton({ userId }: { userId: string }) {
184
214
  - `useWhoAmI()` - Current auth context
185
215
 
186
216
  ### Users
187
- - `useUsers(filters?)` - List users
188
- - `useUser(userId)` - Get user by ID
217
+ - `useUser(userId?)` - Get user by ID (defaults to client's `userId`)
189
218
  - `useCreateUser()` - Create user mutation
190
219
  - `useUpdateUser()` - Update user mutation
191
- - `useDeleteUser()` - Delete user mutation
192
220
 
193
221
  ### Panics
194
222
  - `usePanic(panicId, query?)` - Get panic by ID
195
223
  - `useCreatePanic()` - Create panic mutation
196
224
  - `useUpdatePanicLocation()` - Update location mutation
197
225
  - `useCancelPanic()` - Cancel panic mutation
226
+ - `usePanicTypes(userId?)` - Get available panic types for a user
198
227
  - `usePanicStream(panicId, options?)` - Stream panic updates
199
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
+
200
237
  ### Subscriptions
201
238
  - `useSubscriptionTypes()` - List subscription types
202
- - `useSubscriptions(filters?)` - List subscriptions
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,18 +326,6 @@ 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
- },
328
- ...options
329
- });
330
- }
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
329
  },
339
330
  ...options
340
331
  });
@@ -382,6 +373,77 @@ function useCancelPanic(options) {
382
373
  ...options
383
374
  });
384
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
+ }
385
447
  function useSubscriptionTypes(options) {
386
448
  const client = useSaferCityClient();
387
449
  return reactQuery.useQuery({
@@ -411,6 +473,24 @@ function useCreateSubscription(options) {
411
473
  ...options
412
474
  });
413
475
  }
476
+ function useSubscribeUser(options) {
477
+ const client = useSaferCityClient();
478
+ const queryClient = reactQuery.useQueryClient();
479
+ return reactQuery.useMutation({
480
+ mutationFn: (body) => client.subscriptions.subscribeUser(body),
481
+ onSuccess: () => {
482
+ queryClient.invalidateQueries({ queryKey: saferCityKeys.subscriptions() });
483
+ },
484
+ ...options
485
+ });
486
+ }
487
+ function useBulkTriggerNotifications(options) {
488
+ const client = useSaferCityClient();
489
+ return reactQuery.useMutation({
490
+ mutationFn: (body) => client.notifications.bulkTrigger(body),
491
+ ...options
492
+ });
493
+ }
414
494
  function useLocationSafety(latitude, longitude, radius, options) {
415
495
  const client = useSaferCityClient();
416
496
  return reactQuery.useQuery({
@@ -422,6 +502,17 @@ function useLocationSafety(latitude, longitude, radius, options) {
422
502
  ...options
423
503
  });
424
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
+ }
425
516
  function useCrimes(filters, options) {
426
517
  const client = useSaferCityClient();
427
518
  return reactQuery.useQuery({
@@ -450,6 +541,16 @@ function useCrimeTypes(options) {
450
541
  ...options
451
542
  });
452
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
+ }
453
554
  function usePanicStream(panicId, options = {}) {
454
555
  const client = useSaferCityClient();
455
556
  const {
@@ -647,29 +748,38 @@ function useStream(createStream, options = {}) {
647
748
  exports.SaferCityProvider = SaferCityProvider;
648
749
  exports.saferCityKeys = saferCityKeys;
649
750
  exports.useAuthMode = useAuthMode;
751
+ exports.useBanner = useBanner;
752
+ exports.useBulkTriggerNotifications = useBulkTriggerNotifications;
650
753
  exports.useCancelPanic = useCancelPanic;
651
754
  exports.useCreatePanic = useCreatePanic;
755
+ exports.useCreatePanicInformation = useCreatePanicInformation;
652
756
  exports.useCreateSubscription = useCreateSubscription;
653
757
  exports.useCreateUser = useCreateUser;
654
758
  exports.useCrimeCategories = useCrimeCategories;
759
+ exports.useCrimeCategoriesWithTypes = useCrimeCategoriesWithTypes;
655
760
  exports.useCrimeTypes = useCrimeTypes;
656
761
  exports.useCrimes = useCrimes;
657
- exports.useDeleteUser = useDeleteUser;
762
+ exports.useDeletePanicInformation = useDeletePanicInformation;
658
763
  exports.useHealthCheck = useHealthCheck;
659
764
  exports.useLocationSafety = useLocationSafety;
660
765
  exports.usePanic = usePanic;
766
+ exports.usePanicEligibility = usePanicEligibility;
767
+ exports.usePanicInformation = usePanicInformation;
768
+ exports.usePanicInformationByUser = usePanicInformationByUser;
661
769
  exports.usePanicStream = usePanicStream;
770
+ exports.usePanicTypes = usePanicTypes;
662
771
  exports.useSaferCity = useSaferCity;
663
772
  exports.useSaferCityClient = useSaferCityClient;
664
773
  exports.useSession = useSession;
665
774
  exports.useSessionManager = useSessionManager;
666
775
  exports.useStream = useStream;
776
+ exports.useSubscribeUser = useSubscribeUser;
667
777
  exports.useSubscriptionTypes = useSubscriptionTypes;
668
778
  exports.useSubscriptions = useSubscriptions;
779
+ exports.useUpdatePanicInformation = useUpdatePanicInformation;
669
780
  exports.useUpdatePanicLocation = useUpdatePanicLocation;
670
781
  exports.useUpdateUser = useUpdateUser;
671
782
  exports.useUser = useUser;
672
- exports.useUsers = useUsers;
673
783
  exports.useWhoAmI = useWhoAmI;
674
784
  Object.keys(sdk).forEach(function (k) {
675
785
  if (k !== 'default' && !Object.prototype.hasOwnProperty.call(exports, k)) Object.defineProperty(exports, k, {