From f04e476a6e064a4bbc8db9bfd00ab43fe5ff6fcd Mon Sep 17 00:00:00 2001 From: Anton Gunnarsson Date: Thu, 14 Aug 2025 12:22:19 +0000 Subject: [PATCH] Merged in feat/sw-3240-move-staticmap-to-design-system (pull request #2654) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit feat(SW-3240): Move StaticMap to design-system * Move StaticMap to design-system Approved-by: Joakim Jäderberg --- .../components/Maps/StaticMap/index.tsx | 67 ++++------------- .../types/components/maps/staticMap.ts | 13 ---- apps/scandic-web/utils/map.ts | 65 ---------------- .../lib/components/StaticMap/ReadMe.md | 30 ++++++++ .../lib/components/StaticMap/index.tsx | 75 +++++++++++++++++++ .../lib/components/StaticMap/utils.ts | 64 ++++++++++++++++ packages/design-system/package.json | 1 + 7 files changed, 184 insertions(+), 131 deletions(-) delete mode 100644 apps/scandic-web/types/components/maps/staticMap.ts create mode 100644 packages/design-system/lib/components/StaticMap/ReadMe.md create mode 100644 packages/design-system/lib/components/StaticMap/index.tsx create mode 100644 packages/design-system/lib/components/StaticMap/utils.ts diff --git a/apps/scandic-web/components/Maps/StaticMap/index.tsx b/apps/scandic-web/components/Maps/StaticMap/index.tsx index e71a64a65..ac32503bb 100644 --- a/apps/scandic-web/components/Maps/StaticMap/index.tsx +++ b/apps/scandic-web/components/Maps/StaticMap/index.tsx @@ -1,61 +1,22 @@ -/* eslint-disable @next/next/no-img-element */ +import StaticMapPrimitive from "@scandic-hotels/design-system/StaticMap" + import { env } from "@/env/server" -import { getUrlWithSignature } from "@/utils/map" +import type { ComponentProps } from "react" -import type { StaticMapProps } from "@/types/components/maps/staticMap" - -function getCenter({ - coordinates, - city, - country, -}: { - coordinates?: { lat: number; lng: number } - city?: string - country?: string -}): string | undefined { - if (coordinates) { - return `${coordinates.lat},${coordinates.lng}` - } - if (city && country) { - return `${city}, ${country}` - } - if (country) { - return country - } - return city -} - -export default function StaticMap({ - city, - country, - coordinates, - width, - height, - zoomLevel = 14, - mapType = "roadmap", - altText, - mapId, -}: StaticMapProps) { +type Props = Omit< + ComponentProps, + "googleMapKey" | "googleMapSecret" +> +export default async function StaticMap(props: Props) { const key = env.GOOGLE_STATIC_MAP_KEY const secret = env.GOOGLE_STATIC_MAP_SIGNATURE_SECRET - const baseUrl = "https://maps.googleapis.com/maps/api/staticmap" - const center = getCenter({ coordinates, city, country }) - if (!center) { - return null - } - - // Google Maps Static API only supports images smaller than 640x640px. Read: https://developers.google.com/maps/documentation/maps-static/start#Largerimagesizes - const url = new URL( - `${baseUrl}?center=${center}&zoom=${zoomLevel}&size=${width}x${height}&maptype=${mapType}&key=${key}` + return ( + ) - - if (mapId) { - url.searchParams.append("map_id", mapId) - } - - const src = getUrlWithSignature(url, secret) - - return {altText} } diff --git a/apps/scandic-web/types/components/maps/staticMap.ts b/apps/scandic-web/types/components/maps/staticMap.ts deleted file mode 100644 index 4694eb8bc..000000000 --- a/apps/scandic-web/types/components/maps/staticMap.ts +++ /dev/null @@ -1,13 +0,0 @@ -import type { Coordinates } from "./coordinates" - -export type StaticMapProps = { - city?: string - country?: string - coordinates?: Coordinates - width: number - height: number - zoomLevel?: number - mapType?: "roadmap" | "satellite" | "terrain" | "hybrid" - altText: string - mapId?: string -} diff --git a/apps/scandic-web/utils/map.ts b/apps/scandic-web/utils/map.ts index d7c933209..5bc0c97c8 100644 --- a/apps/scandic-web/utils/map.ts +++ b/apps/scandic-web/utils/map.ts @@ -1,5 +1,3 @@ -import crypto from "node:crypto" - // Helper function to calculate the latitude offset export function calculateLatWithOffset( latitude: number, @@ -23,66 +21,3 @@ export function calculateLatWithOffset( // Return the new latitude by subtracting the offset return latitude - latOffset } - -/** - * Util functions taken from https://developers.google.com/maps/documentation/maps-static/digital-signature#sample-code-for-url-signing - * Used to sign the URL for the Google Static Maps API. - */ - -/** - * Convert from 'web safe' base64 to true base64. - * - * @param {string} safeEncodedString The code you want to translate - * from a web safe form. - * @return {string} - */ -function removeWebSafe(safeEncodedString: string) { - return safeEncodedString.replace(/-/g, "+").replace(/_/g, "/") -} - -/** - * Convert from true base64 to 'web safe' base64 - * - * @param {string} encodedString The code you want to translate to a - * web safe form. - * @return {string} - */ -function makeWebSafe(encodedString: string) { - return encodedString.replace(/\+/g, "-").replace(/\//g, "_") -} - -/** - * Takes a base64 code and decodes it. - * - * @param {string} code The encoded data. - * @return {string} - */ -function decodeBase64Hash(code: string) { - return Buffer.from(code, "base64") -} - -/** - * Takes a key and signs the data with it. - * - * @param {string} key Your unique secret key. - * @param {string} data The url to sign. - * @return {string} - */ -function encodeBase64Hash(key: Buffer, data: string) { - return crypto.createHmac("sha1", key).update(data).digest("base64") -} - -/** - * Sign a URL using a secret key. - * - * @param {URL} url The url you want to sign. - * @param {string} secret Your unique secret key. - * @return {string} - */ -export function getUrlWithSignature(url: URL, secret = "") { - const path = url.pathname + url.search - const safeSecret = decodeBase64Hash(removeWebSafe(secret)) - const hashedSignature = makeWebSafe(encodeBase64Hash(safeSecret, path)) - - return `${url.toString()}&signature=${hashedSignature}` -} diff --git a/packages/design-system/lib/components/StaticMap/ReadMe.md b/packages/design-system/lib/components/StaticMap/ReadMe.md new file mode 100644 index 000000000..ba1640123 --- /dev/null +++ b/packages/design-system/lib/components/StaticMap/ReadMe.md @@ -0,0 +1,30 @@ +# Google Map Static API + +### About + +The Google Maps Static API lets you embed a Google Maps image on your web page. The Google Maps Static API service creates your map based on URL parameters sent through a standard HTTP request and returns the map as an image you can display on your web page. Due to regulations from Google we are not allowed to store and serve copies of images generated using the Google Maps Static API. All web pages that require static images must link the src attribute of an HTML img tag or as a background-image using CSS. + +### API Key Restrictions + +You can restrict your API key on websites, IP addresses, Android apps and iOS apps. For now (12/8-24) the API key is restricted by IP address, hence the request will only work at Scandic HQ network. This will be changed to a referrer website in the future. +[Read more about API key restrictions](https://developers.google.com/maps/api-security-best-practices#restricting-api-keys) + +### Digital Signature + +Requests exceeding 25,000 requests per day require an API key and a digital signature. However, it is strongly recommended by Google to use both an API key and digital signature, regardless of the usage. + +The digital signature is set on the Google Maps Platform. For dynamically generated requests, the signature is handled through server side signing appending the signature as base64 on the request url. +[Read more about digital signature](https://developers.google.com/maps/documentation/maps-static/digital-signature) + +### Generating new API keys + +Regenerating an API key creates a new key that has all the old key's restrictions. This process also starts a 24-hour timer after which the old API key is deleted. During this time window, both the old and new key are accepted, giving you a chance to migrate your apps to use the new key. However, after this time period elapses, any apps still using the old API key stop working. + +**Caution:** Only regenerate an API key if you absolutely must to avoid unauthorized use. This process can shut down legitimate traffic and prevent your apps from functioning properly. +[Read more about regenerating keys](https://developers.google.com/maps/api-security-best-practices#regenerate-apikey) + +### Version history + +| Description | Version | Date | Author | +| ------------------- | ------- | ------- | ---------------- | +| Create ReadMe file. | 1.0.0 | 12/8-24 | Fredrik Thorsson | diff --git a/packages/design-system/lib/components/StaticMap/index.tsx b/packages/design-system/lib/components/StaticMap/index.tsx new file mode 100644 index 000000000..3c2abc3f9 --- /dev/null +++ b/packages/design-system/lib/components/StaticMap/index.tsx @@ -0,0 +1,75 @@ +import { getUrlWithSignature } from './utils' + +export type StaticMapProps = { + city?: string + country?: string + coordinates?: { + lat: number + lng: number + } + width: number + height: number + zoomLevel?: number + mapType?: 'roadmap' | 'satellite' | 'terrain' | 'hybrid' + altText: string + mapId?: string + googleMapKey: string + googleMapSecret: string +} + +function getCenter({ + coordinates, + city, + country, +}: { + coordinates?: { lat: number; lng: number } + city?: string + country?: string +}): string | undefined { + if (coordinates) { + return `${coordinates.lat},${coordinates.lng}` + } + if (city && country) { + return `${city}, ${country}` + } + if (country) { + return country + } + return city +} + +export default async function StaticMap({ + city, + country, + coordinates, + width, + height, + zoomLevel = 14, + mapType = 'roadmap', + altText, + mapId, + googleMapKey, + googleMapSecret, +}: StaticMapProps) { + // const key = env.GOOGLE_STATIC_MAP_KEY + // const secret = env.GOOGLE_STATIC_MAP_SIGNATURE_SECRET + const baseUrl = 'https://maps.googleapis.com/maps/api/staticmap' + const center = getCenter({ coordinates, city, country }) + + if (!center) { + return null + } + + // Google Maps Static API only supports images smaller than 640x640px. Read: https://developers.google.com/maps/documentation/maps-static/start#Largerimagesizes + const url = new URL( + `${baseUrl}?center=${center}&zoom=${zoomLevel}&size=${width}x${height}&maptype=${mapType}&key=${googleMapKey}` + ) + + if (mapId) { + url.searchParams.append('map_id', mapId) + } + + const src = getUrlWithSignature(url, googleMapSecret) + + return {altText} +} diff --git a/packages/design-system/lib/components/StaticMap/utils.ts b/packages/design-system/lib/components/StaticMap/utils.ts new file mode 100644 index 000000000..c6dafbbf5 --- /dev/null +++ b/packages/design-system/lib/components/StaticMap/utils.ts @@ -0,0 +1,64 @@ +import crypto from 'node:crypto' + +/** + * Util functions taken from https://developers.google.com/maps/documentation/maps-static/digital-signature#sample-code-for-url-signing + * Used to sign the URL for the Google Static Maps API. + */ + +/** + * Convert from 'web safe' base64 to true base64. + * + * @param {string} safeEncodedString The code you want to translate + * from a web safe form. + * @return {string} + */ +function removeWebSafe(safeEncodedString: string) { + return safeEncodedString.replace(/-/g, '+').replace(/_/g, '/') +} + +/** + * Convert from true base64 to 'web safe' base64 + * + * @param {string} encodedString The code you want to translate to a + * web safe form. + * @return {string} + */ +function makeWebSafe(encodedString: string) { + return encodedString.replace(/\+/g, '-').replace(/\//g, '_') +} + +/** + * Takes a base64 code and decodes it. + * + * @param {string} code The encoded data. + * @return {string} + */ +function decodeBase64Hash(code: string) { + return Buffer.from(code, 'base64') +} + +/** + * Takes a key and signs the data with it. + * + * @param {string} key Your unique secret key. + * @param {string} data The url to sign. + * @return {string} + */ +function encodeBase64Hash(key: Buffer, data: string) { + return crypto.createHmac('sha1', key).update(data).digest('base64') +} + +/** + * Sign a URL using a secret key. + * + * @param {URL} url The url you want to sign. + * @param {string} secret Your unique secret key. + * @return {string} + */ +export function getUrlWithSignature(url: URL, secret = '') { + const path = url.pathname + url.search + const safeSecret = decodeBase64Hash(removeWebSafe(secret)) + const hashedSignature = makeWebSafe(encodeBase64Hash(safeSecret, path)) + + return `${url.toString()}&signature=${hashedSignature}` +} diff --git a/packages/design-system/package.json b/packages/design-system/package.json index 0e8d06d82..684259596 100644 --- a/packages/design-system/package.json +++ b/packages/design-system/package.json @@ -41,6 +41,7 @@ "./SkeletonShimmer": "./lib/components/SkeletonShimmer/index.tsx", "./SidePeek": "./lib/components/SidePeek/index.tsx", "./SidePeek/SidePeekProvider": "./lib/components/SidePeek/SidePeekContext/SidePeekProvider.tsx", + "./StaticMap": "./lib/components/StaticMap/index.tsx", "./Subtitle": "./lib/components/Subtitle/index.tsx", "./Switch": "./lib/components/Switch/index.tsx", "./Table": "./lib/components/Table/index.tsx",