From 49673e84b6b2a8fe2dcd9a0a9b65c7e9e62700c9 Mon Sep 17 00:00:00 2001 From: Timo Date: Fri, 2 Jan 2026 19:47:43 +0100 Subject: [PATCH] feat: Implement dynamic QR code redirection with comprehensive scan tracking, device/OS detection, and geo-location. --- src/app/r/[slug]/route.ts | 44 +++++++++++++++++++++++++++++++++++---- src/lib/geo.ts | 11 +++++++--- 2 files changed, 48 insertions(+), 7 deletions(-) diff --git a/src/app/r/[slug]/route.ts b/src/app/r/[slug]/route.ts index 41beffa..41338ae 100644 --- a/src/app/r/[slug]/route.ts +++ b/src/app/r/[slug]/route.ts @@ -114,9 +114,40 @@ async function trackScan(qrId: string, request: NextRequest) { // Hash IP for privacy const ipHash = hashIP(ip); - const isTablet = /tablet|ipad|playbook|silk|android(?!.*mobile)/i.test(userAgent); - const isMobile = /mobile|android|iphone/i.test(userAgent); - const device = isTablet ? 'tablet' : isMobile ? 'mobile' : 'desktop'; + // Device Detection Logic: + // 1. Windows or Linux -> Always Desktop + // 2. Explicit iPad/Tablet keywords -> Tablet + // 3. Mac + Chrome browser -> Desktop (real Mac users often use Chrome) + // 4. Mac + Safari + No Referrer -> Likely iPad scanning a QR code + // 5. Mobile keywords -> Mobile + // 6. Everything else -> Desktop + + const isWindows = /windows/i.test(userAgent); + const isLinux = /linux/i.test(userAgent) && !/android/i.test(userAgent); + const isExplicitTablet = /tablet|ipad|playbook|silk/i.test(userAgent); + const isAndroidTablet = /android/i.test(userAgent) && !/mobile/i.test(userAgent); + const isMacintosh = /macintosh/i.test(userAgent); + const isChrome = /chrome/i.test(userAgent); + const isSafari = /safari/i.test(userAgent) && !isChrome; + const hasReferrer = !!referer; + + // iPad in desktop mode: Mac + Safari (no Chrome) + No Referrer (physical scan) + const isLikelyiPadScan = isMacintosh && isSafari && !hasReferrer; + + let device: string; + if (isWindows || isLinux) { + device = 'desktop'; + } else if (isExplicitTablet || isAndroidTablet || isLikelyiPadScan) { + device = 'tablet'; + } else if (/mobile|iphone/i.test(userAgent)) { + device = 'mobile'; + } else if (isMacintosh && isChrome) { + device = 'desktop'; // Mac with Chrome = real desktop + } else if (isMacintosh && hasReferrer) { + device = 'desktop'; // Mac with referrer = probably clicked a link on desktop + } else { + device = 'desktop'; // Default fallback + } // Detect OS let os = 'unknown'; @@ -137,7 +168,9 @@ async function trackScan(qrId: string, request: NextRequest) { const utmMedium = searchParams.get('utm_medium'); const utmCampaign = searchParams.get('utm_campaign'); - // Check if this is a unique scan (first scan from this IP today) + // Check if this is a unique scan (first scan from this IP + Device today) + // We include a simplified device fingerprint so different devices on same IP count as unique + const deviceFingerprint = hashIP(userAgent.substring(0, 100)); // Hash the user agent for privacy const today = new Date(); today.setHours(0, 0, 0, 0); @@ -145,6 +178,9 @@ async function trackScan(qrId: string, request: NextRequest) { where: { qrId, ipHash, + userAgent: { + startsWith: userAgent.substring(0, 50), // Match same device type + }, ts: { gte: today, }, diff --git a/src/lib/geo.ts b/src/lib/geo.ts index 9763359..739a039 100644 --- a/src/lib/geo.ts +++ b/src/lib/geo.ts @@ -24,10 +24,15 @@ export function parseUserAgent(userAgent: string | null): { device: string | nul let device: string | null = null; let os: string | null = null; - // Detect device - check tablet FIRST since iPad can match mobile patterns - if (/Tablet|iPad/i.test(userAgent)) { + // Detect device + // iPadOS 13+ sends "Macintosh" user agent. + // Without referrer info here, we fall back to checking for Safari-only Mac UAs (common for iPad) + const isIPad = /iPad/i.test(userAgent) || + (/Macintosh/i.test(userAgent) && /Safari/i.test(userAgent) && !/Chrome/i.test(userAgent)); + + if (isIPad || /Tablet|PlayBook|Silk/i.test(userAgent)) { device = 'tablet'; - } else if (/Mobile|Android|iPhone/i.test(userAgent)) { + } else if (/Mobile|Android|iPhone/i.test(userAgent) && !isIPad) { device = 'mobile'; } else { device = 'desktop';