UNPKG

@open-condo/miniapp-utils

Version:

A set of helper functions / components / hooks used to build new condo apps fast

206 lines (204 loc) 7.68 kB
// src/helpers/embeddingContext.tsx import { deleteCookie, getCookie, setCookie } from "cookies-next"; import React, { useEffect, useMemo, useState, createContext, useContext } from "react"; import { z } from "zod"; // src/helpers/uuid.ts import { randomBytes } from "crypto"; function generateUUIDv4() { let randomValues; if (typeof crypto !== "undefined" && typeof crypto.randomUUID === "function") { return crypto.randomUUID(); } else if (typeof window !== "undefined" && window.crypto && window.crypto.getRandomValues) { randomValues = new Uint8Array(16); window.crypto.getRandomValues(randomValues); } else { randomValues = randomBytes(16); } randomValues[6] = randomValues[6] & 15 | 64; randomValues[8] = randomValues[8] & 63 | 128; return [...randomValues].map((value, index) => { const hex = value.toString(16).padStart(2, "0"); if (index === 4 || index === 6 || index === 8 || index === 10) { return `-${hex}`; } return hex; }).join(""); } // src/helpers/embeddingContext.tsx var EMBEDDING_CONTEXT_COOKIE_NAME = "embeddingContext"; var EMBEDDING_CONTEXT_QUERY_PARAM = "embeddingContext"; var EMBEDDING_CONTEXT_PRIMARY_TAB_SESSION_STORAGE_KEY = "isEmbeddingContextProvider"; var EMBEDDING_CONTEXT_PROP_NAME = "__EMBEDDING_CONTEXT__"; var EMBEDDING_CONTEXT_CLEANUP_POLLING_TIMEOUT_IN_MS = 2e3; var EMBEDDING_CONTEXT_SCHEMA = z.strictObject({ dv: z.literal(1), app: z.strictObject({ id: z.string(), version: z.string().optional(), build: z.string().optional() }), platform: z.enum(["iOS", "Android", "web"]), os: z.strictObject({ name: z.string(), version: z.string().optional() }).optional(), device: z.strictObject({ id: z.string() }) }); var EMBEDDING_CONTEXT_WITH_SOURCE_SCHEMA = z.strictObject({ ctx: EMBEDDING_CONTEXT_SCHEMA, source: z.enum(["query", "cookie"]) }); var IS_PRIMARY_ALIVE_MESSAGE_SCHEMA = z.object({ type: z.literal("EmbeddingContextPrimaryPolling"), data: z.strictObject({ requestId: z.string() }) }); var IS_PRIMARY_ALIVE_RESPONSE_SCHEMA = z.object({ type: z.literal("EmbeddingContextPrimaryPollingResult"), data: z.strictObject({ requestId: z.string(), isPrimary: z.boolean() }) }); var ReactEmbeddingContext = createContext(null); function useEmbeddingContext() { return useContext(ReactEmbeddingContext); } function b64toContext(b64) { try { const bytes = Uint8Array.from(atob(b64), (c) => c.charCodeAt(0)); const decodedUTFString = new TextDecoder().decode(bytes); const parsedCtx = JSON.parse(decodedUTFString); return EMBEDDING_CONTEXT_SCHEMA.parse(parsedCtx); } catch { return null; } } function contextToB64(ctx) { const stringCtx = JSON.stringify(ctx); const bytes = new TextEncoder().encode(stringCtx); return btoa(String.fromCharCode(...bytes)); } function getEmbeddingContext(req, res) { try { const queryParamValue = req ? new URL(req.url ?? "/", "https://_").searchParams.get(EMBEDDING_CONTEXT_QUERY_PARAM) : new URLSearchParams(window.location.search).get(EMBEDDING_CONTEXT_QUERY_PARAM); if (queryParamValue) { const ctx = b64toContext(decodeURIComponent(queryParamValue)); if (ctx) return { ctx, source: "query" }; } } catch { } const cookieValue = getCookie(EMBEDDING_CONTEXT_COOKIE_NAME, { req, res }); if (cookieValue) { const ctx = b64toContext(cookieValue); if (ctx) return { ctx, source: "cookie" }; } return null; } function withEmbeddingContext(App) { const WithEmbeddingContext = (props) => { const { pageProps } = props; const propsContextWithSource = useMemo(() => { const { success, data } = EMBEDDING_CONTEXT_WITH_SOURCE_SCHEMA.safeParse(pageProps[EMBEDDING_CONTEXT_PROP_NAME]); if (!success) return null; return data; }, [pageProps]); const [embeddingContext, setEmbeddingContext] = useState((propsContextWithSource == null ? void 0 : propsContextWithSource.ctx) ?? null); const [isPrimaryTab, setIsPrimaryTab] = useState((propsContextWithSource == null ? void 0 : propsContextWithSource.source) === "query" ? true : null); const [bcChannel, setBCChannel] = useState(null); useEffect(() => { if (isPrimaryTab === true && typeof window !== "undefined") { window.sessionStorage.setItem(EMBEDDING_CONTEXT_PRIMARY_TAB_SESSION_STORAGE_KEY, "true"); } if (isPrimaryTab === null && typeof window !== "undefined") { setIsPrimaryTab(window.sessionStorage.getItem(EMBEDDING_CONTEXT_PRIMARY_TAB_SESSION_STORAGE_KEY) === "true"); } }, [isPrimaryTab]); useEffect(() => { if (typeof window === "undefined" || !("BroadcastChannel" in window)) return; const bc = new BroadcastChannel("embeddingContext"); setBCChannel(bc); return () => { bc.close(); setBCChannel(null); }; }, []); useEffect(() => { if (isPrimaryTab === null || !bcChannel) return; if (isPrimaryTab) { const primaryListener = (e) => { var _a; const { success, data } = IS_PRIMARY_ALIVE_MESSAGE_SCHEMA.safeParse(e.data); if (!success || !((_a = data == null ? void 0 : data.data) == null ? void 0 : _a.requestId)) return; const response = { type: "EmbeddingContextPrimaryPollingResult", data: { isPrimary: true, requestId: data.data.requestId } }; bcChannel.postMessage(response); }; bcChannel.addEventListener("message", primaryListener); return () => { bcChannel.removeEventListener("message", primaryListener); }; } const requestId = generateUUIDv4(); const timeout = setTimeout(() => { deleteCookie(EMBEDDING_CONTEXT_COOKIE_NAME); setEmbeddingContext(null); setIsPrimaryTab(false); }, EMBEDDING_CONTEXT_CLEANUP_POLLING_TIMEOUT_IN_MS); const secondaryListener = (e) => { const { success, data } = IS_PRIMARY_ALIVE_RESPONSE_SCHEMA.safeParse(e.data); if (!success || (data == null ? void 0 : data.data.requestId) !== requestId) return; clearTimeout(timeout); }; bcChannel.addEventListener("message", secondaryListener); const pollMessage = { type: "EmbeddingContextPrimaryPolling", data: { requestId } }; bcChannel.postMessage(pollMessage); return () => { bcChannel.removeEventListener("message", secondaryListener); clearTimeout(timeout); }; }, [bcChannel, isPrimaryTab]); return /* @__PURE__ */ React.createElement(ReactEmbeddingContext.Provider, { value: embeddingContext }, /* @__PURE__ */ React.createElement(App, { ...props })); }; const appGetInitialProps = App.getInitialProps; if (appGetInitialProps) { WithEmbeddingContext.getInitialProps = async function(context) { const appProps = await appGetInitialProps(context); const { ctx } = context; const embeddingContextWithSource = getEmbeddingContext(ctx.req, ctx.res); if (embeddingContextWithSource && embeddingContextWithSource.source === "query") { setCookie(EMBEDDING_CONTEXT_COOKIE_NAME, contextToB64(embeddingContextWithSource.ctx), { req: ctx.req, res: ctx.res }); } return { ...appProps, pageProps: { ...appProps.pageProps, [EMBEDDING_CONTEXT_PROP_NAME]: embeddingContextWithSource } }; }; } return WithEmbeddingContext; } export { getEmbeddingContext, useEmbeddingContext, withEmbeddingContext }; //# sourceMappingURL=embeddingContext.mjs.map