Extending
Replace any screen of the Storefront App, select built-in screen variants, and add your own screens, routes and tabs — all from the build's brand folder, without forking the app's code.
Extending
The app's navigators refer to screens by a stable screen id, and a registry resolves each id to the app's own screen, a build's replacement, or a selected variant. A build changes screens from brand/storefront.extensions.ts without touching the navigators, so it keeps taking new releases as a plain merge.
| Need | Where |
|---|---|
| Replace a screen | brand/storefront.extensions.ts → screens |
| Choose a built-in variant | the brand file → screens, or screenVariants in the extensions file |
| Add screens (routes) | brand/storefront.extensions.ts → routes |
| Add tabs | brand/storefront.extensions.ts → tabs |
| Change or add copy | brand/translations/<locale>.json |
Replacements are ordinary modules compiled into the build. The app never downloads or runs code from an API; configuration can only select variants that are already compiled in.
Replacing a screen
Write the screen, for example brand/screens/HomeScreen.tsx:
import React from 'react';
import { YStack } from 'tamagui';
import type { StorefrontScreen } from '../../src/extensions';
import { UIText } from '../../src/ui';
const HomeScreen: StorefrontScreen<'store.home'> = ({ route, navigation, DefaultScreen }) => (
<YStack flex={1}>
<UIText variant='title'>Welcome</UIText>
{/* Optional: reuse the app's own screen below yours */}
<DefaultScreen route={route} navigation={navigation} />
</YStack>
);
export default HomeScreen;Register it:
// brand/storefront.extensions.ts
import { defineStorefrontExtensions } from '../src/extensions';
export default defineStorefrontExtensions({
screens: {
'store.home': { load: () => import('./screens/HomeScreen') },
},
});Routes, deep links, sign-in guards and headers stay as they were. HomeScreen.web.tsx and HomeScreen.native.tsx work as usual, and platforms: ['ios', 'android'] applies a replacement on some platforms only.
Variants
A replacement (or the app itself) can offer named variants, chosen in the brand file:
// brand/storefront.extensions.ts
screens: {
'store.home': {
load: () => import('./screens/HomeScreen'),
variants: { editorial: () => import('./screens/EditorialHomeScreen') },
},
},
// brand/storefront.brand.ts
screens: { 'store.home': { variant: 'editorial' } },The app's built-in variants are chosen the same way, for example the Network shop home: screens: { 'network.home': { variant: 'shop' } }. An unknown variant falls back to the replacement, or to the app's screen.
Adding screens and tabs
import { defineStorefrontExtensions } from '../src/extensions';
import { faGift } from '@fortawesome/free-solid-svg-icons';
export default defineStorefrontExtensions({
routes: {
Rewards: { load: () => import('./screens/RewardsScreen'), path: 'rewards' },
RewardDetail: { load: () => import('./screens/RewardDetailScreen'), path: 'rewards/:rewardId', presentation: 'modal' },
},
tabs: {
RewardsTab: { initialRoute: 'Rewards', icon: faGift, labelKey: 'Brand.tabs.rewards', editions: ['store'] },
},
});Routes
- Each route is added to every stack of its editions (
editions, both by default), sonavigation.navigate('RewardDetail', { rewardId })works from any screen and opens in the current tab. - Names are PascalCase. A name that matches one of the app's routes is skipped with a warning; replace app screens with
screensinstead. pathmakes the route deep-linkable.presentation: 'modal'presents it over the current screen.- A route that fails to load or throws shows an error view in its place instead of crashing the app.
Tabs
- A tab opens one of your routes (
initialRoute). Its stack also has every custom route and the edition's shared screens (products, offers, locations; in a Network also stores, categories, reviews and notifications). iconis a FontAwesome icon or a function({ color, size, focused }) => ReactNode.labelKeyis a translation key;labelis plain text or{ en: 'Rewards', mn: '…' }.- Place it with
navigation.store.tabsornavigation.network.tabsin the brand file; otherwise it goes atposition, or just before Cart.initial: trueopens the app on it.
Copy
brand/translations/<locale>.json is merged over the app's translations: change any string, or add keys for your own screens (for example under a Brand namespace). A file for a language the app doesn't ship adds it; offer it with AVAILABLE_LOCALES.
What your screens can use
Client screens may rely on:
src/extensions: types,defineStorefrontExtensions, screen ids and their paramssrc/ui: the app's components, tokens and display helperssrc/branding:defineBranding,useBranding- the hooks
use-cart,use-cart-summary,use-storefront,use-current-location,use-food-trucks,use-store-locations,use-customer-coordinates, anduseStorefrontRuntime,useLanguage,useAuth
Breaking changes to these, to screen ids and params, and to branding options are listed in the release notes. Anything else in src/ may change between releases. Replacements are typed (StorefrontScreen<'store.home'>), so a type check after a merge points at what needs updating.
When something fails
- A replacement that fails to load, has no default export or throws while rendering is swapped for the app's own screen, and the error is logged.
- If that fails too, a "Something went wrong" view is shown.
- In development, unknown screen ids and malformed entries stop the app at start so you notice; in release builds they are skipped with a warning.
Screen ids
| Screen id | What it is | Key params |
|---|---|---|
store.home | The store page (a single store's home; a member store in a Network) | storeId?, categoryId? |
store.catalog | Every category of the store | none |
store.search | Search in the store | none |
store.map | The store's locations on a map | none |
store.info | The store's hours, locations and contact | store, storeLocation? |
catalog.category | Jumps to a category section of the store page | categoryId? |
foodTrucks.home | The trucks screen (map and list) | none |
foodTrucks.search | Search across trucks and stores | none |
foodTrucks.menu | A truck's menu | foodTruckId, truck?, categoryId? |
product.detail | A product, with options, booking and add to cart | productId, storeId?, product? |
cart, cart.item | The cart and one item in it | none, cartItem? |
checkout | Checkout for the build's payment gateway | none |
order.detail | An order, with tracking, chat and receipt | orderId |
order.receipt | An order's receipt | order |
order.history | Active and past orders | none |
order.chat | Chat with the store or the driver about an order | orderId |
account.profile | The account home | none |
account.details | Edit profile (photo, name, email, phone) | none |
auth.login, auth.createAccount | Sign in and create an account | redirectTo? |
network.home | A Network's home (variant shop: the shop home) | none |
network.directory | Member stores by category or tag | categoryId?, tags? |
network.search, network.map | Search across stores; every store on a map | none |
network.store | A member store | storeId |
network.product | A product of a member store | productId, storeId? |
network.shopCategories | Every category of the main store, as a grid | storeId? |
network.shopCategory | One category of the main store, with subcategories and offers | categoryId, storeId? |
reviews.list, reviews.write | A store's reviews; writing one | storeId |
offers.list, offers.detail | Every live offer; one offer | none, offerId |
notifications.inbox, notifications.settings | The notifications inbox and its settings | none |
The boot, location permission, payment gateway, phone verification and account deletion screens can't be replaced.