Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
114 commits
Select commit Hold shift + click to select a range
61271cb
chore: bootstrapped demo Expo app
artus9033 Jan 15, 2026
0282537
chore: use expo config plugin in expo demo app
artus9033 Jan 15, 2026
a9dbb08
chore: reset Expo project
artus9033 Jan 16, 2026
9c92eaa
chore: use workspace specifier for monorepo deps
artus9033 Jan 16, 2026
4a5702a
fix: do not toss up source sets in Gradle plugin
artus9033 Jan 16, 2026
4893be4
feat: base implementation for brownfield Expo config plugin
artus9033 Jan 22, 2026
a65a9f1
chore: update lock files
artus9033 Jan 22, 2026
6aba560
chore: update app.json in ExpoApp
artus9033 Jan 22, 2026
f30f055
chore: enable debug logging in app.json in ExpoApp
artus9033 Jan 22, 2026
594aaf5
feat: use new logging system in the plugin
artus9033 Jan 22, 2026
9e0ba69
feat: templates engine for project modifications
artus9033 Jan 22, 2026
a5840e9
feat: patch for Expo SDK pre v55, fix source file paths
artus9033 Jan 23, 2026
b544e47
feat: implemented Android brownfield plugin
artus9033 Jan 23, 2026
af69bca
chore: update ExpoApp scripts
artus9033 Jan 23, 2026
2afa209
fix: dedent gradle script insertion
artus9033 Jan 23, 2026
64eb48d
fix: depend on Material in AndroidApp; fix typo in gradle scripting c…
artus9033 Jan 23, 2026
23e4c8e
fix: package name in ExpoApp
artus9033 Jan 23, 2026
95b580c
chore: use debug signing config for AndroidApp release variant - for …
artus9033 Jan 23, 2026
a066b02
chore: missing indent in gradleHelpers insertion
artus9033 Jan 23, 2026
0f34957
fix: add missing expo modifications to template build.gradle.kts
artus9033 Jan 23, 2026
0a9c3ca
chore: use the Expo classes in template ReactNativeHostManager.kt
artus9033 Jan 23, 2026
080a124
fix: iOS xcode project processing of target UUID
artus9033 Jan 23, 2026
834d850
fix: make Android Expo config explicitly specify versions of RN Andro…
artus9033 Jan 23, 2026
a4c0913
fix: running package:ios again
thymikee Jan 23, 2026
ccf8f48
chore: udpate rock to 0.12.8
thymikee Jan 23, 2026
4bf29e7
chore: make nodemon watch template files in dev script for brownfield…
artus9033 Jan 23, 2026
ac7abeb
chore: reformat files, extract capitalized string helper, suppress un…
artus9033 Jan 30, 2026
d9050d5
chore: deintegrate isExpo gradle plugin prop in favor of auto-detection
artus9033 Jan 30, 2026
c2648c7
chore: typo in Android native demo app theme name
artus9033 Jan 30, 2026
4a59b38
feat: config Maven publishing for part of the core expo modules
artus9033 Jan 31, 2026
74b0d80
fix: correct artifact, group and version discovery for manually white…
artus9033 Jan 31, 2026
7d9f2f7
feat: inject transitive dependencies into POM publication files
artus9033 Feb 2, 2026
619eb2b
feat: instead of publishing, inject proper POM and Gradle Module JSON…
artus9033 Feb 2, 2026
1d36921
feat: functional Expo demo in AndroidApp
artus9033 Feb 3, 2026
310a7b2
chore: re-enable signing of gradle plugin
artus9033 Feb 3, 2026
1867458
chore: use 0.0.1-snapshot by default in Expo plugin on Android
artus9033 Feb 3, 2026
09c0dce
fix: filter out remaining expo transitive dependencies, add version m…
artus9033 Feb 11, 2026
2ce1d5b
feat: seamless integration of Expo features into the brownfield CLI
artus9033 Feb 12, 2026
1985835
refactor: restructure RNApp sources
artus9033 Feb 12, 2026
a64e2fe
feat: reuse RNApp sources in ExpoApp
artus9033 Feb 12, 2026
adf3ea3
feat: set up flavors for AndroidApp to consume expo or vanilla artifacts
artus9033 Feb 12, 2026
957b5ff
ci: update CI to account for AndroidApp flavors
artus9033 Feb 12, 2026
9390219
chore: remove obsolete comment
artus9033 Feb 12, 2026
7bbc2d1
fix: monorepo-prone expo detection in CLI
artus9033 Feb 12, 2026
e214f81
feat: proper code sharing for RN demo apps
artus9033 Feb 12, 2026
c52ce57
fix: make ReactNativeHostManager singatures match across flavors, pro…
artus9033 Feb 12, 2026
4456f06
fix: restore .brownie.ts files in each project for codegen to work pr…
artus9033 Feb 12, 2026
f0adf90
feat: added an image view to the demo app
artus9033 Feb 12, 2026
341f535
chore: strip obsolete comment
artus9033 Feb 12, 2026
580b19c
fix: proper module name in ios script
artus9033 Feb 12, 2026
5a21b7c
fix: brownie not to crash on Android
artus9033 Feb 12, 2026
afad19d
fix: handle brownie no stores in project in the CLI
artus9033 Feb 12, 2026
b6ba4b7
feat: demo apps reorganized, pulled in clean template for expo app
artus9033 Feb 12, 2026
69b6497
feat: integrate brownie store with Expo demo app
artus9033 Feb 12, 2026
470be26
docs: update docs
artus9033 Feb 12, 2026
a4cd611
Merge branch 'main' into feat/expo-config-plugin
artus9033 Feb 12, 2026
7c3b790
feat: temporarily deintegrate brownie from expo demo before android i…
artus9033 Feb 12, 2026
48fd6e1
Merge branch 'main' into feat/expo-config-plugin
artus9033 Feb 12, 2026
2b1680c
chore: rename expo app project
artus9033 Feb 12, 2026
6058678
chore: re-enable signing of the gradle plugin
artus9033 Feb 12, 2026
77726ee
chore: add changesets
artus9033 Feb 12, 2026
6f7cb58
refactor: format files
artus9033 Feb 12, 2026
9a274d1
fix(ci): path to RN project in androidapp-road-test
artus9033 Feb 12, 2026
e08a74c
refactor: format files to comply with detekt
artus9033 Feb 12, 2026
0f6d0b6
chore: revert changes to brownie
artus9033 Feb 12, 2026
27fb93c
fix: paths in packageIos in RN projects
artus9033 Feb 12, 2026
d867bb3
ci: update workflow for Apple apps
artus9033 Feb 12, 2026
24db114
feat: demo iOS app supporting interchanging brownfield artifacts
artus9033 Feb 12, 2026
0989c60
fix: patchExpoPre55.sh
artus9033 Feb 12, 2026
f4a44b8
fix: reorder build phases in iOS to first run patching after expo con…
artus9033 Feb 13, 2026
54fa5fc
wip: reorder build script phases PoC for Expo config plugin
artus9033 Feb 13, 2026
6d86267
chore: reorder code properly
artus9033 Feb 13, 2026
4f20e9d
chore: use SNAPSHOT for CI and local
hurali97 Feb 16, 2026
9613fda
chore: add plugin publish and patch scripts for local maven
hurali97 Feb 16, 2026
a633cc6
chore: fix appleapp-road-test ci
hurali97 Feb 16, 2026
6d322d1
chore: use macos runner for appleapp CI
hurali97 Feb 16, 2026
515c3fc
fix(ci): update maven path for expo flavor
hurali97 Feb 16, 2026
36771bd
fix(ci): expo android
hurali97 Feb 16, 2026
ffbb9d6
feat: ensure build phase correct order using post_integrate
hurali97 Feb 16, 2026
9bc4954
feat: add ReactNativeHostManager to source files
hurali97 Feb 16, 2026
a5bae9c
fix: use the bundleURL in loadView
hurali97 Feb 16, 2026
70f910f
feat: present Expo RN UI in Apple App
hurali97 Feb 16, 2026
b7f3049
Merge branch 'main' of github.com:callstack/react-native-brownfield i…
hurali97 Feb 17, 2026
52e18f2
chore: use latest version for brownfield-gradle-plugin
hurali97 Feb 17, 2026
5504dd1
feat: add source files to PBXSourcesBuildPhase
hurali97 Feb 17, 2026
e921555
feat: allow entry points other than main
hurali97 Feb 17, 2026
8a9e58f
feat: add configuration for vanilla and expo to AppleApp
hurali97 Feb 17, 2026
45bbe35
fix: add info.plist to AppleApp
hurali97 Feb 17, 2026
881ee41
fix: bundle reference
hurali97 Feb 17, 2026
0588c3c
fix(ci): add guard to only run pods for vanilla
hurali97 Feb 17, 2026
250d8a0
docs: update ExpoApp usages
hurali97 Feb 18, 2026
fee1c47
feat: remove coil dependency from plugin to the lib
hurali97 Feb 18, 2026
f155923
refactor: remove TODO comments
hurali97 Feb 18, 2026
ec05bba
feat: guard script reordering against expo version
hurali97 Feb 18, 2026
1f6bbcb
feat: separate brownie usage by platform
hurali97 Feb 18, 2026
f9bf078
chore: build android app with release variant
hurali97 Feb 18, 2026
1734e3e
docs: add Expo Integration section
hurali97 Feb 18, 2026
1d50f6b
chore: bump versions
hurali97 Feb 18, 2026
6516315
fix: remove color prop
hurali97 Feb 18, 2026
0abb3f7
Update docs/docs/docs/getting-started/expo.mdx
hurali97 Feb 19, 2026
93cc1a3
Update docs/docs/docs/getting-started/expo.mdx
hurali97 Feb 19, 2026
8602909
docs: remove images
hurali97 Feb 19, 2026
0ee3099
refactor: fix indentation
hurali97 Feb 19, 2026
9cb4284
feat: unify expo and bare RN in ReactNativeBrownfield
hurali97 Feb 19, 2026
4c5c3e7
Merge branch 'main' of github.com:callstack/react-native-brownfield i…
hurali97 Feb 20, 2026
96badd9
chore: remove changeset bump
hurali97 Feb 20, 2026
dde6558
refactor: remove unused property and enforce failure
hurali97 Feb 20, 2026
71ee9e6
refactor: split classes by concerns
hurali97 Feb 20, 2026
4347a54
refactor: move duplicate code to a shareable resource
hurali97 Feb 20, 2026
903658c
fix: make the class internal
hurali97 Feb 20, 2026
d4420f4
chore: podfile changes
hurali97 Feb 20, 2026
594e60a
feat: add JSBundle load for Expo and make it reusable
hurali97 Feb 20, 2026
86c077e
fix: fail early for non-extension bundle path
hurali97 Feb 20, 2026
0d55c6d
docs: update
hurali97 Feb 20, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 5 additions & 8 deletions apps/AppleApp/Brownfield Apple App/BrownfieldAppleApp.swift
Original file line number Diff line number Diff line change
Expand Up @@ -10,11 +10,7 @@ class AppDelegate: NSObject, UIApplicationDelegate {
_ application: UIApplication,
didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]? = nil
) -> Bool {
#if USE_EXPO_HOST
return ReactNativeHostManager.shared.application(application, didFinishLaunchingWithOptions: launchOptions)
#else
return true
#endif
return ReactNativeBrownfield.shared.application(application, didFinishLaunchingWithOptions: launchOptions)
}
}

Expand All @@ -23,14 +19,15 @@ struct BrownfieldAppleApp: App {
@UIApplicationDelegateAdaptor(AppDelegate.self) var appDelegate

init() {
#if USE_EXPO_HOST
ReactNativeHostManager.shared.initialize()
#else
ReactNativeBrownfield.shared.bundle = ReactNativeBundle
ReactNativeBrownfield.shared.startReactNative {
print("React Native has been loaded")
}

#if USE_EXPO_HOST
ReactNativeBrownfield.shared.ensureExpoModulesProvider()
#endif

BrownfieldStore.register(initialState)
}

Expand Down
26 changes: 2 additions & 24 deletions apps/AppleApp/Brownfield Apple App/ContentView.swift
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
import ReactBrownfield
import BrownfieldLib
import Brownie
import SwiftUI

Expand All @@ -17,33 +16,12 @@ struct ContentView: View {
}
}

#if USE_EXPO_HOST
struct RNViewRepresentable: UIViewRepresentable {

func makeUIView(context: Context) -> UIView {
return ReactNativeHostManager.shared.loadView(moduleName: "RNApp", initialProps: nil, launchOptions: nil)
}

func updateUIView(_ uiView: UIView, context: Context) {
// Update the view when SwiftUI state changes
}
}
#endif

struct MainScreen: View {
var rnView: some View {
#if USE_EXPO_HOST
RNViewRepresentable()
#else
ReactNativeView(moduleName: "RNApp")
#endif
}

struct MainScreen: View {
var body: some View {
VStack(spacing: 16) {
GreetingCard(name: "iOS")

rnView
ReactNativeView(moduleName: "RNApp")
.navigationBarHidden(true)
.clipShape(RoundedRectangle(cornerRadius: 16))
.background(Color(UIColor.systemBackground))
Expand Down
4 changes: 2 additions & 2 deletions apps/RNApp/ios/Podfile.lock
Original file line number Diff line number Diff line change
Expand Up @@ -2350,7 +2350,7 @@ PODS:
- SocketRocket
- ReactAppDependencyProvider (0.82.1):
- ReactCodegen
- ReactBrownfield (2.2.0):
- ReactBrownfield (3.0.0-rc.1):
- boost
- DoubleConversion
- fast_float
Expand Down Expand Up @@ -2843,7 +2843,7 @@ SPEC CHECKSUMS:
React-utils: f06ff240e06e2bd4b34e48f1b34cac00866e8979
React-webperformancenativemodule: b3398f8175fa96d992c071b1fa59bd6f9646b840
ReactAppDependencyProvider: a45ef34bb22dc1c9b2ac1f74167d9a28af961176
ReactBrownfield: 65d6bf15ccb7501d3982ec05f8d37b697e9b3480
ReactBrownfield: ce231a9060b34e1fe8f91ec8416f21dc6da8b4b5
ReactCodegen: 0bce2d209e2e802589f4c5ff76d21618200e74cb
ReactCommon: 801eff8cb9c940c04d3a89ce399c343ee3eff654
RNScreens: d6413aeb1878cdafd3c721e2c5218faf5d5d3b13
Expand Down
20 changes: 12 additions & 8 deletions docs/docs/docs/getting-started/expo.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -111,15 +111,19 @@ This should only take a few minutes.
- Follow the step for adding the frameworks to your iOS App - [here](/docs/getting-started/ios#6-add-the-framework-to-your-ios-app)
<hr/>

1. Call the `ReactNativeHostManager.shared.initialize()` from your Application Entry point:
1. Call the following functions from your Application Entry point:

```swift
@main
struct IosApp: App {
@UIApplicationDelegateAdaptor(AppDelegate.self) var appDelegate

init() {
ReactNativeHostManager.shared.initialize()
ReactNativeBrownfield.shared.bundle = ReactNativeBundle
ReactNativeBrownfield.shared.startReactNative {
print("React Native has been loaded")
}
ReactNativeBrownfield.shared.ensureExpoModulesProvider()
}

var body: some Scene {
Expand All @@ -140,19 +144,19 @@ class AppDelegate: NSObject, UIApplicationDelegate {
_ application: UIApplication,
didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]? = nil
) -> Bool {
return ReactNativeHostManager.shared.application(application, didFinishLaunchingWithOptions: launchOptions)
return ReactNativeBrownfield.shared.application(application, didFinishLaunchingWithOptions: launchOptions)
}
}
```

3. Use the `ReactNativeHostManager.loadView` to present the UI
3. Use the `ReactNativeView` or `ReactNativeViewController` to present the UI

```
let RNView = ReactNativeHostManager.shared.loadView(moduleName: "ExpoRNApp", initialProps: nil, launchOptions: nil)
```swift
ReactNativeView(moduleName: "ExpoRNApp")
.background(Color(UIColor.systemBackground))
```

> This returns a UIView, so if you're using SwiftUI, you will have to wrap this in UIViewRrepresentable .
You can see the demo integration in [Apple App](https://github.com/callstack/react-native-brownfield/blob/main/apps/AppleApp/Brownfield%20Apple%20App/BrownfieldAppleApp.swift) as well
> You can also use `ReactNativeBrownfield.shared.view(moduleName, initialProps)` which returns a UIView.

4. Build and install the iOS application 🚀

Expand Down
4 changes: 4 additions & 0 deletions packages/react-native-brownfield/ReactBrownfield.podspec
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,10 @@ Pod::Spec.new do |spec|

spec.dependency 'ReactAppDependencyProvider'
add_dependency(spec, "React-RCTAppDelegate")

if ENV['REACT_NATIVE_BROWNFIELD_USE_EXPO_HOST'] == '1'
spec.dependency 'Expo'
end

install_modules_dependencies(spec)
end
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import Foundation

enum BrownfieldBundlePathResolver {
enum Error: Swift.Error {
case invalidBundlePath(String)
}

static func resourceComponents(from bundlePath: String) throws -> (
resourceName: String,
fileExtension: String
) {
let fileExtension = (bundlePath as NSString).pathExtension
let resourceName = (bundlePath as NSString).deletingPathExtension

guard !fileExtension.isEmpty, !resourceName.isEmpty else {
throw Error.invalidBundlePath(bundlePath)
}

return (resourceName, fileExtension)
}
}
140 changes: 140 additions & 0 deletions packages/react-native-brownfield/ios/ExpoHostRuntime.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,140 @@
import UIKit
internal import React
internal import React_RCTAppDelegate
internal import ReactAppDependencyProvider

#if canImport(Expo)
internal import Expo

final class ExpoHostRuntime {
static let shared = ExpoHostRuntime()

private let jsBundleLoadObserver = JSBundleLoadObserver()
private var delegate = ExpoHostRuntimeDelegate()
private var reactNativeFactory: RCTReactNativeFactory?
private var expoDelegate: ExpoAppDelegate?

/**
* Starts React Native with default parameters.
*/
public func startReactNative() {
startReactNative(onBundleLoaded: nil)
}
/**
* Starts React Native with optional callback when bundle is loaded.
*
* @param onBundleLoaded Optional callback invoked after JS bundle is fully loaded.
*/
public func startReactNative(onBundleLoaded: (() -> Void)?) {
guard reactNativeFactory == nil else { return }

let factory = ExpoReactNativeFactory(delegate: delegate)
delegate.dependencyProvider = RCTAppDependencyProvider()

reactNativeFactory = factory

let appDelegate = ExpoAppDelegate()
appDelegate.bindReactNativeFactory(factory)
expoDelegate = appDelegate

if let onBundleLoaded {
jsBundleLoadObserver.observeOnce(onBundleLoaded: onBundleLoaded)
}
}

/**
* Path to JavaScript root.
* Default value: ".expo/.virtual-metro-entry"
*/
public var entryFile: String = ".expo/.virtual-metro-entry" {
didSet {
delegate.entryFile = entryFile
}
}

/**
* Path to JavaScript bundle file.
* Default value: "main.jsbundle"
*/
public var bundlePath: String = "main.jsbundle" {
didSet {
delegate.bundlePath = bundlePath
}
}
/**
* Bundle instance to lookup the JavaScript bundle.
* Default value: Bundle.main
*/
public var bundle: Bundle = Bundle.main {
didSet {
delegate.bundle = bundle
}
}
/**
* Dynamic bundle URL provider called on every bundle load.
* When set, this overrides the default bundleURL() behavior in the delegate.
* Returns a URL to load a custom bundle, or nil to use default behavior.
* Default value: nil
*/
public var bundleURLOverride: (() -> URL?)? = nil {
didSet {
delegate.bundleURLOverride = bundleURLOverride
}
}

func application(
_ application: UIApplication,
didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]? = nil
) -> Bool {
return expoDelegate?.application(
application,
didFinishLaunchingWithOptions: launchOptions
) != nil
}

func view(
moduleName: String,
initialProps: [AnyHashable: Any]?,
launchOptions: [AnyHashable: Any]?
) -> UIView? {
let bundleURL = delegate.bundleURL()

return expoDelegate?.recreateRootView(
withBundleURL: bundleURL,
moduleName: moduleName,
initialProps: initialProps,
launchOptions: launchOptions
)
}
}

class ExpoHostRuntimeDelegate: ExpoReactNativeFactoryDelegate {
var entryFile = ".expo/.virtual-metro-entry"
var bundlePath = "main.jsbundle"
var bundle = Bundle.main
var bundleURLOverride: (() -> URL?)? = nil

override func sourceURL(for bridge: RCTBridge) -> URL? {
// needed to return the correct URL for expo-dev-client.
bridge.bundleURL ?? bundleURL()
}

override func bundleURL() -> URL? {
if let bundleURLProvider = bundleURLOverride { return bundleURLProvider() }
#if DEBUG
return RCTBundleURLProvider.sharedSettings().jsBundleURL(
forBundleRoot: entryFile)
#else
do {
let (resourceName, fileExtension) = try BrownfieldBundlePathResolver.resourceComponents(
from: bundlePath
)
return bundle.url(forResource: resourceName, withExtension: fileExtension)
} catch {
assertionFailure("Invalid bundlePath '\(bundlePath)': \(error)")
return nil
}
#endif
}
}
#endif
38 changes: 38 additions & 0 deletions packages/react-native-brownfield/ios/JSBundleLoadObserver.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
import Foundation
internal import React

final class JSBundleLoadObserver {
private var onBundleLoaded: (() -> Void)?
private var observerToken: NSObjectProtocol?

func observeOnce(onBundleLoaded: @escaping () -> Void) {
removeObserverIfNeeded()
self.onBundleLoaded = onBundleLoaded

observerToken = NotificationCenter.default.addObserver(
forName: NSNotification.Name("RCTInstanceDidLoadBundle"),
object: nil,
queue: nil
) { [weak self] _ in
self?.notifyAndClear()
}
}

deinit {
removeObserverIfNeeded()
}

private func notifyAndClear() {
let callback = onBundleLoaded
onBundleLoaded = nil
removeObserverIfNeeded()
callback?()
}

private func removeObserverIfNeeded() {
if let observerToken {
NotificationCenter.default.removeObserver(observerToken)
self.observerToken = nil
}
}
}
12 changes: 12 additions & 0 deletions packages/react-native-brownfield/ios/Notification+Brownfield.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import Foundation

extension Notification.Name {
/**
* Notification sent when React Native wants to navigate back to native screen.
*/
public static let popToNative = Notification.Name("PopToNativeNotification")
/**
* Notification sent to enable/disable the pop gesture recognizer.
*/
public static let togglePopGestureRecognizer = Notification.Name("TogglePopGestureRecognizerNotification")
}
Loading
Loading