Zuno Store
Technical documentation for the Zuno Store feature. For a general overview, see the Zuno Store guide.
File Structure
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 flagsRouting
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
Storetab icon'stabPressis intercepted in both(app)/(tabs)/_layout.tsxand(non-auth)/(tabs)/_layout.tsx. The listener callse.preventDefault()andnavigate('/shop-webview'), so the Store screen is pushed on top of the tabs stack rather than navigated to as a tab. - In-app navigation —
HomeHeroCardand any other in-app entry callsrouter.navigate('/shop-webview')directly. - External deeplinks —
boxtinstaller://shop-webview[?path=...]lands on the same root route (with the+native-intent.tsxlogged-out bypass intact). /shoptab route —(tabs)/shop.tsxis a defensive<Redirect href="/shop-webview"/>that forwards anypathquery param. It's only hit if thetabPresslistener 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:
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 theuseIsShopTabDisabled()hook - When
true, the Store tab is hidden from the tab bar
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.
<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.
Navigation State Tracking
The WebView tracks navigation state for contextual UI updates:
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).
| State | When | Back (<) | Close (X) |
|---|---|---|---|
| A — Just opened Store | Webview hasn't navigated yet (canGoBack === false) | Runs exit() | Hidden (placeholder slot kept for layout balance) |
| B — Inside webview history | Webview has internal history (canGoBack === true) | Goes back one webview page | Visible — runs exit() |
| C — Cold-start deeplink | router.canGoBack() === false | Runs exit() (which falls through to defaultScreen) | Hidden, same as State A |
exit() is a single shared handler:
router.canGoBack() ? router.back() : router.replace(defaultScreen)defaultScreencomes fromuseDefaultScreen()(insrc/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 vialoginUnidentifiedUser()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
- Shop tab hidden: Set
HIDE_SHOP_NAVtotruein PostHog — verify the tab disappears. - Store unavailable: Set
SHOW_SHOP_CONTENTtofalse— verify the unavailable screen renders inside the same chrome (header still present, Back button still works). - Store available: Set
SHOW_SHOP_CONTENTtotrue— 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_STOREintention. - Cold-start the app via
boxtinstaller://shop-webviewdeeplink, then tap Back — should land on the default screen for the user's auth state (/job-offersfor logged-in installers).