diff --git a/.changeset/yellow-poems-go.md b/.changeset/yellow-poems-go.md new file mode 100644 index 00000000..872005c8 --- /dev/null +++ b/.changeset/yellow-poems-go.md @@ -0,0 +1,9 @@ +--- +'@callstack/brownfield-navigation': minor +'@callstack/brownfield-cli': minor +'brownfield': minor +'@callstack/brownie': minor +'@callstack/react-native-brownfield': minor +--- + +add brownfield navigation diff --git a/.github/actions/setup/action.yml b/.github/actions/setup/action.yml index 942b1d3e..3d22c1b4 100644 --- a/.github/actions/setup/action.yml +++ b/.github/actions/setup/action.yml @@ -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 diff --git a/apps/AndroidApp/app/src/main/AndroidManifest.xml b/apps/AndroidApp/app/src/main/AndroidManifest.xml index bda9be43..8be33948 100644 --- a/apps/AndroidApp/app/src/main/AndroidManifest.xml +++ b/apps/AndroidApp/app/src/main/AndroidManifest.xml @@ -13,6 +13,14 @@ android:supportsRtl="true" android:theme="@style/Theme.AndroidBrownfieldApp" android:usesCleartextTraffic="true"> + + + 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" + } +} diff --git a/apps/AndroidApp/app/src/main/java/com/callstack/brownfield/android/example/SettingsActivity.kt b/apps/AndroidApp/app/src/main/java/com/callstack/brownfield/android/example/SettingsActivity.kt new file mode 100644 index 00000000..fd127452 --- /dev/null +++ b/apps/AndroidApp/app/src/main/java/com/callstack/brownfield/android/example/SettingsActivity.kt @@ -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") + } + } + } + } + } + } +} diff --git a/apps/AppleApp/Brownfield Apple App.xcodeproj/project.pbxproj b/apps/AppleApp/Brownfield Apple App.xcodeproj/project.pbxproj index 94258f6d..78323de0 100644 --- a/apps/AppleApp/Brownfield Apple App.xcodeproj/project.pbxproj +++ b/apps/AppleApp/Brownfield Apple App.xcodeproj/project.pbxproj @@ -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 */ @@ -24,10 +26,11 @@ 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; @@ -35,10 +38,11 @@ /* End PBXCopyFilesBuildPhase section */ /* Begin PBXFileReference section */ - 7926B0DA2F4E5A6100694E68 /* BrownfieldLib.xcframework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcframework; name = BrownfieldLib.xcframework; path = package/BrownfieldLib.xcframework; sourceTree = ""; }; - 7926B0DB2F4E5A6100694E68 /* Brownie.xcframework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcframework; name = Brownie.xcframework; path = package/Brownie.xcframework; sourceTree = ""; }; - 7926B0DC2F4E5A6100694E68 /* hermesvm.xcframework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcframework; name = hermesvm.xcframework; path = package/hermesvm.xcframework; sourceTree = ""; }; - 7926B0DD2F4E5A6100694E68 /* ReactBrownfield.xcframework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcframework; name = ReactBrownfield.xcframework; path = package/ReactBrownfield.xcframework; sourceTree = ""; }; + 614B238D2F50633200CB6363 /* BrownfieldLib.xcframework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcframework; name = BrownfieldLib.xcframework; path = package/BrownfieldLib.xcframework; sourceTree = ""; }; + 614B238E2F50633200CB6363 /* Brownie.xcframework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcframework; name = Brownie.xcframework; path = package/Brownie.xcframework; sourceTree = ""; }; + 614B238F2F50633200CB6363 /* BrownfieldNavigation.xcframework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcframework; name = BrownfieldNavigation.xcframework; path = package/BrownfieldNavigation.xcframework; sourceTree = ""; }; + 614B23902F50633200CB6363 /* hermesvm.xcframework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcframework; name = hermesvm.xcframework; path = package/hermesvm.xcframework; sourceTree = ""; }; + 614B23912F50633200CB6363 /* ReactBrownfield.xcframework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcframework; name = ReactBrownfield.xcframework; path = package/ReactBrownfield.xcframework; sourceTree = ""; }; 793C76A72EEBF938008A2A34 /* Brownfield Apple App.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = "Brownfield Apple App.app"; sourceTree = BUILT_PRODUCTS_DIR; }; /* End PBXFileReference section */ @@ -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 = ""; + }; 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 = ""; }; diff --git a/apps/AppleApp/Brownfield Apple App/BrownfieldAppleApp.swift b/apps/AppleApp/Brownfield Apple App/BrownfieldAppleApp.swift index f4eab6a8..22e2e83f 100644 --- a/apps/AppleApp/Brownfield Apple App/BrownfieldAppleApp.swift +++ b/apps/AppleApp/Brownfield Apple App/BrownfieldAppleApp.swift @@ -2,6 +2,8 @@ import BrownfieldLib import Brownie import ReactBrownfield import SwiftUI +import UIKit +import BrownfieldNavigation class AppDelegate: NSObject, UIApplicationDelegate { var window: UIWindow? @@ -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(_ 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 @@ -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 diff --git a/apps/AppleApp/Brownfield Apple App/components/ReferralsScreen.swift b/apps/AppleApp/Brownfield Apple App/components/ReferralsScreen.swift new file mode 100644 index 00000000..e1f37d1b --- /dev/null +++ b/apps/AppleApp/Brownfield Apple App/components/ReferralsScreen.swift @@ -0,0 +1,28 @@ +import SwiftUI + + +struct ReferralsScreen: View { + let userId: String + + var body: some View { + VStack(spacing: 16) { + Text("Referrals") + .font(.title2) + .fontWeight(.semibold) + + Text("User ID") + .foregroundStyle(.secondary) + Text(userId) + .font(.body.monospaced()) + .textSelection(.enabled) + + Button("Share referral link") { + // Placeholder action for the sample app. + } + .buttonStyle(.borderedProminent) + } + .frame(maxWidth: .infinity, maxHeight: .infinity) + .padding() + .navigationTitle("Referrals") + } +} \ No newline at end of file diff --git a/apps/AppleApp/Brownfield Apple App/components/SettingsScreen.swift b/apps/AppleApp/Brownfield Apple App/components/SettingsScreen.swift new file mode 100644 index 00000000..d5f584c3 --- /dev/null +++ b/apps/AppleApp/Brownfield Apple App/components/SettingsScreen.swift @@ -0,0 +1,22 @@ +import SwiftUI + +struct SettingsScreen: View { + var body: some View { + Form { + Section("Preferences") { + Toggle("Enable notifications", isOn: .constant(true)) + Toggle("Dark mode", isOn: .constant(false)) + } + + Section("About") { + HStack { + Text("Version") + Spacer() + Text("1.0.0") + .foregroundStyle(.secondary) + } + } + } + .navigationTitle("Settings") + } +} \ No newline at end of file diff --git a/apps/AppleApp/prepareXCFrameworks.js b/apps/AppleApp/prepareXCFrameworks.js index 9c6a2be5..f161180d 100644 --- a/apps/AppleApp/prepareXCFrameworks.js +++ b/apps/AppleApp/prepareXCFrameworks.js @@ -79,6 +79,7 @@ const validNames = [ 'Brownie.xcframework', 'hermesvm.xcframework', 'ReactBrownfield.xcframework', + 'BrownfieldNavigation.xcframework', ]; for (const file of fs.readdirSync(targetPackagePath)) { diff --git a/apps/ExpoApp/RNApp.tsx b/apps/ExpoApp/RNApp.tsx index a87add22..1d578d96 100644 --- a/apps/ExpoApp/RNApp.tsx +++ b/apps/ExpoApp/RNApp.tsx @@ -1,6 +1,7 @@ import { SafeAreaView } from 'react-native-safe-area-context'; -import { StyleSheet, Text, View } from 'react-native'; +import { Button, StyleSheet, Text, View } from 'react-native'; import Counter from './components/counter'; +import BrownfieldNavigation from '@callstack/brownfield-navigation'; export default function RNApp() { return ( @@ -9,6 +10,15 @@ export default function RNApp() { + +