diff --git a/src/lib/components/user-area/UserArea.svelte b/src/lib/components/user-area/UserArea.svelte index cdd86cd..e9be684 100644 --- a/src/lib/components/user-area/UserArea.svelte +++ b/src/lib/components/user-area/UserArea.svelte @@ -5,6 +5,7 @@ import { AUTH0_AUTHENTICATOR_URL } from 'lib/config'; import { AUTH_USER_ROLE } from 'lib/config/auth'; import { DISABLE_NUDGES } from "lib/config/profile-toasts.config"; + import { appendUtmParamsToUrl } from 'lib/functions/utm-cookies.handler'; import ToolSelector from '../tool-selector/ToolSelector.svelte'; @@ -65,12 +66,15 @@ function onSignIn(signup?: any) { const locationHref = `${window.location.origin}${window.location.pathname}` - const signupUrl = [ + let signupUrl = [ `${AUTH0_AUTHENTICATOR_URL}?retUrl=${encodeURIComponent(locationHref)}`, signup === true ? '&mode=signUp' : '', $ctx.signupUtmCodes, ].filter(Boolean).join('&') + // Append UTM parameters from cookie if they exist + signupUrl = appendUtmParamsToUrl(signupUrl); + window.location.href = signupUrl; } diff --git a/src/lib/config/nav-menu/all-nav-items.config.ts b/src/lib/config/nav-menu/all-nav-items.config.ts index 823d3fc..d02a2d9 100644 --- a/src/lib/config/nav-menu/all-nav-items.config.ts +++ b/src/lib/config/nav-menu/all-nav-items.config.ts @@ -217,4 +217,9 @@ export const allNavItems: {[key: string]: NavMenuItem} = { marketingPathname: '/talent', url: getMarketingUrl('/talent'), }, + referralProgram: { + label: 'Referral Programme', + marketingPathname: '/nasa-referral', + url: getMarketingUrl('/nasa-referral'), + }, } diff --git a/src/lib/config/nav-menu/main-navigation.config.ts b/src/lib/config/nav-menu/main-navigation.config.ts index da53db8..032d5b2 100644 --- a/src/lib/config/nav-menu/main-navigation.config.ts +++ b/src/lib/config/nav-menu/main-navigation.config.ts @@ -22,6 +22,7 @@ export const mainNavigationItems: NavMenuItem[] = [ allNavItems.aiHub, allNavItems.statistics, allNavItems.discordApp, + allNavItems.referralProgram, ] }, { diff --git a/src/lib/functions/utm-cookies.handler.ts b/src/lib/functions/utm-cookies.handler.ts new file mode 100644 index 0000000..24b2d72 --- /dev/null +++ b/src/lib/functions/utm-cookies.handler.ts @@ -0,0 +1,238 @@ +import { TC_DOMAIN } from '../config/hosts'; +import { getEnvValue } from '../config/env-vars'; + +// UTM cookie configuration types +interface UtmParams { + utm_source?: string; + utm_medium?: string; + utm_campaign?: string; +} + +// Cookie configuration constants +const TC_UTM_COOKIE_NAME = 'tc_utm'; +const DEFAULT_COOKIE_LIFETIME_DAYS = 3; +const COOKIE_PATH = '/'; +const COOKIE_SAMESITE = 'Lax'; + +/** + * Sanitizes a string to remove all characters except A-Z, a-z, 0-9, hyphen (-), underscore (_) + * @param input - The string to sanitize + * @returns Sanitized string + */ +export function sanitize(input: string): string { + if (!input || typeof input !== 'string') { + return ''; + } + // Remove all characters except A-Z, a-z, 0-9, hyphen (-), underscore (_) + return input.replace(/[^A-Za-z0-9\-_]/g, ''); +} + +/** + * Extracts and sanitizes UTM parameters from the URL + * @returns Object containing sanitized utm_source, utm_medium, utm_campaign + */ +function extractUtmParams(): UtmParams { + const params: UtmParams = {}; + + try { + const searchParams = new URLSearchParams(window.location.search); + + const utm_source = searchParams.get('utm_source'); + const utm_medium = searchParams.get('utm_medium'); + const utm_campaign = searchParams.get('utm_campaign'); + + if (utm_source) { + params.utm_source = sanitize(utm_source); + } + if (utm_medium) { + params.utm_medium = sanitize(utm_medium); + } + if (utm_campaign) { + params.utm_campaign = sanitize(utm_campaign); + } + } catch (error) { + console.warn('Error extracting UTM parameters:', error); + } + + return params; +} + +/** + * Gets the cookie lifetime from environment variable or uses default + * @returns Lifetime in days + */ +function getCookieLifetimeDays(): number { + try { + const envValue = getEnvValue('VITE_UTM_COOKIE_LIFETIME_DAYS', String(DEFAULT_COOKIE_LIFETIME_DAYS)); + const days = parseInt(envValue, 10); + return isNaN(days) ? DEFAULT_COOKIE_LIFETIME_DAYS : days; + } catch { + return DEFAULT_COOKIE_LIFETIME_DAYS; + } +} + +/** + * Gets the cookie domain with leading dot for broader subdomain coverage + * @returns Cookie domain (e.g., .topcoder.com) + */ +function getCookieDomain(): string { + return `.${TC_DOMAIN}`; +} + +/** + * Checks if a cookie with the given name exists + * @param name - Cookie name + * @returns true if cookie exists, false otherwise + */ +function cookieExists(name: string): boolean { + const cookies = document.cookie.split(';'); + return cookies.some(cookie => cookie.trim().startsWith(`${name}=`)); +} + +/** + * Sets a cookie with the specified attributes + * @param name - Cookie name + * @param value - Cookie value + * @param options - Cookie options (domain, path, sameSite, secure, maxAge) + */ +function setCookie( + name: string, + value: string, + options: { + domain?: string; + path?: string; + sameSite?: string; + secure?: boolean; + maxAge?: number; + } = {} +): void { + const { + domain = getCookieDomain(), + path = COOKIE_PATH, + sameSite = COOKIE_SAMESITE, + secure = true, + maxAge = DEFAULT_COOKIE_LIFETIME_DAYS * 24 * 60 * 60, // Convert days to seconds + } = options; + + let cookieString = `${name}=${encodeURIComponent(value)}`; + + if (domain) { + cookieString += `; domain=${domain}`; + } + if (path) { + cookieString += `; path=${path}`; + } + if (maxAge) { + cookieString += `; max-age=${maxAge}`; + } + if (sameSite) { + cookieString += `; SameSite=${sameSite}`; + } + if (secure) { + cookieString += '; Secure'; + } + + document.cookie = cookieString; +} + +/** + * Initializes UTM cookie handling on page load + * Extracts UTM parameters from URL, sanitizes them, and persists to cookie + * Only sets the cookie if it doesn't already exist + */ +export function initializeUtmCookieHandler(): void { + try { + // Check if cookie already exists + if (cookieExists(TC_UTM_COOKIE_NAME)) { + console.debug('UTM cookie already exists, skipping initialization'); + return; + } + + // Extract and sanitize UTM parameters + const utmParams = extractUtmParams(); + + // Only set cookie if we have at least one UTM parameter + if (Object.keys(utmParams).length === 0) { + console.debug('No UTM parameters found in URL'); + return; + } + + // Create JSON value with all UTM parameters + const cookieValue = JSON.stringify(utmParams); + + // Get cookie lifetime in seconds + const lifetimeDays = getCookieLifetimeDays(); + const maxAgeSecs = lifetimeDays * 24 * 60 * 60; + + // Set the cookie with proper attributes + setCookie(TC_UTM_COOKIE_NAME, cookieValue, { + domain: getCookieDomain(), + path: COOKIE_PATH, + sameSite: COOKIE_SAMESITE, + secure: true, + maxAge: maxAgeSecs, + }); + + console.debug(`UTM cookie set successfully:`, utmParams); + } catch (error) { + console.error('Error initializing UTM cookie handler:', error); + } +} + +/** + * Retrieves and parses the tc_utm cookie + * @returns Parsed UTM parameters or null if cookie doesn't exist + */ +export function getUtmCookie(): UtmParams | null { + try { + const cookies = document.cookie.split(';'); + const cookieStr = cookies.find(cookie => cookie.trim().startsWith(`${TC_UTM_COOKIE_NAME}=`)); + + if (!cookieStr) { + return null; + } + + const cookieValue = decodeURIComponent(cookieStr.split('=')[1]); + return JSON.parse(cookieValue) as UtmParams; + } catch (error) { + console.warn('Error retrieving UTM cookie:', error); + return null; + } +} + +/** + * Appends UTM parameters from the tc_utm cookie to a given URL + * Only appends parameters that exist in the cookie + * @param url - The base URL to append parameters to + * @returns URL with UTM parameters appended, or original URL if no cookie exists + */ +export function appendUtmParamsToUrl(url: string): string { + if (!url) { + return url; + } + + const utmParams = getUtmCookie(); + if (!utmParams || Object.keys(utmParams).length === 0) { + return url; + } + + try { + const urlObj = new URL(url, window.location.origin); + + // Append only the UTM parameters that exist in the cookie + if (utmParams.utm_source) { + urlObj.searchParams.set('utm_source', utmParams.utm_source); + } + if (utmParams.utm_medium) { + urlObj.searchParams.set('utm_medium', utmParams.utm_medium); + } + if (utmParams.utm_campaign) { + urlObj.searchParams.set('utm_campaign', utmParams.utm_campaign); + } + + return urlObj.toString(); + } catch (error) { + console.warn('Error appending UTM parameters to URL:', error); + return url; + } +} diff --git a/src/main.ts b/src/main.ts index cd3e7ba..603af73 100644 --- a/src/main.ts +++ b/src/main.ts @@ -3,6 +3,7 @@ import type { Writable } from 'svelte/store' import { buildContext, type AuthUser, type NavigationHandler, type SupportMeta } from './lib/app-context' import { PubSub } from './lib/utils/pubsub'; +import { initializeUtmCookieHandler } from './lib/functions/utm-cookies.handler'; import 'lib/styles/main.scss'; @@ -207,4 +208,7 @@ function execQueueCall(method: TcUniNavMethods, ...args: unknown[]) { // replace the method that adds the calls to the queue // with a direct exec call Object.assign(window as any, {[globalName]: execQueueCall.bind(null)}); + + // Initialize UTM cookie handler on module load + initializeUtmCookieHandler(); })() diff --git a/types/src/lib/functions/utm-cookies.handler.d.ts b/types/src/lib/functions/utm-cookies.handler.d.ts new file mode 100644 index 0000000..c3d9844 --- /dev/null +++ b/types/src/lib/functions/utm-cookies.handler.d.ts @@ -0,0 +1,30 @@ +interface UtmParams { + utm_source?: string; + utm_medium?: string; + utm_campaign?: string; +} +/** + * Sanitizes a string to remove all characters except A-Z, a-z, 0-9, hyphen (-), underscore (_) + * @param input - The string to sanitize + * @returns Sanitized string + */ +export declare function sanitize(input: string): string; +/** + * Initializes UTM cookie handling on page load + * Extracts UTM parameters from URL, sanitizes them, and persists to cookie + * Only sets the cookie if it doesn't already exist + */ +export declare function initializeUtmCookieHandler(): void; +/** + * Retrieves and parses the tc_utm cookie + * @returns Parsed UTM parameters or null if cookie doesn't exist + */ +export declare function getUtmCookie(): UtmParams | null; +/** + * Appends UTM parameters from the tc_utm cookie to a given URL + * Only appends parameters that exist in the cookie + * @param url - The base URL to append parameters to + * @returns URL with UTM parameters appended, or original URL if no cookie exists + */ +export declare function appendUtmParamsToUrl(url: string): string; +export {};