Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
79d9166
feat: add BrownfieldNavigation TurboModule and other classes
hurali97 Feb 14, 2026
531c9fd
feat: consume brownfield navigation
hurali97 Feb 14, 2026
03db726
feat: expose brownfield navigation as separate package
hurali97 Feb 22, 2026
3efc755
chore: rename to brownfield-navigation
hurali97 Feb 24, 2026
b6deb6f
feat: add BrownfieldNavigation framework
hurali97 Feb 24, 2026
4bcb4fc
feat: add babel transpiler
hurali97 Feb 24, 2026
0a31c7a
feat: add android template
hurali97 Feb 25, 2026
32ba266
feat: add android codegen support
hurali97 Feb 25, 2026
7b0be42
feat(android): implement BrownfieldNavigationDelegate
hurali97 Feb 25, 2026
ded8658
docs: add brownfield-navigation docs
hurali97 Feb 25, 2026
575d9b1
feat: move navigation codegen to cli
hurali97 Feb 26, 2026
8e2db27
feat: use ts-morph and quicktype
hurali97 Feb 26, 2026
d4e8c70
fix: changes
hurali97 Feb 27, 2026
91b2a98
fix(ci): add build packages script
hurali97 Feb 27, 2026
2e5d58a
chore: align major version
hurali97 Feb 27, 2026
d61f4d6
chore: run changeset
hurali97 Feb 27, 2026
8592f7d
refactor: remove comments
hurali97 Feb 27, 2026
12b33ca
feat: add brownfield-navigation to expo app
hurali97 Feb 27, 2026
3f330e0
chore: run changeset
hurali97 Feb 27, 2026
4bf34f9
fix: add guard for navigation
hurali97 Feb 27, 2026
737a8b3
docs: add note
hurali97 Feb 27, 2026
53d1235
feat: throw error if delegate is nil
hurali97 Feb 27, 2026
ccdb606
fix: align with obj-c convention
hurali97 Feb 27, 2026
9ad26e6
test: add test for navigation codegen
hurali97 Feb 27, 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
9 changes: 9 additions & 0 deletions .changeset/yellow-poems-go.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
---
'@callstack/brownfield-navigation': minor
'@callstack/brownfield-cli': minor
Comment on lines +2 to +3
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should we go with a major version bump? To maintain parity with react-native-brownfield, I have set the version to 3.0.0 for brownfield-navigation and doing a minor bump here.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Always sync

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Bumped all minors to align.

'brownfield': minor
'@callstack/brownie': minor
'@callstack/react-native-brownfield': minor
---

add brownfield navigation
4 changes: 4 additions & 0 deletions .github/actions/setup/action.yml
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,10 @@ runs:
run: yarn install
shell: bash

- name: Build packages
run: yarn build
shell: bash

- name: Restore Turbo cache
if: inputs.restore-turbo-cache == 'true'
uses: actions/cache@9255dc7a253b0ccc959486e2bca901246202afeb # v5
Expand Down
8 changes: 8 additions & 0 deletions apps/AndroidApp/app/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,14 @@
android:supportsRtl="true"
android:theme="@style/Theme.AndroidBrownfieldApp"
android:usesCleartextTraffic="true">
<activity
android:name=".ReferralsActivity"
android:exported="false"
android:theme="@style/Theme.AndroidBrownfieldApp" />
<activity
android:name=".SettingsActivity"
android:exported="false"
android:theme="@style/Theme.AndroidBrownfieldApp" />
<activity
android:name=".MainActivity"
android:exported="true"
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package com.callstack.brownfield.android.example

import android.content.Intent
import android.content.res.Configuration
import android.os.Bundle
import android.widget.Toast
Expand All @@ -26,10 +27,12 @@ import androidx.fragment.compose.AndroidFragment
import com.callstack.brownfield.android.example.components.GreetingCard
import com.callstack.brownfield.android.example.components.PostMessageCard
import com.callstack.brownfield.android.example.ui.theme.AndroidBrownfieldAppTheme
import com.callstack.nativebrownfieldnavigation.BrownfieldNavigationDelegate
import com.callstack.nativebrownfieldnavigation.BrownfieldNavigationManager
import com.callstack.reactnativebrownfield.ReactNativeFragment
import com.callstack.reactnativebrownfield.constants.ReactNativeFragmentArgNames

class MainActivity : AppCompatActivity() {
class MainActivity : AppCompatActivity(), BrownfieldNavigationDelegate {
override fun onConfigurationChanged(newConfig: Configuration) {
super.onConfigurationChanged(newConfig)

Expand All @@ -39,6 +42,7 @@ class MainActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(null)
enableEdgeToEdge()
BrownfieldNavigationManager.setDelegate(this)

if (savedInstanceState == null) {
ReactNativeHostManager.initialize(application) {
Expand All @@ -65,6 +69,19 @@ class MainActivity : AppCompatActivity() {
}
}
}

override fun navigateToSettings() {
startActivity(Intent(this, SettingsActivity::class.java))
}

override fun navigateToReferrals(userId: String) {
startActivity(
Intent(this, ReferralsActivity::class.java).putExtra(
ReferralsActivity.EXTRA_USER_ID,
userId
)
)
}
}

@Composable
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
package com.callstack.brownfield.android.example

import android.os.Bundle
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.activity.enableEdgeToEdge
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.padding
import androidx.compose.material3.Button
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Scaffold
import androidx.compose.material3.Text
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.dp
import com.callstack.brownfield.android.example.ui.theme.AndroidBrownfieldAppTheme

class ReferralsActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
enableEdgeToEdge()

val userId = intent.getStringExtra(EXTRA_USER_ID).orEmpty()

setContent {
AndroidBrownfieldAppTheme {
Scaffold(modifier = Modifier.fillMaxSize()) { innerPadding ->
Column(
modifier = Modifier
.fillMaxSize()
.padding(innerPadding)
.padding(24.dp),
verticalArrangement = Arrangement.spacedBy(16.dp),
horizontalAlignment = Alignment.CenterHorizontally
) {
Text(
text = "Referrals",
style = MaterialTheme.typography.headlineMedium
)
Text(
text = "Opened from BrownfieldNavigation.navigateToReferrals(userId).",
textAlign = TextAlign.Center
)
Text(
text = "userId: $userId",
style = MaterialTheme.typography.bodyLarge
)
Button(onClick = { finish() }) {
Text("Go back")
}
}
}
}
}
}

companion object {
const val EXTRA_USER_ID = "extra_user_id"
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
package com.callstack.brownfield.android.example

import android.os.Bundle
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.activity.enableEdgeToEdge
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.padding
import androidx.compose.material3.Button
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Scaffold
import androidx.compose.material3.Text
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.dp
import com.callstack.brownfield.android.example.ui.theme.AndroidBrownfieldAppTheme

class SettingsActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
enableEdgeToEdge()

setContent {
AndroidBrownfieldAppTheme {
Scaffold(modifier = Modifier.fillMaxSize()) { innerPadding ->
Column(
modifier = Modifier
.fillMaxSize()
.padding(innerPadding)
.padding(24.dp),
verticalArrangement = Arrangement.spacedBy(16.dp),
horizontalAlignment = Alignment.CenterHorizontally
) {
Text(
text = "Settings",
style = MaterialTheme.typography.headlineMedium
)
Text(
text = "Opened from BrownfieldNavigation.navigateToSettings().",
textAlign = TextAlign.Center
)
Button(onClick = { finish() }) {
Text("Go back")
}
}
}
}
}
}
}
62 changes: 38 additions & 24 deletions apps/AppleApp/Brownfield Apple App.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -7,14 +7,16 @@
objects = {

/* Begin PBXBuildFile section */
7926B0E22F4E5A6400694E68 /* BrownfieldLib.xcframework in Frameworks */ = {isa = PBXBuildFile; fileRef = 7926B0DA2F4E5A6100694E68 /* BrownfieldLib.xcframework */; };
7926B0E32F4E5A6400694E68 /* BrownfieldLib.xcframework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = 7926B0DA2F4E5A6100694E68 /* BrownfieldLib.xcframework */; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; };
7926B0E42F4E5A6600694E68 /* hermesvm.xcframework in Frameworks */ = {isa = PBXBuildFile; fileRef = 7926B0DC2F4E5A6100694E68 /* hermesvm.xcframework */; };
7926B0E52F4E5A6600694E68 /* hermesvm.xcframework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = 7926B0DC2F4E5A6100694E68 /* hermesvm.xcframework */; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; };
7926B0E62F4E5A6700694E68 /* ReactBrownfield.xcframework in Frameworks */ = {isa = PBXBuildFile; fileRef = 7926B0DD2F4E5A6100694E68 /* ReactBrownfield.xcframework */; };
7926B0E72F4E5A6700694E68 /* ReactBrownfield.xcframework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = 7926B0DD2F4E5A6100694E68 /* ReactBrownfield.xcframework */; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; };
7926B0E82F4E5A6800694E68 /* Brownie.xcframework in Frameworks */ = {isa = PBXBuildFile; fileRef = 7926B0DB2F4E5A6100694E68 /* Brownie.xcframework */; };
7926B0E92F4E5A6800694E68 /* Brownie.xcframework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = 7926B0DB2F4E5A6100694E68 /* Brownie.xcframework */; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; };
614B23922F50633200CB6363 /* BrownfieldLib.xcframework in Frameworks */ = {isa = PBXBuildFile; fileRef = 614B238D2F50633200CB6363 /* BrownfieldLib.xcframework */; };
614B23932F50633200CB6363 /* BrownfieldLib.xcframework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = 614B238D2F50633200CB6363 /* BrownfieldLib.xcframework */; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; };
614B23942F50633200CB6363 /* Brownie.xcframework in Frameworks */ = {isa = PBXBuildFile; fileRef = 614B238E2F50633200CB6363 /* Brownie.xcframework */; };
614B23952F50633200CB6363 /* Brownie.xcframework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = 614B238E2F50633200CB6363 /* Brownie.xcframework */; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; };
614B23962F50633200CB6363 /* BrownfieldNavigation.xcframework in Frameworks */ = {isa = PBXBuildFile; fileRef = 614B238F2F50633200CB6363 /* BrownfieldNavigation.xcframework */; };
614B23972F50633200CB6363 /* BrownfieldNavigation.xcframework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = 614B238F2F50633200CB6363 /* BrownfieldNavigation.xcframework */; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; };
614B23982F50633200CB6363 /* hermesvm.xcframework in Frameworks */ = {isa = PBXBuildFile; fileRef = 614B23902F50633200CB6363 /* hermesvm.xcframework */; };
614B23992F50633200CB6363 /* hermesvm.xcframework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = 614B23902F50633200CB6363 /* hermesvm.xcframework */; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; };
614B239A2F50633200CB6363 /* ReactBrownfield.xcframework in Frameworks */ = {isa = PBXBuildFile; fileRef = 614B23912F50633200CB6363 /* ReactBrownfield.xcframework */; };
614B239B2F50633200CB6363 /* ReactBrownfield.xcframework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = 614B23912F50633200CB6363 /* ReactBrownfield.xcframework */; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; };
/* End PBXBuildFile section */

/* Begin PBXCopyFilesBuildPhase section */
Expand All @@ -24,21 +26,23 @@
dstPath = "";
dstSubfolderSpec = 10;
files = (
7926B0E92F4E5A6800694E68 /* Brownie.xcframework in Embed Frameworks */,
7926B0E52F4E5A6600694E68 /* hermesvm.xcframework in Embed Frameworks */,
7926B0E72F4E5A6700694E68 /* ReactBrownfield.xcframework in Embed Frameworks */,
7926B0E32F4E5A6400694E68 /* BrownfieldLib.xcframework in Embed Frameworks */,
614B23972F50633200CB6363 /* BrownfieldNavigation.xcframework in Embed Frameworks */,
614B23952F50633200CB6363 /* Brownie.xcframework in Embed Frameworks */,
614B239B2F50633200CB6363 /* ReactBrownfield.xcframework in Embed Frameworks */,
614B23992F50633200CB6363 /* hermesvm.xcframework in Embed Frameworks */,
614B23932F50633200CB6363 /* BrownfieldLib.xcframework in Embed Frameworks */,
);
name = "Embed Frameworks";
runOnlyForDeploymentPostprocessing = 0;
};
/* End PBXCopyFilesBuildPhase section */

/* Begin PBXFileReference section */
7926B0DA2F4E5A6100694E68 /* BrownfieldLib.xcframework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcframework; name = BrownfieldLib.xcframework; path = package/BrownfieldLib.xcframework; sourceTree = "<group>"; };
7926B0DB2F4E5A6100694E68 /* Brownie.xcframework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcframework; name = Brownie.xcframework; path = package/Brownie.xcframework; sourceTree = "<group>"; };
7926B0DC2F4E5A6100694E68 /* hermesvm.xcframework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcframework; name = hermesvm.xcframework; path = package/hermesvm.xcframework; sourceTree = "<group>"; };
7926B0DD2F4E5A6100694E68 /* ReactBrownfield.xcframework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcframework; name = ReactBrownfield.xcframework; path = package/ReactBrownfield.xcframework; sourceTree = "<group>"; };
614B238D2F50633200CB6363 /* BrownfieldLib.xcframework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcframework; name = BrownfieldLib.xcframework; path = package/BrownfieldLib.xcframework; sourceTree = "<group>"; };
614B238E2F50633200CB6363 /* Brownie.xcframework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcframework; name = Brownie.xcframework; path = package/Brownie.xcframework; sourceTree = "<group>"; };
614B238F2F50633200CB6363 /* BrownfieldNavigation.xcframework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcframework; name = BrownfieldNavigation.xcframework; path = package/BrownfieldNavigation.xcframework; sourceTree = "<group>"; };
614B23902F50633200CB6363 /* hermesvm.xcframework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcframework; name = hermesvm.xcframework; path = package/hermesvm.xcframework; sourceTree = "<group>"; };
614B23912F50633200CB6363 /* ReactBrownfield.xcframework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcframework; name = ReactBrownfield.xcframework; path = package/ReactBrownfield.xcframework; sourceTree = "<group>"; };
793C76A72EEBF938008A2A34 /* Brownfield Apple App.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = "Brownfield Apple App.app"; sourceTree = BUILT_PRODUCTS_DIR; };
/* End PBXFileReference section */

Expand All @@ -55,25 +59,35 @@
isa = PBXFrameworksBuildPhase;
buildActionMask = 2147483647;
files = (
7926B0E82F4E5A6800694E68 /* Brownie.xcframework in Frameworks */,
7926B0E62F4E5A6700694E68 /* ReactBrownfield.xcframework in Frameworks */,
7926B0E42F4E5A6600694E68 /* hermesvm.xcframework in Frameworks */,
7926B0E22F4E5A6400694E68 /* BrownfieldLib.xcframework in Frameworks */,
614B23962F50633200CB6363 /* BrownfieldNavigation.xcframework in Frameworks */,
614B23942F50633200CB6363 /* Brownie.xcframework in Frameworks */,
614B239A2F50633200CB6363 /* ReactBrownfield.xcframework in Frameworks */,
614B23982F50633200CB6363 /* hermesvm.xcframework in Frameworks */,
614B23922F50633200CB6363 /* BrownfieldLib.xcframework in Frameworks */,
);
runOnlyForDeploymentPostprocessing = 0;
};
/* End PBXFrameworksBuildPhase section */

/* Begin PBXGroup section */
6108E5322F40A26800EA8FA1 /* Frameworks */ = {
isa = PBXGroup;
children = (
614B238D2F50633200CB6363 /* BrownfieldLib.xcframework */,
614B238F2F50633200CB6363 /* BrownfieldNavigation.xcframework */,
614B238E2F50633200CB6363 /* Brownie.xcframework */,
614B23902F50633200CB6363 /* hermesvm.xcframework */,
614B23912F50633200CB6363 /* ReactBrownfield.xcframework */,
);
name = Frameworks;
sourceTree = "<group>";
};
793C769E2EEBF938008A2A34 = {
isa = PBXGroup;
children = (
793C76A92EEBF938008A2A34 /* Brownfield Apple App */,
6108E5322F40A26800EA8FA1 /* Frameworks */,
793C76A82EEBF938008A2A34 /* Products */,
7926B0DA2F4E5A6100694E68 /* BrownfieldLib.xcframework */,
7926B0DB2F4E5A6100694E68 /* Brownie.xcframework */,
7926B0DC2F4E5A6100694E68 /* hermesvm.xcframework */,
7926B0DD2F4E5A6100694E68 /* ReactBrownfield.xcframework */,
);
sourceTree = "<group>";
};
Expand Down
55 changes: 55 additions & 0 deletions apps/AppleApp/Brownfield Apple App/BrownfieldAppleApp.swift
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@ import BrownfieldLib
import Brownie
import ReactBrownfield
import SwiftUI
import UIKit
import BrownfieldNavigation

class AppDelegate: NSObject, UIApplicationDelegate {
var window: UIWindow?
Expand All @@ -18,6 +20,55 @@ class AppDelegate: NSObject, UIApplicationDelegate {
}
}

public class RNNavigationDelegate: BrownfieldNavigationDelegate {
public func navigateToSettings() {
present(SettingsScreen())
}

public func navigateToReferrals(_ userId: String) {
present(ReferralsScreen(userId: userId))
}

private func present<Content: View>(_ view: Content) {
DispatchQueue.main.async {
let hostingController = UIHostingController(rootView: view)

guard let topController = UIApplication.shared.topMostViewController() else {
return
}

if let navigationController = topController.navigationController {
navigationController.pushViewController(hostingController, animated: true)
return
}

let navigationController = UINavigationController(rootViewController: hostingController)
topController.present(navigationController, animated: true)
}
}
}

private extension UIApplication {
func topMostViewController(
base: UIViewController? = UIApplication.shared.connectedScenes
.compactMap { $0 as? UIWindowScene }
.flatMap { $0.windows }
.first(where: { $0.isKeyWindow })?.rootViewController
) -> UIViewController? {
if let navigationController = base as? UINavigationController {
return topMostViewController(base: navigationController.visibleViewController)
}
if let tabBarController = base as? UITabBarController,
let selected = tabBarController.selectedViewController {
return topMostViewController(base: selected)
}
if let presented = base?.presentedViewController {
return topMostViewController(base: presented)
}
return base
}
}

@main
struct BrownfieldAppleApp: App {
@UIApplicationDelegateAdaptor(AppDelegate.self) var appDelegate
Expand All @@ -28,6 +79,10 @@ struct BrownfieldAppleApp: App {
print("React Native has been loaded")
}

BrownfieldNavigationManager.shared.setDelegate(
navigationDelegate: RNNavigationDelegate()
)

#if USE_EXPO_HOST
ReactNativeBrownfield.shared.ensureExpoModulesProvider()
#endif
Expand Down
Loading