From 3c303f971e42bb47bebd058cdf1a307e3ea4638f Mon Sep 17 00:00:00 2001 From: Hur Ali Date: Sat, 14 Feb 2026 14:55:31 +0500 Subject: [PATCH 1/6] feat: add BrownfieldNavigation TurboModule and other classes --- .../BrownfieldNavigationDelegate.swift | 5 + .../BrownfieldNavigationManager.swift | 18 + .../NativeBrownfieldNavigation.h | 15 + .../NativeBrownfieldNavigation.mm | 26 + packages/react-native-brownfield/package.json | 3 +- .../src/BrownfieldNavigation.ts | 9 + .../src/NativeBrownfieldNavigation.ts | 9 + packages/react-native-brownfield/src/index.ts | 2 + .../src/scripts/brownfield-navigation.ts | 474 ++++++++++++++++++ yarn.lock | 1 + 10 files changed, 561 insertions(+), 1 deletion(-) create mode 100644 packages/react-native-brownfield/ios/BrownfieldNavigation/BrownfieldNavigationDelegate.swift create mode 100644 packages/react-native-brownfield/ios/BrownfieldNavigation/BrownfieldNavigationManager.swift create mode 100644 packages/react-native-brownfield/ios/BrownfieldNavigation/NativeBrownfieldNavigation.h create mode 100644 packages/react-native-brownfield/ios/BrownfieldNavigation/NativeBrownfieldNavigation.mm create mode 100644 packages/react-native-brownfield/src/BrownfieldNavigation.ts create mode 100644 packages/react-native-brownfield/src/NativeBrownfieldNavigation.ts create mode 100644 packages/react-native-brownfield/src/scripts/brownfield-navigation.ts diff --git a/packages/react-native-brownfield/ios/BrownfieldNavigation/BrownfieldNavigationDelegate.swift b/packages/react-native-brownfield/ios/BrownfieldNavigation/BrownfieldNavigationDelegate.swift new file mode 100644 index 00000000..f4b2828d --- /dev/null +++ b/packages/react-native-brownfield/ios/BrownfieldNavigation/BrownfieldNavigationDelegate.swift @@ -0,0 +1,5 @@ +import Foundation + +@objc public protocol BrownfieldNavigationDelegate: AnyObject { + +} diff --git a/packages/react-native-brownfield/ios/BrownfieldNavigation/BrownfieldNavigationManager.swift b/packages/react-native-brownfield/ios/BrownfieldNavigation/BrownfieldNavigationManager.swift new file mode 100644 index 00000000..4e711aae --- /dev/null +++ b/packages/react-native-brownfield/ios/BrownfieldNavigation/BrownfieldNavigationManager.swift @@ -0,0 +1,18 @@ +// +// BrownfieldNavigationManager.swift +// +// Created by Hur Ali on 10/02/2026. +// + +public class BrownfieldNavigationManager: NSObject { + @objc public static let shared = BrownfieldNavigationManager() + private var navigationDelegate: BrownfieldNavigationDelegate? + + public func setDelegate(navigationDelegate: BrownfieldNavigationDelegate) { + self.navigationDelegate = navigationDelegate + } + + @objc public func getDelegate() -> BrownfieldNavigationDelegate { + return self.navigationDelegate! + } +} diff --git a/packages/react-native-brownfield/ios/BrownfieldNavigation/NativeBrownfieldNavigation.h b/packages/react-native-brownfield/ios/BrownfieldNavigation/NativeBrownfieldNavigation.h new file mode 100644 index 00000000..d420164d --- /dev/null +++ b/packages/react-native-brownfield/ios/BrownfieldNavigation/NativeBrownfieldNavigation.h @@ -0,0 +1,15 @@ +// +// NativeBrownfieldNavigation.h +// +// Created by Hur Ali on 10/02/2026. +// + +#ifdef __cplusplus + +#import + +@interface NativeBrownfieldNavigation : NSObject + +@end + +#endif diff --git a/packages/react-native-brownfield/ios/BrownfieldNavigation/NativeBrownfieldNavigation.mm b/packages/react-native-brownfield/ios/BrownfieldNavigation/NativeBrownfieldNavigation.mm new file mode 100644 index 00000000..5d630d60 --- /dev/null +++ b/packages/react-native-brownfield/ios/BrownfieldNavigation/NativeBrownfieldNavigation.mm @@ -0,0 +1,26 @@ +#import "NativeBrownfieldNavigation.h" + +#if __has_include("ReactBrownfield/ReactBrownfield-Swift.h") +#import "ReactBrownfield/ReactBrownfield-Swift.h" +#else +#import "ReactBrownfield-Swift.h" +#endif + +@implementation NativeBrownfieldNavigation + +- (void)temporary { + NSLog(@"temporary"); +} + +- (std::shared_ptr)getTurboModule: + (const facebook::react::ObjCTurboModule::InitParams &)params +{ + return std::make_shared(params); +} + ++ (NSString *)moduleName +{ + return @"NativeBrownfieldNavigation"; +} + +@end diff --git a/packages/react-native-brownfield/package.json b/packages/react-native-brownfield/package.json index e120e059..b36f6d1f 100644 --- a/packages/react-native-brownfield/package.json +++ b/packages/react-native-brownfield/package.json @@ -4,7 +4,8 @@ "license": "MIT", "author": "Michal Chudziak ", "bin": { - "brownfield": "lib/commonjs/scripts/brownfield.js" + "brownfield": "lib/commonjs/scripts/brownfield.js", + "brownfield-navigation-codegen": "lib/commonjs/scripts/brownfield-navigation.js" }, "contributors": [ "Piotr Drapich " diff --git a/packages/react-native-brownfield/src/BrownfieldNavigation.ts b/packages/react-native-brownfield/src/BrownfieldNavigation.ts new file mode 100644 index 00000000..5af516da --- /dev/null +++ b/packages/react-native-brownfield/src/BrownfieldNavigation.ts @@ -0,0 +1,9 @@ +import NativeBrownfieldNavigation from './NativeBrownfieldNavigation'; + +const BrownfieldNavigation = { + temporary: () => { + NativeBrownfieldNavigation.temporary(); + }, +}; + +export default BrownfieldNavigation; diff --git a/packages/react-native-brownfield/src/NativeBrownfieldNavigation.ts b/packages/react-native-brownfield/src/NativeBrownfieldNavigation.ts new file mode 100644 index 00000000..5d4ed84d --- /dev/null +++ b/packages/react-native-brownfield/src/NativeBrownfieldNavigation.ts @@ -0,0 +1,9 @@ +import { TurboModuleRegistry, type TurboModule } from 'react-native'; + +export interface Spec extends TurboModule { + temporary(): void; +} + +export default TurboModuleRegistry.getEnforcing( + 'NativeBrownfieldNavigation' +); diff --git a/packages/react-native-brownfield/src/index.ts b/packages/react-native-brownfield/src/index.ts index 136e60a6..bdd7b669 100644 --- a/packages/react-native-brownfield/src/index.ts +++ b/packages/react-native-brownfield/src/index.ts @@ -1,5 +1,6 @@ import { Platform } from 'react-native'; import ReactNativeBrownfieldModule from './NativeReactNativeBrownfieldModule'; +import BrownfieldNavigation from './BrownfieldNavigation'; const ReactNativeBrownfield = { popToNative: (animated?: boolean): void => { @@ -24,3 +25,4 @@ const ReactNativeBrownfield = { }; export default ReactNativeBrownfield; +export { BrownfieldNavigation }; diff --git a/packages/react-native-brownfield/src/scripts/brownfield-navigation.ts b/packages/react-native-brownfield/src/scripts/brownfield-navigation.ts new file mode 100644 index 00000000..7de423f7 --- /dev/null +++ b/packages/react-native-brownfield/src/scripts/brownfield-navigation.ts @@ -0,0 +1,474 @@ +#!/usr/bin/env node +/** + * Brownfield Navigation Codegen Script + * + * Reads a user's brownfield.navigation.ts spec file and generates/updates: + * - src/NativeBrownfieldNavigation.ts (TurboModule spec) + * - ios/NativeBrownfieldNavigation.mm (Objective-C++ implementation) + * - ios/BrownfieldNavigationDelegate.swift (Swift delegate protocol) + * - src/index.tsx (Exported JavaScript API) + * + * Usage: + * npx brownfield-navigation-codegen + * + * Example user spec (brownfield.navigation.ts): + * export interface BrownfieldNavigationSpec { + * navigateToProfile(userId: string): void; + * navigateToSettings(): void; + * } + */ + +import * as fs from 'node:fs'; +import * as path from 'node:path'; + +// Resolve script directory in both CJS and ESM runtimes. +// `process.argv[1]` is the executed CLI script path. +const scriptFile = process.argv[1] + ? fs.realpathSync(process.argv[1]) + : path.join(process.cwd(), 'brownfield-navigation.js'); +const scriptDir = path.dirname(scriptFile); + +// Package root is one level up from scripts/ +const PACKAGE_ROOT = path.resolve(scriptDir, '../../../'); + +// ============================================================================ +// Types +// ============================================================================ + +interface MethodParam { + name: string; + type: string; + optional: boolean; +} + +interface MethodSignature { + name: string; + params: MethodParam[]; + returnType: string; + isAsync: boolean; +} + +// ============================================================================ +// TypeScript to Native Type Mapping +// ============================================================================ + +const TS_TO_OBJC_TYPE: Record = { + string: 'NSString *', + number: 'double', + boolean: 'BOOL', + void: 'void', + Object: 'NSDictionary *', +}; + +const TS_TO_SWIFT_TYPE: Record = { + string: 'String', + number: 'Double', + boolean: 'Bool', + void: 'Void', + Object: '[String: Any]', +}; + +function mapTsTypeToObjC(tsType: string, nullable: boolean = false): string { + if (tsType.startsWith('Promise<')) { + return 'void'; + } + + const mapped = TS_TO_OBJC_TYPE[tsType]; + if (mapped) { + if (nullable && mapped.includes('*')) { + return mapped.replace(' *', ' * _Nullable'); + } + return mapped; + } + + return nullable ? 'id _Nullable' : 'id'; +} + +function mapTsTypeToSwift(tsType: string, optional: boolean = false): string { + if (tsType.startsWith('Promise<')) { + const inner = tsType.slice(8, -1); + return mapTsTypeToSwift(inner, optional); + } + + const mapped = TS_TO_SWIFT_TYPE[tsType]; + if (mapped) { + return optional ? `${mapped}?` : mapped; + } + + return optional ? 'Any?' : 'Any'; +} + +// ============================================================================ +// Spec Parser +// ============================================================================ + +function parseUserSpec(filePath: string): MethodSignature[] { + const content = fs.readFileSync(filePath, 'utf-8'); + + // Extract interface body - supports both BrownfieldNavigationSpec and Spec + const interfaceMatch = content.match( + /export interface (?:BrownfieldNavigationSpec|Spec)\s*\{([^}]+)\}/s + ); + if (!interfaceMatch || !interfaceMatch[1]) { + throw new Error( + 'Could not find BrownfieldNavigationSpec or Spec interface in spec file' + ); + } + const interfaceBody = interfaceMatch[1]; + + // Parse methods + const methods: MethodSignature[] = []; + const methodRegex = /(\w+)\s*\(([^)]*)\)\s*:\s*(Promise<[^>]+>|[^;]+)\s*;/g; + + let match; + while ((match = methodRegex.exec(interfaceBody)) !== null) { + const name = match[1]; + const paramsStr = match[2]; + const returnType = match[3]; + + if (!name || !returnType) { + continue; + } + + const params: MethodParam[] = []; + if (paramsStr && paramsStr.trim()) { + const paramParts = paramsStr.split(','); + for (const param of paramParts) { + const paramMatch = param.trim().match(/(\w+)(\?)?:\s*(.+)/); + if (paramMatch && paramMatch[1] && paramMatch[3]) { + params.push({ + name: paramMatch[1], + type: paramMatch[3].trim(), + optional: !!paramMatch[2], + }); + } + } + } + + methods.push({ + name, + params, + returnType: returnType.trim(), + isAsync: returnType.trim().startsWith('Promise<'), + }); + } + + return methods; +} + +// ============================================================================ +// Code Generators +// ============================================================================ + +function generateTurboModuleSpec(methods: MethodSignature[]): string { + const methodSignatures = methods + .map((m) => { + const params = m.params + .map((p) => `${p.name}${p.optional ? '?' : ''}: ${p.type}`) + .join(', '); + return ` ${m.name}(${params}): ${m.returnType};`; + }) + .join('\n'); + + return `import { TurboModuleRegistry, type TurboModule } from 'react-native'; + +export interface Spec extends TurboModule { +${methodSignatures} +} + +export default TurboModuleRegistry.getEnforcing( + 'NativeBrownfieldNavigation' +); +`; +} + +function generateBrownfieldNavigationTS(methods: MethodSignature[]): string { + const functionImplementations = methods + .map((m) => { + const params = m.params + .map((p) => `${p.name}${p.optional ? '?' : ''}: ${p.type}`) + .join(', '); + const args = m.params.map((p) => p.name).join(', '); + const returnType = m.returnType === 'void' ? '' : `: ${m.returnType}`; + + if (m.isAsync) { + return ` ${m.name}: async (${params})${returnType} => { + return NativeBrownfieldNavigation.${m.name}(${args}); + }`; + } + return ` ${m.name}: (${params})${returnType} => { + NativeBrownfieldNavigation.${m.name}(${args}); + }`; + }) + .join(',\n'); + + return `import NativeBrownfieldNavigation from './NativeBrownfieldNavigation'; + +const BrownfieldNavigation = { +${functionImplementations}, +}; + +export default BrownfieldNavigation; +`; +} + +function generateSwiftDelegate(methods: MethodSignature[]): string { + const protocolMethods = methods + .map((m) => { + const params = m.params + .map((p) => { + const swiftType = mapTsTypeToSwift(p.type, p.optional); + return `${p.name}: ${swiftType}`; + }) + .join(', '); + + const returnType = + m.returnType === 'void' + ? '' + : ` -> ${mapTsTypeToSwift(m.returnType, false)}`; + + return ` @objc func ${m.name}(${params})${returnType}`; + }) + .join('\n'); + + return `import Foundation + +@objc public protocol BrownfieldNavigationDelegate: AnyObject { +${protocolMethods} +} +`; +} + +function generateObjCImplementation(methods: MethodSignature[]): string { + const methodImpls = methods + .map((m) => { + if (m.isAsync) { + return generateAsyncObjCMethod(m); + } + return generateSyncObjCMethod(m); + }) + .join('\n\n'); + + return `#import "NativeBrownfieldNavigation.h" + +#if __has_include("ReactBrownfield/ReactBrownfield-Swift.h") +#import "ReactBrownfield/ReactBrownfield-Swift.h" +#else +#import "ReactBrownfield-Swift.h" +#endif + +@implementation NativeBrownfieldNavigation + +${methodImpls} + +- (std::shared_ptr)getTurboModule: + (const facebook::react::ObjCTurboModule::InitParams &)params +{ + return std::make_shared(params); +} + ++ (NSString *)moduleName +{ + return @"NativeBrownfieldNavigation"; +} + +@end +`; +} + +function generateSyncObjCMethod(method: MethodSignature): string { + const { name, params, returnType } = method; + + // Build ObjC method signature + let signature = `- (${mapTsTypeToObjC(returnType)})${name}`; + if (params.length > 0) { + signature += params + .map((p, i) => { + const prefix = i === 0 ? ':' : ` ${p.name}:`; + return `${prefix}(${mapTsTypeToObjC(p.type, p.optional)})${p.name}`; + }) + .join(''); + } + + // Build delegate call with Swift-style labels + let delegateCall = `[[[BrownfieldNavigationManager shared] getDelegate] ${name}`; + if (params.length > 0) { + delegateCall += params + .map((p, i) => { + // First param: use "With" prefix for Swift interop + const label = i === 0 ? 'With' + capitalizeFirst(p.name) : p.name; + return `${label}:${p.name}`; + }) + .join(' '); + } + delegateCall += ']'; + + const returnStatement = returnType === 'void' ? '' : 'return '; + + return `${signature} { + ${returnStatement}${delegateCall}; +}`; +} + +function generateAsyncObjCMethod(method: MethodSignature): string { + const { name, params } = method; + + // Build ObjC method signature with resolve/reject blocks + let signature = `- (void)${name}`; + const allParams: Array<{ name: string; type: string; optional: boolean }> = [ + ...params, + { name: 'resolve', type: 'RCTPromiseResolveBlock', optional: false }, + { name: 'reject', type: 'RCTPromiseRejectBlock', optional: false }, + ]; + + signature += ':'; + signature += allParams + .map((p, i) => { + const prefix = i === 0 ? '' : p.name; + const type = + p.type === 'RCTPromiseResolveBlock' + ? 'RCTPromiseResolveBlock' + : p.type === 'RCTPromiseRejectBlock' + ? 'RCTPromiseRejectBlock' + : mapTsTypeToObjC(p.type, p.optional); + return `${prefix}(${type})${p.name}`; + }) + .join(' '); + + return `${signature} { + // TODO: Implement async call to delegate + reject(@"not_implemented", @"${name} is not implemented", nil); +}`; +} + +// ============================================================================ +// Utilities +// ============================================================================ + +function capitalizeFirst(str: string): string { + return str.charAt(0).toUpperCase() + str.slice(1); +} + +// ============================================================================ +// Main +// ============================================================================ + +function main(): void { + const args = process.argv.slice(2); + + if (args.length < 1) { + console.log('Brownfield Navigation Codegen'); + console.log(''); + console.log('Usage:'); + console.log( + ' npx brownfield-navigation-codegen ' + ); + console.log(''); + console.log('Options:'); + console.log(' --dry-run Print generated code without writing files'); + console.log(''); + console.log('Example user spec (brownfield.navigation.ts):'); + console.log(' export interface BrownfieldNavigationSpec {'); + console.log(' navigateToProfile(userId: string): void;'); + console.log(' navigateToSettings(): void;'); + console.log(' }'); + process.exit(1); + } + + const specFile = args[0]; + if (!specFile) { + console.error('Error: No spec file provided'); + process.exit(1); + } + + const dryRun = args.includes('--dry-run'); + + // Resolve spec file path + const specPath = path.isAbsolute(specFile) + ? specFile + : path.resolve(process.cwd(), specFile); + + if (!fs.existsSync(specPath)) { + console.error(`Error: Spec file not found: ${specPath}`); + process.exit(1); + } + + console.log(`Parsing spec file: ${specPath}`); + const methods = parseUserSpec(specPath); + + if (methods.length === 0) { + console.error('Error: No methods found in spec file'); + process.exit(1); + } + + console.log( + `Found ${methods.length} method(s): ${methods + .map((m) => m.name) + .join(', ')}` + ); + + // Generate all files + const turboModuleSpec = generateTurboModuleSpec(methods); + const brownfieldNavigationTS = generateBrownfieldNavigationTS(methods); + const swiftDelegate = generateSwiftDelegate(methods); + const objcImpl = generateObjCImplementation(methods); + + if (dryRun) { + console.log('\n--- Generated: src/NativeBrownfieldNavigation.ts ---'); + console.log(turboModuleSpec); + console.log('\n--- Generated: src/BrownfieldNavigation.ts ---'); + console.log(brownfieldNavigationTS); + console.log('\n--- Generated: ios/BrownfieldNavigationDelegate.swift ---'); + console.log(swiftDelegate); + console.log('\n--- Generated: ios/NativeBrownfieldNavigation.mm ---'); + console.log(objcImpl); + return; + } + + // Write files to package + const paths = { + turboModuleSpec: path.join( + PACKAGE_ROOT, + 'src', + 'NativeBrownfieldNavigation.ts' + ), + navigationTs: path.join(PACKAGE_ROOT, 'src', 'BrownfieldNavigation.ts'), + swiftDelegate: path.join( + PACKAGE_ROOT, + 'ios', + 'BrownfieldNavigation', + 'BrownfieldNavigationDelegate.swift' + ), + objcImpl: path.join( + PACKAGE_ROOT, + 'ios', + 'BrownfieldNavigation', + 'NativeBrownfieldNavigation.mm' + ), + }; + + fs.writeFileSync(paths.turboModuleSpec, turboModuleSpec); + console.log(`\nGenerated: ${paths.turboModuleSpec}`); + + fs.writeFileSync(paths.navigationTs, brownfieldNavigationTS); + console.log(`Generated: ${paths.navigationTs}`); + + fs.writeFileSync(paths.swiftDelegate, swiftDelegate); + console.log(`Generated: ${paths.swiftDelegate}`); + + fs.writeFileSync(paths.objcImpl, objcImpl); + console.log(`Generated: ${paths.objcImpl}`); + + console.log('\nCodegen complete!'); + console.log(''); + console.log('Next steps:'); + console.log('1. Run pod install in your iOS project'); + console.log( + '2. Implement the BrownfieldNavigationDelegate protocol in your native code' + ); + console.log( + '3. Call BrownfieldNavigationManager.shared.setDelegate(yourDelegate) before using the module' + ); +} + +main(); diff --git a/yarn.lock b/yarn.lock index 8d24767a..42646d60 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2436,6 +2436,7 @@ __metadata: "@expo/config-plugins": ^54.0.4 bin: brownfield: lib/commonjs/scripts/brownfield.js + brownfield-navigation-codegen: lib/commonjs/scripts/brownfield-navigation.js languageName: unknown linkType: soft From 9d20ca71e0d36a4fe2ae3fd1b09f87c16e615e69 Mon Sep 17 00:00:00 2001 From: Hur Ali Date: Sat, 14 Feb 2026 14:58:54 +0500 Subject: [PATCH 2/6] feat: consume brownfield navigation --- .../BrownfieldAppleApp.swift | 65 +++++++++++++ .../Brownfield Apple App/ContentView.swift | 47 ++++++++++ apps/RNApp/brownfield.navigation.ts | 12 +++ apps/RNApp/ios/Podfile.lock | 92 +++++++++---------- .../RNApp/ios/RNApp.xcodeproj/project.pbxproj | 16 ++-- apps/RNApp/package.json | 3 +- apps/RNApp/src/HomeScreen.tsx | 13 +++ 7 files changed, 193 insertions(+), 55 deletions(-) create mode 100644 apps/RNApp/brownfield.navigation.ts diff --git a/apps/AppleApp/Brownfield Apple App/BrownfieldAppleApp.swift b/apps/AppleApp/Brownfield Apple App/BrownfieldAppleApp.swift index f4d35f78..7f0a04ef 100644 --- a/apps/AppleApp/Brownfield Apple App/BrownfieldAppleApp.swift +++ b/apps/AppleApp/Brownfield Apple App/BrownfieldAppleApp.swift @@ -2,6 +2,7 @@ import BrownfieldLib import Brownie import SwiftUI import ReactBrownfield +import UIKit class AppDelegate: NSObject, UIApplicationDelegate { var window: UIWindow? @@ -14,6 +15,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 @@ -23,6 +73,21 @@ struct BrownfieldAppleApp: App { ReactNativeBrownfield.shared.startReactNative { print("React Native has been loaded") } + +// let mgr = ReactBrownfield.BrownfieldNavigationManager.shared +// print("SWIFT mgr:", Unmanaged.passUnretained(mgr).toOpaque()) +// print("SWIFT class:", NSStringFromClass(type(of: mgr))) +// print("SWIFT bundle:", Bundle(for: ReactBrownfield.BrownfieldNavigationManager.self).bundlePath) +// ReactBrownfield.BrownfieldNavigationManager.shared.setDelegate(navigationDelegate: RNNavigationDelegate()) + + let mgr = BrownfieldNavigationManager.shared + print("11 SWIFT mgr:", Unmanaged.passUnretained(mgr).toOpaque()) + print("11 SWIFT class:", NSStringFromClass(type(of: mgr))) + print("11 SWIFT bundle:", Bundle(for: BrownfieldNavigationManager.self).bundlePath) + + mgr.setDelegate( + navigationDelegate: RNNavigationDelegate() + ) #if USE_EXPO_HOST ReactNativeBrownfield.shared.ensureExpoModulesProvider() diff --git a/apps/AppleApp/Brownfield Apple App/ContentView.swift b/apps/AppleApp/Brownfield Apple App/ContentView.swift index 4be7c44c..c32cbd6a 100644 --- a/apps/AppleApp/Brownfield Apple App/ContentView.swift +++ b/apps/AppleApp/Brownfield Apple App/ContentView.swift @@ -31,6 +31,53 @@ struct MainScreen: View { } } +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") + } +} + +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") + } +} + struct GreetingCard: View { let name: String @UseStore(\BrownfieldStore.counter) var counter diff --git a/apps/RNApp/brownfield.navigation.ts b/apps/RNApp/brownfield.navigation.ts new file mode 100644 index 00000000..8bc90246 --- /dev/null +++ b/apps/RNApp/brownfield.navigation.ts @@ -0,0 +1,12 @@ +export interface BrownfieldNavigationSpec { + /** + * Navigate to the native settings screen + */ + navigateToSettings(): void; + + /** + * Navigate to the native referrals screen + * @param userId - The user's unique identifier + */ + navigateToReferrals(userId: string): void; +} diff --git a/apps/RNApp/ios/Podfile.lock b/apps/RNApp/ios/Podfile.lock index 238047ad..bee616b0 100644 --- a/apps/RNApp/ios/Podfile.lock +++ b/apps/RNApp/ios/Podfile.lock @@ -1,6 +1,6 @@ PODS: - boost (1.84.0) - - Brownie (0.0.5): + - Brownie (3.0.0-rc.2): - boost - DoubleConversion - fast_float @@ -2350,7 +2350,7 @@ PODS: - SocketRocket - ReactAppDependencyProvider (0.82.1): - ReactCodegen - - ReactBrownfield (3.0.0-rc.1): + - ReactBrownfield (3.0.0-rc.2): - boost - DoubleConversion - fast_float @@ -2772,7 +2772,7 @@ EXTERNAL SOURCES: SPEC CHECKSUMS: boost: 7e761d76ca2ce687f7cc98e698152abd03a18f90 - Brownie: e4291b884a7157a2dbe6d60dd72e962078223791 + Brownie: 45f5c3689d98179147d7b72678552f0cc54b6f90 DoubleConversion: cb417026b2400c8f53ae97020b2be961b59470cb fast_float: b32c788ed9c6a8c584d114d0047beda9664e7cc6 FBLazyVector: 0aa6183b9afe3c31fc65b5d1eeef1f3c19b63bfa @@ -2788,67 +2788,67 @@ SPEC CHECKSUMS: React-Core: 956ac86b4d9b0c0fd9a14b9cc533aa297bb501c0 React-CoreModules: 3a8d39778cf9eeca40e419814e875da1a8e29855 React-cxxreact: db275765e1eb08f038599fb44114cf57ee0d18cd - React-debug: c8356d908286b1dc4cf90cd0977227dd61b7b1eb - React-defaultsnativemodule: df1a41d072194c96d0077dd30ee8d5d452397f26 - React-domnativemodule: 8abd63d26685a5c1c88c8ccc902876dc9c0e2d6f - React-Fabric: 1dea7e164181d7d688cfbd70a6e5f026e2df6bf5 - React-FabricComponents: 2a6f81481fa240a9239536402d72823f9d642925 - React-FabricImage: 513940cfd43193d3befb45dba9911f936bd74df7 - React-featureflags: bc1d980ff8356b931cd87c16700a39aaede1ed5a - React-featureflagsnativemodule: 4f7beedf0c241c44dcffc51e52a6178b5e0d541d - React-graphics: 69311413b44b6d228dbc705d8ce57ad0a4d55daf + React-debug: 1dfa1d1cbd93bdaffa3b140190829f9fd9e27985 + React-defaultsnativemodule: 35f353ba06901fb5e374bc56e750fde05cbb05b9 + React-domnativemodule: cf9e1b1b520ce0e66396c2744b3eb6d419711c13 + React-Fabric: c0b0c1ad70476d354b3da9fef96094f7b37804da + React-FabricComponents: 8c6861c5233cf0d5685cee301a979313090e2f57 + React-FabricImage: cef8883d2fb6c892003fefcad261d2898adbe926 + React-featureflags: 0e2b969019c2b118de64a6d4c55ef7c05f2b0f1d + React-featureflagsnativemodule: e1ef619d14fe0a68d4783b32293309dbb13ef2a5 + React-graphics: 0fc6b7acaff7161bda05bf8bffceacc2b0b4e38d React-hermes: b454b9352bc26e638704d103009f659a125b86d3 - React-idlecallbacksnativemodule: d15d469a152b7677d184a9538fae0744692e4575 - React-ImageManager: cce591e16cc6fa63ad5d45de012b4ddf31fd21e9 - React-jserrorhandler: 05fb248a535148a7eec94c786bd0e9e1413c6b3a + React-idlecallbacksnativemodule: 35ab292f8404c469744db5a5dd5f0f27f95e5ebf + React-ImageManager: 3312c550ebcf6b7d911d9993082adcb3e1407ce8 + React-jserrorhandler: 2a7f2d94566f05f8cb82288afd46bc0fd8b2ffc7 React-jsi: 7aa265cf8372d8385ccc7935729e76d27e694dfe React-jsiexecutor: 8dd53bebfb3bc12f0541282aa4c858a433914e37 - React-jsinspector: 0f62d1ffa7242033a1106f0af9f83ec12a381401 - React-jsinspectorcdp: 5ae22d48dcf03812cd4f8c4a6fd7c7204cd8789d - React-jsinspectornetwork: 9052eb6bbd876bfdafa1605874dd848511236844 - React-jsinspectortracing: 6d89a5caab7b86947607cf654fc94cf1c31f8330 - React-jsitooling: ecbd81f751b79ba748d4d0d54445da1b53e363fd - React-jsitracing: 8068734240da604902fead29287dc21b820bc7d3 + React-jsinspector: f89b9ae62a4e2f6035b452442ef20a7f98f9cb27 + React-jsinspectorcdp: 44e46c1473a8deecf7b188389ed409be83fb3cc7 + React-jsinspectornetwork: dc9524f6e3d7694b1b6f4bd22dedad8ccc2c0a80 + React-jsinspectortracing: 0166ebbdfb125936a5d231895de3c11a19521dfc + React-jsitooling: 34692514ec8d8735938eda3677808a58f41c925b + React-jsitracing: a598dae84a87f8013635d09c5e7884023bda8501 React-logger: 500f2fa5697d224e63c33d913c8a4765319e19bf - React-Mapbuffer: 4c50cf6af44286015a20a5995d5321f625c93459 - React-microtasksnativemodule: a84b9331106616ab1fa36de9ae555718d4bbdcf5 - react-native-safe-area-context: 0a3b034bb63a5b684dd2f5fffd3c90ef6ed41ee8 - React-NativeModulesApple: efd0906463c79d9b86197dbcf0d58358dff8c5ed + React-Mapbuffer: 06d59c448da7e34eb05b3fb2189e12f6a30fec57 + React-microtasksnativemodule: d1ee999dc9052e23f6488b730fa2d383a4ea40e5 + react-native-safe-area-context: c00143b4823773bba23f2f19f85663ae89ceb460 + React-NativeModulesApple: 46690a0fe94ec28fc6fc686ec797b911d251ded0 React-oscompat: 95875e81f5d4b3c7b2c888d5bd2c9d83450d8bdb React-perflogger: 2e229bf33e42c094fd64516d89ec1187a2b79b5b - React-performancecdpmetrics: fd9bbc52960c6aa008fdae263849eb14411ae13e - React-performancetimeline: 16eaea3f8be5d42eb3bf8a261d87df2fe7e6e111 + React-performancecdpmetrics: 05ba4bd83f36acf192071bb5d9c8f45faf04d140 + React-performancetimeline: bfc96fcd2b79f7489dd54e3df4cba186dd8dd141 React-RCTActionSheet: 2399bb6cc8adaef2e5850878102fea2ad1788a0e React-RCTAnimation: d1deb6946e83e22a795a7d0148b94faad8851644 React-RCTAppDelegate: 10b35d5cec3f8653f6de843ae800b3ba8050b801 React-RCTBlob: 85150378edc42862d7c13ff2502693f32b174f91 - React-RCTFabric: f57a14a48756480a7c96670d633cb39692eed453 - React-RCTFBReactNativeSpec: 725c3bb08b2f86741df136455960f2b58dd8f6e4 + React-RCTFabric: 736f9da3ad57e2cef5fa4c132999933a89bb8378 + React-RCTFBReactNativeSpec: 705ec584758966950a31fa235539b57523059837 React-RCTImage: bb6cbdc22698b3afc8eb8d81ef03ee840d24c6f6 React-RCTLinking: e8b006d101c45651925de3e82189f03449eedfe7 React-RCTNetwork: 7999731af05ec8f591cbc6ad4e29d79e209c581a - React-RCTRuntime: cdbbadadafcad5836fb0616073d7011c39c30ffd + React-RCTRuntime: 99d8a2a17747866fb972561cdb205afe9b26d369 React-RCTSettings: 839f334abb92e917bc24322036081ffe15c84086 React-RCTText: 272f60e9a5dbfd14c348c85881ee7d5c7749a67c React-RCTVibration: 1ffa30a21e2227be3afe28d657ac8e6616c91bae - React-rendererconsistency: a51dcbe4b3c1159413cfdb85abace6a5c871a4b3 - React-renderercss: 5fdc31a529021337e7eac6f1e9bf4410947b877e - React-rendererdebug: 8427d2e5d1b7e39971c9c59e55bbfcb7884a942f - React-RuntimeApple: 9ba3723a539ed1701b8ba08dc317f1255c269a37 - React-RuntimeCore: 61b10d50472e29cd1ec98aba797d0d8d4f325283 - React-runtimeexecutor: 8e5135a09dcb012a15a025dc514361c927ea5db9 - React-RuntimeHermes: f06c7288967d0209fc075e5eabd5e851580047e9 - React-runtimescheduler: bd92275b3a847c71d10210ae89a8e04dba076630 - React-timing: 91f11a6537770b698eb8152e4669012992710b27 - React-utils: f06ff240e06e2bd4b34e48f1b34cac00866e8979 - React-webperformancenativemodule: b3398f8175fa96d992c071b1fa59bd6f9646b840 + React-rendererconsistency: 3c3e198aba0255524ed7126aa812d22ce750d217 + React-renderercss: 6b3ce3dfadf991937ae3229112be843ef1438c32 + React-rendererdebug: baf9e1daa07ac7f9aca379555126d29f963ba38b + React-RuntimeApple: 4136aee89257894955ef09e9f9ef74f0c27596be + React-RuntimeCore: e9a743d7de4bbd741b16e10b26078d815d6513ab + React-runtimeexecutor: 781e292362066af82fa2478d95c6b0e374421844 + React-RuntimeHermes: 6ab3c2847516769fc860d711814f1735859cad74 + React-runtimescheduler: 824c83a5fd68b35396de6d4f2f9ae995daac861b + React-timing: 1ebc7102dd52a3edcc63534686bb156e12648411 + React-utils: abf37b162f560cd0e3e5d037af30bb796512246d + React-webperformancenativemodule: 50a57c713a90d27ae3ab947a6c9c8859bcb49709 ReactAppDependencyProvider: a45ef34bb22dc1c9b2ac1f74167d9a28af961176 - ReactBrownfield: ce231a9060b34e1fe8f91ec8416f21dc6da8b4b5 - ReactCodegen: 0bce2d209e2e802589f4c5ff76d21618200e74cb - ReactCommon: 801eff8cb9c940c04d3a89ce399c343ee3eff654 - RNScreens: d6413aeb1878cdafd3c721e2c5218faf5d5d3b13 + ReactBrownfield: a7706e2b5bf21861c73ef20b54674e672ea6aa96 + ReactCodegen: 878add6c7d8ff8cea87697c44d29c03b79b6f2d9 + ReactCommon: 804dc80944fa90b86800b43c871742ec005ca424 + RNScreens: ffbb0296608eb3560de641a711bbdb663ed1f6b4 SocketRocket: d4aabe649be1e368d1318fdf28a022d714d65748 - Yoga: 526f25666395d30c297d53154398ffd249eaf9e1 + Yoga: 689c8e04277f3ad631e60fe2a08e41d411daf8eb PODFILE CHECKSUM: 7c116a16dd0744063c8c6293dbfc638c9d447c19 diff --git a/apps/RNApp/ios/RNApp.xcodeproj/project.pbxproj b/apps/RNApp/ios/RNApp.xcodeproj/project.pbxproj index 36a5015f..ba391078 100644 --- a/apps/RNApp/ios/RNApp.xcodeproj/project.pbxproj +++ b/apps/RNApp/ios/RNApp.xcodeproj/project.pbxproj @@ -8,13 +8,13 @@ /* Begin PBXBuildFile section */ 13B07FBF1A68108700A75B9A /* Images.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 13B07FB51A68108700A75B9A /* Images.xcassets */; }; + 438C996BA823BB6C760606A7 /* libPods-RNApp.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 65D11050DC86B90737C67645 /* libPods-RNApp.a */; }; + 5DD191C4345A176F232A6668 /* libPods-RNApp-BrownfieldLib.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 8CF91B4A8DD47F8D8819A3AB /* libPods-RNApp-BrownfieldLib.a */; }; 761780ED2CA45674006654EE /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 761780EC2CA45674006654EE /* AppDelegate.swift */; }; 79BD1EE92EEBFB76003AA29F /* BrownfieldLib.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 79BD1EE32EEBFB76003AA29F /* BrownfieldLib.framework */; }; 79BD1EEA2EEBFB76003AA29F /* BrownfieldLib.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = 79BD1EE32EEBFB76003AA29F /* BrownfieldLib.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; 79F35E8C2EEC1D4500E64860 /* BrownfieldLib.swift in Sources */ = {isa = PBXBuildFile; fileRef = 79F35E8A2EEC1D4500E64860 /* BrownfieldLib.swift */; }; 81AB9BB82411601600AC10FF /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 81AB9BB72411601600AC10FF /* LaunchScreen.storyboard */; }; - A19F59C8902E0F7A03F2A8C6 /* Pods_RNApp.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = B6CBC8DE61C44A9AB569A3CA /* Pods_RNApp.framework */; }; - BFB326E698C6BD239F8AEAE9 /* Pods_RNApp_BrownfieldLib.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 64481842743F90E340AD414A /* Pods_RNApp_BrownfieldLib.framework */; }; C66C2A65406C527E9529D08F /* PrivacyInfo.xcprivacy in Resources */ = {isa = PBXBuildFile; fileRef = 13B07FB81A68108700A75B9A /* PrivacyInfo.xcprivacy */; }; /* End PBXBuildFile section */ @@ -49,13 +49,13 @@ 13B07FB81A68108700A75B9A /* PrivacyInfo.xcprivacy */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; name = PrivacyInfo.xcprivacy; path = RNApp/PrivacyInfo.xcprivacy; sourceTree = ""; }; 3B4392A12AC88292D35C810B /* Pods-RNApp.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-RNApp.debug.xcconfig"; path = "Target Support Files/Pods-RNApp/Pods-RNApp.debug.xcconfig"; sourceTree = ""; }; 5709B34CF0A7D63546082F79 /* Pods-RNApp.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-RNApp.release.xcconfig"; path = "Target Support Files/Pods-RNApp/Pods-RNApp.release.xcconfig"; sourceTree = ""; }; - 64481842743F90E340AD414A /* Pods_RNApp_BrownfieldLib.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_RNApp_BrownfieldLib.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + 65D11050DC86B90737C67645 /* libPods-RNApp.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; includeInIndex = 0; path = "libPods-RNApp.a"; sourceTree = BUILT_PRODUCTS_DIR; }; 761780EC2CA45674006654EE /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = AppDelegate.swift; path = RNApp/AppDelegate.swift; sourceTree = ""; }; 79BD1EE32EEBFB76003AA29F /* BrownfieldLib.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = BrownfieldLib.framework; sourceTree = BUILT_PRODUCTS_DIR; }; 79F35E8A2EEC1D4500E64860 /* BrownfieldLib.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BrownfieldLib.swift; sourceTree = ""; }; 81AB9BB72411601600AC10FF /* LaunchScreen.storyboard */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = file.storyboard; name = LaunchScreen.storyboard; path = RNApp/LaunchScreen.storyboard; sourceTree = ""; }; 8A02E03D9F74B585B0A8F7F7 /* Pods-RNApp-BrownfieldLib.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-RNApp-BrownfieldLib.debug.xcconfig"; path = "Target Support Files/Pods-RNApp-BrownfieldLib/Pods-RNApp-BrownfieldLib.debug.xcconfig"; sourceTree = ""; }; - B6CBC8DE61C44A9AB569A3CA /* Pods_RNApp.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_RNApp.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + 8CF91B4A8DD47F8D8819A3AB /* libPods-RNApp-BrownfieldLib.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; includeInIndex = 0; path = "libPods-RNApp-BrownfieldLib.a"; sourceTree = BUILT_PRODUCTS_DIR; }; D8C030F60E402FD6CFBB3904 /* Pods-RNApp-BrownfieldLib.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-RNApp-BrownfieldLib.release.xcconfig"; path = "Target Support Files/Pods-RNApp-BrownfieldLib/Pods-RNApp-BrownfieldLib.release.xcconfig"; sourceTree = ""; }; ED297162215061F000B7C4FE /* JavaScriptCore.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = JavaScriptCore.framework; path = System/Library/Frameworks/JavaScriptCore.framework; sourceTree = SDKROOT; }; /* End PBXFileReference section */ @@ -66,7 +66,7 @@ buildActionMask = 2147483647; files = ( 79BD1EE92EEBFB76003AA29F /* BrownfieldLib.framework in Frameworks */, - A19F59C8902E0F7A03F2A8C6 /* Pods_RNApp.framework in Frameworks */, + 438C996BA823BB6C760606A7 /* libPods-RNApp.a in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -74,7 +74,7 @@ isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( - BFB326E698C6BD239F8AEAE9 /* Pods_RNApp_BrownfieldLib.framework in Frameworks */, + 5DD191C4345A176F232A6668 /* libPods-RNApp-BrownfieldLib.a in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -97,8 +97,8 @@ isa = PBXGroup; children = ( ED297162215061F000B7C4FE /* JavaScriptCore.framework */, - B6CBC8DE61C44A9AB569A3CA /* Pods_RNApp.framework */, - 64481842743F90E340AD414A /* Pods_RNApp_BrownfieldLib.framework */, + 65D11050DC86B90737C67645 /* libPods-RNApp.a */, + 8CF91B4A8DD47F8D8819A3AB /* libPods-RNApp-BrownfieldLib.a */, ); name = Frameworks; sourceTree = ""; diff --git a/apps/RNApp/package.json b/apps/RNApp/package.json index 07ea82c4..4c818bdb 100644 --- a/apps/RNApp/package.json +++ b/apps/RNApp/package.json @@ -13,7 +13,8 @@ "lint": "eslint .", "start": "react-native start", "test": "jest", - "codegen": "brownfield codegen" + "codegen": "brownfield codegen", + "brownfield:navigation-codegen": "brownfield-navigation-codegen brownfield.navigation.ts" }, "dependencies": { "@callstack/brownie": "workspace:^", diff --git a/apps/RNApp/src/HomeScreen.tsx b/apps/RNApp/src/HomeScreen.tsx index 99131782..71b932ef 100644 --- a/apps/RNApp/src/HomeScreen.tsx +++ b/apps/RNApp/src/HomeScreen.tsx @@ -2,6 +2,7 @@ import { useEffect } from 'react'; import { StyleSheet, Text, View, Button } from 'react-native'; import type { NativeStackScreenProps } from '@react-navigation/native-stack'; import ReactNativeBrownfield from '@callstack/react-native-brownfield'; +import { BrownfieldNavigation } from '@callstack/react-native-brownfield'; import { getRandomTheme } from './utils'; import type { RootStackParamList } from './navigation/RootStack'; @@ -50,6 +51,18 @@ export function HomeScreen({ color={colors.secondary} title="Go back" /> + +