From 618e44f651c64aaca2973fbfca99bd820466fd1f Mon Sep 17 00:00:00 2001 From: Maksym Dogadailo Date: Thu, 5 Sep 2024 14:07:44 +0200 Subject: [PATCH 1/6] feat: add clientId support to ApiProvider --- docs/api-reference/components/api-provider.md | 17 ++++++- .../__tests__/api-provider.test.tsx | 7 +++ src/components/api-provider.tsx | 46 ++++++++++++++----- src/libraries/google-maps-api-loader.ts | 10 +++- 4 files changed, 65 insertions(+), 15 deletions(-) diff --git a/docs/api-reference/components/api-provider.md b/docs/api-reference/components/api-provider.md index 1beed70c..793fe1bd 100644 --- a/docs/api-reference/components/api-provider.md +++ b/docs/api-reference/components/api-provider.md @@ -33,8 +33,8 @@ first render will in most cases have no effect, cause an error, or both. ## Usage -The `APIProvider` only needs the [Google Maps Platform API Key][gmp-api-keys] to function. -This has to be provided via the `apiKey` prop: +The `APIProvider` only needs the [Google Maps Platform API Key][gmp-api-keys] or [Client ID URL Authorization][gmp-client-id] to function. +This has to be provided via the `apiKey` or `clientId` prop: ```tsx import React from 'react'; @@ -67,6 +67,18 @@ first render, later changes to the values will have no effect. The API Key for the Maps JavaScript API. +or + +#### `clientId`: string (required, first-render only) {#clientId} + +The Client ID for the Maps JavaScript API. + +:::note + +Request can not contain both `apiKey` and `clientId` at the same time. + +::: + ### Optional #### `version`: string (first-render only) {#version} @@ -150,6 +162,7 @@ The following hooks are built to work with the `APIProvider` Component: [gmp-import-library]: https://developers.google.com/maps/documentation/javascript/load-maps-js-api#dynamic-library-import [gmp-api-keys]: https://developers.google.com/maps/documentation/javascript/get-api-key +[gmp-client-id]: https://developers.google.com/maps/premium/authentication/client-id/url-authorization [gmp-params]: https://developers.google.com/maps/documentation/javascript/load-maps-js-api#required_parameters [gmp-api-version]: https://developers.google.com/maps/documentation/javascript/versions [gmp-libs]: https://developers.google.com/maps/documentation/javascript/libraries diff --git a/src/components/__tests__/api-provider.test.tsx b/src/components/__tests__/api-provider.test.tsx index ea0e9d06..52a3355c 100644 --- a/src/components/__tests__/api-provider.test.tsx +++ b/src/components/__tests__/api-provider.test.tsx @@ -103,6 +103,13 @@ test("doesn't set solutionChannel when specified as empty string", () => { expect(actual).not.toHaveProperty('solutionChannel'); }); +test('passes clientId to GoogleMapsAPILoader', () => { + render(); + + const actual = apiLoadSpy.mock.lastCall[0]; + expect(actual).toMatchObject({client: 'client-id'}); +}); + test('renders inner components', async () => { const LoadingStatus = () => { const mapsLoaded = useApiIsLoaded(); diff --git a/src/components/api-provider.tsx b/src/components/api-provider.tsx index ba8aa094..3e8404a5 100644 --- a/src/components/api-provider.tsx +++ b/src/components/api-provider.tsx @@ -33,12 +33,27 @@ const DEFAULT_SOLUTION_CHANNEL = 'GMP_visgl_rgmlibrary_v1_default'; export const APIProviderContext = React.createContext(null); -export type APIProviderProps = { - /** - * apiKey must be provided to load the Google Maps JavaScript API. To create an API key, see: https://developers.google.com/maps/documentation/javascript/get-api-key - * Part of: - */ - apiKey: string; +export type APIProviderKey = + | { + /** + * apiKey must be provided to load the Google Maps JavaScript API. To create an API key, see: https://developers.google.com/maps/documentation/javascript/get-api-key + * apiKey can not be provided when using clientId. + * Part of: + */ + apiKey: string; + /** + * Client ID for enterprise customers. See: https://developers.google.com/maps/premium/overview#client_id + * apiKey can not be provided when using clientId. + * Part of: + */ + clientId?: never; + } + | { + clientId: string; + apiKey?: never; + }; + +export type APIProviderProps = APIProviderKey & { /** * A custom id to reference the script tag can be provided. The default is set to 'google-maps-api' * @default 'google-maps-api' @@ -110,7 +125,14 @@ function useMapInstances() { * @param props */ function useGoogleMapsApiLoader(props: APIProviderProps) { - const {onLoad, apiKey, version, libraries = [], ...otherApiParams} = props; + const { + onLoad, + apiKey, + clientId, + version, + libraries = [], + ...otherApiParams + } = props; const [status, setStatus] = useState( GoogleMapsApiLoader.loadingStatus @@ -127,8 +149,8 @@ function useGoogleMapsApiLoader(props: APIProviderProps) { const librariesString = useMemo(() => libraries?.join(','), [libraries]); const serializedParams = useMemo( - () => JSON.stringify({apiKey, version, ...otherApiParams}), - [apiKey, version, otherApiParams] + () => JSON.stringify({apiKey, version, clientId, ...otherApiParams}), + [apiKey, version, clientId, otherApiParams] ); const importLibrary: typeof google.maps.importLibrary = useCallback( @@ -156,7 +178,9 @@ function useGoogleMapsApiLoader(props: APIProviderProps) { () => { (async () => { try { - const params: ApiParams = {key: apiKey, ...otherApiParams}; + const params: ApiParams = apiKey + ? {key: apiKey, ...otherApiParams} + : {client: clientId, ...otherApiParams}; if (version) params.v = version; if (librariesString?.length > 0) params.libraries = librariesString; @@ -182,7 +206,7 @@ function useGoogleMapsApiLoader(props: APIProviderProps) { })(); }, // eslint-disable-next-line react-hooks/exhaustive-deps - [apiKey, librariesString, serializedParams] + [apiKey, clientId, librariesString, serializedParams] ); return { diff --git a/src/libraries/google-maps-api-loader.ts b/src/libraries/google-maps-api-loader.ts index 794ad986..35cbdef1 100644 --- a/src/libraries/google-maps-api-loader.ts +++ b/src/libraries/google-maps-api-loader.ts @@ -1,7 +1,6 @@ import {APILoadingStatus} from './api-loading-status'; -export type ApiParams = { - key: string; +type ApiCommonParams = { v?: string; language?: string; region?: string; @@ -10,6 +9,13 @@ export type ApiParams = { authReferrerPolicy?: string; }; +export type ApiParams = + | ({key?: string; client?: never} & ApiCommonParams) + | ({ + key?: never; + client?: string; + } & ApiCommonParams); + type LoadingStatusCallback = (status: APILoadingStatus) => void; const MAPS_API_BASE_URL = 'https://maps.googleapis.com/maps/api/js'; From 8553d2942bdc03e2dd8a346bb1edfbc5648a126c Mon Sep 17 00:00:00 2001 From: Maksym Dogadailo Date: Thu, 5 Sep 2024 15:34:07 +0200 Subject: [PATCH 2/6] clean: move ApiCommonParams into ApiParams --- src/libraries/google-maps-api-loader.ts | 15 +++++++-------- 1 file changed, 7 insertions(+), 8 deletions(-) diff --git a/src/libraries/google-maps-api-loader.ts b/src/libraries/google-maps-api-loader.ts index 35cbdef1..1bd63708 100644 --- a/src/libraries/google-maps-api-loader.ts +++ b/src/libraries/google-maps-api-loader.ts @@ -1,6 +1,12 @@ import {APILoadingStatus} from './api-loading-status'; -type ApiCommonParams = { +export type ApiParams = ( + | {key?: string; client?: never} + | { + key?: never; + client?: string; + } +) & { v?: string; language?: string; region?: string; @@ -9,13 +15,6 @@ type ApiCommonParams = { authReferrerPolicy?: string; }; -export type ApiParams = - | ({key?: string; client?: never} & ApiCommonParams) - | ({ - key?: never; - client?: string; - } & ApiCommonParams); - type LoadingStatusCallback = (status: APILoadingStatus) => void; const MAPS_API_BASE_URL = 'https://maps.googleapis.com/maps/api/js'; From 444eda1937b0b05ef37b2286a1311d9358784217 Mon Sep 17 00:00:00 2001 From: Maksym Dogadailo Date: Fri, 6 Sep 2024 16:11:32 +0200 Subject: [PATCH 3/6] docs(examples): show how to use with externally loaded api --- examples/external-js-api/README.md | 38 ++++++++++++++++ examples/external-js-api/index.html | 33 ++++++++++++++ examples/external-js-api/package.json | 14 ++++++ examples/external-js-api/src/app.tsx | 37 ++++++++++++++++ .../external-js-api/src/control-panel.tsx | 27 ++++++++++++ .../src/useExternallyLoadedMapsAPI.ts | 44 +++++++++++++++++++ examples/external-js-api/vite.config.js | 17 +++++++ website/src/examples-sidebar.js | 3 +- website/src/examples/external-js-api.mdx | 5 +++ 9 files changed, 217 insertions(+), 1 deletion(-) create mode 100644 examples/external-js-api/README.md create mode 100644 examples/external-js-api/index.html create mode 100644 examples/external-js-api/package.json create mode 100644 examples/external-js-api/src/app.tsx create mode 100644 examples/external-js-api/src/control-panel.tsx create mode 100644 examples/external-js-api/src/useExternallyLoadedMapsAPI.ts create mode 100644 examples/external-js-api/vite.config.js create mode 100644 website/src/examples/external-js-api.mdx diff --git a/examples/external-js-api/README.md b/examples/external-js-api/README.md new file mode 100644 index 00000000..c2bf0520 --- /dev/null +++ b/examples/external-js-api/README.md @@ -0,0 +1,38 @@ +# Externally loaded Google Maps JavaScript API + +![image](https://user-images.githubusercontent.com/39244966/208682692-d5b23518-9e51-4a87-8121-29f71e41c777.png) + +This is an example to show how to set up a simple Google map with the `` with externally loaded Google Maps JavaScript API. +For instance, you can use the Google Maps JavaScript API URL to load the API when you need to use Client ID instead of API Key. + +## Google Maps Platform API URL + +This example does not come with an API URL. Running the examples locally requires a valid Google Maps JavaScript API URL. +See [the official documentation][get-load-maps-js-api] on how to create a URL. + +The API URL has to be provided via an environment variable `GOOGLE_MAPS_API_URL`. This can be done by creating a +file named `.env` in the example directory with the following content: + +```shell title=".env" +GOOGLE_MAPS_API_URL="" +``` + +If you are on the CodeSandbox playground you can also choose to [provide the API URL like this](https://codesandbox.io/docs/learn/environment/secrets) + +## Development + +Go into the example-directory and run + +```shell +npm install +``` + +To start the example with the local library run + +```shell +npm run start-local +``` + +The regular `npm start` task is only used for the standalone versions of the example (CodeSandbox for example) + +[get-load-maps-js-api]: https://developers.google.com/maps/documentation/javascript/load-maps-js-api#direct_script_loading_url_parameters diff --git a/examples/external-js-api/index.html b/examples/external-js-api/index.html new file mode 100644 index 00000000..6e7ddfed --- /dev/null +++ b/examples/external-js-api/index.html @@ -0,0 +1,33 @@ + + + + + + Externally loaded Google Maps JavaScript API + + + + +
+ + + diff --git a/examples/external-js-api/package.json b/examples/external-js-api/package.json new file mode 100644 index 00000000..05765f98 --- /dev/null +++ b/examples/external-js-api/package.json @@ -0,0 +1,14 @@ +{ + "type": "module", + "dependencies": { + "@vis.gl/react-google-maps": "latest", + "react": "^18.2.0", + "react-dom": "^18.2.0", + "vite": "^5.0.4" + }, + "scripts": { + "start": "vite", + "start-local": "vite --config ../vite.config.local.js", + "build": "vite build" + } +} diff --git a/examples/external-js-api/src/app.tsx b/examples/external-js-api/src/app.tsx new file mode 100644 index 00000000..a6cf4cf5 --- /dev/null +++ b/examples/external-js-api/src/app.tsx @@ -0,0 +1,37 @@ +import React from 'react'; +import {createRoot} from 'react-dom/client'; + +import {APIProvider, Map} from '@vis.gl/react-google-maps'; +import ControlPanel from './control-panel'; +import {useExternallyLoadedMapsAPI} from './useExternallyLoadedMapsAPI'; + +const API_URL = + globalThis.GOOGLE_MAPS_API_URL ?? (process.env.GOOGLE_MAPS_API_URL as string); + +const App = () => { + const isLoaded = useExternallyLoadedMapsAPI(API_URL); + return ( + isLoaded && ( + + + + + ) + ); +}; +export default App; + +export function renderToDom(container: HTMLElement) { + const root = createRoot(container); + + root.render( + + + + ); +} diff --git a/examples/external-js-api/src/control-panel.tsx b/examples/external-js-api/src/control-panel.tsx new file mode 100644 index 00000000..1f5d0bc2 --- /dev/null +++ b/examples/external-js-api/src/control-panel.tsx @@ -0,0 +1,27 @@ +import * as React from 'react'; + +function ControlPanel() { + return ( +
+

Externally loaded Google Maps JavaScript API

+

+ The example demonstrates how to load the Google Maps API externally. +

+ +
+ ); +} + +export default React.memo(ControlPanel); diff --git a/examples/external-js-api/src/useExternallyLoadedMapsAPI.ts b/examples/external-js-api/src/useExternallyLoadedMapsAPI.ts new file mode 100644 index 00000000..66771695 --- /dev/null +++ b/examples/external-js-api/src/useExternallyLoadedMapsAPI.ts @@ -0,0 +1,44 @@ +import {useEffect, useRef, useState} from 'react'; + +/** + * Simple hook to load an external Maps API script and check if it is loaded. + * @param {String} url - URL of the script to load. + */ +export function useExternallyLoadedMapsAPI(url: string) { + const [isLoaded, setIsLoaded] = useState(false); + const intervalRef = useRef(null); + const scriptRef = useRef(null); + + useEffect(() => { + if (scriptRef.current?.src !== url) { + const script = document.createElement('script'); + script.src = url; + script.async = true; + script.defer = true; + script.onload = () => { + // Validate if google.maps.importLibrary is loaded, + // it is not enough to check if window.google is loaded.maps + intervalRef.current = setInterval(() => { + if (window.google?.maps?.importLibrary as unknown) { + clearInterval(intervalRef.current as unknown as number); + setIsLoaded(true); + } + }, 10); + }; + + scriptRef.current = script; + document.head.appendChild(script); + } + + return () => { + if (intervalRef.current) { + clearInterval(intervalRef.current as unknown as number); + } + if (scriptRef.current) { + document.head.removeChild(scriptRef.current); + } + }; + }, [url]); + + return isLoaded; +} diff --git a/examples/external-js-api/vite.config.js b/examples/external-js-api/vite.config.js new file mode 100644 index 00000000..4792d63c --- /dev/null +++ b/examples/external-js-api/vite.config.js @@ -0,0 +1,17 @@ +import {defineConfig, loadEnv} from 'vite'; + +export default defineConfig(({}) => { + const {GOOGLE_MAPS_API_URL = ''} = loadEnv(mode, process.cwd(), ''); + + return { + define: { + 'process.env.GOOGLE_MAPS_API_URL': JSON.stringify(GOOGLE_MAPS_API_URL) + }, + resolve: { + alias: { + '@vis.gl/react-google-maps/examples.js': + 'https://visgl.github.io/react-google-maps/scripts/examples.js' + } + } + }; +}); diff --git a/website/src/examples-sidebar.js b/website/src/examples-sidebar.js index e9c5fcbd..0a767a8d 100644 --- a/website/src/examples-sidebar.js +++ b/website/src/examples-sidebar.js @@ -24,7 +24,8 @@ const sidebars = { 'directions', 'deckgl-overlay', 'map-3d', - 'extended-component-library' + 'extended-component-library', + 'external-js-api', ] } ] diff --git a/website/src/examples/external-js-api.mdx b/website/src/examples/external-js-api.mdx new file mode 100644 index 00000000..78fcde3e --- /dev/null +++ b/website/src/examples/external-js-api.mdx @@ -0,0 +1,5 @@ +# Deck.gl Overlay + +import App from 'website-examples/external-js-api/src/app'; + + From f9e10ad11ccf3ae7144ecd7d7d810928bc676972 Mon Sep 17 00:00:00 2001 From: Maksym Dogadailo Date: Fri, 6 Sep 2024 16:12:47 +0200 Subject: [PATCH 4/6] feat: remove client id from api-provider --- docs/api-reference/components/api-provider.md | 16 +------ .../__tests__/api-provider.test.tsx | 7 --- src/components/api-provider.tsx | 46 +++++-------------- 3 files changed, 12 insertions(+), 57 deletions(-) diff --git a/docs/api-reference/components/api-provider.md b/docs/api-reference/components/api-provider.md index 793fe1bd..7651b71d 100644 --- a/docs/api-reference/components/api-provider.md +++ b/docs/api-reference/components/api-provider.md @@ -33,8 +33,7 @@ first render will in most cases have no effect, cause an error, or both. ## Usage -The `APIProvider` only needs the [Google Maps Platform API Key][gmp-api-keys] or [Client ID URL Authorization][gmp-client-id] to function. -This has to be provided via the `apiKey` or `clientId` prop: +The `APIProvider` only needs the [Google Maps Platform API Key][gmp-api-keys] to function. This has to be provided via the `apiKey` or `clientId` prop: ```tsx import React from 'react'; @@ -67,18 +66,6 @@ first render, later changes to the values will have no effect. The API Key for the Maps JavaScript API. -or - -#### `clientId`: string (required, first-render only) {#clientId} - -The Client ID for the Maps JavaScript API. - -:::note - -Request can not contain both `apiKey` and `clientId` at the same time. - -::: - ### Optional #### `version`: string (first-render only) {#version} @@ -162,7 +149,6 @@ The following hooks are built to work with the `APIProvider` Component: [gmp-import-library]: https://developers.google.com/maps/documentation/javascript/load-maps-js-api#dynamic-library-import [gmp-api-keys]: https://developers.google.com/maps/documentation/javascript/get-api-key -[gmp-client-id]: https://developers.google.com/maps/premium/authentication/client-id/url-authorization [gmp-params]: https://developers.google.com/maps/documentation/javascript/load-maps-js-api#required_parameters [gmp-api-version]: https://developers.google.com/maps/documentation/javascript/versions [gmp-libs]: https://developers.google.com/maps/documentation/javascript/libraries diff --git a/src/components/__tests__/api-provider.test.tsx b/src/components/__tests__/api-provider.test.tsx index 52a3355c..ea0e9d06 100644 --- a/src/components/__tests__/api-provider.test.tsx +++ b/src/components/__tests__/api-provider.test.tsx @@ -103,13 +103,6 @@ test("doesn't set solutionChannel when specified as empty string", () => { expect(actual).not.toHaveProperty('solutionChannel'); }); -test('passes clientId to GoogleMapsAPILoader', () => { - render(); - - const actual = apiLoadSpy.mock.lastCall[0]; - expect(actual).toMatchObject({client: 'client-id'}); -}); - test('renders inner components', async () => { const LoadingStatus = () => { const mapsLoaded = useApiIsLoaded(); diff --git a/src/components/api-provider.tsx b/src/components/api-provider.tsx index 3e8404a5..ba8aa094 100644 --- a/src/components/api-provider.tsx +++ b/src/components/api-provider.tsx @@ -33,27 +33,12 @@ const DEFAULT_SOLUTION_CHANNEL = 'GMP_visgl_rgmlibrary_v1_default'; export const APIProviderContext = React.createContext(null); -export type APIProviderKey = - | { - /** - * apiKey must be provided to load the Google Maps JavaScript API. To create an API key, see: https://developers.google.com/maps/documentation/javascript/get-api-key - * apiKey can not be provided when using clientId. - * Part of: - */ - apiKey: string; - /** - * Client ID for enterprise customers. See: https://developers.google.com/maps/premium/overview#client_id - * apiKey can not be provided when using clientId. - * Part of: - */ - clientId?: never; - } - | { - clientId: string; - apiKey?: never; - }; - -export type APIProviderProps = APIProviderKey & { +export type APIProviderProps = { + /** + * apiKey must be provided to load the Google Maps JavaScript API. To create an API key, see: https://developers.google.com/maps/documentation/javascript/get-api-key + * Part of: + */ + apiKey: string; /** * A custom id to reference the script tag can be provided. The default is set to 'google-maps-api' * @default 'google-maps-api' @@ -125,14 +110,7 @@ function useMapInstances() { * @param props */ function useGoogleMapsApiLoader(props: APIProviderProps) { - const { - onLoad, - apiKey, - clientId, - version, - libraries = [], - ...otherApiParams - } = props; + const {onLoad, apiKey, version, libraries = [], ...otherApiParams} = props; const [status, setStatus] = useState( GoogleMapsApiLoader.loadingStatus @@ -149,8 +127,8 @@ function useGoogleMapsApiLoader(props: APIProviderProps) { const librariesString = useMemo(() => libraries?.join(','), [libraries]); const serializedParams = useMemo( - () => JSON.stringify({apiKey, version, clientId, ...otherApiParams}), - [apiKey, version, clientId, otherApiParams] + () => JSON.stringify({apiKey, version, ...otherApiParams}), + [apiKey, version, otherApiParams] ); const importLibrary: typeof google.maps.importLibrary = useCallback( @@ -178,9 +156,7 @@ function useGoogleMapsApiLoader(props: APIProviderProps) { () => { (async () => { try { - const params: ApiParams = apiKey - ? {key: apiKey, ...otherApiParams} - : {client: clientId, ...otherApiParams}; + const params: ApiParams = {key: apiKey, ...otherApiParams}; if (version) params.v = version; if (librariesString?.length > 0) params.libraries = librariesString; @@ -206,7 +182,7 @@ function useGoogleMapsApiLoader(props: APIProviderProps) { })(); }, // eslint-disable-next-line react-hooks/exhaustive-deps - [apiKey, clientId, librariesString, serializedParams] + [apiKey, librariesString, serializedParams] ); return { From 73f1ca2bbf92402ef6244684d486d1da1d896b03 Mon Sep 17 00:00:00 2001 From: Maksym Dogadailo Date: Fri, 6 Sep 2024 16:14:17 +0200 Subject: [PATCH 5/6] docs: remove client id description --- docs/api-reference/components/api-provider.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/docs/api-reference/components/api-provider.md b/docs/api-reference/components/api-provider.md index 7651b71d..1beed70c 100644 --- a/docs/api-reference/components/api-provider.md +++ b/docs/api-reference/components/api-provider.md @@ -33,7 +33,8 @@ first render will in most cases have no effect, cause an error, or both. ## Usage -The `APIProvider` only needs the [Google Maps Platform API Key][gmp-api-keys] to function. This has to be provided via the `apiKey` or `clientId` prop: +The `APIProvider` only needs the [Google Maps Platform API Key][gmp-api-keys] to function. +This has to be provided via the `apiKey` prop: ```tsx import React from 'react'; From 236bed0d6de63776e88824e638517fcc71ec0114 Mon Sep 17 00:00:00 2001 From: Maksym Dogadailo Date: Fri, 6 Sep 2024 16:16:25 +0200 Subject: [PATCH 6/6] feat: remove ApiParams client values --- src/libraries/google-maps-api-loader.ts | 9 ++------- 1 file changed, 2 insertions(+), 7 deletions(-) diff --git a/src/libraries/google-maps-api-loader.ts b/src/libraries/google-maps-api-loader.ts index 1bd63708..794ad986 100644 --- a/src/libraries/google-maps-api-loader.ts +++ b/src/libraries/google-maps-api-loader.ts @@ -1,12 +1,7 @@ import {APILoadingStatus} from './api-loading-status'; -export type ApiParams = ( - | {key?: string; client?: never} - | { - key?: never; - client?: string; - } -) & { +export type ApiParams = { + key: string; v?: string; language?: string; region?: string;