@chaosity/location-client-react 0.1.2 → 0.1.5
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
|
@@ -12,12 +12,12 @@ npm install @chaosity/location-client-react @chaosity/location-client
|
|
|
12
12
|
|
|
13
13
|
```tsx
|
|
14
14
|
import { LocationClientProvider, useLocationClient } from '@chaosity/location-client-react'
|
|
15
|
-
import {
|
|
15
|
+
import { SuggestCommand } from '@chaosity/location-client'
|
|
16
16
|
|
|
17
17
|
// 1. Wrap your app with the provider
|
|
18
18
|
function App() {
|
|
19
19
|
return (
|
|
20
|
-
<LocationClientProvider
|
|
20
|
+
<LocationClientProvider getConfig={getLocationConfig}>
|
|
21
21
|
<MapComponent />
|
|
22
22
|
</LocationClientProvider>
|
|
23
23
|
)
|
|
@@ -25,10 +25,12 @@ function App() {
|
|
|
25
25
|
|
|
26
26
|
// 2. Use the client in any component
|
|
27
27
|
function MapComponent() {
|
|
28
|
-
const client = useLocationClient()
|
|
28
|
+
const { client, config, loading, error } = useLocationClient()
|
|
29
29
|
|
|
30
30
|
const searchPlaces = async (query: string) => {
|
|
31
|
-
|
|
31
|
+
if (!client) return
|
|
32
|
+
|
|
33
|
+
const command = new SuggestCommand({
|
|
32
34
|
QueryText: query,
|
|
33
35
|
MaxResults: 5
|
|
34
36
|
})
|
|
@@ -47,17 +49,13 @@ function MapComponent() {
|
|
|
47
49
|
Provides the location client to all child components.
|
|
48
50
|
|
|
49
51
|
```tsx
|
|
50
|
-
<LocationClientProvider
|
|
51
|
-
apiUrl="https://api.example.com"
|
|
52
|
-
token="your-bearer-token"
|
|
53
|
-
>
|
|
52
|
+
<LocationClientProvider getConfig={getLocationConfig}>
|
|
54
53
|
{children}
|
|
55
54
|
</LocationClientProvider>
|
|
56
55
|
```
|
|
57
56
|
|
|
58
57
|
**Props:**
|
|
59
|
-
- `
|
|
60
|
-
- `token` (string, required) - Bearer token for authentication
|
|
58
|
+
- `getConfig` (function, required) - Async function that returns `{ apiUrl: string, token: string }`
|
|
61
59
|
- `children` (ReactNode, required) - Child components
|
|
62
60
|
|
|
63
61
|
### useLocationClient
|
|
@@ -65,14 +63,18 @@ Provides the location client to all child components.
|
|
|
65
63
|
Hook to access the location client in any component.
|
|
66
64
|
|
|
67
65
|
```tsx
|
|
68
|
-
const client = useLocationClient()
|
|
66
|
+
const { client, config, loading, error } = useLocationClient()
|
|
69
67
|
```
|
|
70
68
|
|
|
71
|
-
**Returns:**
|
|
69
|
+
**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
|
|
72
74
|
|
|
73
75
|
**Throws:** Error if used outside `LocationClientProvider`
|
|
74
76
|
|
|
75
|
-
## Usage with Server Actions
|
|
77
|
+
## Usage with Next.js Server Actions
|
|
76
78
|
|
|
77
79
|
```tsx
|
|
78
80
|
// app/actions/location.ts (Server-side)
|
|
@@ -96,63 +98,149 @@ export async function getLocationConfig() {
|
|
|
96
98
|
}
|
|
97
99
|
}
|
|
98
100
|
|
|
99
|
-
// app/
|
|
101
|
+
// app/layout.tsx (Client-side)
|
|
100
102
|
'use client'
|
|
103
|
+
|
|
101
104
|
import { LocationClientProvider } from '@chaosity/location-client-react'
|
|
102
105
|
import { getLocationConfig } from './actions/location'
|
|
103
106
|
|
|
104
|
-
export default
|
|
105
|
-
const config = await getLocationConfig()
|
|
106
|
-
|
|
107
|
+
export default function RootLayout({ children }) {
|
|
107
108
|
return (
|
|
108
|
-
<LocationClientProvider {
|
|
109
|
-
|
|
109
|
+
<LocationClientProvider getConfig={getLocationConfig}>
|
|
110
|
+
{children}
|
|
110
111
|
</LocationClientProvider>
|
|
111
112
|
)
|
|
112
113
|
}
|
|
113
114
|
```
|
|
114
115
|
|
|
115
|
-
##
|
|
116
|
-
|
|
117
|
-
All AWS Location Service commands are available through the client:
|
|
116
|
+
## Complete Example with MapLibre
|
|
118
117
|
|
|
119
118
|
```tsx
|
|
120
|
-
|
|
119
|
+
'use client'
|
|
121
120
|
|
|
122
|
-
|
|
123
|
-
|
|
121
|
+
import { useLocationClient } from '@chaosity/location-client-react'
|
|
122
|
+
import { GeoPlaces } from '@chaosity/location-client'
|
|
123
|
+
import maplibregl from 'maplibre-gl'
|
|
124
|
+
import MaplibreGeocoder from '@maplibre/maplibre-gl-geocoder'
|
|
125
|
+
import { useEffect, useRef } from 'react'
|
|
126
|
+
|
|
127
|
+
export default function MapComponent() {
|
|
128
|
+
const mapContainer = useRef<HTMLDivElement>(null)
|
|
129
|
+
const map = useRef<maplibregl.Map | null>(null)
|
|
130
|
+
const { config, client, loading, error } = useLocationClient()
|
|
131
|
+
|
|
132
|
+
useEffect(() => {
|
|
133
|
+
if (!mapContainer.current || map.current || loading || !config || !client) return
|
|
134
|
+
|
|
135
|
+
// Initialize map
|
|
136
|
+
const mapInstance = new maplibregl.Map({
|
|
137
|
+
container: mapContainer.current,
|
|
138
|
+
style: `${config.apiUrl}/maps/Standard/descriptor`,
|
|
139
|
+
center: [-123.12, 49.28],
|
|
140
|
+
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
|
+
}
|
|
150
|
+
})
|
|
151
|
+
|
|
152
|
+
// Add navigation controls
|
|
153
|
+
mapInstance.addControl(new maplibregl.NavigationControl(), 'top-right')
|
|
124
154
|
|
|
125
|
-
//
|
|
126
|
-
new
|
|
155
|
+
// Add geocoder
|
|
156
|
+
const geoPlaces = new GeoPlaces(config.apiUrl, config.token, mapInstance)
|
|
157
|
+
const geocoder = new MaplibreGeocoder(geoPlaces, {
|
|
158
|
+
maplibregl,
|
|
159
|
+
showResultsWhileTyping: true,
|
|
160
|
+
limit: 30
|
|
161
|
+
})
|
|
162
|
+
mapInstance.addControl(geocoder, 'top-left')
|
|
163
|
+
|
|
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
|
+
}
|
|
171
|
+
})
|
|
127
172
|
|
|
128
|
-
|
|
129
|
-
new places.ReverseGeocodeCommand({ QueryPosition: [-123.1207, 49.2827] })
|
|
173
|
+
map.current = mapInstance
|
|
130
174
|
|
|
131
|
-
|
|
132
|
-
|
|
175
|
+
return () => {
|
|
176
|
+
if (map.current) {
|
|
177
|
+
map.current.remove()
|
|
178
|
+
map.current = null
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
}, [config, client, loading])
|
|
133
182
|
|
|
134
|
-
|
|
135
|
-
|
|
183
|
+
if (error) {
|
|
184
|
+
return <div>Error: {error}</div>
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
if (loading) {
|
|
188
|
+
return <div>Loading map...</div>
|
|
189
|
+
}
|
|
136
190
|
|
|
137
|
-
|
|
138
|
-
|
|
191
|
+
return <div ref={mapContainer} style={{ width: '100%', height: '600px' }} />
|
|
192
|
+
}
|
|
139
193
|
```
|
|
140
194
|
|
|
141
|
-
|
|
195
|
+
## Available Commands
|
|
196
|
+
|
|
197
|
+
All AWS Location Service commands are available through the client:
|
|
198
|
+
|
|
199
|
+
```tsx
|
|
200
|
+
import {
|
|
201
|
+
SuggestCommand,
|
|
202
|
+
GeocodeCommand,
|
|
203
|
+
ReverseGeocodeCommand,
|
|
204
|
+
GetPlaceCommand,
|
|
205
|
+
SearchTextCommand,
|
|
206
|
+
SearchNearbyCommand
|
|
207
|
+
} from '@chaosity/location-client'
|
|
208
|
+
|
|
209
|
+
function MyComponent() {
|
|
210
|
+
const { client } = useLocationClient()
|
|
211
|
+
|
|
212
|
+
const searchPlaces = async () => {
|
|
213
|
+
const response = await client.send(
|
|
214
|
+
new SuggestCommand({ QueryText: 'Vancouver', MaxResults: 5 })
|
|
215
|
+
)
|
|
216
|
+
return response.ResultItems
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
```
|
|
142
220
|
|
|
143
221
|
## TypeScript Support
|
|
144
222
|
|
|
145
223
|
Full TypeScript support with types from AWS SDK:
|
|
146
224
|
|
|
147
225
|
```tsx
|
|
148
|
-
import { places } from '@chaosity/location-client'
|
|
149
226
|
import type { SuggestCommandOutput } from '@aws-sdk/client-geo-places'
|
|
150
227
|
|
|
228
|
+
const { client } = useLocationClient()
|
|
229
|
+
|
|
151
230
|
const response: SuggestCommandOutput = await client.send(
|
|
152
|
-
new
|
|
231
|
+
new SuggestCommand({ QueryText: 'Vancouver' })
|
|
153
232
|
)
|
|
154
233
|
```
|
|
155
234
|
|
|
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
|
+
|
|
156
244
|
## License
|
|
157
245
|
|
|
158
246
|
MIT
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import
|
|
1
|
+
import type { ReactNode } from 'react';
|
|
2
2
|
import { GeoPlacesClient, ClientConfig } from '@chaosity/location-client';
|
|
3
3
|
interface LocationClientContextValue {
|
|
4
4
|
config: ClientConfig | null;
|
|
@@ -7,9 +7,12 @@ interface LocationClientContextValue {
|
|
|
7
7
|
error: string | null;
|
|
8
8
|
}
|
|
9
9
|
export interface LocationClientProviderProps {
|
|
10
|
-
children:
|
|
11
|
-
getConfig: () => Promise<ClientConfig
|
|
10
|
+
children: ReactNode;
|
|
11
|
+
getConfig: () => Promise<ClientConfig & {
|
|
12
|
+
expiresAt?: number;
|
|
13
|
+
}>;
|
|
14
|
+
refreshBuffer?: number;
|
|
12
15
|
}
|
|
13
|
-
export declare function LocationClientProvider({ children, getConfig }: LocationClientProviderProps):
|
|
16
|
+
export declare function LocationClientProvider({ children, getConfig, refreshBuffer }: LocationClientProviderProps): import("react/jsx-runtime").JSX.Element;
|
|
14
17
|
export declare function useLocationClient(): LocationClientContextValue;
|
|
15
18
|
export {};
|
|
@@ -1,24 +1,78 @@
|
|
|
1
|
-
|
|
1
|
+
'use client';
|
|
2
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
3
|
+
import { createContext, useContext, useEffect, useState, useRef, useCallback } from 'react';
|
|
2
4
|
import { GeoPlacesClient } from '@chaosity/location-client';
|
|
5
|
+
import debug from 'debug';
|
|
6
|
+
const log = debug('location-client-react:provider');
|
|
3
7
|
const LocationClientContext = createContext(undefined);
|
|
4
|
-
export function LocationClientProvider({ children, getConfig }) {
|
|
8
|
+
export function LocationClientProvider({ children, getConfig, refreshBuffer = 60 }) {
|
|
5
9
|
const [config, setConfig] = useState(null);
|
|
6
10
|
const [client, setClient] = useState(null);
|
|
7
11
|
const [loading, setLoading] = useState(true);
|
|
8
12
|
const [error, setError] = useState(null);
|
|
13
|
+
const expiresAtRef = useRef(null);
|
|
14
|
+
const getConfigRef = useRef(getConfig);
|
|
15
|
+
// Update ref when getConfig changes
|
|
9
16
|
useEffect(() => {
|
|
17
|
+
getConfigRef.current = getConfig;
|
|
18
|
+
}, [getConfig]);
|
|
19
|
+
// Check if token is expired or about to expire
|
|
20
|
+
const isTokenExpired = useCallback(() => {
|
|
21
|
+
if (!expiresAtRef.current)
|
|
22
|
+
return false;
|
|
23
|
+
return Date.now() >= (expiresAtRef.current - refreshBuffer * 1000);
|
|
24
|
+
}, [refreshBuffer]);
|
|
25
|
+
// Refresh token if needed
|
|
26
|
+
const ensureValidToken = useCallback(async () => {
|
|
27
|
+
if (!isTokenExpired())
|
|
28
|
+
return;
|
|
29
|
+
const timeUntilExpiry = expiresAtRef.current ? Math.floor((expiresAtRef.current - Date.now()) / 1000) : 0;
|
|
30
|
+
log('Token expired or expiring soon (expires in %ds), refreshing...', timeUntilExpiry);
|
|
31
|
+
try {
|
|
32
|
+
const cfg = await getConfigRef.current();
|
|
33
|
+
setConfig(cfg);
|
|
34
|
+
setClient(new GeoPlacesClient(cfg));
|
|
35
|
+
if (cfg.expiresAt) {
|
|
36
|
+
expiresAtRef.current = cfg.expiresAt;
|
|
37
|
+
const newExpiry = Math.floor((cfg.expiresAt - Date.now()) / 1000);
|
|
38
|
+
log('Token refreshed successfully (new token expires in %ds)', newExpiry);
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
catch (err) {
|
|
42
|
+
log('Token refresh failed: %s', err instanceof Error ? err.message : 'Unknown error');
|
|
43
|
+
setError(err instanceof Error ? err.message : 'Failed to refresh token');
|
|
44
|
+
}
|
|
45
|
+
}, [isTokenExpired]);
|
|
46
|
+
// Initial load
|
|
47
|
+
useEffect(() => {
|
|
48
|
+
log('Initializing LocationClientProvider');
|
|
10
49
|
getConfig()
|
|
11
50
|
.then((cfg) => {
|
|
12
51
|
setConfig(cfg);
|
|
13
52
|
setClient(new GeoPlacesClient(cfg));
|
|
53
|
+
if (cfg.expiresAt) {
|
|
54
|
+
expiresAtRef.current = cfg.expiresAt;
|
|
55
|
+
const expiry = Math.floor((cfg.expiresAt - Date.now()) / 1000);
|
|
56
|
+
log('Client initialized (token expires in %ds)', expiry);
|
|
57
|
+
}
|
|
58
|
+
else {
|
|
59
|
+
log('Client initialized (no expiry info)');
|
|
60
|
+
}
|
|
14
61
|
setLoading(false);
|
|
15
62
|
})
|
|
16
63
|
.catch((err) => {
|
|
64
|
+
log('Initialization failed: %s', err instanceof Error ? err.message : 'Unknown error');
|
|
17
65
|
setError(err instanceof Error ? err.message : 'Failed to initialize client');
|
|
18
66
|
setLoading(false);
|
|
19
67
|
});
|
|
20
|
-
}, []);
|
|
21
|
-
|
|
68
|
+
}, [getConfig]);
|
|
69
|
+
// Check token validity on every state change
|
|
70
|
+
useEffect(() => {
|
|
71
|
+
if (!loading && config) {
|
|
72
|
+
ensureValidToken();
|
|
73
|
+
}
|
|
74
|
+
}, [config, loading, ensureValidToken]);
|
|
75
|
+
return (_jsx(LocationClientContext.Provider, { value: { config, client, loading, error }, children: children }));
|
|
22
76
|
}
|
|
23
77
|
export function useLocationClient() {
|
|
24
78
|
const context = useContext(LocationClientContext);
|
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.5",
|
|
4
4
|
"description": "React bindings for Chaosity Location Service client",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"types": "dist/index.d.ts",
|
|
@@ -27,15 +27,17 @@
|
|
|
27
27
|
"url": "https://github.com/chaosity-io/location-service-client-react/issues"
|
|
28
28
|
},
|
|
29
29
|
"dependencies": {
|
|
30
|
-
"@chaosity/location-client": "^0.1.
|
|
30
|
+
"@chaosity/location-client": "^0.1.3",
|
|
31
|
+
"debug": "^4.4.3"
|
|
31
32
|
},
|
|
32
33
|
"peerDependencies": {
|
|
33
34
|
"react": "^18.0.0 || ^19.0.0"
|
|
34
35
|
},
|
|
35
36
|
"devDependencies": {
|
|
37
|
+
"@types/debug": "^4.1.12",
|
|
36
38
|
"@types/react": "^18.0.0",
|
|
37
|
-
"
|
|
38
|
-
"
|
|
39
|
+
"react": "^18.0.0",
|
|
40
|
+
"typescript": "^5.0.0"
|
|
39
41
|
},
|
|
40
42
|
"files": [
|
|
41
43
|
"dist",
|