@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 { places } from '@chaosity/location-client'
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 apiUrl="https://api.example.com" token="your-token">
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
- const command = new places.SuggestCommand({
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
- - `apiUrl` (string, required) - API endpoint URL
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:** `GeoPlacesClient` instance
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 (Next.js)
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/page.tsx (Client-side)
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 async function Page() {
105
- const config = await getLocationConfig()
106
-
107
+ export default function RootLayout({ children }) {
107
108
  return (
108
- <LocationClientProvider {...config}>
109
- <MapComponent />
109
+ <LocationClientProvider getConfig={getLocationConfig}>
110
+ {children}
110
111
  </LocationClientProvider>
111
112
  )
112
113
  }
113
114
  ```
114
115
 
115
- ## Available Commands
116
-
117
- All AWS Location Service commands are available through the client:
116
+ ## Complete Example with MapLibre
118
117
 
119
118
  ```tsx
120
- import { places } from '@chaosity/location-client'
119
+ 'use client'
121
120
 
122
- // Autocomplete
123
- new places.SuggestCommand({ QueryText: 'Van', MaxResults: 5 })
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
- // Geocoding
126
- new places.GeocodeCommand({ QueryText: 'Vancouver, BC' })
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
- // Reverse Geocoding
129
- new places.ReverseGeocodeCommand({ QueryPosition: [-123.1207, 49.2827] })
173
+ map.current = mapInstance
130
174
 
131
- // Place Details
132
- new places.GetPlaceCommand({ PlaceId: 'place-id' })
175
+ return () => {
176
+ if (map.current) {
177
+ map.current.remove()
178
+ map.current = null
179
+ }
180
+ }
181
+ }, [config, client, loading])
133
182
 
134
- // Search Nearby
135
- new places.SearchNearbyCommand({ QueryPosition: [-123.1207, 49.2827] })
183
+ if (error) {
184
+ return <div>Error: {error}</div>
185
+ }
186
+
187
+ if (loading) {
188
+ return <div>Loading map...</div>
189
+ }
136
190
 
137
- // Text Search
138
- new places.SearchTextCommand({ QueryText: 'coffee shops' })
191
+ return <div ref={mapContainer} style={{ width: '100%', height: '600px' }} />
192
+ }
139
193
  ```
140
194
 
141
- See [@chaosity/location-client](https://www.npmjs.com/package/@chaosity/location-client) for complete documentation.
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 places.SuggestCommand({ QueryText: 'Vancouver' })
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 React from 'react';
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: React.ReactNode;
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): React.JSX.Element;
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
- import React, { createContext, useContext, useEffect, useState } from 'react';
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
- return (React.createElement(LocationClientContext.Provider, { value: { config, client, loading, error } }, children));
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.2",
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.2"
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
- "typescript": "^5.0.0",
38
- "react": "^18.0.0"
39
+ "react": "^18.0.0",
40
+ "typescript": "^5.0.0"
39
41
  },
40
42
  "files": [
41
43
  "dist",