Skip to content
Open
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
20 changes: 20 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,26 @@ function MyComponent() {
}
```

You can also pass a function that returns a token (sync or async). MapKit JS may call this throughout a session when it needs a new token — for example when a short-lived JWT expires. Prefer minting tokens on your server; never put your MapKit private key in client-side code.

```tsx
function MyComponent() {
return (
<Map
token={async () => {
const res = await fetch('/api/mapkit-token');
if (!res.ok) throw new Error('Failed to fetch MapKit token');
return res.text();
}}
>
<Marker latitude={46.52} longitude={6.57} />
</Map>
);
}
```

MapKit JS is initialized once per page, so all `<Map>` instances share the same authorization context — use a single token source for the whole app.

You can see all the supported parameters in Storybook (see above).

## Features
Expand Down
15 changes: 13 additions & 2 deletions src/components/Map.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import React, {
} from 'react';
import { useMediaQuery } from 'usehooks-ts';
import MapContext from '../context/MapContext';
import load from '../util/loader';
import load, { setMapKitToken } from '../util/loader';
import {
ColorScheme, Distances, FeatureVisibility, LoadPriority, MapType,
fromMapKitMapType,
Expand Down Expand Up @@ -73,10 +73,21 @@ const Map = React.forwardRef<mapkit.Map | null, React.PropsWithChildren<MapProps
const element = useRef<HTMLDivElement>(null);
const exists = useRef<boolean>(false);

// MapKit may re-invoke authorizationCallback mid-session (e.g. JWT expiry).
// We only need the *latest* token available for those later calls — not to
// run work when `token` changes — so a ref (synced during render) is a
// better fit than useEffect. Init still runs once below; this just keeps
// the default loader's provider current. Custom `load` owns its own auth.
const tokenRef = useRef(token);
tokenRef.current = token;
if (typeof customLoad !== 'function') {
setMapKitToken(tokenRef.current);
}

// Load the map
useEffect(() => {
const loadMap = typeof customLoad === 'function' ? customLoad : load;
loadMap(token).then(() => {
loadMap(tokenRef.current).then(() => {
if (exists.current) return;
const options = initialRegion
? { region: toMapKitCoordinateRegion(initialRegion) }
Expand Down
15 changes: 12 additions & 3 deletions src/components/MapProps.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,17 +4,26 @@ import {
PointOfInterestCategory,
FeatureVisibility,
} from '../util/parameters';
import { MapKitToken } from '../util/token';

export default interface MapProps {
/**
* Custom load method for MapKit JS.
* Receives the same token (or provider) as the `token` prop.
* When using a custom load, you own `mapkit.init` and authorization.
*/
load?: (token: string) => Promise<void>;
load?: (token: MapKitToken) => Promise<void>;

/**
* The token provided by MapKit JS.
* A MapKit JS JWT string, or a function that returns one (sync or async).
*
* MapKit JS may request a token throughout a session when the previous one
* expires. Use a provider function to mint short-lived tokens from your
* server — never put your MapKit private key in client-side code.
*
* @see {@link https://developer.apple.com/documentation/mapkitjs/mapkitinitializationoptions/authorizationcallback}
*/
token: string;
token: MapKitToken;

/**
* The map’s color scheme when displaying standard or muted standard map types.
Expand Down
3 changes: 3 additions & 0 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,3 +23,6 @@ export type {
MapInteractionEvent,
UserLocationChangeEvent, UserLocationError, UserLocationErrorEvent,
} from './events';

export type { MapKitToken } from './util/token';
export { resolveMapKitToken } from './util/token';
36 changes: 34 additions & 2 deletions src/stories/Map.stories.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import {
} from '../util/parameters';
import Marker from '../components/Marker';
import { MapInteractionEvent } from '..';
import { resolveMapKitToken } from '../util/token';

// @ts-ignore
const token = import.meta.env.STORYBOOK_MAPKIT_JS_TOKEN!;
Expand Down Expand Up @@ -303,6 +304,34 @@ export const RegionChangeEvent = () => {
);
};

export const TokenProvider = () => {
const initialRegion: CoordinateRegion = useMemo(
() => ({
centerLatitude: 40.7538,
centerLongitude: -73.986,
latitudeDelta: 0.03,
longitudeDelta: 0.03,
}),
[],
);

// Simulates a short-lived token from your backend. MapKit may call this
// again during a session when it needs a fresh JWT.
return (
<Map
token={async () => {
await new Promise((r) => {
setTimeout(r, 50);
});
return token;
}}
initialRegion={initialRegion}
showsMapTypeControl={false}
/>
);
};
TokenProvider.storyName = 'Token Provider Function';

export const CustomLoadFunction = () => {
const initialRegion: CoordinateRegion = useMemo(
() => ({
Expand All @@ -324,16 +353,19 @@ export const CustomLoadFunction = () => {
delete window.initMapKit;
window.mapkit.init({
authorizationCallback: (done) => {
done(customLoadToken);
resolveMapKitToken(customLoadToken).then(done);
},
});
resolve();
};
element.src = 'https://cdn.apple-mapkit.com/mk/5.x.x/mapkit.core.js';
element.dataset.callback = 'initMapKit';
element.dataset.initialToken = customLoadToken;
element.dataset.libraries = 'map';
element.crossOrigin = 'anonymous';
// initialToken only accepts a string JWT
if (typeof customLoadToken === 'string') {
element.dataset.initialToken = customLoadToken;
}
document.head.appendChild(element);
})}
token={token}
Expand Down
42 changes: 37 additions & 5 deletions src/util/loader.ts
Original file line number Diff line number Diff line change
@@ -1,15 +1,34 @@
import { MapKitToken, resolveMapKitToken } from './token';

let loadingPromise: Promise<void> | null = null;

/**
* Loads the MapKit JS API with the given token.
* Latest token provider. MapKit may re-invoke authorizationCallback during a
* session, so this must stay up to date after the initial load.
*/
let currentToken: MapKitToken | null = null;

/**
* Updates the token (or token provider) used by MapKit JS authorization.
* Safe to call after MapKit has already been initialized.
*/
export function setMapKitToken(token: MapKitToken): void {
currentToken = token;
}

/**
* Loads the MapKit JS API with the given token or token provider.
*
* If the library is already loaded or loading, this function will not attempt
* to load it a second time.
* to load it a second time. The token provider is always updated so later
* MapKit authorization requests use the latest value.
*
* @param token The MapKit JS token
* @param token The MapKit JS token, or a function that returns one
* @returns A promise resolving when the library is loaded.
*/
export default function load(token: string): Promise<void> {
export default function load(token: MapKitToken): Promise<void> {
setMapKitToken(token);

if (loadingPromise !== null) {
return loadingPromise;
}
Expand All @@ -18,7 +37,20 @@ export default function load(token: string): Promise<void> {
const script = document.createElement('script');
script.addEventListener('load', () => {
mapkit.init({
authorizationCallback: (done) => done(token),
authorizationCallback: (done) => {
if (currentToken === null) {
// eslint-disable-next-line no-console
console.error('[mapkit-react] No MapKit token has been set');
return;
}
resolveMapKitToken(currentToken)
.then(done)
.catch((error) => {
// MapKit's done() has no error channel
// eslint-disable-next-line no-console
console.error('[mapkit-react] Failed to obtain MapKit token', error);
});
},
});

resolve();
Expand Down
17 changes: 17 additions & 0 deletions src/util/token.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
/**
* A MapKit JS authorization token, or a function (sync or async) that returns one.
*
* MapKit JS may call the provider throughout a session (not only at init)
* when it needs a new token. Prefer a short-lived JWT minted by your server;
* never put your MapKit private key in client-side code.
*
* @see {@link https://developer.apple.com/documentation/mapkitjs/mapkitinitializationoptions/authorizationcallback}
*/
export type MapKitToken = string | (() => string | Promise<string>);

/**
* Resolves a {@link MapKitToken} to a JWT string.
*/
export function resolveMapKitToken(token: MapKitToken): Promise<string> {
return Promise.resolve(typeof token === 'function' ? token() : token);
}
2 changes: 1 addition & 1 deletion support.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
| Feature | Supported |
| --------------------------------------- | ------------------------------------------------------------------------------------- |
| MapKitInitOptions.language | ❌ |
| MapKitInitOptions.authorizationCallback | ⚠️<br><small>You can only pass token as strings, callbacks are not supported.</small> |
| MapKitInitOptions.authorizationCallback | <br><small>Pass a JWT string or a provider function via the <code>token</code> prop (<code>string \| () =&gt; string \| Promise&lt;string&gt;</code>). MapKit may re-invoke the provider throughout a session.</small> |

### Properties

Expand Down