Skip to content

Commit 992994d

Browse files
felansuf.gonzalezusefulthink
authored
feat: add fetchAppCheckToken prop to APIProvider (#913)
Adds support for Firebase AppCheck to the APIProvider for enhanced API key security. --------- Co-authored-by: f.gonzalez <f.gonzalez@cloq.com.br> Co-authored-by: Martin Schuhfuss <m.schuhfuss@gmail.com>
1 parent e44933e commit 992994d

4 files changed

Lines changed: 110 additions & 2 deletions

File tree

docs/api-reference/components/api-provider.md

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -131,6 +131,57 @@ used even when this option is enabled.
131131

132132
Read more in the [documentation][gmp-usage-attribution].
133133

134+
#### `fetchAppCheckToken`: () => Promise\<google.maps.MapsAppCheckTokenResult\>
135+
136+
A function that returns a Promise resolving to a Firebase App Check token.
137+
When provided, this function will be set on `google.maps.Settings.getInstance().fetchAppCheckToken`
138+
after the Google Maps JavaScript API has loaded.
139+
140+
[Firebase App Check][firebase-app-check] helps protect your Google Maps Platform API key by
141+
blocking traffic from unauthorized sources, preventing malicious requests and
142+
unauthorized API calls that could incur charges. It works by validating that requests
143+
come from legitimate apps using attestation providers like [reCAPTCHA Enterprise][recaptcha-enterprise].
144+
145+
**Example usage with Firebase:**
146+
147+
A custom wrapper component that initializes Firebase App Check and passes
148+
the token fetcher to `APIProvider`:
149+
150+
```tsx
151+
import React, {PropsWithChildren, useCallback} from 'react';
152+
import {APIProvider, APIProviderProps} from '@vis.gl/react-google-maps';
153+
import {initializeApp} from 'firebase/app';
154+
import {
155+
getToken,
156+
initializeAppCheck,
157+
ReCaptchaEnterpriseProvider
158+
} from 'firebase/app-check';
159+
160+
// Firebase and App Check initialization
161+
const app = initializeApp({/* ... */});
162+
const appCheck = initializeAppCheck(firebaseApp, {
163+
provider: new ReCaptchaEnterpriseProvider(RECAPTCHA_SITE_KEY),
164+
isTokenAutoRefreshEnabled: true
165+
});
166+
167+
// custom wrapper for the APIProvider
168+
export function CustomAPIProvider({children, ...props}) {
169+
const fetchAppCheckToken = useCallback(() => getToken(appCheck, false), []);
170+
171+
return (
172+
<APIProvider {...props} fetchAppCheckToken={fetchAppCheckToken}>
173+
{children}
174+
</APIProvider>
175+
);
176+
}
177+
```
178+
179+
For more information, see:
180+
181+
- [Using App Check with Maps JavaScript API][gmp-app-check]
182+
- [Firebase App Check documentation][firebase-app-check]
183+
- [App Check codelab][gmp-app-check-codelab]
184+
134185
### Events
135186

136187
#### `onLoad`: () => void {#onLoad}
@@ -179,6 +230,10 @@ The following hooks are built to work with the `APIProvider` Component:
179230
[gmp-lang]: https://developers.google.com/maps/documentation/javascript/localization
180231
[gmp-solutions-usage]: https://developers.google.com/maps/reporting-and-monitoring/reporting#solutions-usage
181232
[gmp-usage-attribution]: https://developers.google.com/maps/documentation/javascript/reference/map#MapOptions.internalUsageAttributionIds
233+
[gmp-app-check]: https://developers.google.com/maps/documentation/javascript/maps-app-check
234+
[gmp-app-check-codelab]: https://developers.google.com/codelabs/maps-platform/maps-platform-firebase-appcheck
235+
[firebase-app-check]: https://firebase.google.com/docs/app-check
236+
[recaptcha-enterprise]: https://cloud.google.com/security/products/recaptcha
182237
[api-provider-src]: https://github.com/visgl/react-google-maps/blob/main/src/components/api-provider.tsx
183238
[rgm-new-issue]: https://github.com/visgl/react-google-maps/issues/new/choose
184239
[gmp-channel-usage]: https://developers.google.com/maps/reporting-and-monitoring/reporting#usage-tracking-per-channel

src/components/__tests__/api-provider.test.tsx

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,8 @@ let importLibraryPromise: Promise<ImportLibraryResult>;
2525
let resolveImportLibrary: (value: ImportLibraryResult) => void;
2626
let rejectImportLibrary: (reason?: unknown) => void;
2727

28+
let settingsInstance: google.maps.Settings;
29+
2830
const resetImportLibraryPromise = () => {
2931
({
3032
promise: importLibraryPromise,
@@ -71,6 +73,12 @@ beforeEach(() => {
7173
window.google.maps.importLibrary = undefined;
7274
resetAPIProviderState();
7375
resetImportLibraryPromise();
76+
77+
// Mock google.maps.Settings (missing in @googlemaps/jest-mocks)
78+
settingsInstance = {fetchAppCheckToken: null} as google.maps.Settings;
79+
google.maps.Settings = {
80+
getInstance: () => settingsInstance
81+
} as unknown as typeof google.maps.Settings;
7482
});
7583

7684
afterEach(() => {
@@ -220,6 +228,22 @@ test('calls onError when loading the Google Maps JavaScript API fails', async ()
220228
await waitFor(() => expect(onErrorMock).toHaveBeenCalled());
221229
});
222230

231+
test('sets fetchAppCheckToken on google.maps.Settings after API loads', async () => {
232+
const mockFetchToken = jest.fn().mockResolvedValue({token: 'test-token'});
233+
234+
render(
235+
<APIProvider apiKey={'apikey'} fetchAppCheckToken={mockFetchToken}>
236+
<ContextSpyComponent />
237+
</APIProvider>
238+
);
239+
240+
await act(async () => {
241+
triggerMapsApiLoaded();
242+
});
243+
244+
expect(settingsInstance.fetchAppCheckToken).toBe(mockFetchToken);
245+
});
246+
223247
describe('internalUsageAttributionIds', () => {
224248
test('provides default attribution IDs in context', () => {
225249
render(

src/components/api-provider.tsx

Lines changed: 21 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -106,6 +106,12 @@ export type APIProviderProps = PropsWithChildren<{
106106
* A function that will be called if there was an error when loading the Google Maps JavaScript API.
107107
*/
108108
onError?: (error: unknown) => void;
109+
/**
110+
* A function that returns a Promise resolving to an App Check token.
111+
* When provided, it will be set on `google.maps.Settings.getInstance().fetchAppCheckToken`
112+
* after the Google Maps JavaScript API has been loaded.
113+
*/
114+
fetchAppCheckToken?: () => Promise<google.maps.MapsAppCheckTokenResult>;
109115
}>;
110116

111117
// loading the Maps JavaScript API can only happen once in the runtime, so these
@@ -198,7 +204,8 @@ function useGoogleMapsApiLoader(props: APIProviderProps) {
198204
language,
199205
authReferrerPolicy,
200206
channel,
201-
solutionChannel
207+
solutionChannel,
208+
fetchAppCheckToken
202209
} = props;
203210

204211
const [status, setStatus] = useState<APILoadingStatus>(loadingStatus);
@@ -263,7 +270,7 @@ function useGoogleMapsApiLoader(props: APIProviderProps) {
263270
};
264271
}, []);
265272

266-
// effect:
273+
// effect: set and store options
267274
useEffect(
268275
() => {
269276
(async () => {
@@ -360,6 +367,18 @@ function useGoogleMapsApiLoader(props: APIProviderProps) {
360367
[currentSerializedParams, onLoad, onError, importLibraryCallback, libraries]
361368
);
362369

370+
// set the fetchAppCheckToken if provided
371+
useEffect(() => {
372+
if (status !== APILoadingStatus.LOADED) return;
373+
374+
const settings = google.maps.Settings.getInstance();
375+
if (fetchAppCheckToken) {
376+
settings.fetchAppCheckToken = fetchAppCheckToken;
377+
} else if (settings.fetchAppCheckToken) {
378+
settings.fetchAppCheckToken = null;
379+
}
380+
}, [status, fetchAppCheckToken]);
381+
363382
return {
364383
status,
365384
loadedLibraries,

types/google.maps.d.ts

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -800,6 +800,16 @@ declare namespace google.maps {
800800
}
801801
}
802802

803+
interface MapsAppCheckTokenResult {
804+
token: string;
805+
}
806+
807+
interface Settings {
808+
fetchAppCheckToken:
809+
| (() => Promise<google.maps.MapsAppCheckTokenResult>)
810+
| null;
811+
}
812+
803813
/**
804814
* Maps3D Library interface for use with importLibrary('maps3d').
805815
*/

0 commit comments

Comments
 (0)