Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
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
45 changes: 45 additions & 0 deletions .yarn/patches/expo-updates-npm-29.0.16-1c5c89eb83.patch
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
diff --git a/ios/EXUpdates/UpdatesConfig.swift b/ios/EXUpdates/UpdatesConfig.swift
index 09a1e145a9f5a938e10dd3c59eedfd213ad54346..787c1a8f7ae6a010fff4200ffebe31742e9a25eb 100644
--- a/ios/EXUpdates/UpdatesConfig.swift
+++ b/ios/EXUpdates/UpdatesConfig.swift
@@ -141,7 +141,8 @@ public final class UpdatesConfig: NSObject {
}

private static func configDictionaryWithExpoPlist(mergingOtherDictionary: [String: Any]?) throws -> [String: Any] {
- guard let configPlistPath = Bundle.main.path(forResource: PlistName, ofType: "plist") else {
+ let bundle = Bundle(for: UpdatesConfig.self)
+ guard let configPlistPath = bundle.path(forResource: PlistName, ofType: "plist") else {
throw UpdatesConfigError.ExpoUpdatesConfigPlistError
}

diff --git a/ios/EXUpdates/UpdatesUtils.swift b/ios/EXUpdates/UpdatesUtils.swift
index a1e2c9af4ebaf9e50db2acaabfa876fc46d22454..0b96a7062dbcd021339a9daeb4397b3c7715b96d 100644
--- a/ios/EXUpdates/UpdatesUtils.swift
+++ b/ios/EXUpdates/UpdatesUtils.swift
@@ -108,18 +108,22 @@ public final class UpdatesUtils: NSObject {
return assetFilesMap
}

+ private static func getBundle() -> Bundle {
+ return Bundle(for: UpdatesUtils.self)
+ }
+
internal static func url(forBundledAsset asset: UpdateAsset) -> URL? {
guard let mainBundleDir = asset.mainBundleDir else {
- return Bundle.main.url(forResource: asset.mainBundleFilename, withExtension: asset.type)
+ return getBundle().url(forResource: asset.mainBundleFilename, withExtension: asset.type)
}
- return Bundle.main.url(forResource: asset.mainBundleFilename, withExtension: asset.type, subdirectory: mainBundleDir)
+ return getBundle().url(forResource: asset.mainBundleFilename, withExtension: asset.type, subdirectory: mainBundleDir)
}

internal static func path(forBundledAsset asset: UpdateAsset) -> String? {
guard let mainBundleDir = asset.mainBundleDir else {
- return Bundle.main.path(forResource: asset.mainBundleFilename, ofType: asset.type)
+ return getBundle().path(forResource: asset.mainBundleFilename, ofType: asset.type)
}
- return Bundle.main.path(forResource: asset.mainBundleFilename, ofType: asset.type, inDirectory: mainBundleDir)
+ return getBundle().path(forResource: asset.mainBundleFilename, ofType: asset.type, inDirectory: mainBundleDir)
}

/**
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@
shouldAutocreateTestPlan = "YES">
</TestAction>
<LaunchAction
buildConfiguration = "Debug"
buildConfiguration = "Debug Expo"
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
launchStyle = "0"
Expand Down
4 changes: 4 additions & 0 deletions apps/AppleApp/Brownfield Apple App/BrownfieldAppleApp.swift
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,10 @@ class AppDelegate: NSObject, UIApplicationDelegate {
didFinishLaunchingWithOptions: launchOptions
)
}

func application(_ application: UIApplication, willFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey : Any]? = nil) -> Bool {
return ReactNativeBrownfield.shared.application(application, willFinishLaunchingWithOptions: launchOptions)
}
}

public class RNNavigationDelegate: BrownfieldNavigationDelegate {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,4 +19,4 @@ struct SettingsScreen: View {
}
.navigationTitle("Settings")
}
}
}
4 changes: 3 additions & 1 deletion apps/ExpoApp54/RNApp.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,12 @@ import { Button, StyleSheet, Text, View } from 'react-native';
import BrownfieldNavigation from '@callstack/brownfield-navigation';

import Counter from './components/counter';
import { checkAndFetchUpdate } from './utils/expo-rn-updates';

export default function RNApp() {
return (
<SafeAreaView style={styles.container}>
<Text style={styles.title}>Expo React Native Brownfield</Text>
<Text style={styles.title}>123123 Expo React Native Brownfield</Text>

<View style={styles.content}>
<Counter />
Expand All @@ -20,6 +21,7 @@ export default function RNApp() {
title="Navigate to Referrals"
onPress={() => BrownfieldNavigation.navigateToReferrals('123')}
/>
<Button title="Fetch Update" onPress={checkAndFetchUpdate} />
</View>
</SafeAreaView>
);
Expand Down
23 changes: 22 additions & 1 deletion apps/ExpoApp54/app.json
Original file line number Diff line number Diff line change
Expand Up @@ -41,11 +41,32 @@
}
}
],
"@callstack/react-native-brownfield"
[
"@callstack/react-native-brownfield",
{
"debug": true
}
]
],
"experiments": {
"typedRoutes": true,
"reactCompiler": true
},
"extra": {
"router": {},
"eas": {
"projectId": "5696d23a-8a54-484c-9a7c-c473d0f28c05"
}
},
"runtimeVersion": {
"policy": "appVersion"
},
"updates": {
"checkAutomatically": "NEVER",
"url": "https://u.expo.dev/5696d23a-8a54-484c-9a7c-c473d0f28c05",
"requestHeaders": {
"expo-channel-name": "production"
}
}
}
}
4 changes: 3 additions & 1 deletion apps/ExpoApp54/app/(tabs)/index.tsx
Original file line number Diff line number Diff line change
@@ -1,11 +1,12 @@
import { Image } from 'expo-image';
import { Link } from 'expo-router';
import { Platform, StyleSheet, View } from 'react-native';
import { Button, Platform, StyleSheet, View } from 'react-native';

import { HelloWave } from '@/components/hello-wave';
import ParallaxScrollView from '@/components/parallax-scroll-view';
import { ThemedText } from '@/components/themed-text';
import { ThemedView } from '@/components/themed-view';
import { checkAndFetchUpdate } from '@/utils/expo-rn-updates';

export default function HomeScreen() {
return (
Expand All @@ -24,6 +25,7 @@ export default function HomeScreen() {
<HelloWave />
</ThemedView>
<ThemedView style={styles.stepContainer}>
<Button title="Fetch Update" onPress={checkAndFetchUpdate} />
<ThemedText type="subtitle">Step 1: Try it</ThemedText>
<ThemedText>
Edit{' '}
Expand Down
4 changes: 3 additions & 1 deletion apps/ExpoApp54/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,8 @@
"brownfield:prepare:android:ci": "cd .. && node --experimental-strip-types --no-warnings ./scripts/prepare-android-build-gradle-for-ci.ts ExpoApp54",
"brownfield:package:android": "brownfield package:android --module-name brownfieldlib --variant release",
"brownfield:publish:android": "brownfield publish:android --module-name brownfieldlib",
"brownfield:package:ios": "brownfield package:ios --scheme BrownfieldLib --configuration Release"
"brownfield:package:ios": "EX_UPDATES_CUSTOM_INIT=1 brownfield package:ios --scheme BrownfieldLib --configuration Release",
"eas:prod": "eas update --channel production --message 'testing 8th prod channel update' --platform ios"
},
"dependencies": {
"@callstack/brownfield-navigation": "workspace:^",
Expand All @@ -31,6 +32,7 @@
"expo-status-bar": "~3.0.9",
"expo-symbols": "~1.0.8",
"expo-system-ui": "~6.0.9",
"expo-updates": "patch:expo-updates@npm%3A29.0.16#~/.yarn/patches/expo-updates-npm-29.0.16-1c5c89eb83.patch",
"expo-web-browser": "~15.0.10",
"react": "19.1.0",
"react-dom": "19.1.0",
Expand Down
25 changes: 25 additions & 0 deletions apps/ExpoApp54/utils/expo-rn-updates.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import * as Updates from 'expo-updates';
import { Alert } from 'react-native';

export async function checkAndFetchUpdate() {
try {
const update = await Updates.checkForUpdateAsync();
if (update.isAvailable) {
await Updates.fetchUpdateAsync();

await Updates.reloadAsync({
reloadScreenOptions: {
spinner: {
enabled: true,
color: 'red',
size: 'large',
},
},
});
} else {
Alert.alert('No update available');
}
} catch (error) {
Alert.alert('Update check failed', String(error));
}
}
1 change: 1 addition & 0 deletions packages/react-native-brownfield/ReactBrownfield.podspec
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ Pod::Spec.new do |spec|

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

install_modules_dependencies(spec)
Expand Down
28 changes: 23 additions & 5 deletions packages/react-native-brownfield/ios/ExpoHostRuntime.swift
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,9 @@ internal import ReactAppDependencyProvider

#if canImport(Expo)
internal import Expo
#if canImport(EXUpdates)
internal import EXUpdates
#endif

final class ExpoHostRuntime {
static let shared = ExpoHostRuntime()
Expand Down Expand Up @@ -40,7 +43,7 @@ final class ExpoHostRuntime {
appDelegate.bindReactNativeFactory(factory)
#endif
expoDelegate = appDelegate

if let onBundleLoaded {
jsBundleLoadObserver.observeOnce(onBundleLoaded: onBundleLoaded)
}
Expand Down Expand Up @@ -90,10 +93,12 @@ final class ExpoHostRuntime {
_ application: UIApplication,
didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]? = nil
) -> Bool {
return expoDelegate?.application(
application,
didFinishLaunchingWithOptions: launchOptions
) != nil
#if canImport(EXUpdates)
if !AppController.isInitialized() {
AppController.initializeWithoutStarting()
}
#endif
return ExpoAppDelegateSubscriberManager.application(application, didFinishLaunchingWithOptions: launchOptions)
}

// Linking API; base implementation courtesy of Expo, licensed under the MIT License - changes were made to call the method on expo delegate - https://github.com/expo/expo/blob/main/apps/bare-expo/ios/AppDelegate.swift
Expand All @@ -114,6 +119,13 @@ final class ExpoHostRuntime {
let result = RCTLinkingManager.application(application, continue: userActivity, restorationHandler: restorationHandler)
return (expoDelegate?.application(application, continue: userActivity, restorationHandler: restorationHandler) ?? false) || result
}

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

func view(
moduleName: String,
Expand Down Expand Up @@ -159,6 +171,12 @@ class ExpoHostRuntimeDelegate: ExpoReactNativeFactoryDelegate {
return RCTBundleURLProvider.sharedSettings().jsBundleURL(
forBundleRoot: entryFile)
#else
#if canImport(EXUpdates)
if AppController.isInitialized(),
let launchAssetURL = AppController.sharedInstance.launchAssetUrl() {
return launchAssetURL
}
#endif
do {
let (resourceName, fileExtension) = try BrownfieldBundlePathResolver.resourceComponents(
from: bundlePath
Expand Down
17 changes: 17 additions & 0 deletions packages/react-native-brownfield/ios/ReactNativeBrownfield.swift
Original file line number Diff line number Diff line change
Expand Up @@ -122,6 +122,23 @@ internal import Expo
#endif
}

@objc public func application(
_ application: UIApplication,
willFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]? = nil
) -> Bool {
#if canImport(Expo)
return ExpoHostRuntime.shared.application(
application,
willFinishLaunchingWithOptions: launchOptions
)
#else
return ReactNativeHostRuntime.shared.application(
application,
willFinishLaunchingWithOptions: launchOptions
)
#endif
}

@objc public func application(
_ app: UIApplication,
open url: URL,
Expand Down
10 changes: 10 additions & 0 deletions packages/react-native-brownfield/ios/ReactNativeHostRuntime.swift
Original file line number Diff line number Diff line change
Expand Up @@ -120,6 +120,16 @@ final class ReactNativeHostRuntime {
return true
}

/**
* Mirrors host manager app delegate API for bare React Native.
*/
public func application(
_ application: UIApplication,
willFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]? = nil
) -> Bool {
return true
}

public func application(
_ app: UIApplication,
open url: URL,
Expand Down
Original file line number Diff line number Diff line change
@@ -1,13 +1,25 @@
import UIKit
internal import React
#if canImport(EXUpdates)
internal import EXUpdates
#endif

@objc public class ReactNativeViewController: UIViewController {
private var moduleName: String
private var initialProperties: [String: Any]?

#if canImport(EXUpdates)
private let expoUpdatesDelegate = ReactNativeExpoUpdatesDelegate()
#endif


@objc public init(moduleName: String, initialProperties: [String: Any]? = nil) {
self.moduleName = moduleName
self.initialProperties = initialProperties
#if canImport(EXUpdates)
AppController.sharedInstance.delegate = expoUpdatesDelegate
AppController.sharedInstance.start()
#endif
super.init(nibName: nil, bundle: nil)
}

Expand All @@ -17,14 +29,17 @@ internal import React

public override func viewDidLoad() {
super.viewDidLoad()
#if canImport(EXUpdates)
expoUpdatesDelegate.onDidStart = { [weak self] in
self?.renderReactNativeView()
}
#endif

if !moduleName.isEmpty {
view = ReactNativeBrownfield.shared.view(
moduleName: moduleName,
initialProps: initialProperties,
launchOptions: nil
)

#if !canImport(EXUpdates)
renderReactNativeView()
#endif

NotificationCenter.default.addObserver(
self,
selector: #selector(togglePopGestureRecognizer(_:)),
Expand Down Expand Up @@ -62,4 +77,26 @@ internal import React
self?.navigationController?.popViewController(animated: animated)
}
}

private func renderReactNativeView() {
guard !moduleName.isEmpty else { return }

DispatchQueue.main.async { [weak self] in
guard let self else { return }
guard let reactView = ReactNativeBrownfield.shared.view(
moduleName: self.moduleName,
initialProps: self.initialProperties,
launchOptions: nil
) else { return }
self.view = reactView
}
}
}

private final class ReactNativeExpoUpdatesDelegate: NSObject, AppControllerDelegate {
var onDidStart: (() -> Void)?

func appController(_ appController: any EXUpdates.AppControllerInterface, didStartWithSuccess success: Bool) {
onDidStart?()
}
}
Loading
Loading