{"version":3,"file":"helpers.mjs","sources":["../../../../src/clients/guide/helpers.ts"],"sourcesContent":["import {\n  GroupStage,\n  GuideActivationUrlRuleData,\n  GuideData,\n  GuideGroupData,\n  KnockGuide,\n  KnockGuideActivationUrlPattern,\n  SelectFilterParams,\n  StoreState,\n} from \"./types\";\n\nexport const formatGroupStage = (stage: GroupStage) => {\n  return `status=${stage.status}, resolved=${stage.resolved}`;\n};\n\nexport const formatState = (state: StoreState) => {\n  return `loc=${state.location}`;\n};\n\nexport const formatFilters = (filters: SelectFilterParams = {}) => {\n  return [\n    filters.key && `key=${filters.key}`,\n    filters.type && `type=${filters.type}`,\n  ]\n    .filter((x) => x)\n    .join(\", \");\n};\n\nexport const byKey = <T extends { key: string }>(items: T[]) => {\n  return items.reduce((acc, item) => ({ ...acc, [item.key]: item }), {});\n};\n\nconst sortGuides = <T extends GuideData>(guides: T[]) => {\n  return [...guides].sort(\n    (a, b) =>\n      new Date(a.inserted_at).getTime() - new Date(b.inserted_at).getTime(),\n  );\n};\n\n// Default global guide group key.\nexport const DEFAULT_GROUP_KEY = \"default\";\n\n// Prefixed with a special char $ to distinguish from an actual default group.\nconst MOCK_DEFAULT_GROUP_KEY = \"$default\";\n\n// Build a notional default group to fall back on in case there is none, only\n// for safety as there should always be a default guide group created.\nexport const mockDefaultGroup = (entries: GuideData[] = []) => {\n  const now = new Date();\n\n  return {\n    __typename: \"GuideGroup\",\n    key: MOCK_DEFAULT_GROUP_KEY,\n    display_sequence: sortGuides(entries).map((g) => g.key),\n    display_interval: null,\n    inserted_at: now.toISOString(),\n    updated_at: now.toISOString(),\n  } as GuideGroupData;\n};\n\nexport const findDefaultGroup = (guideGroups: GuideGroupData[]) =>\n  guideGroups.find(\n    (group) =>\n      group.key === DEFAULT_GROUP_KEY || group.key === MOCK_DEFAULT_GROUP_KEY,\n  );\n\nexport const checkStateIfThrottled = (state: StoreState) => {\n  if (state.debug?.ignoreDisplayInterval) {\n    return false;\n  }\n\n  const defaultGroup = findDefaultGroup(state.guideGroups);\n  const throttleWindowStartedAt =\n    state.guideGroupDisplayLogs[DEFAULT_GROUP_KEY];\n\n  if (\n    defaultGroup &&\n    defaultGroup.display_interval &&\n    throttleWindowStartedAt\n  ) {\n    return checkTimeIfThrottled(\n      throttleWindowStartedAt,\n      defaultGroup.display_interval,\n    );\n  }\n\n  // Fall back to false, though this should never happen.\n  return false;\n};\n\n// Checks whether we are currently throttled (inside a \"throttle window\").\n// A throttle window opens when a user dismisses (archives) a guide, and lasts\n// for the configured display interval of the guide group used (currently only\n// the default global group).\nconst checkTimeIfThrottled = (\n  throttleWindowStartedAtTs: string,\n  windowDurationInSeconds: number,\n) => {\n  // 1. Parse the given timestamp string into a Date object.\n  // Date.parse() handles ISO 8601 strings correctly and returns milliseconds\n  // since epoch. This inherently handles timezones by converting everything to\n  // a universal time representation (UTC).\n  const throttleWindowStartDate = new Date(throttleWindowStartedAtTs);\n\n  // Check if the given throttle window start timestamp string was valid, and\n  // if not disregard.\n  if (isNaN(throttleWindowStartDate.getTime())) {\n    return false;\n  }\n\n  // 2. Calculate the throttle window end time in milliseconds by adding the\n  // duration to the throttle window start time.\n  const throttleWindowEndInMilliseconds =\n    throttleWindowStartDate.getTime() + windowDurationInSeconds * 1000;\n\n  // 3. Get the current timestamp in milliseconds since epoch.\n  const currentTimeInMilliseconds = new Date().getTime();\n\n  // 4. Compare the current timestamp with the calculated future timestamp.\n  // Both are in milliseconds since epoch (UTC), so direct comparison is\n  // accurate regardless of local timezones.\n  return currentTimeInMilliseconds <= throttleWindowEndInMilliseconds;\n};\n\n// Safely parse and build a new URL object.\nexport const newUrl = (location: string) => {\n  try {\n    return new URL(location);\n  } catch {\n    return undefined;\n  }\n};\n\n// Evaluates whether the given location url satisfies the url rule.\nexport const evaluateUrlRule = (\n  url: URL,\n  urlRule: GuideActivationUrlRuleData,\n) => {\n  if (urlRule.variable === \"pathname\") {\n    if (urlRule.operator === \"equal_to\") {\n      const argument = urlRule.argument.startsWith(\"/\")\n        ? urlRule.argument\n        : `/${urlRule.argument}`;\n\n      return argument === url.pathname;\n    }\n\n    if (urlRule.operator === \"contains\") {\n      return url.pathname.includes(urlRule.argument);\n    }\n\n    return false;\n  }\n\n  return false;\n};\n\nexport const predicateUrlRules = (\n  url: URL,\n  urlRules: GuideActivationUrlRuleData[],\n) => {\n  const hasBlockRulesOnly = urlRules.every((r) => r.directive === \"block\");\n  const predicateDefault = hasBlockRulesOnly ? true : undefined;\n\n  return urlRules.reduce<boolean | undefined>((acc, urlRule) => {\n    // Any matched block rule prevails so no need to evaluate further\n    // as soon as there is one.\n    if (acc === false) return false;\n\n    // At this point we either have a matched allow rule (acc is true),\n    // or no matched rule found yet (acc is undefined).\n\n    switch (urlRule.directive) {\n      case \"allow\": {\n        // No need to evaluate more allow rules once we matched one\n        // since any matched allowed rule means allow.\n        if (acc === true) return true;\n\n        const matched = evaluateUrlRule(url, urlRule);\n        return matched ? true : undefined;\n      }\n\n      case \"block\": {\n        // Always test block rules (unless already matched to block)\n        // because they'd prevail over matched allow rules.\n        const matched = evaluateUrlRule(url, urlRule);\n        return matched ? false : acc;\n      }\n    }\n  }, predicateDefault);\n};\n\nexport const predicateUrlPatterns = (\n  url: URL,\n  urlPatterns: KnockGuideActivationUrlPattern[],\n) => {\n  const hasBlockPatternsOnly = urlPatterns.every(\n    (r) => r.directive === \"block\",\n  );\n  const predicateDefault = hasBlockPatternsOnly ? true : undefined;\n\n  return urlPatterns.reduce<boolean | undefined>((acc, urlPattern) => {\n    // Any matched block rule prevails so no need to evaluate further\n    // as soon as there is one.\n    if (acc === false) return false;\n\n    // At this point we either have a matched allow rule (acc is true),\n    // or no matched rule found yet (acc is undefined).\n\n    switch (urlPattern.directive) {\n      case \"allow\": {\n        // No need to evaluate more allow rules once we matched one\n        // since any matched allowed rule means allow.\n        if (acc === true) return true;\n\n        const matched = urlPattern.pattern.test(url);\n        return matched ? true : undefined;\n      }\n\n      case \"block\": {\n        // Always test block rules (unless already matched to block)\n        // because they'd prevail over matched allow rules.\n        const matched = urlPattern.pattern.test(url);\n        return matched ? false : acc;\n      }\n    }\n  }, predicateDefault);\n};\n\n// Use this param to start Toolbar v2 and enter into a debugging session when\n// it is present and set to true.\nconst TOOLBAR_QUERY_PARAM = \"knock_guide_toolbar\";\n\n// Optional, when present pin/focus on this guide.\nconst GUIDE_KEY_PARAM = \"focused_guide_key\";\n\nexport type ToolbarV2RunConfig = {\n  isVisible: boolean;\n  focusedGuideKeys?: Record<KnockGuide[\"key\"], true>;\n};\n\nexport const getToolbarRunConfigFromUrl = (): ToolbarV2RunConfig => {\n  const fallback = { isVisible: false };\n\n  if (typeof window === \"undefined\" || !window.location) {\n    return fallback;\n  }\n\n  const urlSearchParams = new URLSearchParams(window.location.search);\n  const toolbarParamValue = urlSearchParams.get(TOOLBAR_QUERY_PARAM);\n  const guideKeyParamValue = urlSearchParams.get(GUIDE_KEY_PARAM);\n\n  if (toolbarParamValue === null) {\n    return fallback;\n  }\n\n  const config: ToolbarV2RunConfig = {\n    isVisible: toolbarParamValue === \"true\",\n  };\n  if (guideKeyParamValue) {\n    config.focusedGuideKeys = { [guideKeyParamValue]: true };\n  }\n\n  return config;\n};\n"],"names":["formatGroupStage","stage","formatState","state","formatFilters","filters","x","byKey","items","acc","item","sortGuides","guides","a","b","DEFAULT_GROUP_KEY","MOCK_DEFAULT_GROUP_KEY","mockDefaultGroup","entries","now","g","findDefaultGroup","guideGroups","group","checkStateIfThrottled","_a","defaultGroup","throttleWindowStartedAt","checkTimeIfThrottled","throttleWindowStartedAtTs","windowDurationInSeconds","throttleWindowStartDate","throttleWindowEndInMilliseconds","newUrl","location","evaluateUrlRule","url","urlRule","predicateUrlRules","urlRules","predicateDefault","predicateUrlPatterns","urlPatterns","urlPattern","TOOLBAR_QUERY_PARAM","GUIDE_KEY_PARAM","getToolbarRunConfigFromUrl","fallback","urlSearchParams","toolbarParamValue","guideKeyParamValue","config"],"mappings":"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"}