# 0.35.0 (/docs/changelog/0-35-0)



<Callout type="warn" title="Native changes: every app needs a store release">
  * `@gauntlet/brewery` now declares two native peers: the new
    `@app-brew/tab-bar` package and `react-native-haptic-feedback`, plus the
    JS-only `@react-navigation/bottom-tabs` at 7.4.7. Add all three to the
    app's `package.json` (`pnpm exec appbrew-sync-deps` does this), run
    `pod install`, and ship a [store release](/docs/ship/app-releases).
  * `@app-brew/tab-bar` is iOS only and refuses to compile below Xcode 26
    (the iOS 26 SDK), so iOS builds now need Xcode 26. Android does not link
    it.
  * `@app-brew/webview` gains a `contentInsetBottom` prop on iOS and Android,
    so the page clears a bar floating over it.
  * `@app-brew/quick-actions` changes on Android: adding a shortcut reports
    whether its remote icon failed to load.
</Callout>

## Breaking [#breaking]

**The bottom bar runs on React Navigation's bottom-tabs navigator.** Tab
screens are now hosted by `@react-navigation/bottom-tabs` instead of the
child navigator. Tabs mount when first opened and freeze while in the
background. The bar itself looks and behaves as before unless the new
options below are set. If a tab misbehaves, set
`navigation.bottomBar.engine` to `'legacy-child-nav'` to go back to the old
host while you report it; that switch will be removed in a later release.

**`ChildNav` is no longer exported from `@gauntlet/brewery`.** It was the
tab host's navigator. A shell that forks brewery's should create its own
with `createChildNavigator()` from `@gauntlet/child-navigator`.

**Phone inputs default to the `first` country-code layout.** With no
`countryCodelayout` set, the flag and calling code now sit together in the
country picker button, and the separate calling-code text is hidden. Before,
the default was `second`. Set `countryCodelayout: 'second'` in the input's
config options to keep the old look. The separate-borders layout is
unaffected.

**`view_item` is sent from navigation state, not from `pushRoute`.**
`pushRoute` and `replaceRoute` no longer send it; brewery's `Shell` sends it
when the navigator settles on a product screen (see Changed). An app that
renders its own `NavigationContainer` instead of `Shell` no longer gets
`view_item` and must send it from its own `onStateChange`.

**Subscription blocks without an `appId` show every selling plan.** The
subscription selector, its dropdown, and the selling-plan price, discount
and compare-at-price components used to render nothing when `options.appId`
was missing. They now accept the plans of every selling plan group on the
product. Set `appId` to keep them scoped to one subscription app.

**`backgroundOptionsSchema` in `@gauntlet/types` is a refined schema.** It
gains a `liquid` kind and now rejects a liquid background without
`liquid.fallback`. As a refined zod schema it no longer has `.extend()` or
`.shape`.

## New [#new]

**Bottom bar options.** `navigation.bottomBar` gains a `behavior` object:
`animation` (`none`, `fade`, `shift`, `scale`, `spring`; default `fade`),
`hideOnKeyboard`, `hideOnScroll`, `hapticFeedback`, `preloadAdjacentScreens`,
`lazy`, `freezeOnBlur` and `rePressReload`. The top-level
`preloadAdjacentScreens` and `hideOnScroll` still work and are deprecated.
Tabs gain a `badge` (cart count, wishlist count, a static value, a store
path, or none, shown as a count or a dot) that replaces `badgeLocation`, a
`themeKey` for per-tab styles in `theme.bottomBar.tabOverrides`, and a
screen can hide the bar with `bottomBar.hide`.

**Liquid Glass bar on iOS 26.** Set `theme.bottomBar.liquid.enabled` and the
bar becomes UIKit's own tab bar on devices running iOS 26 with a binary that
links `@app-brew/tab-bar`. Every tab needs a `nativeIcon` (an SF Symbol or an
image URL) or the search `role`, and the bar holds at most five tabs;
otherwise, and on Android and older iOS, the docked bar renders as before. A
screen's footer can float over the list on glass with
`footer.liquid` (`glass`, `pill`, `accessory` or `float`), and a `Box`
background can be `liquid` with a solid `fallback` colour for every other
device.

**`@gauntlet/bss-b2b` — B2B wholesale prices, published for the first
time.** Signed-in customers see their wholesale prices and volume tiers in
the shared price components, and check out at those prices through a draft
order. Register `BssB2bProvider` as the app's `pricing` module; the
`bss-b2b-volume-tiers` and `bss-b2b-checkout` blocks complete it.

**Remote components fail on their own.** Every component an instant-code
bundle registers renders inside its own error boundary. A crash falls back
to the component it replaced, or to nothing, instead of replacing the whole
app with the error screen, and it is reported as `InstantCodeError` with
the component's kind and key. Load failures and rejected manifests are
reported too; network drops are not.

**Purchase options block.** `purchase-options` lets a customer choose
between one-time purchase and subscription on the product page, and opens
the subscription frequency sheet.

**Apply-coupon modal.** The `apply-coupon-modal` block lists the cart's
coupons as ticket cards under a sheet header. Applying one shows a toast and
goes back. `view-coupons`
can open it, or any other link, through `options.applyCouponLink`.

**Flits referral links open in the app.** Mount `FlitsReferralPrompt` next
to `Shell` and a Flits referral link opens a prompt that accepts the invite,
sending signed-out customers to sign in first. The
`flits-share-referral-link` block shares the referral link instead of the
code.

**Server-side GA4 purchase for Firebase.** With the Firebase integration's
`serverPurchase` setting on and Measurement API secrets set, the GA4
`purchase` event is sent from the order webhook rather than the app, so
orders whose buyer leaves for a payment app are counted. The app then skips
its own `purchase` and stamps Firebase identifiers on the cart.

**Smaller additions.**

* `image-list`: `options.heading.numberOfLines` truncates item titles.
* `payment-details`: `source.totalInfoText` adds a note under the total,
  such as "Inclusive of all taxes".
* `add-to-cart`: `style.loader` colours or hides the loading shimmer.
* WebEngage (tracker v2): `uniqueId` picks `customerId` (the default),
  `email` or `phone` as the user identity.
* Stamped: `settings.global.sendVariantSkuToStamped` sends the default
  variant's SKU with review requests, so Stamped product groups apply. Off
  by default.
* `settings.global.preserveGoogleUtmSource` keeps `utm_source=google` when
  the link also carries a `gclid`, which otherwise rewrites it to
  `google_ads`. Off by default.

## Changed [#changed]

**`view_item` is sent for every way of reaching a product.** Before, it was
sent only when in-app navigation pushed or replaced a product screen.
Products opened from a deep link, a push notification, a Meta or other
campaign link, or a link inside a web page sent nothing. Now each new
product screen sends it once, and opening a different product in a running
app sends it again. Going back to a product, or switching tabs and
returning, does not. Expect higher `view_item` counts in every destination
that receives it, such as GA4, Meta, AppsFlyer, Branch and Klaviyo, in
proportion to how much traffic arrives by link.

**Link UTM parameters set the event source.** A Firebase push notification
link, and an in-app link whose URL carries `utm_*` parameters, now record
those parameters as the event source.

**WebEngage (tracker v2) follows event renames.** Login, logout and purchase
handling now works when those events are renamed through `eventsMapper`, and
screen views go through the mapper too, so a renamed `page_view` is sent
under its new name.

**Quick actions register once.** Home-screen shortcuts are registered when
their config changes rather than on every launch, so a kill during launch no
longer leaves the menu empty.

## Fixed [#fixed]

* **Deep links and an unparseable screen path.** A screen whose id or
  `deepLinkPath` React Navigation cannot parse, such as one with a colon
  inside a segment, made every deep link in the app fail: the app opened on
  its first screen and the link was ignored. That screen's path is now left
  out of the linking config, so every other link works, and a link to that
  screen opens the store page in a web view.
* **Meta ads open at the destination.** A Meta ad that opened the app
  through App Links carried its target inside `al_applink_data`, and the app
  tried to read a product path from that escaped blob, landing on a page
  that did not exist. The app now opens the ad's target URL, also for
  deferred links on first launch after install.
* **Deep links to fallback tab screens** resolve inside the tabs, and query
  values such as `%2B` in a deep link are no longer decoded twice.
* **GoKwik KwikPass sign-in** returns to the redirect screen by popping the
  sign-in and OTP screens, and closes the sheet hosting it, so back no longer
  lands on the OTP step.
* **Cold-start quick actions** wait for the splash and app init before
  opening their screen, so launch no longer swallows the tap.
* **Keyboard.** The screen list no longer lifts when a field inside a web
  page has focus.
* **Address form.** After adding an address, the form goes back to the
  screen that opened it instead of pushing a second address list.
* **Move to cart** passes all of its config options to the add-to-cart
  button, not just the title and type.
* **Flits** sends referral-code acceptance as form data, which the API
  expects.

## Performance [#performance]

Product cards render less. The card, its leaf components and the PDP product
info are memoised with props scoped to each field, and `useModal` selects
with shallow equality, so its consumers no longer re-render on every store
write.

## Tooling [#tooling]

**`@app-brew/build-tools` shares app dependencies with remotes.** The host
now shares every dependency in the app's `package.json` with instant-code
remotes, not just the core set, so a remote reuses the host's copy instead
of bundling its own, and stateful libraries such as toasts run as one
instance. A remote still bundles a fallback for each, so a remote built now
runs on binaries built before, and remotes built before keep working on new
binaries. Remote images now load by URL from the remote's own
`remote-assets/` folder, so images that only a remote ships render in
release builds.

**`env-watch-loader.cjs` ships in the package.** Apps that install
build-tools from the registry no longer fail with `Cannot find module
'./env'`.

Upgrading? The workflow is at [Upgrading @gauntlet packages](/docs/ship/upgrading).
