expo-updates 1.0.0-canary-20250131-5c4e588 → 1.0.0-canary-20250219-4a5dade

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
@@ -16,6 +16,9 @@
16
16
  - Fix an issue where `launchFallbackUpdateFromDisk` is called from a foreground thread leading to ANRs. ([#33693](https://github.com/expo/expo/pull/33693) by [@alanjhughes](https://github.com/alanjhughes))
17
17
  - [android] Use more robust mechanism for determining empty multipart bodies. ([#33977](https://github.com/expo/expo/pull/33977) by [@wschurman](https://github.com/wschurman))
18
18
  - fix E2E tests in Detox debug build ([#32951](https://github.com/expo/expo/pull/32951) by [@matejkriz](https://github.com/matejkriz))
19
+ - Fix issue where syncing codesigning config for bare projects would clobber existing Expo.plist config ([#34597](https://github.com/expo/expo/pull/34597) by [@brentvatne](https://github.com/brentvatne))
20
+ - Removed Apache Commons IO dependency and fixed crash issue on Android 7. ([#34638](https://github.com/expo/expo/pull/34638) by [@kudo](https://github.com/kudo))
21
+ - [Android] Fix `bytesToHex` `ArrayIndexOutOfBoundsException` during conversion. ([#34855](https://github.com/expo/expo/pull/34855) by [@gabrieldonadel](https://github.com/gabrieldonadel))
19
22
 
20
23
  ### 💡 Others
21
24
 
@@ -24,6 +27,9 @@
24
27
  - [apple] Migrate remaining `expo-module.config.json` to unified platform syntax. ([#34445](https://github.com/expo/expo/pull/34445) by [@reichhartd](https://github.com/reichhartd))
25
28
  - Fixed build error on iOS Expo Go. ([#34485](https://github.com/expo/expo/pull/34485) by [@kudo](https://github.com/kudo))
26
29
  - Fixed Android unit test errors in BuilDataTest. ([#34510](https://github.com/expo/expo/pull/34510) by [@kudo](https://github.com/kudo))
30
+ - Fixed incorrect error log on Android. ([#34785](https://github.com/expo/expo/pull/34785) by [@kudo](https://github.com/kudo))
31
+ - [Android] Started using expo modules gradle plugin. ([#34806](https://github.com/expo/expo/pull/34806) by [@lukmccall](https://github.com/lukmccall))
32
+ - Add update id headers to asset requests ([#34453](https://github.com/expo/expo/pull/34453) by [@gabrieldonadel](https://github.com/gabrieldonadel))
27
33
 
28
34
  ## 0.26.10 - 2024-12-05
29
35
 
@@ -1,31 +1,42 @@
1
1
  buildscript {
2
- def expoModulesCorePlugin = new File(project(":expo-modules-core").projectDir.absolutePath, "ExpoModulesCorePlugin.gradle")
3
- apply from: expoModulesCorePlugin
4
- applyKotlinExpoModulesCorePlugin()
2
+ ext {
3
+ boolish = { value ->
4
+ return value.toString().toBoolean()
5
+ }
6
+ getKspVersion = {
7
+ if (rootProject.hasProperty("kspVersion")) {
8
+ return rootProject["kspVersion"]
9
+ }
10
+
11
+ // We can remove this path once we update the test environment
12
+ // to use the same version of Kotlin as the `expo/expo` repo.
13
+ def kotlinVersion = rootProject["kotlinVersion"]
14
+ if (kotlinVersion == "2.0.21") {
15
+ return "2.0.21-1.0.28"
16
+ } else if (kotlinVersion == "1.9.25") {
17
+ return "1.9.25-1.0.20"
18
+ }
19
+
20
+ return "1.9.24-1.0.20"
21
+ }
22
+ }
5
23
 
6
24
  repositories {
7
25
  mavenCentral()
8
26
  }
9
27
 
10
28
  dependencies {
11
- classpath "com.google.devtools.ksp:symbol-processing-gradle-plugin:${kspVersion()}"
29
+ classpath "com.google.devtools.ksp:symbol-processing-gradle-plugin:${getKspVersion()}"
12
30
  }
13
31
  }
14
32
 
15
33
  apply plugin: 'com.android.library'
34
+ apply plugin: 'expo-module-gradle-plugin'
16
35
  apply plugin: 'com.google.devtools.ksp'
17
36
 
18
37
  group = 'host.exp.exponent'
19
38
  version = '0.26.9'
20
39
 
21
- def expoModulesCorePlugin = new File(project(":expo-modules-core").projectDir.absolutePath, "ExpoModulesCorePlugin.gradle")
22
- apply from: expoModulesCorePlugin
23
- applyKotlinExpoModulesCorePlugin()
24
- applyKspJvmToolchain()
25
- useCoreDependencies()
26
- useDefaultAndroidSdkVersions()
27
- useExpoPublishing()
28
-
29
40
  // Utility method to derive boolean values from the environment or from Java properties,
30
41
  // and return them as strings to be used in BuildConfig fields
31
42
  def getBoolStringFromPropOrEnv(String name, Boolean defaultValue) {
@@ -112,13 +123,12 @@ dependencies {
112
123
  implementation("com.squareup.okhttp3:okhttp:4.9.2")
113
124
  implementation("com.squareup.okhttp3:okhttp-urlconnection:4.9.2")
114
125
  implementation("com.squareup.okhttp3:okhttp-brotli:4.9.2")
115
- implementation("commons-io:commons-io:2.16.1")
116
126
  implementation("org.bouncycastle:bcutil-jdk15to18:1.78.1")
117
127
 
118
128
  testImplementation 'junit:junit:4.13.2'
119
129
  testImplementation 'androidx.test:core:1.5.0'
120
130
  testImplementation "io.mockk:mockk:$mockk_version"
121
- testImplementation "org.jetbrains.kotlin:kotlin-test-junit:${kotlinVersion()}"
131
+ testImplementation "org.jetbrains.kotlin:kotlin-test-junit:${kotlinVersion}"
122
132
  testImplementation 'org.robolectric:robolectric:4.14.1'
123
133
 
124
134
  androidTestImplementation 'com.squareup.okio:okio:2.9.0'
@@ -127,7 +137,7 @@ dependencies {
127
137
  androidTestImplementation 'androidx.test:rules:1.5.0'
128
138
  androidTestImplementation "io.mockk:mockk-android:$mockk_version"
129
139
  androidTestImplementation "androidx.room:room-testing:$room_version"
130
- androidTestImplementation "org.jetbrains.kotlin:kotlin-test-junit:${kotlinVersion()}"
140
+ androidTestImplementation "org.jetbrains.kotlin:kotlin-test-junit:${kotlinVersion}"
131
141
 
132
- implementation "org.jetbrains.kotlin:kotlin-reflect:${kotlinVersion()}"
142
+ implementation "org.jetbrains.kotlin:kotlin-reflect:${kotlinVersion}"
133
143
  }
@@ -6,8 +6,6 @@ import android.net.Uri
6
6
  import android.util.Log
7
7
  import expo.modules.core.errors.InvalidArgumentException
8
8
  import expo.modules.updates.codesigning.CodeSigningConfiguration
9
- import org.apache.commons.io.IOUtils
10
- import java.nio.charset.StandardCharsets
11
9
 
12
10
  enum class UpdatesConfigurationValidationResult {
13
11
  VALID,
@@ -215,7 +213,7 @@ data class UpdatesConfiguration(
215
213
 
216
214
  if (context != null && runtimeVersion == UPDATES_CONFIGURATION_RUNTIME_VERSION_READ_FINGERPRINT_FILE_SENTINEL) {
217
215
  return context.assets.open(FINGERPRINT_FILE_NAME).use { stream ->
218
- IOUtils.toString(stream, StandardCharsets.UTF_8)
216
+ stream.bufferedReader(Charsets.UTF_8).use { it.readText() }
219
217
  }
220
218
  }
221
219
 
@@ -8,7 +8,6 @@ import expo.modules.updates.UpdatesConfiguration.CheckAutomaticallyConfiguration
8
8
  import expo.modules.updates.db.entity.AssetEntity
9
9
  import expo.modules.updates.logging.UpdatesErrorCode
10
10
  import expo.modules.updates.logging.UpdatesLogger
11
- import org.apache.commons.io.FileUtils
12
11
  import org.json.JSONArray
13
12
  import org.json.JSONObject
14
13
  import java.io.*
@@ -112,7 +111,12 @@ object UpdatesUtils {
112
111
  // write file atomically by writing it to a temporary path and then renaming
113
112
  // this protects us against partially written files if the process is interrupted
114
113
  val tmpFile = File(destination.absolutePath + ".tmp")
115
- FileUtils.copyInputStreamToFile(digestInputStream, tmpFile)
114
+ tmpFile.parentFile?.mkdirs()
115
+ digestInputStream.use { input ->
116
+ tmpFile.outputStream().use { output ->
117
+ input.copyTo(output)
118
+ }
119
+ }
116
120
 
117
121
  // this message digest must be read after the input stream has been consumed in order to get the hash correctly
118
122
  val md = digestInputStream.messageDigest
@@ -181,7 +185,7 @@ object UpdatesUtils {
181
185
  fun bytesToHex(bytes: ByteArray): String {
182
186
  val hexChars = CharArray(bytes.size * 2)
183
187
  for (j in bytes.indices) {
184
- val v = (bytes[j] and 0xFF.toByte()).toInt()
188
+ val v = bytes[j].toInt() and 0xFF
185
189
  hexChars[j * 2] = HEX_ARRAY[v ushr 4]
186
190
  hexChars[j * 2 + 1] = HEX_ARRAY[v and 0x0F]
187
191
  }
@@ -15,8 +15,10 @@ import expo.modules.updates.loader.LoaderFiles
15
15
  import expo.modules.updates.logging.UpdatesErrorCode
16
16
  import expo.modules.updates.logging.UpdatesLogger
17
17
  import expo.modules.updates.manifest.EmbeddedManifestUtils
18
+ import expo.modules.updates.manifest.EmbeddedUpdate
18
19
  import expo.modules.updates.manifest.ManifestMetadata
19
20
  import expo.modules.updates.selectionpolicy.SelectionPolicy
21
+ import org.json.JSONObject
20
22
  import java.io.File
21
23
 
22
24
  /**
@@ -92,7 +94,10 @@ class DatabaseLauncher(
92
94
  this.callback!!.onFailure(Exception("Launch asset relative path should not be null. Debug info: ${launchedUpdate!!.debugInfo()}"))
93
95
  }
94
96
 
95
- val launchAssetFile = ensureAssetExists(launchAsset, database)
97
+ val embeddedUpdate = EmbeddedManifestUtils.getEmbeddedUpdate(context, configuration)
98
+ val extraHeaders = FileDownloader.getExtraHeadersForRemoteAssetRequest(launchedUpdate, embeddedUpdate?.updateEntity, launchedUpdate)
99
+
100
+ val launchAssetFile = ensureAssetExists(launchAsset, database, embeddedUpdate, extraHeaders)
96
101
  if (launchAssetFile != null) {
97
102
  this.launchAssetFile = launchAssetFile.toString()
98
103
  }
@@ -107,7 +112,7 @@ class DatabaseLauncher(
107
112
  }
108
113
  val filename = asset.relativePath
109
114
  if (filename != null) {
110
- val assetFile = ensureAssetExists(asset, database)
115
+ val assetFile = ensureAssetExists(asset, database, embeddedUpdate, extraHeaders)
111
116
  if (assetFile != null) {
112
117
  this[asset] = Uri.fromFile(assetFile).toString()
113
118
  }
@@ -170,13 +175,12 @@ class DatabaseLauncher(
170
175
  }
171
176
  }
172
177
 
173
- fun ensureAssetExists(asset: AssetEntity, database: UpdatesDatabase): File? {
178
+ fun ensureAssetExists(asset: AssetEntity, database: UpdatesDatabase, embeddedUpdate: EmbeddedUpdate?, extraHeaders: JSONObject): File? {
174
179
  val assetFile = File(updatesDirectory, asset.relativePath ?: "")
175
180
  var assetFileExists = assetFile.exists()
176
181
  if (!assetFileExists) {
177
182
  // something has gone wrong, we're missing this asset
178
183
  // first we check to see if a copy is embedded in the binary
179
- val embeddedUpdate = EmbeddedManifestUtils.getEmbeddedUpdate(context, configuration)
180
184
  if (embeddedUpdate != null) {
181
185
  val embeddedAssets = embeddedUpdate.assetEntityList
182
186
  var matchingEmbeddedAsset: AssetEntity? = null
@@ -204,9 +208,11 @@ class DatabaseLauncher(
204
208
  return if (!assetFileExists) {
205
209
  // we still don't have the asset locally, so try downloading it remotely
206
210
  assetsToDownload++
211
+
207
212
  fileDownloader.downloadAsset(
208
213
  asset,
209
214
  updatesDirectory,
215
+ extraHeaders,
210
216
  object : AssetDownloadCallback {
211
217
  override fun onFailure(e: Exception, assetEntity: AssetEntity) {
212
218
  logger.error("Failed to load asset from disk or network", e, UpdatesErrorCode.AssetsFailedToLoad)
@@ -4,7 +4,6 @@ import android.content.Context
4
4
  import android.os.AsyncTask
5
5
  import expo.modules.updates.loader.EmbeddedLoader
6
6
  import expo.modules.updates.logging.UpdatesLogger
7
- import org.apache.commons.io.FileUtils
8
7
  import java.io.File
9
8
 
10
9
  /**
@@ -30,7 +29,7 @@ class NoDatabaseLauncher @JvmOverloads constructor(
30
29
  try {
31
30
  val errorLogFile = File(context.filesDir, ERROR_LOG_FILENAME)
32
31
  val exceptionString = fatalException.toString()
33
- FileUtils.writeStringToFile(errorLogFile, exceptionString, "UTF-8", true)
32
+ errorLogFile.appendText(exceptionString, Charsets.UTF_8)
34
33
  } catch (e: Exception) {
35
34
  logger.error("Failed to write fatal error to log", e)
36
35
  }
@@ -47,7 +46,7 @@ class NoDatabaseLauncher @JvmOverloads constructor(
47
46
  if (!errorLogFile.exists()) {
48
47
  return null
49
48
  }
50
- val logContents = FileUtils.readFileToString(errorLogFile, "UTF-8")
49
+ val logContents = errorLogFile.readText(Charsets.UTF_8)
51
50
  errorLogFile.delete()
52
51
  logContents
53
52
  } catch (e: Exception) {
@@ -7,6 +7,7 @@ import expo.modules.updates.db.UpdatesDatabase
7
7
  import expo.modules.updates.loader.FileDownloader.AssetDownloadCallback
8
8
  import expo.modules.updates.loader.FileDownloader.RemoteUpdateDownloadCallback
9
9
  import expo.modules.updates.UpdatesUtils
10
+ import expo.modules.updates.db.entity.UpdateEntity
10
11
  import expo.modules.updates.logging.UpdatesLogger
11
12
  import java.io.File
12
13
  import java.io.FileNotFoundException
@@ -71,6 +72,8 @@ class EmbeddedLoader internal constructor(
71
72
  assetEntity: AssetEntity,
72
73
  updatesDirectory: File?,
73
74
  configuration: UpdatesConfiguration,
75
+ requestedUpdate: UpdateEntity?,
76
+ embeddedUpdate: UpdateEntity?,
74
77
  callback: AssetDownloadCallback
75
78
  ) {
76
79
  val filename = UpdatesUtils.createFilenameForAsset(assetEntity)
@@ -450,6 +450,7 @@ class FileDownloader(
450
450
  fun downloadAsset(
451
451
  asset: AssetEntity,
452
452
  destinationDirectory: File?,
453
+ extraHeaders: JSONObject,
453
454
  callback: AssetDownloadCallback
454
455
  ) {
455
456
  if (asset.url == null) {
@@ -467,7 +468,7 @@ class FileDownloader(
467
468
  } else {
468
469
  try {
469
470
  downloadAssetAndVerifyHashAndWriteToPath(
470
- createRequestForAsset(asset, configuration, context),
471
+ createRequestForAsset(asset, extraHeaders, configuration, context),
471
472
  asset.expectedHash,
472
473
  path,
473
474
  object : FileDownloadCallback {
@@ -595,12 +596,14 @@ class FileDownloader(
595
596
 
596
597
  internal fun createRequestForAsset(
597
598
  assetEntity: AssetEntity,
599
+ extraHeaders: JSONObject,
598
600
  configuration: UpdatesConfiguration,
599
601
  context: Context
600
602
  ): Request {
601
603
  return Request.Builder()
602
604
  .url(assetEntity.url!!.toString())
603
605
  .addHeadersFromJSONObject(assetEntity.extraRequestHeaders)
606
+ .addHeadersFromJSONObject(extraHeaders)
604
607
  .header("Expo-Platform", "android")
605
608
  .header("Expo-Protocol-Version", "1")
606
609
  .header("Expo-API-Version", "1")
@@ -701,5 +704,25 @@ class FileDownloader(
701
704
 
702
705
  return extraHeaders
703
706
  }
707
+
708
+ fun getExtraHeadersForRemoteAssetRequest(
709
+ launchedUpdate: UpdateEntity?,
710
+ embeddedUpdate: UpdateEntity?,
711
+ requestedUpdate: UpdateEntity?
712
+ ): JSONObject {
713
+ val extraHeaders = JSONObject()
714
+
715
+ launchedUpdate?.let {
716
+ extraHeaders.put("Expo-Current-Update-ID", it.id.toString().lowercase())
717
+ }
718
+ embeddedUpdate?.let {
719
+ extraHeaders.put("Expo-Embedded-Update-ID", it.id.toString().lowercase())
720
+ }
721
+ requestedUpdate?.let {
722
+ extraHeaders.put("Expo-Requested-Update-ID", it.id.toString().lowercase())
723
+ }
724
+
725
+ return extraHeaders
726
+ }
704
727
  }
705
728
  }
@@ -86,6 +86,8 @@ abstract class Loader protected constructor(
86
86
  assetEntity: AssetEntity,
87
87
  updatesDirectory: File?,
88
88
  configuration: UpdatesConfiguration,
89
+ requestedUpdate: UpdateEntity?,
90
+ embeddedUpdate: UpdateEntity?,
89
91
  callback: AssetDownloadCallback
90
92
  )
91
93
 
@@ -204,7 +206,7 @@ abstract class Loader protected constructor(
204
206
  // however, it's not ready, so we should try to download all the assets again.
205
207
  updateEntity = existingUpdateEntity
206
208
  }
207
- downloadAllAssets(update.assetEntityList)
209
+ downloadAllAssets(update)
208
210
  }
209
211
  }
210
212
 
@@ -214,8 +216,11 @@ abstract class Loader protected constructor(
214
216
  ERRORED
215
217
  }
216
218
 
217
- private fun downloadAllAssets(assetList: List<AssetEntity>) {
219
+ private fun downloadAllAssets(update: Update) {
220
+ val assetList = update.assetEntityList
218
221
  assetTotal = assetList.size
222
+
223
+ val embeddedUpdate = loaderFiles.readEmbeddedUpdate(context, configuration)
219
224
  for (assetEntityCur in assetList) {
220
225
  var assetEntity = assetEntityCur
221
226
 
@@ -243,6 +248,8 @@ abstract class Loader protected constructor(
243
248
  assetEntity,
244
249
  updatesDirectory,
245
250
  configuration,
251
+ requestedUpdate = update.updateEntity,
252
+ embeddedUpdate = embeddedUpdate?.updateEntity,
246
253
  object : AssetDownloadCallback {
247
254
  override fun onFailure(e: Exception, assetEntity: AssetEntity) {
248
255
  val identifier = if (assetEntity.hash != null) {
@@ -69,7 +69,7 @@ open class LoaderFiles {
69
69
  context.resources.openRawResource(id)
70
70
  .use { inputStream -> return UpdatesUtils.verifySHA256AndWriteToFile(inputStream, destination, null) }
71
71
  } catch (e: Exception) {
72
- Log.e(TAG, "Failed to copy asset " + asset.embeddedAssetFilename, e)
72
+ Log.e(TAG, "Failed to copy resource asset ${asset.resourcesFolder}/${asset.embeddedAssetFilename}", e)
73
73
  throw e
74
74
  }
75
75
  }
@@ -55,9 +55,12 @@ class RemoteLoader internal constructor(
55
55
  assetEntity: AssetEntity,
56
56
  updatesDirectory: File?,
57
57
  configuration: UpdatesConfiguration,
58
+ requestedUpdate: UpdateEntity?,
59
+ embeddedUpdate: UpdateEntity?,
58
60
  callback: AssetDownloadCallback
59
61
  ) {
60
- mFileDownloader.downloadAsset(assetEntity, updatesDirectory, callback)
62
+ val extraHeaders = FileDownloader.getExtraHeadersForRemoteAssetRequest(launchedUpdate, embeddedUpdate, requestedUpdate)
63
+ mFileDownloader.downloadAsset(assetEntity, updatesDirectory, extraHeaders, callback)
61
64
  }
62
65
 
63
66
  companion object {
@@ -3,9 +3,7 @@ package expo.modules.updates.manifest
3
3
  import android.content.Context
4
4
  import android.util.Log
5
5
  import expo.modules.updates.UpdatesConfiguration
6
- import org.apache.commons.io.IOUtils
7
6
  import org.json.JSONObject
8
- import java.nio.charset.StandardCharsets
9
7
 
10
8
  /**
11
9
  * Helper object for accessing and memoizing the manifest embedded in the application package.
@@ -24,7 +22,7 @@ object EmbeddedManifestUtils {
24
22
  if (sEmbeddedUpdate == null) {
25
23
  try {
26
24
  context.assets.open(MANIFEST_FILENAME).use { stream ->
27
- val manifestString = IOUtils.toString(stream, StandardCharsets.UTF_8)
25
+ val manifestString = stream.bufferedReader(Charsets.UTF_8).use { it.readText() }
28
26
  val manifestJson = JSONObject(manifestString)
29
27
  // automatically verify embedded manifest since it was already codesigned
30
28
  manifestJson.put("isVerified", true)
@@ -29,7 +29,7 @@ async function syncConfigurationToNativeAsync(options) {
29
29
  exports.syncConfigurationToNativeAsync = syncConfigurationToNativeAsync;
30
30
  async function syncConfigurationToNativeAndroidAsync(options) {
31
31
  const { exp } = (0, config_1.getConfig)(options.projectRoot, {
32
- isPublicConfig: true,
32
+ isPublicConfig: false,
33
33
  skipSDKVersionRequirement: true,
34
34
  });
35
35
  // sync AndroidManifest.xml
@@ -50,7 +50,7 @@ async function syncConfigurationToNativeAndroidAsync(options) {
50
50
  }
51
51
  async function syncConfigurationToNativeIosAsync(options) {
52
52
  const { exp } = (0, config_1.getConfig)(options.projectRoot, {
53
- isPublicConfig: true,
53
+ isPublicConfig: false,
54
54
  skipSDKVersionRequirement: true,
55
55
  });
56
56
  const expoPlist = await readExpoPlistAsync(options.projectRoot);
@@ -37,7 +37,7 @@ async function syncConfigurationToNativeAndroidAsync(
37
37
  options: SyncConfigurationToNativeOptions
38
38
  ): Promise<void> {
39
39
  const { exp } = getConfig(options.projectRoot, {
40
- isPublicConfig: true,
40
+ isPublicConfig: false, // This must be false or it will drop codesigning config
41
41
  skipSDKVersionRequirement: true,
42
42
  });
43
43
 
@@ -84,7 +84,7 @@ async function syncConfigurationToNativeIosAsync(
84
84
  options: SyncConfigurationToNativeOptions
85
85
  ): Promise<void> {
86
86
  const { exp } = getConfig(options.projectRoot, {
87
- isPublicConfig: true,
87
+ isPublicConfig: false, // This must be false or it will drop codesigning config
88
88
  skipSDKVersionRequirement: true,
89
89
  });
90
90
 
@@ -54,14 +54,16 @@ function getExpoDependencyChunks({
54
54
  'expo-audio',
55
55
  'expo-av',
56
56
  'expo-blur',
57
+ 'expo-crypto',
57
58
  'expo-image',
58
59
  'expo-linear-gradient',
59
60
  'expo-linking',
60
61
  'expo-localization',
61
- 'expo-crypto',
62
+ 'expo-media-library',
62
63
  'expo-network',
63
64
  'expo-secure-store',
64
65
  'expo-symbols',
66
+ 'expo-system-ui',
65
67
  'expo-ui',
66
68
  'expo-video',
67
69
  ],
@@ -314,10 +316,10 @@ async function preparePackageJson(
314
316
 
315
317
  const extraDevDependencies = configureE2E
316
318
  ? {
317
- '@config-plugins/detox': '^5.0.1',
319
+ '@config-plugins/detox': '^9.0.0',
318
320
  '@types/express': '^4.17.17',
319
321
  '@types/jest': '^29.4.0',
320
- detox: '^20.4.0',
322
+ detox: '^20.33.0',
321
323
  express: '^4.18.2',
322
324
  'form-data': '^4.0.0',
323
325
  jest: '^29.3.1',
@@ -367,7 +369,7 @@ async function preparePackageJson(
367
369
  ...packageJson,
368
370
  dependencies: {
369
371
  ...packageJson.dependencies,
370
- 'react-native': 'npm:react-native-tvos@~0.76.6-0',
372
+ 'react-native': 'npm:react-native-tvos@~0.77.0-0',
371
373
  '@react-native-tvos/config-tv': '^0.1.1',
372
374
  },
373
375
  expo: {
@@ -460,7 +462,15 @@ function transformAppJsonForE2E(
460
462
  runtimeVersion: string,
461
463
  isTV: boolean
462
464
  ) {
463
- const plugins: any[] = ['expo-updates', '@config-plugins/detox'];
465
+ const plugins: any[] = [
466
+ 'expo-updates',
467
+ [
468
+ '@config-plugins/detox',
469
+ {
470
+ subdomains: Array.from(new Set(['10.0.2.2', 'localhost', process.env.UPDATES_HOST])),
471
+ },
472
+ ],
473
+ ];
464
474
  if (isTV) {
465
475
  plugins.push([
466
476
  '@react-native-tvos/config-tv',
@@ -576,7 +586,15 @@ export function transformAppJsonForUpdatesDisabledE2E(
576
586
  projectName: string,
577
587
  runtimeVersion: string
578
588
  ) {
579
- const plugins: any[] = ['expo-updates', '@config-plugins/detox'];
589
+ const plugins: any[] = [
590
+ 'expo-updates',
591
+ [
592
+ '@config-plugins/detox',
593
+ {
594
+ subdomains: Array.from(new Set(['10.0.2.2', 'localhost', process.env.UPDATES_HOST])),
595
+ },
596
+ ],
597
+ ];
580
598
  return {
581
599
  ...appJson,
582
600
  expo: {
@@ -770,7 +788,7 @@ export async function initAsync(
770
788
  // enable proguard on Android
771
789
  await fs.appendFile(
772
790
  path.join(projectRoot, 'android', 'gradle.properties'),
773
- '\nandroid.enableProguardInReleaseBuilds=true\nandroid.kotlinVersion=1.9.24\nEXPO_UPDATES_NATIVE_DEBUG=true',
791
+ '\nandroid.enableProguardInReleaseBuilds=true\nEXPO_UPDATES_NATIVE_DEBUG=true',
774
792
  'utf-8'
775
793
  );
776
794
 
@@ -97,7 +97,7 @@ open class AppLoader: NSObject {
97
97
  preconditionFailure("Must override in concrete class")
98
98
  }
99
99
 
100
- open func downloadAsset(_ asset: UpdateAsset) {
100
+ open func downloadAsset(_ asset: UpdateAsset, extraHeaders: [String: Any]) {
101
101
  preconditionFailure("Must override in concrete class")
102
102
  }
103
103
 
@@ -202,6 +202,13 @@ open class AppLoader: NSObject {
202
202
  if let assets = updateManifest.assets(),
203
203
  !assets.isEmpty {
204
204
  self.assetsToLoad = assets
205
+ let embeddedUpdate = EmbeddedAppLoader.embeddedManifest(withConfig: self.config, database: self.database)
206
+ let extraHeaders = FileDownloader.extraHeadersForRemoteAssetRequest(
207
+ launchedUpdate: self.launchedUpdate,
208
+ embeddedUpdate: embeddedUpdate,
209
+ requestedUpdate: updateManifest
210
+ )
211
+
205
212
  for asset in assets {
206
213
  // before downloading, check to see if we already have this asset in the database
207
214
  let matchingDbEntry = try? self.database.asset(withKey: asset.key)
@@ -226,11 +233,11 @@ open class AppLoader: NSObject {
226
233
  self.handleAssetDownloadAlreadyExists(asset)
227
234
  }
228
235
  } else {
229
- self.downloadAsset(asset)
236
+ self.downloadAsset(asset, extraHeaders: extraHeaders)
230
237
  }
231
238
  }
232
239
  } else {
233
- self.downloadAsset(asset)
240
+ self.downloadAsset(asset, extraHeaders: extraHeaders)
234
241
  }
235
242
  }
236
243
  } else {
@@ -119,7 +119,7 @@ public final class EmbeddedAppLoader: AppLoader {
119
119
  ))
120
120
  }
121
121
 
122
- override public func downloadAsset(_ asset: UpdateAsset) {
122
+ override public func downloadAsset(_ asset: UpdateAsset, extraHeaders: [String: Any]) {
123
123
  FileDownloader.assetFilesQueue.async {
124
124
  self.handleAssetDownloadAlreadyExists(asset)
125
125
  }
@@ -221,6 +221,30 @@ public final class FileDownloader {
221
221
  return extraHeaders
222
222
  }
223
223
 
224
+ /**
225
+ * Get extra headers to pass into `downloadAsset:`
226
+ */
227
+ static func extraHeadersForRemoteAssetRequest(
228
+ launchedUpdate: Update?,
229
+ embeddedUpdate: Update?,
230
+ requestedUpdate: Update?
231
+ ) -> [String: Any] {
232
+ var extraHeaders: [String: Any] = [:]
233
+ if let launchedUpdate {
234
+ extraHeaders["Expo-Current-Update-ID"] = launchedUpdate.updateId.uuidString.lowercased()
235
+ }
236
+
237
+ if let embeddedUpdate {
238
+ extraHeaders["Expo-Embedded-Update-ID"] = embeddedUpdate.updateId.uuidString.lowercased()
239
+ }
240
+
241
+ if let requestedUpdate {
242
+ extraHeaders["Expo-Requested-Update-ID"] = requestedUpdate.updateId.uuidString.lowercased()
243
+ }
244
+
245
+ return extraHeaders
246
+ }
247
+
224
248
  private static func setHTTPHeaderFields(_ headers: [String: Any?]?, onRequest request: inout URLRequest) {
225
249
  guard let headers = headers else {
226
250
  return
@@ -84,7 +84,7 @@ public final class RemoteAppLoader: AppLoader {
84
84
  }
85
85
  }
86
86
 
87
- override public func downloadAsset(_ asset: UpdateAsset) {
87
+ override public func downloadAsset(_ asset: UpdateAsset, extraHeaders: [String: Any]) {
88
88
  let urlOnDisk = self.directory.appendingPathComponent(asset.filename)
89
89
 
90
90
  FileDownloader.assetFilesQueue.async {
@@ -101,12 +101,11 @@ public final class RemoteAppLoader: AppLoader {
101
101
  )
102
102
  return
103
103
  }
104
-
105
104
  self.downloader.downloadAsset(
106
105
  fromURL: assetUrl,
107
106
  verifyingHash: asset.expectedHash,
108
107
  toPath: urlOnDisk.path,
109
- extraHeaders: asset.extraRequestHeaders ?? [:]
108
+ extraHeaders: extraHeaders.merging(asset.extraRequestHeaders ?? [:]) { current, _ in current }
110
109
  ) { data, response, _ in
111
110
  DispatchQueue.global().async {
112
111
  self.handleAssetDownload(withData: data, response: response, asset: asset)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "expo-updates",
3
- "version": "1.0.0-canary-20250131-5c4e588",
3
+ "version": "1.0.0-canary-20250219-4a5dade",
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-20250131-5c4e588",
43
- "@expo/config-plugins": "9.0.15-canary-20250131-5c4e588",
42
+ "@expo/config": "11.0.0-canary-20250219-4a5dade",
43
+ "@expo/config-plugins": "9.0.16-canary-20250219-4a5dade",
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.3-canary-20250131-5c4e588",
48
- "expo-manifests": "0.15.6-canary-20250131-5c4e588",
49
- "expo-structured-headers": "4.0.1-canary-20250131-5c4e588",
50
- "expo-updates-interface": "1.0.1-canary-20250131-5c4e588",
47
+ "expo-eas-client": "0.13.3-canary-20250219-4a5dade",
48
+ "expo-manifests": "0.15.7-canary-20250219-4a5dade",
49
+ "expo-structured-headers": "4.0.1-canary-20250219-4a5dade",
50
+ "expo-updates-interface": "1.0.1-canary-20250219-4a5dade",
51
51
  "fast-glob": "^3.3.2",
52
52
  "fbemitter": "^3.0.0",
53
53
  "ignore": "^5.3.1",
@@ -57,7 +57,7 @@
57
57
  "@types/jest": "^29.2.1",
58
58
  "@types/node": "^18.19.34",
59
59
  "@types/node-forge": "^1.0.0",
60
- "expo-module-scripts": "4.0.4-canary-20250131-5c4e588",
60
+ "expo-module-scripts": "4.0.5-canary-20250219-4a5dade",
61
61
  "express": "^4.21.1",
62
62
  "form-data": "^4.0.0",
63
63
  "fs-extra": "~8.1.0",
@@ -65,7 +65,8 @@
65
65
  "xstate": "^4.37.2"
66
66
  },
67
67
  "peerDependencies": {
68
- "expo": "53.0.0-canary-20250131-5c4e588",
68
+ "expo": "53.0.0-canary-20250219-4a5dade",
69
69
  "react": "*"
70
- }
70
+ },
71
+ "gitHead": "4a5daded61d3d8b9d501059039ac74c09c25675b"
71
72
  }