DEV Community

Kiran Kumar
Kiran Kumar

Posted on

How I Built a QR Code That Knows Whether You're on Android or iOS

Here's the full article:

How I Built a QR Code That Knows Whether You're on Android or iOS
And the edge cases that almost broke it

I was helping a client launch their mobile app. They had packaging printed — boxes, stickers, flyers — all with a QR code pointing to the Play Store.
Then someone asked: "What about iPhone users?"
We stared at each other.
You can't print two QR codes on a box. You can't ask your customer to figure out which one to scan. And you definitely can't afford to lose half your potential installs because someone scanned the "wrong" code.
That's the problem I set out to solve. The result is SingleQR — one QR code that detects whether you're on Android or iOS and redirects you to the right store automatically.
Here's exactly how I built it.

The Core Idea: Redirect at the URL Layer
The QR code itself is just a URL. It doesn't know anything about the device scanning it — that's the job of the server.
When someone scans the QR, they hit my redirect endpoint. At that point, I have one thing to work with: the HTTP User-Agent header.
The flow looks like this:
QR Scan
→ HTTP GET to redirect.singleqr.in/{code}
→ Read User-Agent header
→ Android? → Play Store URL
→ iOS? → App Store URL
→ Other? → Fallback landing page
Simple in theory. The edge cases are where it gets interesting.

The Detection Logic
Here's the core of the UA detection:
javascriptfunction detectPlatform(userAgent) {
const ua = userAgent.toLowerCase();

// iOS detection — must check before Android
// because some Android browsers include "like Mac OS X"
if (/iphone|ipad|ipod/.test(ua)) {
return 'ios';
}

// Android detection
if (/android/.test(ua)) {
return 'android';
}

// Everything else — desktop, unknown devices
return 'other';
}
Notice I check iOS before Android. This matters — some Android browser UAs contain phrases like "like Mac OS X" that can create false positives if you're not careful about order.

Edge Case 1: iPads in Desktop Mode
Since iOS 13, Safari on iPad defaults to desktop mode. This means the User-Agent no longer says "iPad" — it sends a macOS Safari UA instead.
// Old iPad UA (iOS 12 and below)
Mozilla/5.0 (iPad; CPU OS 12_0 like Mac OS X) AppleWebKit/605.1.15

// New iPad UA (iOS 13+ with desktop mode ON — default)
Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15) AppleWebKit/605.1.15
If you only check the UA, iPads on iOS 13+ will land on your desktop fallback page. That's a real miss.
The fix: Check for the maxTouchPoints hint via a client-side script on the fallback page:
javascript// On the fallback/desktop landing page
if (navigator.maxTouchPoints > 1 && /Macintosh/.test(navigator.userAgent)) {
// This is almost certainly an iPad in desktop mode
window.location.href = APP_STORE_URL;
}

Edge Case 2: Old Android WebViews Lying About Their UA
Some old WebViews (particularly on Android 4.x) would send something like:
Mozilla/5.0 (Linux; U; en-us) AppleWebKit/528.5+
No "Android" in sight. These users hit the fallback page and get confused.
The fix: Secondary signals — Accept-Language header, screen resolution, referrer chain. For most production use cases, the raw Android/iOS check covers 98%+ of scans. Log the ambiguous ones and review periodically.

Edge Case 3: Huawei Devices Without the Play Store
Post-2019, Huawei phones ship without Google Play Services. These devices do send Android UAs — but if you redirect them to the Play Store, the app won't be there.
Detect Huawei specifically:
javascriptfunction detectPlatform(userAgent) {
const ua = userAgent.toLowerCase();

if (/iphone|ipad|ipod/.test(ua)) return 'ios';

if (/android/.test(ua) && (/huawei|honor|hms|emui/.test(ua))) {
return 'huawei'; // Route to AppGallery
}

if (/android/.test(ua)) return 'android';

return 'other';
}
Or show a smart fallback page with both Play Store and AppGallery buttons — that's what SingleQR does by default.

Edge Case 4: Bots and Crawlers
Bots pollute your scan analytics and get confused by store redirects.
javascriptconst BOT_PATTERNS = [
/googlebot/i, /bingbot/i, /slurp/i,
/facebot/i, /twitterbot/i, /whatsapp/i,
];

function isBot(userAgent) {
return BOT_PATTERNS.some(pattern => pattern.test(userAgent));
}
For bots, serve a plain HTML page with Open Graph meta tags — so social share previews look good — without triggering a redirect.

Performance: Getting Below 100ms
The redirect needs to be fast. A user scanning a QR code has zero tolerance for a loading spinner.

Edge functions (Cloudflare Workers) — UA parsing happens at the edge, close to the user
No database lookup on the hot path — mappings are cached at the edge after the first request
Simple 302 redirect — no JavaScript, no page load, just a header

Result: median redirect time of ~65ms globally.
javascriptexport default {
async fetch(request, env) {
const code = extractCode(request.url);
const mapping = await env.QR_CACHE.get(code);

if (!mapping) return Response.redirect('https://singleqr.in/not-found', 302);

const { androidUrl, iosUrl, fallbackUrl } = JSON.parse(mapping);
const ua = request.headers.get('User-Agent') || '';

if (isBot(ua)) return serveBotPage(mapping);

const platform = detectPlatform(ua);
const destination =
  platform === 'android' ? androidUrl :
  platform === 'ios'     ? iosUrl :
                           fallbackUrl;

return Response.redirect(destination, 302);
Enter fullscreen mode Exit fullscreen mode

}
}

What I'd Do Differently

Build the analytics dashboard earlier. Turned out to be the feature clients ask about most.
Handle iPad desktop mode from day one. Added it two weeks after launch after bug reports.
Add a "test scan" button in the dashboard. Users want to verify their redirect works before printing 10,000 boxes.

Try It
If you're building an app and dealing with this problem, the tool is live at singleqr.in.
Free tier available. Happy to answer questions in the comments — especially if you've hit UA detection edge cases I haven't covered.
#android #ios #webdev #showdev

Top comments (0)