export const ATTRIBUTION_COOKIE_NAME = 'qrmaster_first_touch'; export const ONBOARDING_CHECKLIST_DISMISS_KEY = 'qrmaster_onboarding_checklist_dismissed'; export const ONBOARDING_DOWNLOAD_COMPLETE_KEY = 'qrmaster_onboarding_download_complete'; export const ONBOARDING_DOWNLOAD_COMPLETE_EVENT = 'qrmaster:onboarding-download-complete'; export type LifecycleStage = | 'cold' | 'activated' | 'warm' | 'hot' | 'upgrade_candidate' | 'paid'; export type SignupSourceOption = { value: string; label: string; }; export type OnboardingOption = { value: string; label: string; description?: string; }; export type AttributionSnapshot = { signupSource?: string | null; signupMedium?: string | null; signupCampaign?: string | null; signupContent?: string | null; signupTerm?: string | null; signupReferrer?: string | null; signupLandingPath?: string | null; signupFirstSeenAt?: string | null; }; const FREEMAIL_DOMAINS = new Set([ 'gmail.com', 'yahoo.com', 'hotmail.com', 'outlook.com', 'icloud.com', 'gmx.de', 'gmx.net', 'web.de', 'mail.com', 'aol.com', 'proton.me', 'protonmail.com', ]); export const SIGNUP_SOURCE_OPTIONS: SignupSourceOption[] = [ { value: 'google_search', label: 'Google Search' }, { value: 'instagram', label: 'Instagram' }, { value: 'facebook', label: 'Facebook' }, { value: 'tiktok', label: 'TikTok' }, { value: 'linkedin', label: 'LinkedIn' }, { value: 'youtube', label: 'YouTube' }, { value: 'blog_article', label: 'Blog or article' }, { value: 'friend_colleague', label: 'Friend or colleague' }, { value: 'direct', label: 'Direct / I typed the URL' }, { value: 'other', label: 'Other' }, ]; export const PRIMARY_USE_CASE_OPTIONS: OnboardingOption[] = [ { value: 'website_qr', label: 'Website QR Code' }, { value: 'menu_pdf', label: 'Menu or PDF QR Code' }, { value: 'contact_card', label: 'Contact Card QR Code' }, { value: 'wifi_qr', label: 'WiFi QR Code' }, { value: 'marketing_campaign', label: 'Marketing Campaign QR Code' }, { value: 'barcode', label: 'Barcode' }, { value: 'bulk_qr', label: 'Bulk QR Codes' }, { value: 'something_else', label: 'Something else' }, ]; export const PRIMARY_GOAL_OPTIONS: OnboardingOption[] = [ { value: 'drive_website_traffic', label: 'Drive website traffic' }, { value: 'track_printed_campaigns', label: 'Track printed campaigns' }, { value: 'share_contact_details', label: 'Share contact details' }, { value: 'replace_printed_menus', label: 'Replace printed menus' }, { value: 'generate_leads', label: 'Generate leads' }, { value: 'label_products', label: 'Label products' }, { value: 'manage_multiple_qr_codes', label: 'Manage multiple QR codes' }, { value: 'something_else', label: 'Something else' }, ]; export const JOB_ROLE_OPTIONS: OnboardingOption[] = [ { value: 'founder_owner', label: 'Founder / Owner' }, { value: 'marketing_manager', label: 'Marketing Manager' }, { value: 'operations', label: 'Operations' }, { value: 'agency_freelancer', label: 'Agency / Freelancer' }, { value: 'it_technical', label: 'IT / Technical' }, { value: 'sales', label: 'Sales' }, { value: 'designer', label: 'Designer' }, { value: 'other', label: 'Other' }, ]; export const TEAM_SIZE_OPTIONS: OnboardingOption[] = [ { value: 'just_me', label: 'Just me' }, { value: '2_5', label: '2–5' }, { value: '6_20', label: '6–20' }, { value: '21_100', label: '21–100' }, { value: '100_plus', label: '100+' }, ]; export function serializeAttributionCookie(snapshot: AttributionSnapshot): string { return JSON.stringify(snapshot); } export function parseAttributionCookie(value?: string | null): AttributionSnapshot | null { if (!value) { return null; } try { return JSON.parse(value) as AttributionSnapshot; } catch { return null; } } export function getEmailDomain(email?: string | null): string | null { if (!email || !email.includes('@')) { return null; } return email.split('@')[1]?.trim().toLowerCase() || null; } export function isFreemailDomain(domain?: string | null): boolean { return Boolean(domain && FREEMAIL_DOMAINS.has(domain.toLowerCase())); } export function getSourceLabel(value?: string | null): string { const option = SIGNUP_SOURCE_OPTIONS.find((item) => item.value === value); return option?.label || 'Unknown'; } export function getUseCaseLabel(value?: string | null): string { const option = PRIMARY_USE_CASE_OPTIONS.find((item) => item.value === value); return option?.label || 'Unknown'; } export function getGoalLabel(value?: string | null): string { const option = PRIMARY_GOAL_OPTIONS.find((item) => item.value === value); return option?.label || 'Unknown'; } export function getRoleLabel(value?: string | null): string { const option = JOB_ROLE_OPTIONS.find((item) => item.value === value); return option?.label || 'Unknown'; } export function getTeamSizeLabel(value?: string | null): string { const option = TEAM_SIZE_OPTIONS.find((item) => item.value === value); return option?.label || 'Unknown'; } export function normalizeSource(input: { utmSource?: string | null; referrer?: string | null; landingPath?: string | null; }): string { const utmSource = input.utmSource?.toLowerCase().trim(); const referrer = input.referrer?.toLowerCase().trim(); const landingPath = input.landingPath?.toLowerCase().trim(); if (utmSource) { if (utmSource.includes('google')) return 'google_search'; if (utmSource.includes('instagram')) return 'instagram'; if (utmSource.includes('facebook') || utmSource.includes('meta')) return 'facebook'; if (utmSource.includes('tiktok')) return 'tiktok'; if (utmSource.includes('linkedin')) return 'linkedin'; if (utmSource.includes('youtube')) return 'youtube'; if (utmSource.includes('blog') || utmSource.includes('article') || utmSource.includes('seo')) return 'blog_article'; } if (referrer) { if (referrer.includes('google.')) return 'google_search'; if (referrer.includes('instagram.')) return 'instagram'; if (referrer.includes('facebook.') || referrer.includes('fb.com')) return 'facebook'; if (referrer.includes('tiktok.')) return 'tiktok'; if (referrer.includes('linkedin.')) return 'linkedin'; if (referrer.includes('youtube.')) return 'youtube'; if (referrer.includes('medium.com') || referrer.includes('substack.com') || referrer.includes('/blog')) return 'blog_article'; } if (landingPath && landingPath.startsWith('/blog')) { return 'blog_article'; } return 'direct'; } export function buildAttributionSnapshot(params: { utmSource?: string | null; utmMedium?: string | null; utmCampaign?: string | null; utmContent?: string | null; utmTerm?: string | null; referrer?: string | null; landingPath?: string | null; firstSeenAt?: Date | string | null; }): AttributionSnapshot { const signupFirstSeenAt = params.firstSeenAt instanceof Date ? params.firstSeenAt.toISOString() : params.firstSeenAt || new Date().toISOString(); return { signupSource: normalizeSource({ utmSource: params.utmSource, referrer: params.referrer, landingPath: params.landingPath, }), signupMedium: params.utmMedium || null, signupCampaign: params.utmCampaign || null, signupContent: params.utmContent || null, signupTerm: params.utmTerm || null, signupReferrer: params.referrer || null, signupLandingPath: params.landingPath || null, signupFirstSeenAt, }; } export function shouldResumeOnboarding(user: { onboardingStartedAt?: Date | null; onboardingCompletedAt?: Date | null; } | null): boolean { if (!user) { return false; } return Boolean(user.onboardingStartedAt && !user.onboardingCompletedAt); } export function getLifecycleStageLabel(stage?: string | null): string { switch (stage) { case 'activated': return 'Activated'; case 'warm': return 'Warm'; case 'hot': return 'Hot'; case 'upgrade_candidate': return 'Upgrade Candidate'; case 'paid': return 'Paid'; default: return 'Cold'; } } export function getOnboardingHeadlineForUseCase(useCase?: string | null): string { switch (useCase) { case 'marketing_campaign': return 'Create your first campaign QR code'; case 'menu_pdf': return 'Create your first menu QR code'; case 'contact_card': return 'Create your first contact QR code'; case 'barcode': return 'Create your first barcode'; case 'bulk_qr': return 'Set up your first business-ready QR'; default: return 'Create your first QR code'; } } export function getCreatePresetForUseCase(useCase?: string | null): { contentType: string; dynamic: boolean; title: string; content: Record; } { switch (useCase) { case 'menu_pdf': return { contentType: 'PDF', dynamic: true, title: 'Restaurant menu', content: {}, }; case 'contact_card': return { contentType: 'VCARD', dynamic: true, title: 'Business contact card', content: {}, }; case 'barcode': return { contentType: 'BARCODE', dynamic: false, title: 'Product barcode', content: { format: 'CODE128' }, }; case 'bulk_qr': return { contentType: 'URL', dynamic: true, title: 'Bulk campaign starter', content: { url: '' }, }; case 'wifi_qr': return { contentType: 'TEXT', dynamic: false, title: 'WiFi access QR', content: { text: 'WIFI:T:WPA;S:MyNetwork;P:password;;' }, }; case 'website_qr': return { contentType: 'URL', dynamic: true, title: 'Website QR', content: { url: '' }, }; case 'marketing_campaign': default: return { contentType: 'URL', dynamic: true, title: 'Campaign landing page', content: { url: '' }, }; } } export function getChecklistItems(user: { signupSourceSelfReported?: string | null; primaryUseCase?: string | null; firstQrCreatedAt?: string | Date | null; firstDynamicQrAt?: string | Date | null; firstScanAt?: string | Date | null; activationAt?: string | Date | null; }): Array<{ id: string; label: string; done: boolean }> { return [ { id: 'source', label: 'Confirm how you found QR Master', done: Boolean(user.signupSourceSelfReported), }, { id: 'use-case', label: 'Choose your use case', done: Boolean(user.primaryUseCase), }, { id: 'first-qr', label: 'Create your first QR code', done: Boolean(user.firstQrCreatedAt), }, { id: 'first-dynamic', label: 'Create your first dynamic QR code', done: Boolean(user.firstDynamicQrAt), }, { id: 'download', label: 'Download your QR code', done: false, }, { id: 'scan', label: 'Get your first scan', done: Boolean(user.firstScanAt), }, ]; }