Skip to content

Commit 3e33b89

Browse files
authored
feat: add brownfield navigation (#236)
* feat: add BrownfieldNavigation TurboModule and other classes * feat: consume brownfield navigation * feat: expose brownfield navigation as separate package * chore: rename to brownfield-navigation * feat: add BrownfieldNavigation framework * feat: add babel transpiler * feat: add android template * feat: add android codegen support * feat(android): implement BrownfieldNavigationDelegate * docs: add brownfield-navigation docs * feat: move navigation codegen to cli * feat: use ts-morph and quicktype * fix: changes * fix(ci): add build packages script * chore: align major version * chore: run changeset * refactor: remove comments * feat: add brownfield-navigation to expo app * chore: run changeset * fix: add guard for navigation * docs: add note * feat: throw error if delegate is nil * fix: align with obj-c convention * test: add test for navigation codegen * docs: update
1 parent d5c8609 commit 3e33b89

68 files changed

Lines changed: 2683 additions & 82 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.changeset/yellow-poems-go.md

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
---
2+
'@callstack/brownfield-navigation': minor
3+
'@callstack/brownfield-cli': minor
4+
'brownfield': minor
5+
'@callstack/brownie': minor
6+
'@callstack/react-native-brownfield': minor
7+
---
8+
9+
add brownfield navigation

.github/actions/setup/action.yml

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,10 @@ runs:
2020
run: yarn install
2121
shell: bash
2222

23+
- name: Build packages
24+
run: yarn build
25+
shell: bash
26+
2327
- name: Restore Turbo cache
2428
if: inputs.restore-turbo-cache == 'true'
2529
uses: actions/cache@9255dc7a253b0ccc959486e2bca901246202afeb # v5

apps/AndroidApp/app/src/main/AndroidManifest.xml

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,14 @@
1313
android:supportsRtl="true"
1414
android:theme="@style/Theme.AndroidBrownfieldApp"
1515
android:usesCleartextTraffic="true">
16+
<activity
17+
android:name=".ReferralsActivity"
18+
android:exported="false"
19+
android:theme="@style/Theme.AndroidBrownfieldApp" />
20+
<activity
21+
android:name=".SettingsActivity"
22+
android:exported="false"
23+
android:theme="@style/Theme.AndroidBrownfieldApp" />
1624
<activity
1725
android:name=".MainActivity"
1826
android:exported="true"

apps/AndroidApp/app/src/main/java/com/callstack/brownfield/android/example/MainActivity.kt

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
package com.callstack.brownfield.android.example
22

3+
import android.content.Intent
34
import android.content.res.Configuration
45
import android.os.Bundle
56
import android.widget.Toast
@@ -26,10 +27,12 @@ import androidx.fragment.compose.AndroidFragment
2627
import com.callstack.brownfield.android.example.components.GreetingCard
2728
import com.callstack.brownfield.android.example.components.PostMessageCard
2829
import com.callstack.brownfield.android.example.ui.theme.AndroidBrownfieldAppTheme
30+
import com.callstack.nativebrownfieldnavigation.BrownfieldNavigationDelegate
31+
import com.callstack.nativebrownfieldnavigation.BrownfieldNavigationManager
2932
import com.callstack.reactnativebrownfield.ReactNativeFragment
3033
import com.callstack.reactnativebrownfield.constants.ReactNativeFragmentArgNames
3134

32-
class MainActivity : AppCompatActivity() {
35+
class MainActivity : AppCompatActivity(), BrownfieldNavigationDelegate {
3336
override fun onConfigurationChanged(newConfig: Configuration) {
3437
super.onConfigurationChanged(newConfig)
3538

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

4347
if (savedInstanceState == null) {
4448
ReactNativeHostManager.initialize(application) {
@@ -65,6 +69,19 @@ class MainActivity : AppCompatActivity() {
6569
}
6670
}
6771
}
72+
73+
override fun navigateToSettings() {
74+
startActivity(Intent(this, SettingsActivity::class.java))
75+
}
76+
77+
override fun navigateToReferrals(userId: String) {
78+
startActivity(
79+
Intent(this, ReferralsActivity::class.java).putExtra(
80+
ReferralsActivity.EXTRA_USER_ID,
81+
userId
82+
)
83+
)
84+
}
6885
}
6986

7087
@Composable
Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
package com.callstack.brownfield.android.example
2+
3+
import android.os.Bundle
4+
import androidx.activity.ComponentActivity
5+
import androidx.activity.compose.setContent
6+
import androidx.activity.enableEdgeToEdge
7+
import androidx.compose.foundation.layout.Arrangement
8+
import androidx.compose.foundation.layout.Column
9+
import androidx.compose.foundation.layout.fillMaxSize
10+
import androidx.compose.foundation.layout.padding
11+
import androidx.compose.material3.Button
12+
import androidx.compose.material3.MaterialTheme
13+
import androidx.compose.material3.Scaffold
14+
import androidx.compose.material3.Text
15+
import androidx.compose.ui.Alignment
16+
import androidx.compose.ui.Modifier
17+
import androidx.compose.ui.text.style.TextAlign
18+
import androidx.compose.ui.unit.dp
19+
import com.callstack.brownfield.android.example.ui.theme.AndroidBrownfieldAppTheme
20+
21+
class ReferralsActivity : ComponentActivity() {
22+
override fun onCreate(savedInstanceState: Bundle?) {
23+
super.onCreate(savedInstanceState)
24+
enableEdgeToEdge()
25+
26+
val userId = intent.getStringExtra(EXTRA_USER_ID).orEmpty()
27+
28+
setContent {
29+
AndroidBrownfieldAppTheme {
30+
Scaffold(modifier = Modifier.fillMaxSize()) { innerPadding ->
31+
Column(
32+
modifier = Modifier
33+
.fillMaxSize()
34+
.padding(innerPadding)
35+
.padding(24.dp),
36+
verticalArrangement = Arrangement.spacedBy(16.dp),
37+
horizontalAlignment = Alignment.CenterHorizontally
38+
) {
39+
Text(
40+
text = "Referrals",
41+
style = MaterialTheme.typography.headlineMedium
42+
)
43+
Text(
44+
text = "Opened from BrownfieldNavigation.navigateToReferrals(userId).",
45+
textAlign = TextAlign.Center
46+
)
47+
Text(
48+
text = "userId: $userId",
49+
style = MaterialTheme.typography.bodyLarge
50+
)
51+
Button(onClick = { finish() }) {
52+
Text("Go back")
53+
}
54+
}
55+
}
56+
}
57+
}
58+
}
59+
60+
companion object {
61+
const val EXTRA_USER_ID = "extra_user_id"
62+
}
63+
}
Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
package com.callstack.brownfield.android.example
2+
3+
import android.os.Bundle
4+
import androidx.activity.ComponentActivity
5+
import androidx.activity.compose.setContent
6+
import androidx.activity.enableEdgeToEdge
7+
import androidx.compose.foundation.layout.Arrangement
8+
import androidx.compose.foundation.layout.Column
9+
import androidx.compose.foundation.layout.fillMaxSize
10+
import androidx.compose.foundation.layout.padding
11+
import androidx.compose.material3.Button
12+
import androidx.compose.material3.MaterialTheme
13+
import androidx.compose.material3.Scaffold
14+
import androidx.compose.material3.Text
15+
import androidx.compose.ui.Alignment
16+
import androidx.compose.ui.Modifier
17+
import androidx.compose.ui.text.style.TextAlign
18+
import androidx.compose.ui.unit.dp
19+
import com.callstack.brownfield.android.example.ui.theme.AndroidBrownfieldAppTheme
20+
21+
class SettingsActivity : ComponentActivity() {
22+
override fun onCreate(savedInstanceState: Bundle?) {
23+
super.onCreate(savedInstanceState)
24+
enableEdgeToEdge()
25+
26+
setContent {
27+
AndroidBrownfieldAppTheme {
28+
Scaffold(modifier = Modifier.fillMaxSize()) { innerPadding ->
29+
Column(
30+
modifier = Modifier
31+
.fillMaxSize()
32+
.padding(innerPadding)
33+
.padding(24.dp),
34+
verticalArrangement = Arrangement.spacedBy(16.dp),
35+
horizontalAlignment = Alignment.CenterHorizontally
36+
) {
37+
Text(
38+
text = "Settings",
39+
style = MaterialTheme.typography.headlineMedium
40+
)
41+
Text(
42+
text = "Opened from BrownfieldNavigation.navigateToSettings().",
43+
textAlign = TextAlign.Center
44+
)
45+
Button(onClick = { finish() }) {
46+
Text("Go back")
47+
}
48+
}
49+
}
50+
}
51+
}
52+
}
53+
}

apps/AppleApp/Brownfield Apple App.xcodeproj/project.pbxproj

Lines changed: 38 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -7,14 +7,16 @@
77
objects = {
88

99
/* Begin PBXBuildFile section */
10-
7926B0E22F4E5A6400694E68 /* BrownfieldLib.xcframework in Frameworks */ = {isa = PBXBuildFile; fileRef = 7926B0DA2F4E5A6100694E68 /* BrownfieldLib.xcframework */; };
11-
7926B0E32F4E5A6400694E68 /* BrownfieldLib.xcframework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = 7926B0DA2F4E5A6100694E68 /* BrownfieldLib.xcframework */; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; };
12-
7926B0E42F4E5A6600694E68 /* hermesvm.xcframework in Frameworks */ = {isa = PBXBuildFile; fileRef = 7926B0DC2F4E5A6100694E68 /* hermesvm.xcframework */; };
13-
7926B0E52F4E5A6600694E68 /* hermesvm.xcframework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = 7926B0DC2F4E5A6100694E68 /* hermesvm.xcframework */; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; };
14-
7926B0E62F4E5A6700694E68 /* ReactBrownfield.xcframework in Frameworks */ = {isa = PBXBuildFile; fileRef = 7926B0DD2F4E5A6100694E68 /* ReactBrownfield.xcframework */; };
15-
7926B0E72F4E5A6700694E68 /* ReactBrownfield.xcframework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = 7926B0DD2F4E5A6100694E68 /* ReactBrownfield.xcframework */; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; };
16-
7926B0E82F4E5A6800694E68 /* Brownie.xcframework in Frameworks */ = {isa = PBXBuildFile; fileRef = 7926B0DB2F4E5A6100694E68 /* Brownie.xcframework */; };
17-
7926B0E92F4E5A6800694E68 /* Brownie.xcframework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = 7926B0DB2F4E5A6100694E68 /* Brownie.xcframework */; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; };
10+
614B23922F50633200CB6363 /* BrownfieldLib.xcframework in Frameworks */ = {isa = PBXBuildFile; fileRef = 614B238D2F50633200CB6363 /* BrownfieldLib.xcframework */; };
11+
614B23932F50633200CB6363 /* BrownfieldLib.xcframework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = 614B238D2F50633200CB6363 /* BrownfieldLib.xcframework */; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; };
12+
614B23942F50633200CB6363 /* Brownie.xcframework in Frameworks */ = {isa = PBXBuildFile; fileRef = 614B238E2F50633200CB6363 /* Brownie.xcframework */; };
13+
614B23952F50633200CB6363 /* Brownie.xcframework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = 614B238E2F50633200CB6363 /* Brownie.xcframework */; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; };
14+
614B23962F50633200CB6363 /* BrownfieldNavigation.xcframework in Frameworks */ = {isa = PBXBuildFile; fileRef = 614B238F2F50633200CB6363 /* BrownfieldNavigation.xcframework */; };
15+
614B23972F50633200CB6363 /* BrownfieldNavigation.xcframework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = 614B238F2F50633200CB6363 /* BrownfieldNavigation.xcframework */; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; };
16+
614B23982F50633200CB6363 /* hermesvm.xcframework in Frameworks */ = {isa = PBXBuildFile; fileRef = 614B23902F50633200CB6363 /* hermesvm.xcframework */; };
17+
614B23992F50633200CB6363 /* hermesvm.xcframework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = 614B23902F50633200CB6363 /* hermesvm.xcframework */; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; };
18+
614B239A2F50633200CB6363 /* ReactBrownfield.xcframework in Frameworks */ = {isa = PBXBuildFile; fileRef = 614B23912F50633200CB6363 /* ReactBrownfield.xcframework */; };
19+
614B239B2F50633200CB6363 /* ReactBrownfield.xcframework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = 614B23912F50633200CB6363 /* ReactBrownfield.xcframework */; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; };
1820
/* End PBXBuildFile section */
1921

2022
/* Begin PBXCopyFilesBuildPhase section */
@@ -24,21 +26,23 @@
2426
dstPath = "";
2527
dstSubfolderSpec = 10;
2628
files = (
27-
7926B0E92F4E5A6800694E68 /* Brownie.xcframework in Embed Frameworks */,
28-
7926B0E52F4E5A6600694E68 /* hermesvm.xcframework in Embed Frameworks */,
29-
7926B0E72F4E5A6700694E68 /* ReactBrownfield.xcframework in Embed Frameworks */,
30-
7926B0E32F4E5A6400694E68 /* BrownfieldLib.xcframework in Embed Frameworks */,
29+
614B23972F50633200CB6363 /* BrownfieldNavigation.xcframework in Embed Frameworks */,
30+
614B23952F50633200CB6363 /* Brownie.xcframework in Embed Frameworks */,
31+
614B239B2F50633200CB6363 /* ReactBrownfield.xcframework in Embed Frameworks */,
32+
614B23992F50633200CB6363 /* hermesvm.xcframework in Embed Frameworks */,
33+
614B23932F50633200CB6363 /* BrownfieldLib.xcframework in Embed Frameworks */,
3134
);
3235
name = "Embed Frameworks";
3336
runOnlyForDeploymentPostprocessing = 0;
3437
};
3538
/* End PBXCopyFilesBuildPhase section */
3639

3740
/* Begin PBXFileReference section */
38-
7926B0DA2F4E5A6100694E68 /* BrownfieldLib.xcframework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcframework; name = BrownfieldLib.xcframework; path = package/BrownfieldLib.xcframework; sourceTree = "<group>"; };
39-
7926B0DB2F4E5A6100694E68 /* Brownie.xcframework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcframework; name = Brownie.xcframework; path = package/Brownie.xcframework; sourceTree = "<group>"; };
40-
7926B0DC2F4E5A6100694E68 /* hermesvm.xcframework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcframework; name = hermesvm.xcframework; path = package/hermesvm.xcframework; sourceTree = "<group>"; };
41-
7926B0DD2F4E5A6100694E68 /* ReactBrownfield.xcframework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcframework; name = ReactBrownfield.xcframework; path = package/ReactBrownfield.xcframework; sourceTree = "<group>"; };
41+
614B238D2F50633200CB6363 /* BrownfieldLib.xcframework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcframework; name = BrownfieldLib.xcframework; path = package/BrownfieldLib.xcframework; sourceTree = "<group>"; };
42+
614B238E2F50633200CB6363 /* Brownie.xcframework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcframework; name = Brownie.xcframework; path = package/Brownie.xcframework; sourceTree = "<group>"; };
43+
614B238F2F50633200CB6363 /* BrownfieldNavigation.xcframework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcframework; name = BrownfieldNavigation.xcframework; path = package/BrownfieldNavigation.xcframework; sourceTree = "<group>"; };
44+
614B23902F50633200CB6363 /* hermesvm.xcframework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcframework; name = hermesvm.xcframework; path = package/hermesvm.xcframework; sourceTree = "<group>"; };
45+
614B23912F50633200CB6363 /* ReactBrownfield.xcframework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcframework; name = ReactBrownfield.xcframework; path = package/ReactBrownfield.xcframework; sourceTree = "<group>"; };
4246
793C76A72EEBF938008A2A34 /* Brownfield Apple App.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = "Brownfield Apple App.app"; sourceTree = BUILT_PRODUCTS_DIR; };
4347
/* End PBXFileReference section */
4448

@@ -55,25 +59,35 @@
5559
isa = PBXFrameworksBuildPhase;
5660
buildActionMask = 2147483647;
5761
files = (
58-
7926B0E82F4E5A6800694E68 /* Brownie.xcframework in Frameworks */,
59-
7926B0E62F4E5A6700694E68 /* ReactBrownfield.xcframework in Frameworks */,
60-
7926B0E42F4E5A6600694E68 /* hermesvm.xcframework in Frameworks */,
61-
7926B0E22F4E5A6400694E68 /* BrownfieldLib.xcframework in Frameworks */,
62+
614B23962F50633200CB6363 /* BrownfieldNavigation.xcframework in Frameworks */,
63+
614B23942F50633200CB6363 /* Brownie.xcframework in Frameworks */,
64+
614B239A2F50633200CB6363 /* ReactBrownfield.xcframework in Frameworks */,
65+
614B23982F50633200CB6363 /* hermesvm.xcframework in Frameworks */,
66+
614B23922F50633200CB6363 /* BrownfieldLib.xcframework in Frameworks */,
6267
);
6368
runOnlyForDeploymentPostprocessing = 0;
6469
};
6570
/* End PBXFrameworksBuildPhase section */
6671

6772
/* Begin PBXGroup section */
73+
6108E5322F40A26800EA8FA1 /* Frameworks */ = {
74+
isa = PBXGroup;
75+
children = (
76+
614B238D2F50633200CB6363 /* BrownfieldLib.xcframework */,
77+
614B238F2F50633200CB6363 /* BrownfieldNavigation.xcframework */,
78+
614B238E2F50633200CB6363 /* Brownie.xcframework */,
79+
614B23902F50633200CB6363 /* hermesvm.xcframework */,
80+
614B23912F50633200CB6363 /* ReactBrownfield.xcframework */,
81+
);
82+
name = Frameworks;
83+
sourceTree = "<group>";
84+
};
6885
793C769E2EEBF938008A2A34 = {
6986
isa = PBXGroup;
7087
children = (
7188
793C76A92EEBF938008A2A34 /* Brownfield Apple App */,
89+
6108E5322F40A26800EA8FA1 /* Frameworks */,
7290
793C76A82EEBF938008A2A34 /* Products */,
73-
7926B0DA2F4E5A6100694E68 /* BrownfieldLib.xcframework */,
74-
7926B0DB2F4E5A6100694E68 /* Brownie.xcframework */,
75-
7926B0DC2F4E5A6100694E68 /* hermesvm.xcframework */,
76-
7926B0DD2F4E5A6100694E68 /* ReactBrownfield.xcframework */,
7791
);
7892
sourceTree = "<group>";
7993
};

apps/AppleApp/Brownfield Apple App/BrownfieldAppleApp.swift

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@ import BrownfieldLib
22
import Brownie
33
import ReactBrownfield
44
import SwiftUI
5+
import UIKit
6+
import BrownfieldNavigation
57

68
class AppDelegate: NSObject, UIApplicationDelegate {
79
var window: UIWindow?
@@ -18,6 +20,55 @@ class AppDelegate: NSObject, UIApplicationDelegate {
1820
}
1921
}
2022

23+
public class RNNavigationDelegate: BrownfieldNavigationDelegate {
24+
public func navigateToSettings() {
25+
present(SettingsScreen())
26+
}
27+
28+
public func navigateToReferrals(_ userId: String) {
29+
present(ReferralsScreen(userId: userId))
30+
}
31+
32+
private func present<Content: View>(_ view: Content) {
33+
DispatchQueue.main.async {
34+
let hostingController = UIHostingController(rootView: view)
35+
36+
guard let topController = UIApplication.shared.topMostViewController() else {
37+
return
38+
}
39+
40+
if let navigationController = topController.navigationController {
41+
navigationController.pushViewController(hostingController, animated: true)
42+
return
43+
}
44+
45+
let navigationController = UINavigationController(rootViewController: hostingController)
46+
topController.present(navigationController, animated: true)
47+
}
48+
}
49+
}
50+
51+
private extension UIApplication {
52+
func topMostViewController(
53+
base: UIViewController? = UIApplication.shared.connectedScenes
54+
.compactMap { $0 as? UIWindowScene }
55+
.flatMap { $0.windows }
56+
.first(where: { $0.isKeyWindow })?.rootViewController
57+
) -> UIViewController? {
58+
if let navigationController = base as? UINavigationController {
59+
return topMostViewController(base: navigationController.visibleViewController)
60+
}
61+
if let tabBarController = base as? UITabBarController,
62+
let selected = tabBarController.selectedViewController {
63+
return topMostViewController(base: selected)
64+
}
65+
if let presented = base?.presentedViewController {
66+
return topMostViewController(base: presented)
67+
}
68+
return base
69+
}
70+
}
71+
2172
@main
2273
struct BrownfieldAppleApp: App {
2374
@UIApplicationDelegateAdaptor(AppDelegate.self) var appDelegate
@@ -28,6 +79,10 @@ struct BrownfieldAppleApp: App {
2879
print("React Native has been loaded")
2980
}
3081

82+
BrownfieldNavigationManager.shared.setDelegate(
83+
navigationDelegate: RNNavigationDelegate()
84+
)
85+
3186
#if USE_EXPO_HOST
3287
ReactNativeBrownfield.shared.ensureExpoModulesProvider()
3388
#endif

0 commit comments

Comments
 (0)