expo-updates 1.0.0-canary-20250306-d9d3e02 → 1.0.0-canary-20250331-817737a

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -10,6 +10,7 @@
10
10
  ### 🎉 New features
11
11
 
12
12
  - Add new state machine context about startup procedure. ([#32433](https://github.com/expo/expo/pull/32433) by [@wschurman](https://github.com/wschurman))
13
+ - Support for updates.useNativeDebug. ([#35468](https://github.com/expo/expo/pull/35468) by [@douglowder](https://github.com/douglowder))
13
14
 
14
15
  ### 🐛 Bug fixes
15
16
 
@@ -25,10 +26,29 @@
25
26
  - Fixed build error on iOS Expo Go. ([#34485](https://github.com/expo/expo/pull/34485) by [@kudo](https://github.com/kudo))
26
27
  - Fixed Android unit test errors in BuilDataTest. ([#34510](https://github.com/expo/expo/pull/34510) by [@kudo](https://github.com/kudo))
27
28
  - [Android] Started using expo modules gradle plugin. ([#34806](https://github.com/expo/expo/pull/34806) by [@lukmccall](https://github.com/lukmccall))
28
- - Add update id headers to asset requests ([#34453](https://github.com/expo/expo/pull/34453) by [@gabrieldonadel](https://github.com/gabrieldonadel))
29
29
  - Drop `fs-extra` in favor of `fs`. ([#35036](https://github.com/expo/expo/pull/35036) by [@kitten](https://github.com/kitten))
30
30
  - Drop `fast-glob` in favor of `glob`. ([#35082](https://github.com/expo/expo/pull/35082) by [@kitten](https://github.com/kitten))
31
31
  - Drop `fbemitter` in favor of custom emitter. ([#35317](https://github.com/expo/expo/pull/35317) by [@kitten](https://github.com/kitten))
32
+ - E2E tests for custom init. ([#35569](https://github.com/expo/expo/pull/35569) by [@douglowder](https://github.com/douglowder))
33
+ - Refactored `RCTReactNativeFactory` integration. ([#35679](https://github.com/expo/expo/pull/35679) by [@kudo](https://github.com/kudo))
34
+
35
+ ## 0.27.4 - 2025-03-18
36
+
37
+ ### 🎉 New features
38
+
39
+ - Support brownfield apps with EX_UPDATES_CUSTOM_INIT flag. ([#35391](https://github.com/expo/expo/pull/35391) by [@douglowder](https://github.com/douglowder))
40
+
41
+ ## 0.27.3 - 2025-03-11
42
+
43
+ ### 🐛 Bug fixes
44
+
45
+ - Pass through the package version to config plugin sync utilities ([#35372](https://github.com/expo/expo/pull/35372) by [@brentvatne](https://github.com/brentvatne))
46
+
47
+ ## 0.27.2 - 2025-02-26
48
+
49
+ ### 💡 Others
50
+
51
+ - Add update id headers to asset requests ([#34453](https://github.com/expo/expo/pull/34453) by [@gabrieldonadel](https://github.com/gabrieldonadel))
32
52
 
33
53
  ## 0.27.1 - 2025-02-21
34
54
 
@@ -60,6 +60,10 @@ def getBoolStringFromPropOrEnv(String name, Boolean defaultValue) {
60
60
  // debug builds. (default false)
61
61
  def exUpdatesNativeDebug = getBoolStringFromPropOrEnv("EX_UPDATES_NATIVE_DEBUG", false)
62
62
 
63
+ // If true, app is using custom code to initialize expo-updates, so default initialization code
64
+ // will be disabled.
65
+ def exUpdatesCustomInit = getBoolStringFromPropOrEnv("EX_UPDATES_CUSTOM_INIT", false)
66
+
63
67
  // If true, code will run that delays app loading until updates is initialized, to prevent ANR issues.
64
68
  // (default true)
65
69
  def exUpdatesAndroidDelayLoadApp = getBoolStringFromPropOrEnv("EX_UPDATES_ANDROID_DELAY_LOAD_APP", true)
@@ -79,6 +83,7 @@ android {
79
83
  testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
80
84
 
81
85
  buildConfigField("boolean", "EX_UPDATES_NATIVE_DEBUG", exUpdatesNativeDebug)
86
+ buildConfigField("boolean", "EX_UPDATES_CUSTOM_INIT", exUpdatesCustomInit)
82
87
  buildConfigField("boolean", "EX_UPDATES_ANDROID_DELAY_LOAD_APP", exUpdatesAndroidDelayLoadApp)
83
88
  buildConfigField("boolean", "USE_DEV_CLIENT", useDevClient.toString())
84
89
  }
@@ -108,7 +108,7 @@ interface IUpdatesController {
108
108
  this["runtimeVersion"] = runtimeVersion ?: ""
109
109
  this["checkAutomatically"] = checkOnLaunch.toJSString()
110
110
  this["channel"] = requestHeaders["expo-channel-name"] ?: ""
111
- this["shouldDeferToNativeForAPIMethodAvailabilityInDevelopment"] = shouldDeferToNativeForAPIMethodAvailabilityInDevelopment || BuildConfig.EX_UPDATES_NATIVE_DEBUG
111
+ this["shouldDeferToNativeForAPIMethodAvailabilityInDevelopment"] = shouldDeferToNativeForAPIMethodAvailabilityInDevelopment || UpdatesPackage.isUsingNativeDebug
112
112
  this["initialContext"] = initialContext.bundle
113
113
 
114
114
  if (launchedUpdate != null) {
@@ -35,7 +35,7 @@ object UpdatesController {
35
35
  }
36
36
  val useDeveloperSupport =
37
37
  (context as? ReactApplication)?.reactNativeHost?.useDeveloperSupport ?: false
38
- if (useDeveloperSupport && !BuildConfig.EX_UPDATES_NATIVE_DEBUG) {
38
+ if (useDeveloperSupport && !UpdatesPackage.isUsingNativeDebug) {
39
39
  if (BuildConfig.USE_DEV_CLIENT) {
40
40
  val devLauncherController = initializeAsDevLauncherWithoutStarting(context)
41
41
  singletonInstance = devLauncherController
@@ -22,7 +22,6 @@ import kotlinx.coroutines.withContext
22
22
  * applicable environments.
23
23
  */
24
24
  class UpdatesPackage : Package {
25
- private val useNativeDebug = BuildConfig.EX_UPDATES_NATIVE_DEBUG
26
25
 
27
26
  override fun createReactNativeHostHandlers(context: Context): List<ReactNativeHostHandler> {
28
27
  val handler: ReactNativeHostHandler = object : ReactNativeHostHandler {
@@ -57,12 +56,12 @@ class UpdatesPackage : Package {
57
56
  override fun createReactActivityHandlers(activityContext: Context): List<ReactActivityHandler> {
58
57
  val handler = object : ReactActivityHandler {
59
58
  override fun getDelayLoadAppHandler(activity: ReactActivity, reactNativeHost: ReactNativeHost): ReactActivityHandler.DelayLoadAppHandler? {
60
- if (!BuildConfig.EX_UPDATES_ANDROID_DELAY_LOAD_APP) {
59
+ if (!BuildConfig.EX_UPDATES_ANDROID_DELAY_LOAD_APP || isUsingCustomInit) {
61
60
  return null
62
61
  }
63
62
  val context = activity.applicationContext
64
63
  val useDeveloperSupport = reactNativeHost.useDeveloperSupport
65
- if (!useDeveloperSupport || BuildConfig.EX_UPDATES_NATIVE_DEBUG) {
64
+ if (!useDeveloperSupport || isUsingNativeDebug) {
66
65
  return ReactActivityHandler.DelayLoadAppHandler { whenReadyRunnable ->
67
66
  CoroutineScope(Dispatchers.IO).launch {
68
67
  startUpdatesController(context)
@@ -76,9 +75,11 @@ class UpdatesPackage : Package {
76
75
  @WorkerThread
77
76
  private suspend fun startUpdatesController(context: Context) {
78
77
  withContext(Dispatchers.IO) {
79
- UpdatesController.initialize(context)
80
- // Call the synchronous `launchAssetFile()` function to wait for updates ready
81
- UpdatesController.instance.launchAssetFile
78
+ if (!UpdatesPackage.isUsingCustomInit) {
79
+ UpdatesController.initialize(context)
80
+ // Call the synchronous `launchAssetFile()` function to wait for updates ready
81
+ UpdatesController.instance.launchAssetFile
82
+ }
82
83
  }
83
84
  }
84
85
 
@@ -119,5 +120,7 @@ class UpdatesPackage : Package {
119
120
 
120
121
  companion object {
121
122
  private val TAG = UpdatesPackage::class.java.simpleName
123
+ val isUsingNativeDebug = BuildConfig.EX_UPDATES_NATIVE_DEBUG
124
+ internal val isUsingCustomInit = BuildConfig.EX_UPDATES_CUSTOM_INIT
122
125
  }
123
126
  }
@@ -3,6 +3,7 @@ package expo.modules.updates.errorrecovery
3
3
  import android.os.Handler
4
4
  import android.os.HandlerThread
5
5
  import com.facebook.react.bridge.DefaultJSExceptionHandler
6
+ import com.facebook.react.bridge.JSExceptionHandler
6
7
  import com.facebook.react.bridge.ReactMarker
7
8
  import com.facebook.react.bridge.ReactMarker.MarkerListener
8
9
  import com.facebook.react.bridge.ReactMarkerConstants
@@ -114,11 +115,8 @@ class ErrorRecovery(
114
115
  return
115
116
  }
116
117
 
117
- val defaultJSExceptionHandler = object : DefaultJSExceptionHandler() {
118
- override fun handleException(e: Exception) {
119
- this@ErrorRecovery.handleException(e)
120
- }
121
- }
118
+ val defaultJSExceptionHandler = JSExceptionHandler { e -> this@ErrorRecovery.handleException(e) }
119
+
122
120
  val devSupportManagerClass = devSupportManager.javaClass
123
121
  previousExceptionHandler = devSupportManagerClass.getDeclaredField("defaultJSExceptionHandler").let { field ->
124
122
  field.isAccessible = true
@@ -32,13 +32,14 @@ async function syncConfigurationToNativeAndroidAsync(options) {
32
32
  isPublicConfig: false,
33
33
  skipSDKVersionRequirement: true,
34
34
  });
35
+ const packageVersion = require('../../package.json').version;
35
36
  // sync AndroidManifest.xml
36
37
  const androidManifestPath = await config_plugins_1.AndroidConfig.Paths.getAndroidManifestAsync(options.projectRoot);
37
38
  if (!androidManifestPath) {
38
39
  throw new Error(`Could not find AndroidManifest.xml in project directory: "${options.projectRoot}"`);
39
40
  }
40
41
  const androidManifest = await config_plugins_1.AndroidConfig.Manifest.readAndroidManifestAsync(androidManifestPath);
41
- const updatedAndroidManifest = await config_plugins_1.AndroidConfig.Updates.setUpdatesConfigAsync(options.projectRoot, exp, androidManifest);
42
+ const updatedAndroidManifest = await config_plugins_1.AndroidConfig.Updates.setUpdatesConfigAsync(options.projectRoot, exp, androidManifest, packageVersion);
42
43
  await config_plugins_1.AndroidConfig.Manifest.writeAndroidManifestAsync(androidManifestPath, updatedAndroidManifest);
43
44
  // sync strings.xml
44
45
  const stringsJSONPath = await config_plugins_1.AndroidConfig.Strings.getProjectStringsXMLPathAsync(options.projectRoot);
@@ -53,8 +54,9 @@ async function syncConfigurationToNativeIosAsync(options) {
53
54
  isPublicConfig: false,
54
55
  skipSDKVersionRequirement: true,
55
56
  });
57
+ const packageVersion = require('../../package.json').version;
56
58
  const expoPlist = await readExpoPlistAsync(options.projectRoot);
57
- const updatedExpoPlist = await config_plugins_1.IOSConfig.Updates.setUpdatesConfigAsync(options.projectRoot, exp, expoPlist);
59
+ const updatedExpoPlist = await config_plugins_1.IOSConfig.Updates.setUpdatesConfigAsync(options.projectRoot, exp, expoPlist, packageVersion);
58
60
  await writeExpoPlistAsync(options.projectRoot, updatedExpoPlist);
59
61
  }
60
62
  async function readExpoPlistAsync(projectDir) {
@@ -41,6 +41,8 @@ async function syncConfigurationToNativeAndroidAsync(
41
41
  skipSDKVersionRequirement: true,
42
42
  });
43
43
 
44
+ const packageVersion = require('../../package.json').version;
45
+
44
46
  // sync AndroidManifest.xml
45
47
  const androidManifestPath = await AndroidConfig.Paths.getAndroidManifestAsync(
46
48
  options.projectRoot
@@ -56,7 +58,8 @@ async function syncConfigurationToNativeAndroidAsync(
56
58
  const updatedAndroidManifest = await AndroidConfig.Updates.setUpdatesConfigAsync(
57
59
  options.projectRoot,
58
60
  exp,
59
- androidManifest
61
+ androidManifest,
62
+ packageVersion
60
63
  );
61
64
  await AndroidConfig.Manifest.writeAndroidManifestAsync(
62
65
  androidManifestPath,
@@ -88,11 +91,14 @@ async function syncConfigurationToNativeIosAsync(
88
91
  skipSDKVersionRequirement: true,
89
92
  });
90
93
 
94
+ const packageVersion = require('../../package.json').version;
95
+
91
96
  const expoPlist = await readExpoPlistAsync(options.projectRoot);
92
97
  const updatedExpoPlist = await IOSConfig.Updates.setUpdatesConfigAsync(
93
98
  options.projectRoot,
94
99
  exp,
95
- expoPlist
100
+ expoPlist,
101
+ packageVersion
96
102
  );
97
103
  await writeExpoPlistAsync(options.projectRoot, updatedExpoPlist);
98
104
  }
@@ -0,0 +1,141 @@
1
+ import Expo
2
+ import EXUpdates
3
+ import React
4
+ import UIKit
5
+
6
+ @UIApplicationMain
7
+ class AppDelegate: ExpoAppDelegate {
8
+ var launchOptions: [UIApplication.LaunchOptionsKey: Any]?
9
+ // AppDelegate keeps a nullable reference to the updates controller
10
+ var updatesController: (any InternalAppControllerInterface)?
11
+
12
+ let packagerUrl = URL(string: "http://localhost:8081/index.bundle?platform=ios&dev=true")
13
+ let bundledUrl = Bundle.main.url(forResource: "main", withExtension: "jsbundle")
14
+
15
+ static func shared() -> AppDelegate {
16
+ guard let delegate = UIApplication.shared.delegate as? AppDelegate else {
17
+ fatalError("Could not get app delegate")
18
+ }
19
+ return delegate
20
+ }
21
+
22
+ override func bundleURL() -> URL? {
23
+ if AppDelegate.isRunningWithPackager() {
24
+ return packagerUrl
25
+ }
26
+ if let updatesUrl = updatesController?.launchAssetUrl() {
27
+ return updatesUrl
28
+ }
29
+ return bundledUrl
30
+ }
31
+
32
+ // If this is a debug build, and native debugging not enabled,
33
+ // then this returns true.
34
+ static func isRunningWithPackager() -> Bool {
35
+ return EXAppDefines.APP_DEBUG && !UpdatesUtils.isNativeDebuggingEnabled()
36
+ }
37
+
38
+ // Required initialization of react-native and expo-updates
39
+ private func initializeReactNativeAndUpdates(_ launchOptions: [UIApplication.LaunchOptionsKey: Any]?) {
40
+ self.launchOptions = launchOptions
41
+ self.moduleName = "main"
42
+ self.initialProps = [:]
43
+ self.reactNativeFactory = ExpoReactNativeFactory(delegate: self, reactDelegate: self.reactDelegate)
44
+ // AppController instance must always be created first.
45
+ // expo-updates creates a different type of controller
46
+ // depending on whether updates is enabled, and whether
47
+ // we are running in development mode or not.
48
+ AppController.initializeWithoutStarting()
49
+ }
50
+
51
+ /**
52
+ Application launch initializes the custom view controller: all React Native
53
+ and updates initialization is handled there
54
+ */
55
+ override func application(
56
+ _ application: UIApplication,
57
+ didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]? = nil
58
+ ) -> Bool {
59
+ initializeReactNativeAndUpdates(launchOptions)
60
+
61
+ // Create custom view controller, where the React Native view will be created
62
+ self.window = UIWindow(frame: UIScreen.main.bounds)
63
+ let controller = CustomViewController()
64
+ controller.view.clipsToBounds = true
65
+ self.window?.rootViewController = controller
66
+ window?.makeKeyAndVisible()
67
+
68
+ return true
69
+ }
70
+
71
+ override func application(_ app: UIApplication, open url: URL, options: [UIApplication.OpenURLOptionsKey: Any] = [:]) -> Bool {
72
+ return super.application(app, open: url, options: options) ||
73
+ RCTLinkingManager.application(app, open: url, options: options)
74
+ }
75
+ }
76
+
77
+ /**
78
+ Custom view controller that handles React Native and expo-updates initialization
79
+ */
80
+ public class CustomViewController: UIViewController, AppControllerDelegate {
81
+ let appDelegate = AppDelegate.shared()
82
+
83
+ /**
84
+ If updates is enabled, the initializer starts the expo-updates system,
85
+ and view initialization is deferred to the expo-updates completion handler (onSuccess())
86
+ */
87
+ public convenience init() {
88
+ self.init(nibName: nil, bundle: nil)
89
+ self.view.backgroundColor = .clear
90
+ if AppDelegate.isRunningWithPackager() {
91
+ // No expo-updates, just create the view
92
+ createView()
93
+ } else {
94
+ // Set the updatesController property in AppDelegate so its bundleURL() method
95
+ // works as expected
96
+ appDelegate.updatesController = AppController.sharedInstance
97
+ AppController.sharedInstance.delegate = self
98
+ AppController.sharedInstance.start()
99
+ }
100
+ }
101
+
102
+ required public override init(nibName nibNameOrNil: String?, bundle nibBundleOrNil: Bundle?) {
103
+ super.init(nibName: nibNameOrNil, bundle: nibBundleOrNil)
104
+ }
105
+
106
+ @available(*, unavailable)
107
+ required public init?(coder aDecoder: NSCoder) {
108
+ fatalError("init(coder:) has not been implemented")
109
+ }
110
+
111
+ /**
112
+ expo-updates completion handler creates the root view and adds it to the controller's view
113
+ */
114
+ public func appController(
115
+ _ appController: AppControllerInterface,
116
+ didStartWithSuccess success: Bool
117
+ ) {
118
+ createView()
119
+ }
120
+
121
+ private func createView() {
122
+ guard let rootViewFactory: RCTRootViewFactory = appDelegate.reactNativeFactory?.rootViewFactory else {
123
+ fatalError("rootViewFactory has not been initialized")
124
+ }
125
+ let rootView = rootViewFactory.view(
126
+ withModuleName: appDelegate.moduleName,
127
+ initialProperties: appDelegate.initialProps,
128
+ launchOptions: appDelegate.launchOptions
129
+ )
130
+ let controller = self
131
+ controller.view.clipsToBounds = true
132
+ controller.view.addSubview(rootView)
133
+ rootView.translatesAutoresizingMaskIntoConstraints = false
134
+ NSLayoutConstraint.activate([
135
+ rootView.topAnchor.constraint(equalTo: controller.view.safeAreaLayoutGuide.topAnchor),
136
+ rootView.bottomAnchor.constraint(equalTo: controller.view.safeAreaLayoutGuide.bottomAnchor),
137
+ rootView.leadingAnchor.constraint(equalTo: controller.view.safeAreaLayoutGuide.leadingAnchor),
138
+ rootView.trailingAnchor.constraint(equalTo: controller.view.safeAreaLayoutGuide.trailingAnchor)
139
+ ])
140
+ }
141
+ }
@@ -0,0 +1,49 @@
1
+ package dev.expo.updatese2e
2
+
3
+ import android.content.Context
4
+ import android.os.Bundle
5
+ import com.facebook.react.ReactActivity
6
+ import com.facebook.react.ReactActivityDelegate
7
+ import com.facebook.react.defaults.DefaultNewArchitectureEntryPoint.fabricEnabled
8
+ import com.facebook.react.defaults.DefaultReactActivityDelegate
9
+ import expo.modules.ReactActivityDelegateWrapper
10
+ import expo.modules.updates.UpdatesController
11
+ import kotlinx.coroutines.CoroutineScope
12
+ import kotlinx.coroutines.Dispatchers
13
+ import kotlinx.coroutines.launch
14
+
15
+ class MainActivity : ReactActivity() {
16
+ override fun onCreate(savedInstanceState: Bundle?) {
17
+ super.onCreate(savedInstanceState)
18
+ CoroutineScope(Dispatchers.IO).launch {
19
+ startUpdatesController(applicationContext)
20
+ }
21
+ }
22
+
23
+ private fun startUpdatesController(context: Context) {
24
+ UpdatesController.initialize(context)
25
+ // Call the synchronous `launchAssetFile()` function to wait for updates ready
26
+ UpdatesController.instance.launchAssetFile
27
+ }
28
+
29
+ /**
30
+ * Returns the name of the main component registered from JavaScript. This is used to schedule
31
+ * rendering of the component.
32
+ */
33
+ override fun getMainComponentName(): String = "main"
34
+
35
+ /**
36
+ * Returns the instance of the [ReactActivityDelegate]. We use [DefaultReactActivityDelegate]
37
+ * which allows you to enable New Architecture with a single boolean flags [fabricEnabled]
38
+ */
39
+ override fun createReactActivityDelegate(): ReactActivityDelegate {
40
+ return ReactActivityDelegateWrapper(
41
+ this,
42
+ BuildConfig.IS_NEW_ARCHITECTURE_ENABLED,
43
+ object : DefaultReactActivityDelegate(
44
+ this,
45
+ mainComponentName,
46
+ fabricEnabled
47
+ ) {})
48
+ }
49
+ }
@@ -0,0 +1,48 @@
1
+ package dev.expo.updatese2e
2
+
3
+ import android.app.Application
4
+ import com.facebook.react.PackageList
5
+ import com.facebook.react.ReactApplication
6
+ import com.facebook.react.ReactHost
7
+ import com.facebook.react.ReactNativeHost
8
+ import com.facebook.react.ReactPackage
9
+ import com.facebook.react.defaults.DefaultNewArchitectureEntryPoint.load
10
+ import com.facebook.react.defaults.DefaultReactNativeHost
11
+ import com.facebook.react.soloader.OpenSourceMergedSoMapping
12
+ import com.facebook.soloader.SoLoader
13
+ import expo.modules.ReactNativeHostWrapper
14
+ import expo.modules.updates.UpdatesPackage
15
+
16
+ class MainApplication : Application(), ReactApplication {
17
+
18
+ override val reactNativeHost: ReactNativeHost = ReactNativeHostWrapper(
19
+ this,
20
+ object : DefaultReactNativeHost(this) {
21
+ override fun getPackages(): List<ReactPackage> {
22
+ val packages = PackageList(this).packages
23
+ // Packages that cannot be autolinked yet can be added manually here, for example:
24
+ // packages.add(new MyReactNativePackage());
25
+ return packages
26
+ }
27
+
28
+ override fun getJSMainModuleName(): String = ".expo/.virtual-metro-entry"
29
+
30
+ override fun getUseDeveloperSupport(): Boolean = BuildConfig.DEBUG && !UpdatesPackage.isUsingNativeDebug
31
+
32
+ override val isNewArchEnabled: Boolean = BuildConfig.IS_NEW_ARCHITECTURE_ENABLED
33
+ override val isHermesEnabled: Boolean = BuildConfig.IS_HERMES_ENABLED
34
+ }
35
+ )
36
+
37
+ override val reactHost: ReactHost
38
+ get() = ReactNativeHostWrapper.createReactHost(applicationContext, reactNativeHost)
39
+
40
+ override fun onCreate() {
41
+ super.onCreate()
42
+ SoLoader.init(this, OpenSourceMergedSoMapping)
43
+ if (BuildConfig.IS_NEW_ARCHITECTURE_ENABLED) {
44
+ // If you opted-in for the New Architecture, we load the native entry point for this app.
45
+ load()
46
+ }
47
+ }
48
+ }
@@ -6,7 +6,8 @@
6
6
  "base": {
7
7
  "node": "20.14.0",
8
8
  "android": {
9
- "image": "latest"
9
+ "image": "ubuntu-22.04-jdk-17-ndk-r21e",
10
+ "resourceClass": "large"
10
11
  },
11
12
  "ios": {
12
13
  "image": "latest"
@@ -24,9 +25,6 @@
24
25
  },
25
26
  "updates_testing_debug": {
26
27
  "extends": "base",
27
- "env": {
28
- "EX_UPDATES_NATIVE_DEBUG": "1"
29
- },
30
28
  "android": {
31
29
  "applicationArchivePath": "eas.json",
32
30
  "gradleCommand": ":app:assembleDebug :app:assembleAndroidTest -DtestBuildType=debug",
@@ -42,9 +40,6 @@
42
40
  },
43
41
  "updates_testing_release": {
44
42
  "extends": "base",
45
- "env": {
46
- "EX_UPDATES_NATIVE_DEBUG": "1"
47
- },
48
43
  "android": {
49
44
  "gradleCommand": ":app:assembleRelease :app:assembleAndroidTest -DtestBuildType=release",
50
45
  "withoutCredentials": true
@@ -0,0 +1,42 @@
1
+ #!/usr/bin/env yarn --silent ts-node --transpile-only
2
+
3
+ import nullthrows from 'nullthrows';
4
+ import path from 'path';
5
+
6
+ import { initAsync, setupE2EAppAsync, transformAppJsonForE2EWithCustomInit } from './project';
7
+
8
+ const repoRoot = nullthrows(process.env.EXPO_REPO_ROOT, 'EXPO_REPO_ROOT is not defined');
9
+ const workingDir = path.resolve(repoRoot, '..');
10
+ const runtimeVersion = '1.0.0';
11
+
12
+ /**
13
+ *
14
+ * This generates a project at the location TEST_PROJECT_ROOT,
15
+ * that is configured to build a test app and run both suites
16
+ * of updates E2E tests in the Detox environment.
17
+ *
18
+ * This test project will use the custom init flow for updates, using
19
+ * the expo-template-custom-init native files.
20
+ *
21
+ * See `packages/expo-updates/e2e/README.md` for instructions on how
22
+ * to run these tests locally.
23
+ *
24
+ */
25
+
26
+ (async function () {
27
+ if (!process.env.EXPO_REPO_ROOT || !process.env.UPDATES_HOST || !process.env.UPDATES_PORT) {
28
+ throw new Error('Missing one or more environment variables; see instructions in e2e/README.md');
29
+ }
30
+ const projectRoot = process.env.TEST_PROJECT_ROOT || path.join(workingDir, 'updates-e2e');
31
+ const localCliBin = path.join(repoRoot, 'packages/@expo/cli/build/bin/cli');
32
+
33
+ await initAsync(projectRoot, {
34
+ repoRoot,
35
+ runtimeVersion,
36
+ localCliBin,
37
+ useCustomInit: true,
38
+ transformAppJson: transformAppJsonForE2EWithCustomInit,
39
+ });
40
+
41
+ await setupE2EAppAsync(projectRoot, { localCliBin, repoRoot });
42
+ })();
@@ -17,9 +17,11 @@ const dirName = __dirname; /* eslint-disable-line */
17
17
  function getExpoDependencyChunks({
18
18
  includeDevClient,
19
19
  includeTV,
20
+ includeSplashScreen,
20
21
  }: {
21
22
  includeDevClient: boolean;
22
23
  includeTV: boolean;
24
+ includeSplashScreen: boolean;
23
25
  }) {
24
26
  return [
25
27
  ['@expo/config-types', '@expo/env', '@expo/json-file'],
@@ -39,12 +41,12 @@ function getExpoDependencyChunks({
39
41
  'expo-font',
40
42
  'expo-json-utils',
41
43
  'expo-keep-awake',
42
- 'expo-splash-screen',
43
44
  'expo-status-bar',
44
45
  'expo-structured-headers',
45
46
  'expo-updates',
46
47
  'expo-updates-interface',
47
48
  ],
49
+ ...(includeSplashScreen ? [['expo-splash-screen']] : []),
48
50
  ...(includeDevClient
49
51
  ? [['expo-dev-menu-interface'], ['expo-dev-menu'], ['expo-dev-launcher'], ['expo-dev-client']]
50
52
  : []),
@@ -252,13 +254,18 @@ async function preparePackageJson(
252
254
  configureE2E: boolean,
253
255
  isTV: boolean,
254
256
  shouldGenerateTestUpdateBundles: boolean,
255
- includeDevClient: boolean
257
+ includeDevClient: boolean,
258
+ useCustomInit: boolean
256
259
  ) {
257
260
  // Create the project subfolder to hold NPM tarballs built from the current state of the repo
258
261
  const dependenciesPath = path.join(projectRoot, 'dependencies');
259
262
  await fs.mkdir(dependenciesPath);
260
263
 
261
- const allDependencyChunks = getExpoDependencyChunks({ includeDevClient, includeTV: isTV });
264
+ const allDependencyChunks = getExpoDependencyChunks({
265
+ includeDevClient,
266
+ includeTV: isTV,
267
+ includeSplashScreen: !useCustomInit,
268
+ });
262
269
 
263
270
  console.time('Done packing dependencies');
264
271
  for (const dependencyChunk of allDependencyChunks) {
@@ -368,7 +375,7 @@ async function preparePackageJson(
368
375
  ...packageJson,
369
376
  dependencies: {
370
377
  ...packageJson.dependencies,
371
- 'react-native': 'npm:react-native-tvos@~0.78.0-0',
378
+ 'react-native': 'npm:react-native-tvos@0.79.0-0rc2',
372
379
  '@react-native-tvos/config-tv': '^0.1.1',
373
380
  },
374
381
  expo: {
@@ -495,6 +502,7 @@ function transformAppJsonForE2E(
495
502
  ...appJson.expo.updates,
496
503
  url: `http://${process.env.UPDATES_HOST}:${process.env.UPDATES_PORT}/update`,
497
504
  assetPatternsToBeBundled: ['includedAssets/*'],
505
+ useNativeDebug: true,
498
506
  },
499
507
  extra: {
500
508
  eas: {
@@ -505,6 +513,22 @@ function transformAppJsonForE2E(
505
513
  };
506
514
  }
507
515
 
516
+ export function transformAppJsonForE2EWithCustomInit(
517
+ appJson: any,
518
+ projectName: string,
519
+ runtimeVersion: string,
520
+ isTV: boolean
521
+ ) {
522
+ const transformedForE2E = transformAppJsonForE2E(appJson, projectName, runtimeVersion, isTV);
523
+ return {
524
+ ...transformedForE2E,
525
+ expo: {
526
+ ...transformedForE2E.expo,
527
+ newArchEnabled: true,
528
+ },
529
+ };
530
+ }
531
+
508
532
  /**
509
533
  * Modifies app.json in the E2E test app to add the properties we need, and sets the runtime version policy to fingerprint
510
534
  */
@@ -605,6 +629,10 @@ export function transformAppJsonForUpdatesDisabledE2E(
605
629
  newArchEnabled: false,
606
630
  android: { ...appJson.expo.android, package: 'dev.expo.updatese2e' },
607
631
  ios: { ...appJson.expo.ios, bundleIdentifier: 'dev.expo.updatese2e' },
632
+ updates: {
633
+ enabled: false,
634
+ useNativeDebug: true,
635
+ },
608
636
  extra: {
609
637
  eas: {
610
638
  projectId: '55685a57-9cf3-442d-9ba8-65c7b39849ef',
@@ -664,6 +692,7 @@ export async function initAsync(
664
692
  shouldGenerateTestUpdateBundles = true,
665
693
  shouldConfigureCodeSigning = true,
666
694
  includeDevClient = false,
695
+ useCustomInit = false,
667
696
  }: {
668
697
  repoRoot: string;
669
698
  runtimeVersion: string;
@@ -679,6 +708,7 @@ export async function initAsync(
679
708
  shouldGenerateTestUpdateBundles?: boolean;
680
709
  shouldConfigureCodeSigning?: boolean;
681
710
  includeDevClient?: boolean;
711
+ useCustomInit?: boolean;
682
712
  }
683
713
  ) {
684
714
  console.log('Creating expo app');
@@ -730,7 +760,8 @@ export async function initAsync(
730
760
  configureE2E,
731
761
  isTV,
732
762
  shouldGenerateTestUpdateBundles,
733
- includeDevClient
763
+ includeDevClient,
764
+ useCustomInit
734
765
  );
735
766
 
736
767
  // configure app.json
@@ -760,7 +791,6 @@ export async function initAsync(
760
791
  await spawnAsync(localCliBin, ['prebuild', '--no-install', '--template', localTemplatePathName], {
761
792
  env: {
762
793
  ...process.env,
763
- EX_UPDATES_NATIVE_DEBUG: '1',
764
794
  EXPO_DEBUG: '1',
765
795
  CI: '1',
766
796
  },
@@ -784,10 +814,10 @@ export async function initAsync(
784
814
  stdio: 'inherit',
785
815
  });
786
816
 
787
- // enable proguard on Android
817
+ // enable proguard on Android, and custom init if needed
788
818
  await fs.appendFile(
789
819
  path.join(projectRoot, 'android', 'gradle.properties'),
790
- '\nandroid.enableProguardInReleaseBuilds=true\nEXPO_UPDATES_NATIVE_DEBUG=true',
820
+ `\nandroid.enableProguardInReleaseBuilds=true${useCustomInit ? '\nEX_UPDATES_CUSTOM_INIT=true' : ''}`,
791
821
  'utf-8'
792
822
  );
793
823
 
@@ -807,6 +837,71 @@ export async function initAsync(
807
837
  ].join('\n'),
808
838
  'utf-8'
809
839
  );
840
+
841
+ // Add custom init to iOS Podfile.properties.json if needed
842
+ if (useCustomInit) {
843
+ const podfilePropertiesJsonPath = path.join(projectRoot, 'ios', 'Podfile.properties.json');
844
+ const podfilePropertiesJsonString = await fs.readFile(podfilePropertiesJsonPath, {
845
+ encoding: 'utf-8',
846
+ });
847
+ const podfilePropertiesJson: any = JSON.parse(podfilePropertiesJsonString);
848
+ podfilePropertiesJson.updatesCustomInit = 'true';
849
+ await fs.writeFile(podfilePropertiesJsonPath, JSON.stringify(podfilePropertiesJson, null, 2), {
850
+ encoding: 'utf-8',
851
+ });
852
+ }
853
+
854
+ const customInitSourcesDirectory = path.join(
855
+ repoRoot,
856
+ 'packages',
857
+ 'expo-updates',
858
+ 'e2e',
859
+ 'fixtures',
860
+ 'custom_init'
861
+ );
862
+ // If custom init, copy native source files
863
+ if (useCustomInit) {
864
+ const filesToCopyForCustomInit = [
865
+ {
866
+ sourcePath: path.join(customInitSourcesDirectory, 'AppDelegate.swift'),
867
+ destPath: path.join(projectRoot, 'ios', 'updatese2e', 'AppDelegate.swift'),
868
+ },
869
+ {
870
+ sourcePath: path.join(customInitSourcesDirectory, 'MainApplication.kt'),
871
+ destPath: path.join(
872
+ projectRoot,
873
+ 'android',
874
+ 'app',
875
+ 'src',
876
+ 'main',
877
+ 'java',
878
+ 'dev',
879
+ 'expo',
880
+ 'updatese2e',
881
+ 'MainApplication.kt'
882
+ ),
883
+ },
884
+ {
885
+ sourcePath: path.join(customInitSourcesDirectory, 'MainActivity.kt'),
886
+ destPath: path.join(
887
+ projectRoot,
888
+ 'android',
889
+ 'app',
890
+ 'src',
891
+ 'main',
892
+ 'java',
893
+ 'dev',
894
+ 'expo',
895
+ 'updatese2e',
896
+ 'MainActivity.kt'
897
+ ),
898
+ },
899
+ ];
900
+ for (const fileToCopy of filesToCopyForCustomInit) {
901
+ await fs.copyFile(fileToCopy.sourcePath, fileToCopy.destPath);
902
+ }
903
+ }
904
+
810
905
  await fs.appendFile(
811
906
  path.join(projectRoot, 'android', 'app', 'build.gradle'),
812
907
  [
@@ -16,6 +16,7 @@ import org.slf4j.LoggerFactory
16
16
  import java.io.ByteArrayOutputStream
17
17
  import java.io.File
18
18
  import java.util.Locale
19
+ import java.util.Properties
19
20
 
20
21
  abstract class ExpoUpdatesPlugin : Plugin<Project> {
21
22
  override fun apply(project: Project) {
@@ -26,7 +27,7 @@ abstract class ExpoUpdatesPlugin : Plugin<Project> {
26
27
  val entryFile = detectedEntryFile(reactExtension)
27
28
  val androidComponents = project.extensions.getByType(AndroidComponentsExtension::class.java)
28
29
 
29
- if (System.getenv("EX_UPDATES_NATIVE_DEBUG") == "1") {
30
+ if (isNativeDebuggingEnabled(project)) {
30
31
  logger.warn("Disable all react.debuggableVariants because EX_UPDATES_NATIVE_DEBUG=1")
31
32
  reactExtension.debuggableVariants.set(listOf())
32
33
  }
@@ -126,3 +127,10 @@ private fun detectedEntryFile(config: ReactExtension): File {
126
127
  else -> File(reactRoot, "index.js")
127
128
  }
128
129
  }
130
+
131
+ private fun isNativeDebuggingEnabled(project: Project): Boolean {
132
+ if (System.getenv("EX_UPDATES_NATIVE_DEBUG") == "1") {
133
+ return true
134
+ }
135
+ return project.findProperty("EX_UPDATES_NATIVE_DEBUG") == "true"
136
+ }
@@ -22,6 +22,10 @@ public final class ExpoUpdatesReactDelegateHandler: ExpoReactDelegateHandler, Ap
22
22
  initialProperties: [AnyHashable: Any]?,
23
23
  launchOptions: [UIApplication.LaunchOptionsKey: Any]?
24
24
  ) -> UIView? {
25
+ if UpdatesUtils.isUsingCustomInitialization() {
26
+ return nil
27
+ }
28
+
25
29
  AppController.initializeWithoutStarting()
26
30
  let controller = AppController.sharedInstance
27
31
  if !controller.isActiveController {
@@ -59,12 +63,15 @@ public final class ExpoUpdatesReactDelegateHandler: ExpoReactDelegateHandler, Ap
59
63
  // MARK: AppControllerDelegate implementations
60
64
 
61
65
  public func appController(_ appController: AppControllerInterface, didStartWithSuccess success: Bool) {
66
+ if UpdatesUtils.isUsingCustomInitialization() {
67
+ return
68
+ }
62
69
  guard let reactDelegate = self.reactDelegate else {
63
70
  fatalError("`reactDelegate` should not be nil")
64
71
  }
65
72
 
66
- guard let appDelegate = (UIApplication.shared.delegate as? ReactNativeFactoryProvider) ??
67
- ((UIApplication.shared.delegate as? NSObject)?.value(forKey: "_expoAppDelegate") as? ReactNativeFactoryProvider) else {
73
+ guard let appDelegate = (UIApplication.shared.delegate as? (any ReactNativeFactoryProvider)) ??
74
+ ((UIApplication.shared.delegate as? NSObject)?.value(forKey: "_expoAppDelegate") as? (any ReactNativeFactoryProvider)) else {
68
75
  fatalError("`UIApplication.shared.delegate` must be an `ExpoAppDelegate` or `EXAppDelegateWrapper`")
69
76
  }
70
77
 
@@ -133,7 +133,7 @@ public final class UpdatesUtils: NSObject {
133
133
  }
134
134
  }
135
135
 
136
- internal static func isNativeDebuggingEnabled() -> Bool {
136
+ public static func isNativeDebuggingEnabled() -> Bool {
137
137
  #if EX_UPDATES_NATIVE_DEBUG
138
138
  return true
139
139
  #else
@@ -141,6 +141,14 @@ public final class UpdatesUtils: NSObject {
141
141
  #endif
142
142
  }
143
143
 
144
+ internal static func isUsingCustomInitialization() -> Bool {
145
+ #if EX_UPDATES_CUSTOM_INIT
146
+ return true
147
+ #else
148
+ return false
149
+ #endif
150
+ }
151
+
144
152
  internal static func runBlockOnMainThread(_ block: @escaping () -> Void) {
145
153
  if Thread.isMainThread {
146
154
  block()
@@ -3,9 +3,19 @@ require 'json'
3
3
  package = JSON.parse(File.read(File.join(__dir__, '..', 'package.json')))
4
4
  podfile_properties = JSON.parse(File.read("#{Pod::Config.instance.installation_root}/Podfile.properties.json")) rescue {}
5
5
 
6
+ if ENV['EX_UPDATES_NATIVE_DEBUG'] != '1'
7
+ ENV['EX_UPDATES_NATIVE_DEBUG'] = podfile_properties['updatesNativeDebug'] == 'true' ? '1' : '0'
8
+ end
9
+ if ENV['EX_UPDATES_CUSTOM_INIT'] != '1'
10
+ ENV['EX_UPDATES_CUSTOM_INIT'] = podfile_properties['updatesCustomInit'] == 'true' ? '1' : '0'
11
+ end
12
+
6
13
  use_dev_client = false
7
14
  begin
8
- use_dev_client = `node --print "require('expo-dev-client/package.json').version" 2>/dev/null`.length > 0
15
+ # No dev client if we are using native debug
16
+ if ENV['EX_UPDATES_NATIVE_DEBUG'] != '1'
17
+ use_dev_client = `node --print "require('expo-dev-client/package.json').version" 2>/dev/null`.length > 0
18
+ end
9
19
  rescue
10
20
  use_dev_client = false
11
21
  end
@@ -43,17 +53,26 @@ Pod::Spec.new do |s|
43
53
  end
44
54
  install_modules_dependencies(s)
45
55
 
46
- other_c_flags = '$(inherited)'
47
- other_swift_flags = '$(inherited)'
56
+ other_debug_c_flags = '$(inherited)'
57
+ other_debug_swift_flags = '$(inherited)'
58
+ other_release_c_flags = '$(inherited)'
59
+ other_release_swift_flags = '$(inherited)'
48
60
 
49
61
  ex_updates_native_debug = ENV['EX_UPDATES_NATIVE_DEBUG'] == '1'
62
+ ex_updates_custom_init = ENV['EX_UPDATES_CUSTOM_INIT'] == '1'
50
63
  if ex_updates_native_debug
51
- other_c_flags << ' -DEX_UPDATES_NATIVE_DEBUG=1'
52
- other_swift_flags << ' -DEX_UPDATES_NATIVE_DEBUG'
64
+ other_debug_c_flags << ' -DEX_UPDATES_NATIVE_DEBUG=1'
65
+ other_debug_swift_flags << ' -DEX_UPDATES_NATIVE_DEBUG'
66
+ end
67
+ if ex_updates_custom_init
68
+ other_debug_c_flags << ' -DEX_UPDATES_CUSTOM_INIT=1'
69
+ other_debug_swift_flags << ' -DEX_UPDATES_CUSTOM_INIT'
70
+ other_release_c_flags << ' -DEX_UPDATES_CUSTOM_INIT=1'
71
+ other_release_swift_flags << ' -DEX_UPDATES_CUSTOM_INIT'
53
72
  end
54
73
  if use_dev_client
55
- other_c_flags << ' -DUSE_DEV_CLIENT=1'
56
- other_swift_flags << ' -DUSE_DEV_CLIENT'
74
+ other_debug_c_flags << ' -DUSE_DEV_CLIENT=1'
75
+ other_debug_swift_flags << ' -DUSE_DEV_CLIENT'
57
76
  end
58
77
 
59
78
  s.pod_target_xcconfig = {
@@ -61,8 +80,10 @@ Pod::Spec.new do |s|
61
80
  'GCC_TREAT_IMPLICIT_FUNCTION_DECLARATIONS_AS_ERRORS' => 'YES',
62
81
  'DEFINES_MODULE' => 'YES',
63
82
  'SWIFT_COMPILATION_MODE' => 'wholemodule',
64
- 'OTHER_CFLAGS[config=*Debug*]' => other_c_flags,
65
- 'OTHER_SWIFT_FLAGS[config=*Debug*]' => other_swift_flags
83
+ 'OTHER_CFLAGS[config=*Debug*]' => other_debug_c_flags,
84
+ 'OTHER_SWIFT_FLAGS[config=*Debug*]' => other_debug_swift_flags,
85
+ 'OTHER_CFLAGS[config=*Release*]' => other_release_c_flags,
86
+ 'OTHER_SWIFT_FLAGS[config=*Release*]' => other_release_swift_flags
66
87
  }
67
88
  s.user_target_xcconfig = {
68
89
  'HEADER_SEARCH_PATHS' => '"${PODS_CONFIGURATION_BUILD_DIR}/EXUpdates/Swift Compatibility Header"',
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "expo-updates",
3
- "version": "1.0.0-canary-20250306-d9d3e02",
3
+ "version": "1.0.0-canary-20250331-817737a",
4
4
  "description": "Fetches and manages remotely-hosted assets and updates to your app's JS bundle.",
5
5
  "main": "build/index.js",
6
6
  "types": "build/index.d.ts",
@@ -39,15 +39,15 @@
39
39
  },
40
40
  "dependencies": {
41
41
  "@expo/code-signing-certificates": "0.0.5",
42
- "@expo/config": "11.0.0-canary-20250306-d9d3e02",
43
- "@expo/config-plugins": "9.1.0-canary-20250306-d9d3e02",
42
+ "@expo/config": "11.0.0-canary-20250331-817737a",
43
+ "@expo/config-plugins": "9.1.0-canary-20250331-817737a",
44
44
  "@expo/spawn-async": "^1.7.2",
45
45
  "arg": "4.1.0",
46
46
  "chalk": "^4.1.2",
47
- "expo-eas-client": "0.13.4-canary-20250306-d9d3e02",
48
- "expo-manifests": "0.15.8-canary-20250306-d9d3e02",
49
- "expo-structured-headers": "4.0.1-canary-20250306-d9d3e02",
50
- "expo-updates-interface": "1.0.1-canary-20250306-d9d3e02",
47
+ "expo-eas-client": "0.13.4-canary-20250331-817737a",
48
+ "expo-manifests": "0.15.8-canary-20250331-817737a",
49
+ "expo-structured-headers": "4.0.1-canary-20250331-817737a",
50
+ "expo-updates-interface": "1.0.1-canary-20250331-817737a",
51
51
  "glob": "^10.4.2",
52
52
  "ignore": "^5.3.1",
53
53
  "resolve-from": "^5.0.0"
@@ -56,15 +56,14 @@
56
56
  "@types/jest": "^29.2.1",
57
57
  "@types/node": "^18.19.34",
58
58
  "@types/node-forge": "^1.0.0",
59
- "expo-module-scripts": "4.0.5-canary-20250306-d9d3e02",
59
+ "expo-module-scripts": "4.0.5-canary-20250331-817737a",
60
60
  "express": "^4.21.1",
61
61
  "form-data": "^4.0.0",
62
62
  "memfs": "^3.2.0",
63
63
  "xstate": "^4.37.2"
64
64
  },
65
65
  "peerDependencies": {
66
- "expo": "53.0.0-canary-20250306-d9d3e02",
66
+ "expo": "53.0.0-canary-20250331-817737a",
67
67
  "react": "*"
68
- },
69
- "gitHead": "d9d3e024d8742099c307754673f17117a20c1dea"
68
+ }
70
69
  }