@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 +95 -110
- package/dist/hooks/useMapLanguage.d.ts +25 -0
- package/dist/hooks/useMapLanguage.js +32 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1 -0
- package/package.json +9 -2
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)
|
|
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
|
-
|
|
15
|
-
|
|
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
|
-
|
|
18
|
-
function App() {
|
|
36
|
+
export default function RootLayout({ children }: { children: React.ReactNode }) {
|
|
19
37
|
return (
|
|
20
38
|
<LocationClientProvider getConfig={getLocationConfig}>
|
|
21
|
-
|
|
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
|
-
|
|
34
|
-
|
|
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
|
|
76
|
+
<LocationClientProvider
|
|
77
|
+
getConfig={getLocationConfig}
|
|
78
|
+
refreshBuffer={60}
|
|
79
|
+
>
|
|
53
80
|
{children}
|
|
54
81
|
</LocationClientProvider>
|
|
55
82
|
```
|
|
56
83
|
|
|
57
84
|
**Props:**
|
|
58
|
-
- `getConfig`
|
|
59
|
-
- `
|
|
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,
|
|
94
|
+
const { client, getToken, loading, error } = useLocationClient()
|
|
67
95
|
```
|
|
68
96
|
|
|
69
97
|
**Returns:**
|
|
70
|
-
- `client` (GeoPlacesClient | null)
|
|
71
|
-
- `
|
|
72
|
-
- `loading` (boolean)
|
|
73
|
-
- `error` (string | null)
|
|
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
|
-
|
|
103
|
+
**Throws:** Error if used outside `LocationClientProvider`.
|
|
78
104
|
|
|
79
|
-
|
|
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
|
-
|
|
102
|
-
'use client'
|
|
107
|
+
The provider automatically handles token lifecycle:
|
|
103
108
|
|
|
104
|
-
|
|
105
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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 {
|
|
132
|
+
const { client, getToken, loading, error } = useLocationClient()
|
|
131
133
|
|
|
132
134
|
useEffect(() => {
|
|
133
|
-
if (!mapContainer.current || map.current || loading || !
|
|
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: `${
|
|
142
|
+
style: `${API_URL}/maps/Standard/descriptor`,
|
|
139
143
|
center: [-123.12, 49.28],
|
|
140
144
|
zoom: 10,
|
|
141
|
-
transformRequest: (
|
|
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
|
-
//
|
|
156
|
-
const geoPlaces = new GeoPlaces(
|
|
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
|
-
//
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
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
|
-
}, [
|
|
182
|
-
|
|
183
|
-
if (error) {
|
|
184
|
-
return <div>Error: {error}</div>
|
|
185
|
-
}
|
|
173
|
+
}, [client, getToken, loading])
|
|
186
174
|
|
|
187
|
-
if (
|
|
188
|
-
|
|
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
|
|
213
|
-
const response = await client
|
|
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
package/dist/index.js
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@chaosity/location-client-react",
|
|
3
|
-
"version": "0.1.
|
|
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"
|