How I blocked app-only discount codes on web browsers using Checkout UI Extensions + Theme JS Shopify Plus

Hey Shopify Community,

I want to share a solution I just built for a Shopify Plus store (gant.ae) that I think will help others facing the same challenge.

The Challenge:
We have a mobile app built with Buildnatively and wanted app-exclusive discount codes (APP20, APP21) to only work inside the app completely blocked for web browser users.

Why it’s tricky:

  • Shopify checkout runs on a separate domain, so theme JS won’t run there
  • Shopify Functions API doesn’t give easy access to both discount codes and cart attributes together
  • iOS WebView detection is notoriously difficult

The Solution — 3 layers:

Layer 1 — Cart Page (theme.liquid)
Detect the Buildnatively app using navigator.userAgent.toLowerCase().includes(‘natively’) — this works for both iOS and Android. Set a cart attribute checkout_context = mobile_app for app users. Block app-only codes on the cart page for web users.

Layer 2 — Checkout Page (Checkout UI Extension)
Built a UI Extension using @shopify/ui-extensions/checkout/preact with:

  • useDiscountCodes() to detect applied codes
  • useAttributes() to read the cart attribute
  • useApplyDiscountCodeChange() to auto-remove the code
  • useBuyerJourneyIntercept() to block checkout progression
  • A 12-second error message timer so the user sees the message even after the code is removed

Layer 3 — iOS Fallback
Used both the cart attribute AND the user agent check as a fallback in the extension so iOS users are always correctly identified.

Key code snippets:

User agent detection:
const isApp = navigator.userAgent.toLowerCase().includes(‘natively’);

Checkout UI Extension core logic:
const isApp = checkoutContext === ‘mobile_app’ || ua.includes(‘natively’);
const shouldBlock = hasAppOnlyCode && !isApp;

Result:
:white_check_mark: Web users blocked on cart page
:white_check_mark: Web users blocked on checkout page
:white_check_mark: App-only codes auto-removed
:white_check_mark: iOS and Android both working perfectly

Hope this helps someone facing the same challenge! Happy to share full code if needed.

-– Noman

Nice work on the iOS fallback that’s usually where these setups break!

Thanks! Yeah iOS was definitely the trickiest part :sweat_smile: Safari and iOS WebView look almost identical from the outside. The breakthrough was when Buildnatively confirmed they inject “natively” into the user agent on all platforms that one simple string check solved what multiple complex detection conditions couldn’t. Sometimes the simplest fix is the best one! :raising_hands: