AppbrewDevelopers
Changelog

0.35.0

A new bottom bar engine with the iOS 26 Liquid Glass bar, view_item sent for every way of reaching a product, deep links that survive an unparseable screen path, Meta ads that open at their destination, B2B wholesale prices, and remote components that fail on their own.

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.
  • @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.

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

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

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

  • 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

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

@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.

On this page