Skip to content

Zuno Store

Technical documentation for the Zuno Store feature. For a general overview, see the Zuno Store guide.

File Structure

text
src/
├── app/(app)/(tabs)/
│   └── shop.tsx                   # Tab redirect — defensive Redirect to /shop-webview
├── app/(non-auth)/(tabs)/
│   └── shop.tsx                   # Tab redirect — defensive Redirect to /shop-webview
├── app/
│   └── shop-webview.tsx           # Full-screen root route — WebView screen + show-shop-content gate + tri-state header
├── components/shop/
│   ├── MenuDropdown/              # Dropdown menu component
│   │   ├── MenuDropdown.tsx
│   │   └── index.ts
│   └── ShopUnavailable/           # Unavailable screen component (rendered inside shop-webview.tsx)
│       ├── ShopUnavailable.tsx
│       └── index.ts
├── types/posthog/
│   └── flags.ts                   # Feature flag enum definitions
└── utils/posthog/
    └── usePostHogFlags.ts         # PostHog hooks for flags

Routing

The Store screen is a root-level full-screen Stack route at /shop-webview (registered in src/app/_layout.tsx), deliberately outside the (tabs) group so the bottom tab bar disappears while shopping.

Entry points:

  • Tab tap — the Store tab icon's tabPress is intercepted in both (app)/(tabs)/_layout.tsx and (non-auth)/(tabs)/_layout.tsx. The listener calls e.preventDefault() and navigate('/shop-webview'), so the Store screen is pushed on top of the tabs stack rather than navigated to as a tab.
  • In-app navigationHomeHeroCard and any other in-app entry calls router.navigate('/shop-webview') directly.
  • External deeplinksboxtinstaller://shop-webview[?path=...] lands on the same root route (with the +native-intent.tsx logged-out bypass intact).
  • /shop tab route(tabs)/shop.tsx is a defensive <Redirect href="/shop-webview"/> that forwards any path query param. It's only hit if the tabPress listener is bypassed (programmatic navigation, etc.) — under normal taps the listener short-circuits before the tab focuses.

Feature Flags

Both flags are defined in src/types/posthog/flags.ts:

typescript
export enum PostHogFlags {
  HIDE_SHOP_NAV = 'hide-shop-nav',
  SHOW_SHOP_CONTENT = 'show-shop-content',
}

HIDE_SHOP_NAV

Controls visibility of the Store tab in main navigation.

  • Located in src/app/(app)/(tabs)/_layout.tsx
  • Uses <Tabs.Protected> guard with the useIsShopTabDisabled() hook
  • When true, the Store tab is hidden from the tab bar
typescript
const isShopTabDisabled = useIsShopTabDisabled();

<Tabs.Protected guard={!isShopTabDisabled}>
  <Tabs.Screen name="shop" {...} />
</Tabs.Protected>

SHOW_SHOP_CONTENT

Controls whether users can shop or see a maintenance screen. The flag is evaluated inside the ShopWebView component (src/app/shop-webview.tsx). The screen always renders its header; the body conditionally renders either the WebView or <ShopUnavailable />. Because the Store screen is the only entry-point screen (everything else redirects/pushes into it), every entry goes through the same gate.

typescript
<View style={styles.contentContainer}>
  {isShopContentEnabled ? <WebView ... /> : <ShopUnavailable />}
</View>

WebView

URL: https://zuno.store/?utm_source=zuno_app&utm_medium=mobile

The shop WebView (src/app/shop-webview.tsx) is the full-screen Store experience. It opens the Shopify-hosted store inside the app.

The WebView tracks navigation state for contextual UI updates:

typescript
onNavigationStateChange={(navState) => {
  setCanGoBack(navState.canGoBack);
  setCurrentUrl(navState.url);
  setPageTitle(navState.title || 'Zuno Store');
}}

Header behaviour

The header is always rendered (even when ShopUnavailable is shown in the body) so users always have a way out. Behaviour of the left-hand buttons depends on two pieces of state: the webview's internal canGoBack, and router.canGoBack() (whether the app's nav stack has anywhere to pop to).

StateWhenBack (<)Close (X)
A — Just opened StoreWebview hasn't navigated yet (canGoBack === false)Runs exit()Hidden (placeholder slot kept for layout balance)
B — Inside webview historyWebview has internal history (canGoBack === true)Goes back one webview pageVisible — runs exit()
C — Cold-start deeplinkrouter.canGoBack() === falseRuns exit() (which falls through to defaultScreen)Hidden, same as State A

exit() is a single shared handler:

  • router.canGoBack() ? router.back() : router.replace(defaultScreen)
  • defaultScreen comes from useDefaultScreen() (in src/utils/navigation/useDefaultScreen.ts) — so logged-in users land on /job-offers, logged-out users get the appropriate non-auth landing, and EULA/what's-new gates are respected.

Right-hand buttons

  • Chat — Opens Intercom with contact intention ZUNO_STORE. Non-authenticated users get an anonymous Intercom session via loginUnidentifiedUser() before the space is presented.
  • More menu — Copy link to clipboard, or open in external browser.

Session Persistence

sharedCookiesEnabled={true} uses the platform's shared cookie store, maintaining login state and cart between visits within the app.

Unavailable Screen

Displays when SHOW_SHOP_CONTENT is disabled. The webview's header stays rendered above it, so the Back button still works (and falls back to defaultScreen for deeplink cold-starts). Shows a maintenance message and a "Contact us" button that opens Intercom with the ZUNO_STORE contact intention. Non-authenticated users get an anonymous Intercom session via loginUnidentifiedUser() before the space is presented.

Testing

Feature Flags

  1. Shop tab hidden: Set HIDE_SHOP_NAV to true in PostHog — verify the tab disappears.
  2. Store unavailable: Set SHOW_SHOP_CONTENT to false — verify the unavailable screen renders inside the same chrome (header still present, Back button still works).
  3. Store available: Set SHOW_SHOP_CONTENT to true — verify the WebView opens directly from the Store tab.

WebView

  • Navigate to a product page, verify the close button appears.
  • Tap back — should go to previous page; tap close — should exit.
  • Tap ⋯ → "Copy link" — verify notification and correct URL.
  • Tap ⋯ → "Open in external browser" — verify it opens in Safari/Chrome.
  • Tap chat icon — verify Intercom opens with ZUNO_STORE intention.
  • Cold-start the app via boxtinstaller://shop-webview deeplink, then tap Back — should land on the default screen for the user's auth state (/job-offers for logged-in installers).