@chaosity/location-client-react 0.1.7 → 0.1.9

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
@@ -1,6 +1,6 @@
1
1
  # @chaosity/location-client-react
2
2
 
3
- React bindings for [@chaosity/location-client](https://www.npmjs.com/package/@chaosity/location-client) - AWS Location Service compatible client.
3
+ React bindings for [@chaosity/location-client](https://www.npmjs.com/package/@chaosity/location-client) with automatic token refresh.
4
4
 
5
5
  ## Installation
6
6
 
@@ -10,34 +10,58 @@ npm install @chaosity/location-client-react @chaosity/location-client
10
10
 
11
11
  ## Quick Start
12
12
 
13
+ ### 1. Create a Server Action to fetch config
14
+
15
+ ```typescript
16
+ // app/actions/location.ts
17
+ 'use server'
18
+
19
+ import { getClientConfig } from '@chaosity/location-client/server'
20
+
21
+ export async function getLocationConfig() {
22
+ // Auto-reads LOCATION_API_URL, LOCATION_CLIENT_ID, LOCATION_CLIENT_SECRET
23
+ return await getClientConfig()
24
+ }
25
+ ```
26
+
27
+ ### 2. Wrap your app with the provider
28
+
13
29
  ```tsx
14
- import { LocationClientProvider, useLocationClient } from '@chaosity/location-client-react'
15
- import { SuggestCommand } from '@chaosity/location-client'
30
+ // app/layout.tsx
31
+ 'use client'
32
+
33
+ import { LocationClientProvider } from '@chaosity/location-client-react'
34
+ import { getLocationConfig } from './actions/location'
16
35
 
17
- // 1. Wrap your app with the provider
18
- function App() {
36
+ export default function RootLayout({ children }: { children: React.ReactNode }) {
19
37
  return (
20
38
  <LocationClientProvider getConfig={getLocationConfig}>
21
- <MapComponent />
39
+ {children}
22
40
  </LocationClientProvider>
23
41
  )
24
42
  }
43
+ ```
44
+
45
+ ### 3. Use the client in any component
46
+
47
+ ```tsx
48
+ import { useLocationClient } from '@chaosity/location-client-react'
49
+ import { SuggestCommand } from '@chaosity/location-client'
50
+
51
+ function SearchComponent() {
52
+ const { client, loading, error } = useLocationClient()
25
53
 
26
- // 2. Use the client in any component
27
- function MapComponent() {
28
- const { client, config, loading, error } = useLocationClient()
29
-
30
54
  const searchPlaces = async (query: string) => {
31
55
  if (!client) return
32
-
33
- const command = new SuggestCommand({
34
- QueryText: query,
35
- MaxResults: 5
36
- })
37
- const response = await client.send(command)
56
+ const response = await client.send(
57
+ new SuggestCommand({ QueryText: query, MaxResults: 5 })
58
+ )
38
59
  return response.ResultItems
39
60
  }
40
-
61
+
62
+ if (loading) return <div>Loading...</div>
63
+ if (error) return <div>Error: {error}</div>
64
+
41
65
  return <div>...</div>
42
66
  }
43
67
  ```
@@ -46,128 +70,96 @@ function MapComponent() {
46
70
 
47
71
  ### LocationClientProvider
48
72
 
49
- Provides the location client to all child components.
73
+ Provides the location client and automatic token refresh to all child components.
50
74
 
51
75
  ```tsx
52
- <LocationClientProvider getConfig={getLocationConfig}>
76
+ <LocationClientProvider
77
+ getConfig={getLocationConfig}
78
+ refreshBuffer={60}
79
+ >
53
80
  {children}
54
81
  </LocationClientProvider>
55
82
  ```
56
83
 
57
84
  **Props:**
58
- - `getConfig` (function, required) - Async function that returns `{ apiUrl: string, token: string }`
59
- - `children` (ReactNode, required) - Child components
85
+ - `getConfig` — Async function that returns `{ apiUrl: string, token: string, expiresAt?: number }`. Called on init and whenever the token needs refreshing.
86
+ - `refreshBuffer` (optional, default: `60`) — Seconds before token expiry to proactively refresh. Prevents mid-request expiration.
87
+ - `children` — Child components.
60
88
 
61
89
  ### useLocationClient
62
90
 
63
91
  Hook to access the location client in any component.
64
92
 
65
93
  ```tsx
66
- const { client, config, loading, error } = useLocationClient()
94
+ const { client, getToken, loading, error } = useLocationClient()
67
95
  ```
68
96
 
69
97
  **Returns:**
70
- - `client` (GeoPlacesClient | null) - The location client instance
71
- - `config` (ClientConfig | null) - The client configuration (apiUrl, token)
72
- - `loading` (boolean) - Whether the client is initializing
73
- - `error` (string | null) - Error message if initialization failed
74
-
75
- **Throws:** Error if used outside `LocationClientProvider`
98
+ - `client` (`GeoPlacesClient | null`) — The location client instance. Automatically refreshes the token before each `send()` call if needed.
99
+ - `getToken` (`() => string | undefined`) — Returns the current token. Useful for direct API calls (e.g., map style fetch).
100
+ - `loading` (`boolean`) — Whether the client is initializing.
101
+ - `error` (`string | null`) — Error message if initialization or token refresh failed.
76
102
 
77
- ## Usage with Next.js Server Actions
103
+ **Throws:** Error if used outside `LocationClientProvider`.
78
104
 
79
- ```tsx
80
- // app/actions/location.ts (Server-side)
81
- 'use server'
82
-
83
- export async function getLocationConfig() {
84
- const response = await fetch('https://api.example.com/auth/token', {
85
- method: 'POST',
86
- headers: { 'Content-Type': 'application/json' },
87
- body: JSON.stringify({
88
- grant_type: 'client_credentials',
89
- client_id: process.env.LOCATION_CLIENT_ID!,
90
- client_secret: process.env.LOCATION_CLIENT_SECRET!
91
- })
92
- })
93
-
94
- const data = await response.json()
95
- return {
96
- apiUrl: 'https://api.example.com',
97
- token: data.access_token
98
- }
99
- }
105
+ ## Token Refresh
100
106
 
101
- // app/layout.tsx (Client-side)
102
- 'use client'
107
+ The provider automatically handles token lifecycle:
103
108
 
104
- import { LocationClientProvider } from '@chaosity/location-client-react'
105
- import { getLocationConfig } from './actions/location'
109
+ 1. Fetches an initial token via `getConfig` on mount
110
+ 2. Before each `client.send()` call, checks if the token is expired or within the `refreshBuffer` window
111
+ 3. If expired, calls `getConfig` again to get a fresh token
112
+ 4. Concurrent refresh requests are deduplicated — multiple `send()` calls wait for the same refresh
106
113
 
107
- export default function RootLayout({ children }) {
108
- return (
109
- <LocationClientProvider getConfig={getLocationConfig}>
110
- {children}
111
- </LocationClientProvider>
112
- )
113
- }
114
- ```
114
+ No manual token management needed. The `client` always uses a valid token.
115
115
 
116
116
  ## Complete Example with MapLibre
117
117
 
118
118
  ```tsx
119
119
  'use client'
120
120
 
121
+ import { useEffect, useRef } from 'react'
121
122
  import { useLocationClient } from '@chaosity/location-client-react'
122
- import { GeoPlaces } from '@chaosity/location-client'
123
+ import { GeoPlaces, createTransformRequest } from '@chaosity/location-client'
123
124
  import maplibregl from 'maplibre-gl'
124
125
  import MaplibreGeocoder from '@maplibre/maplibre-gl-geocoder'
125
- import { useEffect, useRef } from 'react'
126
+
127
+ const API_URL = process.env.NEXT_PUBLIC_LOCATION_API_URL!
126
128
 
127
129
  export default function MapComponent() {
128
130
  const mapContainer = useRef<HTMLDivElement>(null)
129
131
  const map = useRef<maplibregl.Map | null>(null)
130
- const { config, client, loading, error } = useLocationClient()
132
+ const { client, getToken, loading, error } = useLocationClient()
131
133
 
132
134
  useEffect(() => {
133
- if (!mapContainer.current || map.current || loading || !config || !client) return
135
+ if (!mapContainer.current || map.current || loading || !client) return
136
+
137
+ const token = getToken()
138
+ if (!token) return
134
139
 
135
- // Initialize map
136
140
  const mapInstance = new maplibregl.Map({
137
141
  container: mapContainer.current,
138
- style: `${config.apiUrl}/maps/Standard/descriptor`,
142
+ style: `${API_URL}/maps/Standard/descriptor`,
139
143
  center: [-123.12, 49.28],
140
144
  zoom: 10,
141
- transformRequest: (url) => {
142
- if (url.startsWith(config.apiUrl)) {
143
- return {
144
- url,
145
- headers: { 'Authorization': `Bearer ${config.token}` }
146
- }
147
- }
148
- return { url }
149
- }
145
+ transformRequest: createTransformRequest(API_URL, getToken),
150
146
  })
151
147
 
152
- // Add navigation controls
153
148
  mapInstance.addControl(new maplibregl.NavigationControl(), 'top-right')
154
149
 
155
- // Add geocoder
156
- const geoPlaces = new GeoPlaces(config.apiUrl, config.token, mapInstance)
150
+ // GeoPlaces adapter takes the client instance (not raw apiUrl/token)
151
+ const geoPlaces = new GeoPlaces(client, mapInstance)
157
152
  const geocoder = new MaplibreGeocoder(geoPlaces, {
158
153
  maplibregl,
159
154
  showResultsWhileTyping: true,
160
- limit: 30
155
+ limit: 30,
161
156
  })
162
157
  mapInstance.addControl(geocoder, 'top-left')
163
158
 
164
- // Handle result selection
165
- geocoder.on('result', async (event) => {
166
- const { id, result_type } = event.result
167
- if (result_type === 'Place') {
168
- const details = await geoPlaces.searchByPlaceId(id)
169
- console.log('Place details:', details)
170
- }
159
+ // The geocoder resolves places internally via searchByPlaceId.
160
+ // The 'result' event fires with the resolved place feature.
161
+ geocoder.on('result', (event: any) => {
162
+ console.log('Selected place:', event.result)
171
163
  })
172
164
 
173
165
  map.current = mapInstance
@@ -178,15 +170,10 @@ export default function MapComponent() {
178
170
  map.current = null
179
171
  }
180
172
  }
181
- }, [config, client, loading])
182
-
183
- if (error) {
184
- return <div>Error: {error}</div>
185
- }
173
+ }, [client, getToken, loading])
186
174
 
187
- if (loading) {
188
- return <div>Loading map...</div>
189
- }
175
+ if (error) return <div>Error: {error}</div>
176
+ if (loading) return <div>Loading map...</div>
190
177
 
191
178
  return <div ref={mapContainer} style={{ width: '100%', height: '600px' }} />
192
179
  }
@@ -203,14 +190,14 @@ import {
203
190
  ReverseGeocodeCommand,
204
191
  GetPlaceCommand,
205
192
  SearchTextCommand,
206
- SearchNearbyCommand
193
+ SearchNearbyCommand,
207
194
  } from '@chaosity/location-client'
208
195
 
209
196
  function MyComponent() {
210
197
  const { client } = useLocationClient()
211
198
 
212
- const searchPlaces = async () => {
213
- const response = await client.send(
199
+ const search = async () => {
200
+ const response = await client!.send(
214
201
  new SuggestCommand({ QueryText: 'Vancouver', MaxResults: 5 })
215
202
  )
216
203
  return response.ResultItems
@@ -218,6 +205,14 @@ function MyComponent() {
218
205
  }
219
206
  ```
220
207
 
208
+ ## Logging
209
+
210
+ Enable debug logging with the `DEBUG` environment variable:
211
+
212
+ ```bash
213
+ DEBUG=location-client-react:* npm run dev
214
+ ```
215
+
221
216
  ## TypeScript Support
222
217
 
223
218
  Full TypeScript support with types from AWS SDK:
@@ -226,21 +221,11 @@ Full TypeScript support with types from AWS SDK:
226
221
  import type { SuggestCommandOutput } from '@aws-sdk/client-geo-places'
227
222
 
228
223
  const { client } = useLocationClient()
229
-
230
- const response: SuggestCommandOutput = await client.send(
224
+ const response: SuggestCommandOutput = await client!.send(
231
225
  new SuggestCommand({ QueryText: 'Vancouver' })
232
226
  )
233
227
  ```
234
228
 
235
- ## Security Best Practices
236
-
237
- ⚠️ **NEVER expose client credentials in browser code!**
238
-
239
- - The `getConfig` function should call a server-side API or Server Action
240
- - Store `client_id` and `client_secret` in server environment variables only
241
- - Only the JWT token should be sent to the browser
242
- - Tokens should be short-lived and refreshed as needed
243
-
244
229
  ## License
245
230
 
246
231
  MIT
@@ -0,0 +1,25 @@
1
+ /**
2
+ * Minimal interface describing the MapLibre Map methods used by this hook.
3
+ * Using a structural type avoids maplibre-gl version conflicts between packages.
4
+ */
5
+ interface MapLike {
6
+ isStyleLoaded(): boolean | void;
7
+ on(event: string, listener: (...args: unknown[]) => void): void;
8
+ off(event: string, listener: (...args: unknown[]) => void): void;
9
+ }
10
+ /**
11
+ * React hook that keeps map label language in sync with the `language` prop.
12
+ *
13
+ * Registers a persistent `style.load` listener so language is automatically
14
+ * reapplied whenever `map.setStyle()` is called (e.g. style or color scheme change).
15
+ * Also applies immediately if the style is already loaded.
16
+ *
17
+ * @param map - MapLibre Map instance, or null while the map is initializing
18
+ * @param language - ISO 639-1 language code (e.g. 'en', 'fr', 'de', 'ja')
19
+ *
20
+ * @example
21
+ * const [mapInstance, setMapInstance] = useState<maplibregl.Map | null>(null)
22
+ * useMapLanguage(mapInstance, language)
23
+ */
24
+ export declare function useMapLanguage(map: MapLike | null, language: string): void;
25
+ export {};
@@ -0,0 +1,32 @@
1
+ 'use client';
2
+ import { useEffect } from 'react';
3
+ import { applyMapLanguage } from '@chaosity/location-client';
4
+ /**
5
+ * React hook that keeps map label language in sync with the `language` prop.
6
+ *
7
+ * Registers a persistent `style.load` listener so language is automatically
8
+ * reapplied whenever `map.setStyle()` is called (e.g. style or color scheme change).
9
+ * Also applies immediately if the style is already loaded.
10
+ *
11
+ * @param map - MapLibre Map instance, or null while the map is initializing
12
+ * @param language - ISO 639-1 language code (e.g. 'en', 'fr', 'de', 'ja')
13
+ *
14
+ * @example
15
+ * const [mapInstance, setMapInstance] = useState<maplibregl.Map | null>(null)
16
+ * useMapLanguage(mapInstance, language)
17
+ */
18
+ export function useMapLanguage(map, language) {
19
+ useEffect(() => {
20
+ if (!map)
21
+ return;
22
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
23
+ const applyFn = () => applyMapLanguage(map, language);
24
+ if (map.isStyleLoaded()) {
25
+ applyFn();
26
+ }
27
+ map.on('style.load', applyFn);
28
+ return () => {
29
+ map.off('style.load', applyFn);
30
+ };
31
+ }, [map, language]);
32
+ }
package/dist/index.d.ts CHANGED
@@ -1,2 +1,3 @@
1
1
  export { LocationClientProvider, useLocationClient } from './provider/LocationClientProvider';
2
2
  export type { LocationClientProviderProps } from './provider/LocationClientProvider';
3
+ export { useMapLanguage } from './hooks/useMapLanguage';
package/dist/index.js CHANGED
@@ -1 +1,2 @@
1
1
  export { LocationClientProvider, useLocationClient } from './provider/LocationClientProvider';
2
+ export { useMapLanguage } from './hooks/useMapLanguage';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@chaosity/location-client-react",
3
- "version": "0.1.7",
3
+ "version": "0.1.9",
4
4
  "description": "React bindings for Chaosity Location Service client",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
@@ -23,7 +23,7 @@
23
23
  "license": "MIT",
24
24
  "repository": {
25
25
  "type": "git",
26
- "url": "https://github.com/chaosity-io/location-service-client-react.git"
26
+ "url": "git+https://github.com/chaosity-io/location-service-client-react.git"
27
27
  },
28
28
  "homepage": "https://github.com/chaosity-io/location-service-client-react",
29
29
  "bugs": {
@@ -34,14 +34,21 @@
34
34
  "debug": "^4.4.3"
35
35
  },
36
36
  "peerDependencies": {
37
+ "maplibre-gl": "^5.0.0",
37
38
  "react": "^18.0.0 || ^19.0.0"
38
39
  },
40
+ "peerDependenciesMeta": {
41
+ "maplibre-gl": {
42
+ "optional": true
43
+ }
44
+ },
39
45
  "devDependencies": {
40
46
  "@testing-library/react": "^16.0.0",
41
47
  "@testing-library/user-event": "^14.0.0",
42
48
  "@types/debug": "^4.1.12",
43
49
  "@types/react": "^19.0.0",
44
50
  "happy-dom": "^17.0.0",
51
+ "maplibre-gl": "^5.0.0",
45
52
  "react": "^19.0.0",
46
53
  "typescript": "^5.0.0",
47
54
  "vitest": "^3.0.0"