Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
55 changes: 55 additions & 0 deletions docs/api-reference/components/api-provider.md
Original file line number Diff line number Diff line change
Expand Up @@ -131,6 +131,57 @@ used even when this option is enabled.

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

#### `fetchAppCheckToken`: () => Promise\<google.maps.MapsAppCheckTokenResult\>

A function that returns a Promise resolving to a Firebase App Check token.
When provided, this function will be set on `google.maps.Settings.getInstance().fetchAppCheckToken`
after the Google Maps JavaScript API has loaded.

[Firebase App Check][firebase-app-check] helps protect your Google Maps Platform API key by
blocking traffic from unauthorized sources, preventing malicious requests and
unauthorized API calls that could incur charges. It works by validating that requests
come from legitimate apps using attestation providers like [reCAPTCHA Enterprise][recaptcha-enterprise].

**Example usage with Firebase:**

A custom wrapper component that initializes Firebase App Check and passes
the token fetcher to `APIProvider`:

```tsx
import React, {PropsWithChildren, useCallback} from 'react';
import {APIProvider, APIProviderProps} from '@vis.gl/react-google-maps';
import {initializeApp} from 'firebase/app';
import {
getToken,
initializeAppCheck,
ReCaptchaEnterpriseProvider
} from 'firebase/app-check';

// Firebase and App Check initialization
const app = initializeApp({/* ... */});
const appCheck = initializeAppCheck(firebaseApp, {
provider: new ReCaptchaEnterpriseProvider(RECAPTCHA_SITE_KEY),
isTokenAutoRefreshEnabled: true
});

// custom wrapper for the APIProvider
export function CustomAPIProvider({children, ...props}) {
const fetchAppCheckToken = useCallback(() => getToken(appCheck, false), []);

return (
<APIProvider {...props} fetchAppCheckToken={fetchAppCheckToken}>
{children}
</APIProvider>
);
}
```

For more information, see:

- [Using App Check with Maps JavaScript API][gmp-app-check]
- [Firebase App Check documentation][firebase-app-check]
- [App Check codelab][gmp-app-check-codelab]

### Events

#### `onLoad`: () => void {#onLoad}
Expand Down Expand Up @@ -179,6 +230,10 @@ The following hooks are built to work with the `APIProvider` Component:
[gmp-lang]: https://developers.google.com/maps/documentation/javascript/localization
[gmp-solutions-usage]: https://developers.google.com/maps/reporting-and-monitoring/reporting#solutions-usage
[gmp-usage-attribution]: https://developers.google.com/maps/documentation/javascript/reference/map#MapOptions.internalUsageAttributionIds
[gmp-app-check]: https://developers.google.com/maps/documentation/javascript/maps-app-check
[gmp-app-check-codelab]: https://developers.google.com/codelabs/maps-platform/maps-platform-firebase-appcheck
[firebase-app-check]: https://firebase.google.com/docs/app-check
[recaptcha-enterprise]: https://cloud.google.com/security/products/recaptcha
[api-provider-src]: https://github.com/visgl/react-google-maps/blob/main/src/components/api-provider.tsx
[rgm-new-issue]: https://github.com/visgl/react-google-maps/issues/new/choose
[gmp-channel-usage]: https://developers.google.com/maps/reporting-and-monitoring/reporting#usage-tracking-per-channel
24 changes: 24 additions & 0 deletions src/components/__tests__/api-provider.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,8 @@ let importLibraryPromise: Promise<ImportLibraryResult>;
let resolveImportLibrary: (value: ImportLibraryResult) => void;
let rejectImportLibrary: (reason?: unknown) => void;

let settingsInstance: google.maps.Settings;

const resetImportLibraryPromise = () => {
({
promise: importLibraryPromise,
Expand Down Expand Up @@ -71,6 +73,12 @@ beforeEach(() => {
window.google.maps.importLibrary = undefined;
resetAPIProviderState();
resetImportLibraryPromise();

// Mock google.maps.Settings (missing in @googlemaps/jest-mocks)
settingsInstance = {fetchAppCheckToken: null} as google.maps.Settings;
google.maps.Settings = {
getInstance: () => settingsInstance
} as unknown as typeof google.maps.Settings;
});

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

test('sets fetchAppCheckToken on google.maps.Settings after API loads', async () => {
const mockFetchToken = jest.fn().mockResolvedValue({token: 'test-token'});

render(
<APIProvider apiKey={'apikey'} fetchAppCheckToken={mockFetchToken}>
<ContextSpyComponent />
</APIProvider>
);

await act(async () => {
triggerMapsApiLoaded();
});

expect(settingsInstance.fetchAppCheckToken).toBe(mockFetchToken);
});

describe('internalUsageAttributionIds', () => {
test('provides default attribution IDs in context', () => {
render(
Expand Down
23 changes: 21 additions & 2 deletions src/components/api-provider.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -106,6 +106,12 @@ export type APIProviderProps = PropsWithChildren<{
* A function that will be called if there was an error when loading the Google Maps JavaScript API.
*/
onError?: (error: unknown) => void;
/**
* A function that returns a Promise resolving to an App Check token.
* When provided, it will be set on `google.maps.Settings.getInstance().fetchAppCheckToken`
* after the Google Maps JavaScript API has been loaded.
*/
fetchAppCheckToken?: () => Promise<google.maps.MapsAppCheckTokenResult>;
}>;

// loading the Maps JavaScript API can only happen once in the runtime, so these
Expand Down Expand Up @@ -198,7 +204,8 @@ function useGoogleMapsApiLoader(props: APIProviderProps) {
language,
authReferrerPolicy,
channel,
solutionChannel
solutionChannel,
fetchAppCheckToken
} = props;

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

// effect:
// effect: set and store options
useEffect(
() => {
(async () => {
Expand Down Expand Up @@ -360,6 +367,18 @@ function useGoogleMapsApiLoader(props: APIProviderProps) {
[currentSerializedParams, onLoad, onError, importLibraryCallback, libraries]
);

// set the fetchAppCheckToken if provided
useEffect(() => {
if (status !== APILoadingStatus.LOADED) return;

const settings = google.maps.Settings.getInstance();
if (fetchAppCheckToken) {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The if here prevents unsetting an existing fetchAppCheckToken setting.

That's likely not a regular occurrence, but always setting the value here shouldn't be an issue.

settings.fetchAppCheckToken = fetchAppCheckToken;
} else if (settings.fetchAppCheckToken) {
settings.fetchAppCheckToken = null;
}
}, [status, fetchAppCheckToken]);

return {
status,
loadedLibraries,
Expand Down
10 changes: 10 additions & 0 deletions types/google.maps.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -800,6 +800,16 @@ declare namespace google.maps {
}
}

interface MapsAppCheckTokenResult {
token: string;
}

interface Settings {
fetchAppCheckToken:
| (() => Promise<google.maps.MapsAppCheckTokenResult>)
| null;
}

/**
* Maps3D Library interface for use with importLibrary('maps3d').
*/
Expand Down