@open-condo/miniapp-utils
Version:
A set of helper functions / components / hooks used to build new condo apps fast
1 lines • 15.8 kB
Source Map (JSON)
{"version":3,"sources":["../../src/helpers/embeddingContext.tsx","../../src/helpers/uuid.ts"],"sourcesContent":["import { deleteCookie, getCookie, setCookie } from 'cookies-next'\nimport React, { useEffect, useMemo, useState, createContext, useContext } from 'react'\nimport { z } from 'zod'\n\nimport { generateUUIDv4 } from './uuid'\n\nimport type { AppType, Optional } from './common/types'\nimport type { IncomingMessage, ServerResponse } from 'http'\n\nconst EMBEDDING_CONTEXT_COOKIE_NAME = 'embeddingContext'\nconst EMBEDDING_CONTEXT_QUERY_PARAM = 'embeddingContext'\nconst EMBEDDING_CONTEXT_PRIMARY_TAB_SESSION_STORAGE_KEY = 'isEmbeddingContextProvider'\nconst EMBEDDING_CONTEXT_PROP_NAME = '__EMBEDDING_CONTEXT__'\nconst EMBEDDING_CONTEXT_CLEANUP_POLLING_TIMEOUT_IN_MS = 2_000\n\nconst EMBEDDING_CONTEXT_SCHEMA = z.strictObject({\n dv: z.literal(1),\n app: z.strictObject({\n id: z.string(),\n version: z.string().optional(),\n build: z.string().optional(),\n }),\n platform: z.enum(['iOS', 'Android', 'web']),\n os: z.strictObject({\n name: z.string(),\n version: z.string().optional(),\n }).optional(),\n device: z.strictObject({\n id: z.string(),\n }),\n})\n\nconst EMBEDDING_CONTEXT_WITH_SOURCE_SCHEMA = z.strictObject({\n ctx: EMBEDDING_CONTEXT_SCHEMA,\n source: z.enum(['query', 'cookie']),\n})\n\nconst IS_PRIMARY_ALIVE_MESSAGE_SCHEMA = z.object({\n type: z.literal('EmbeddingContextPrimaryPolling'),\n data: z.strictObject({\n requestId: z.string(),\n }),\n})\n\nconst IS_PRIMARY_ALIVE_RESPONSE_SCHEMA = z.object({\n type: z.literal('EmbeddingContextPrimaryPollingResult'),\n data: z.strictObject({\n requestId: z.string(),\n isPrimary: z.boolean(),\n }),\n})\n\nexport type EmbeddingContext = z.infer<typeof EMBEDDING_CONTEXT_SCHEMA>\ntype EmbeddingContextWithSource = z.infer<typeof EMBEDDING_CONTEXT_WITH_SOURCE_SCHEMA>\ntype IsPrimaryAliveMessage = z.infer<typeof IS_PRIMARY_ALIVE_MESSAGE_SCHEMA>\ntype IsPrimaryAliveResponse = z.infer<typeof IS_PRIMARY_ALIVE_RESPONSE_SCHEMA>\n\nconst ReactEmbeddingContext = createContext<EmbeddingContext | null>(null)\n\nexport function useEmbeddingContext (): EmbeddingContext | null {\n return useContext(ReactEmbeddingContext)\n}\n\nfunction b64toContext (b64: string): EmbeddingContext | null {\n try {\n const bytes = Uint8Array.from(atob(b64), c => c.charCodeAt(0))\n const decodedUTFString = new TextDecoder().decode(bytes)\n const parsedCtx = JSON.parse(decodedUTFString)\n return EMBEDDING_CONTEXT_SCHEMA.parse(parsedCtx)\n } catch {\n return null\n }\n}\n\nfunction contextToB64 (ctx: EmbeddingContext): string {\n const stringCtx = JSON.stringify(ctx)\n const bytes = new TextEncoder().encode(stringCtx)\n return btoa(String.fromCharCode(...bytes))\n}\n\nexport function getEmbeddingContext (req?: Optional<IncomingMessage>, res?: Optional<ServerResponse>): EmbeddingContextWithSource | null {\n // NOTE: context can be found in query for primary tab\n try {\n const queryParamValue = req\n ? new URL(req.url ?? '/', 'https://_').searchParams.get(EMBEDDING_CONTEXT_QUERY_PARAM)\n : new URLSearchParams(window.location.search).get(EMBEDDING_CONTEXT_QUERY_PARAM)\n if (queryParamValue) {\n const ctx = b64toContext(decodeURIComponent(queryParamValue))\n if (ctx) return { ctx, source: 'query' }\n }\n } catch {\n // NOTE: decodeURIComponent might throw on invalid input, ignore it as non-valid query-param\n }\n\n // NOTE: context can be found in cookie for secondary tabs\n const cookieValue = getCookie(EMBEDDING_CONTEXT_COOKIE_NAME, { req, res })\n if (cookieValue) {\n const ctx = b64toContext(cookieValue)\n if (ctx) return { ctx, source: 'cookie' }\n }\n\n return null\n}\n\nexport function withEmbeddingContext<\n PropsType extends Record<string, unknown>,\n ComponentType,\n RouterType,\n> (App: AppType<PropsType, ComponentType, RouterType>): AppType<PropsType, ComponentType, RouterType> {\n const WithEmbeddingContext: AppType<PropsType, ComponentType, RouterType> = (props) => {\n const { pageProps } = props\n\n const propsContextWithSource = useMemo(() => {\n const { success, data } = EMBEDDING_CONTEXT_WITH_SOURCE_SCHEMA.safeParse(pageProps[EMBEDDING_CONTEXT_PROP_NAME])\n if (!success) return null\n return data\n }, [pageProps])\n\n const [embeddingContext, setEmbeddingContext] = useState<EmbeddingContext | null>(propsContextWithSource?.ctx ?? null)\n const [isPrimaryTab, setIsPrimaryTab] = useState<boolean | null>(propsContextWithSource?.source === 'query' ? true : null)\n const [bcChannel, setBCChannel] = useState<BroadcastChannel | null>(null)\n\n useEffect(() => {\n // NOTE: if primary tab, save it in session storage, so it won't be lost on user navigation\n if (isPrimaryTab === true && typeof window !== 'undefined') {\n window.sessionStorage.setItem(EMBEDDING_CONTEXT_PRIMARY_TAB_SESSION_STORAGE_KEY, 'true')\n }\n // NOTE: restore primary tab status if it was lost on navigation\n if (isPrimaryTab === null && typeof window !== 'undefined') {\n setIsPrimaryTab(window.sessionStorage.getItem(EMBEDDING_CONTEXT_PRIMARY_TAB_SESSION_STORAGE_KEY) === 'true')\n }\n }, [isPrimaryTab])\n\n useEffect(() => {\n if (typeof window === 'undefined' || !('BroadcastChannel' in window)) return\n\n const bc = new BroadcastChannel('embeddingContext')\n setBCChannel(bc)\n\n return () => {\n bc.close()\n setBCChannel(null)\n }\n }, [])\n\n // NOTE: Embedding context is shared between tabs by browser technology (cookies)\n // so each new page can obtain it in initial props in SSR and CSR\n // The problem is cookie might not be cleaned up on tab close, so we use 2 tricks:\n // 1. save primary tab status in session storage, so it won't be lost on user navigation\n // 2. use BroadcastChannel to poll if primary tab is still alive, if not, clean up cookie\n useEffect(() => {\n if (isPrimaryTab === null || !bcChannel) return\n\n if (isPrimaryTab) {\n const primaryListener = (e: MessageEvent) => {\n const { success, data } = IS_PRIMARY_ALIVE_MESSAGE_SCHEMA.safeParse(e.data)\n if (!success || !data?.data?.requestId) return\n\n const response: IsPrimaryAliveResponse = {\n type: 'EmbeddingContextPrimaryPollingResult',\n data: {\n isPrimary: true,\n requestId: data.data.requestId,\n },\n }\n\n bcChannel.postMessage(response)\n }\n\n bcChannel.addEventListener('message', primaryListener)\n\n return () => {\n bcChannel.removeEventListener('message', primaryListener)\n }\n }\n\n const requestId = generateUUIDv4()\n const timeout = setTimeout(() => {\n deleteCookie(EMBEDDING_CONTEXT_COOKIE_NAME)\n setEmbeddingContext(null)\n setIsPrimaryTab(false)\n }, EMBEDDING_CONTEXT_CLEANUP_POLLING_TIMEOUT_IN_MS)\n\n const secondaryListener = (e: MessageEvent) => {\n const { success, data } = IS_PRIMARY_ALIVE_RESPONSE_SCHEMA.safeParse(e.data)\n if (!success || data?.data.requestId !== requestId) return\n\n clearTimeout(timeout)\n }\n\n bcChannel.addEventListener('message', secondaryListener)\n\n const pollMessage: IsPrimaryAliveMessage = {\n type: 'EmbeddingContextPrimaryPolling',\n data: {\n requestId,\n },\n }\n bcChannel.postMessage(pollMessage)\n\n return () => {\n bcChannel.removeEventListener('message', secondaryListener)\n clearTimeout(timeout)\n }\n\n }, [bcChannel, isPrimaryTab])\n\n return (\n <ReactEmbeddingContext.Provider value={embeddingContext}>\n <App {...props} />\n </ReactEmbeddingContext.Provider>\n )\n }\n\n const appGetInitialProps = App.getInitialProps\n if (appGetInitialProps) {\n WithEmbeddingContext.getInitialProps = async function (context) {\n const appProps = await appGetInitialProps(context)\n const { ctx } = context\n const embeddingContextWithSource = getEmbeddingContext(ctx.req, ctx.res)\n if (embeddingContextWithSource && embeddingContextWithSource.source === 'query') {\n // Save context in cookie for new tabs\n setCookie(EMBEDDING_CONTEXT_COOKIE_NAME, contextToB64(embeddingContextWithSource.ctx), {\n req: ctx.req,\n res: ctx.res,\n })\n }\n\n return {\n ...appProps,\n pageProps: {\n ...appProps.pageProps,\n [EMBEDDING_CONTEXT_PROP_NAME]: embeddingContextWithSource,\n },\n }\n }\n }\n\n return WithEmbeddingContext\n}","import { randomBytes } from 'crypto'\n\n/**\n * Generates v4 UUIDs in both browser and Node environments\n * @example\n * const uuid = generateUUIDv4()\n */\nexport function generateUUIDv4 (): string {\n let randomValues: Uint8Array\n\n if (typeof crypto !== 'undefined' && typeof crypto.randomUUID === 'function') {\n // Browser or Node.js (if Node 19+ supports crypto.randomUUID)\n return crypto.randomUUID()\n } else if (typeof window !== 'undefined' && window.crypto && window.crypto.getRandomValues) {\n // Browser environment\n randomValues = new Uint8Array(16)\n window.crypto.getRandomValues(randomValues)\n } else {\n // Node.js environment\n randomValues = randomBytes(16)\n }\n\n // Setting the version (4) and variant (RFC4122)\n randomValues[6] = (randomValues[6] & 0x0f) | 0x40 // version 4\n randomValues[8] = (randomValues[8] & 0x3f) | 0x80 // variant\n\n return [...randomValues]\n .map((value, index) => {\n const hex = value.toString(16).padStart(2, '0')\n if (index === 4 || index === 6 || index === 8 || index === 10) {\n return `-${hex}`\n }\n return hex\n })\n .join('')\n}\n"],"mappings":"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","names":[]}