expo-updates 1.0.0-canary-20250122-166c2cb → 1.0.0-canary-20250207-8bc5146

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.
Files changed (42) hide show
  1. package/CHANGELOG.md +8 -0
  2. package/android/build.gradle +1 -1
  3. package/android/src/main/java/expo/modules/updates/DisabledUpdatesController.kt +4 -0
  4. package/android/src/main/java/expo/modules/updates/EnabledUpdatesController.kt +7 -0
  5. package/android/src/main/java/expo/modules/updates/IUpdatesController.kt +2 -0
  6. package/android/src/main/java/expo/modules/updates/UpdatesConfiguration.kt +75 -14
  7. package/android/src/main/java/expo/modules/updates/UpdatesConfigurationOverride.kt +57 -0
  8. package/android/src/main/java/expo/modules/updates/UpdatesDevLauncherController.kt +9 -2
  9. package/android/src/main/java/expo/modules/updates/UpdatesModule.kt +16 -0
  10. package/android/src/main/java/expo/modules/updates/UpdatesUtils.kt +6 -2
  11. package/android/src/main/java/expo/modules/updates/db/BuildData.kt +22 -25
  12. package/android/src/main/java/expo/modules/updates/db/dao/JSONDataDao.kt +26 -11
  13. package/android/src/main/java/expo/modules/updates/errorrecovery/ErrorRecovery.kt +5 -4
  14. package/android/src/main/java/expo/modules/updates/launcher/NoDatabaseLauncher.kt +2 -3
  15. package/android/src/main/java/expo/modules/updates/manifest/EmbeddedManifestUtils.kt +1 -3
  16. package/android/src/main/java/expo/modules/updates/manifest/ManifestMetadata.kt +19 -12
  17. package/android/src/main/java/expo/modules/updates/procedures/StartupProcedure.kt +2 -1
  18. package/build/Updates.d.ts +11 -0
  19. package/build/Updates.d.ts.map +1 -1
  20. package/build/Updates.js +10 -0
  21. package/build/Updates.js.map +1 -1
  22. package/cli/build/syncConfigurationToNativeAsync.js +2 -2
  23. package/cli/src/syncConfigurationToNativeAsync.ts +2 -2
  24. package/e2e/fixtures/App.tsx +11 -0
  25. package/e2e/fixtures/Updates-bricking-measures-disabled.e2e.ts +77 -0
  26. package/e2e/fixtures/project_files/e2e/tests/utils/server.ts +26 -2
  27. package/e2e/setup/create-bricking-measures-disabled-eas-project.ts +43 -0
  28. package/e2e/setup/project.ts +73 -8
  29. package/expo-module.config.json +1 -3
  30. package/ios/EXUpdates/AppController.swift +1 -0
  31. package/ios/EXUpdates/Database/UpdatesBuildData.swift +37 -8
  32. package/ios/EXUpdates/Database/UpdatesDatabase.swift +36 -19
  33. package/ios/EXUpdates/DevLauncherAppController.swift +4 -0
  34. package/ios/EXUpdates/DisabledAppController.swift +4 -0
  35. package/ios/EXUpdates/EnabledAppController.swift +7 -0
  36. package/ios/EXUpdates/Exceptions.swift +10 -0
  37. package/ios/EXUpdates/UpdatesConfig.swift +60 -12
  38. package/ios/EXUpdates/UpdatesConfigOverride.swift +33 -0
  39. package/ios/EXUpdates/UpdatesModule.swift +16 -0
  40. package/ios/Tests/UpdatesBuildDataSpec.swift +103 -0
  41. package/package.json +11 -10
  42. package/src/Updates.ts +13 -0
package/CHANGELOG.md CHANGED
@@ -9,15 +9,23 @@
9
9
  ### 🎉 New features
10
10
 
11
11
  - Add new state machine context about startup procedure. ([#32433](https://github.com/expo/expo/pull/32433) by [@wschurman](https://github.com/wschurman))
12
+ - Add `Updates.setUpdateURLAndRequestHeadersOverride()` to allow overriding update URL configuration from the JS API. ([#34422](https://github.com/expo/expo/pull/34422), [#34423](https://github.com/expo/expo/pull/34423), [#34425](https://github.com/expo/expo/pull/34425), [#34426](https://github.com/expo/expo/pull/34426), [#34454](https://github.com/expo/expo/pull/34454), [#34455](https://github.com/expo/expo/pull/34455), [#34428](https://github.com/expo/expo/pull/34428), [#34404](https://github.com/expo/expo/pull/34404) by [@kudo](https://github.com/kudo), [@wschurman](https://github.com/wschurman))
12
13
 
13
14
  ### 🐛 Bug fixes
14
15
 
15
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))
16
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
+ - 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))
17
21
 
18
22
  ### 💡 Others
19
23
 
24
+ - [Android] Made ReactNativeFeatureFlags a parameter to the constructor of the ErrorRecovery class to be able to make tests pass ([#34363](https://github.com/expo/expo/pull/34363) by [@chrfalch](https://github.com/chrfalch))
20
25
  - [iOS] Inject logger from controllers down to dependent objects. ([#34035](https://github.com/expo/expo/pull/34035) by [@wschurman](https://github.com/wschurman))
26
+ - [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))
27
+ - Fixed build error on iOS Expo Go. ([#34485](https://github.com/expo/expo/pull/34485) by [@kudo](https://github.com/kudo))
28
+ - Fixed Android unit test errors in BuilDataTest. ([#34510](https://github.com/expo/expo/pull/34510) by [@kudo](https://github.com/kudo))
21
29
 
22
30
  ## 0.26.10 - 2024-12-05
23
31
 
@@ -112,13 +112,13 @@ dependencies {
112
112
  implementation("com.squareup.okhttp3:okhttp:4.9.2")
113
113
  implementation("com.squareup.okhttp3:okhttp-urlconnection:4.9.2")
114
114
  implementation("com.squareup.okhttp3:okhttp-brotli:4.9.2")
115
- implementation("commons-io:commons-io:2.16.1")
116
115
  implementation("org.bouncycastle:bcutil-jdk15to18:1.78.1")
117
116
 
118
117
  testImplementation 'junit:junit:4.13.2'
119
118
  testImplementation 'androidx.test:core:1.5.0'
120
119
  testImplementation "io.mockk:mockk:$mockk_version"
121
120
  testImplementation "org.jetbrains.kotlin:kotlin-test-junit:${kotlinVersion()}"
121
+ testImplementation 'org.robolectric:robolectric:4.14.1'
122
122
 
123
123
  androidTestImplementation 'com.squareup.okio:okio:2.9.0'
124
124
  androidTestImplementation 'androidx.test:runner:1.5.2'
@@ -162,6 +162,10 @@ class DisabledUpdatesController(
162
162
  callback.onFailure(UpdatesDisabledException("Updates.setExtraParamAsync() is not supported when expo-updates is not enabled."))
163
163
  }
164
164
 
165
+ override fun setUpdateURLAndRequestHeadersOverride(configOverride: UpdatesConfigurationOverride?) {
166
+ throw UpdatesDisabledException("Updates.setUpdateURLAndRequestHeadersOverride() is not supported when expo-updates is not enabled.")
167
+ }
168
+
165
169
  @Synchronized
166
170
  private fun notifyController() {
167
171
  if (launcher == null) {
@@ -262,6 +262,13 @@ class EnabledUpdatesController(
262
262
  }
263
263
  }
264
264
 
265
+ override fun setUpdateURLAndRequestHeadersOverride(configOverride: UpdatesConfigurationOverride?) {
266
+ if (!updatesConfiguration.disableAntiBrickingMeasures) {
267
+ throw CodedException("ERR_UPDATES_RUNTIME_OVERRIDE", "Must set disableAntiBrickingMeasures configuration to use updates overriding", null)
268
+ }
269
+ UpdatesConfigurationOverride.save(context, configOverride)
270
+ }
271
+
265
272
  companion object {
266
273
  private val TAG = EnabledUpdatesController::class.java.simpleName
267
274
  }
@@ -165,4 +165,6 @@ interface IUpdatesController {
165
165
  fun getExtraParams(callback: ModuleCallback<Bundle>)
166
166
 
167
167
  fun setExtraParam(key: String, value: String?, callback: ModuleCallback<Unit>)
168
+
169
+ fun setUpdateURLAndRequestHeadersOverride(configOverride: UpdatesConfigurationOverride?)
168
170
  }
@@ -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,
@@ -39,7 +37,8 @@ data class UpdatesConfiguration(
39
37
  val codeSigningMetadata: Map<String, String>?,
40
38
  val codeSigningIncludeManifestResponseCertificateChain: Boolean,
41
39
  private val codeSigningAllowUnsignedManifests: Boolean,
42
- val enableExpoUpdatesProtocolV0CompatibilityMode: Boolean // used only in Expo Go to prevent loading rollbacks and other directives, which don't make much sense in the context of Expo Go
40
+ val enableExpoUpdatesProtocolV0CompatibilityMode: Boolean, // used only in Expo Go to prevent loading rollbacks and other directives, which don't make much sense in the context of Expo Go
41
+ val disableAntiBrickingMeasures: Boolean
43
42
  ) {
44
43
  enum class CheckAutomaticallyConfiguration {
45
44
  NEVER {
@@ -60,12 +59,27 @@ data class UpdatesConfiguration(
60
59
  }
61
60
  }
62
61
 
63
- constructor(context: Context?, overrideMap: Map<String, Any>?) : this(
62
+ constructor(
63
+ context: Context?,
64
+ overrideMap: Map<String, Any>?
65
+ ) : this(
66
+ context,
67
+ overrideMap,
68
+ disableAntiBrickingMeasures = getDisableAntiBrickingMeasures(context, overrideMap),
69
+ configOverride = context?.let { UpdatesConfigurationOverride.load(context) }
70
+ )
71
+
72
+ internal constructor(
73
+ context: Context?,
74
+ overrideMap: Map<String, Any>?,
75
+ disableAntiBrickingMeasures: Boolean,
76
+ configOverride: UpdatesConfigurationOverride?
77
+ ) : this(
64
78
  scopeKey = maybeGetDefaultScopeKey(
65
79
  overrideMap?.readValueCheckingType<String>(UPDATES_CONFIGURATION_SCOPE_KEY_KEY) ?: context?.getMetadataValue("expo.modules.updates.EXPO_SCOPE_KEY"),
66
- updateUrl = getUpdatesUrl(context, overrideMap)!!
80
+ updateUrl = getUpdateUrl(context, overrideMap, disableAntiBrickingMeasures, configOverride)!!
67
81
  ),
68
- updateUrl = getUpdatesUrl(context, overrideMap)!!,
82
+ updateUrl = getUpdateUrl(context, overrideMap, disableAntiBrickingMeasures, configOverride)!!,
69
83
  runtimeVersionRaw = getRuntimeVersion(context, overrideMap),
70
84
  launchWaitMs = overrideMap?.readValueCheckingType<Int>(UPDATES_CONFIGURATION_LAUNCH_WAIT_MS_KEY) ?: context?.getMetadataValue("expo.modules.updates.EXPO_UPDATES_LAUNCH_WAIT_MS") ?: UPDATES_CONFIGURATION_LAUNCH_WAIT_MS_DEFAULT_VALUE,
71
85
  checkOnLaunch = overrideMap?.readValueCheckingType<String>(UPDATES_CONFIGURATION_CHECK_ON_LAUNCH_KEY)?.let {
@@ -85,10 +99,8 @@ data class UpdatesConfiguration(
85
99
  CheckAutomaticallyConfiguration.ALWAYS
86
100
  }
87
101
  },
88
- hasEmbeddedUpdate = overrideMap?.readValueCheckingType<Boolean>(UPDATES_CONFIGURATION_HAS_EMBEDDED_UPDATE_KEY) ?: context?.getMetadataValue("expo.modules.updates.HAS_EMBEDDED_UPDATE") ?: true,
89
- requestHeaders = overrideMap?.readValueCheckingType<Map<String, String>>(UPDATES_CONFIGURATION_REQUEST_HEADERS_KEY) ?: (context?.getMetadataValue<String>("expo.modules.updates.UPDATES_CONFIGURATION_REQUEST_HEADERS_KEY") ?: "{}").let {
90
- UpdatesUtils.getMapFromJSONString(it)
91
- },
102
+ hasEmbeddedUpdate = getHasEmbeddedUpdate(context, overrideMap, disableAntiBrickingMeasures, configOverride),
103
+ requestHeaders = getRequestHeaders(context, overrideMap, disableAntiBrickingMeasures, configOverride),
92
104
  codeSigningCertificate = overrideMap?.readValueCheckingType<String>(UPDATES_CONFIGURATION_CODE_SIGNING_CERTIFICATE) ?: context?.getMetadataValue("expo.modules.updates.CODE_SIGNING_CERTIFICATE"),
93
105
  codeSigningMetadata = overrideMap?.readValueCheckingType<Map<String, String>>(UPDATES_CONFIGURATION_CODE_SIGNING_METADATA) ?: (context?.getMetadataValue<String>("expo.modules.updates.CODE_SIGNING_METADATA") ?: "{}").let {
94
106
  UpdatesUtils.getMapFromJSONString(it)
@@ -99,7 +111,8 @@ data class UpdatesConfiguration(
99
111
  codeSigningAllowUnsignedManifests = overrideMap?.readValueCheckingType<Boolean>(
100
112
  UPDATES_CONFIGURATION_CODE_SIGNING_ALLOW_UNSIGNED_MANIFESTS
101
113
  ) ?: context?.getMetadataValue("expo.modules.updates.CODE_SIGNING_ALLOW_UNSIGNED_MANIFESTS") ?: false,
102
- enableExpoUpdatesProtocolV0CompatibilityMode = overrideMap?.readValueCheckingType<Boolean>(UPDATES_CONFIGURATION_ENABLE_EXPO_UPDATES_PROTOCOL_V0_COMPATIBILITY_MODE) ?: context?.getMetadataValue("expo.modules.updates.ENABLE_EXPO_UPDATES_PROTOCOL_V0_COMPATIBILITY_MODE") ?: false
114
+ enableExpoUpdatesProtocolV0CompatibilityMode = overrideMap?.readValueCheckingType<Boolean>(UPDATES_CONFIGURATION_ENABLE_EXPO_UPDATES_PROTOCOL_V0_COMPATIBILITY_MODE) ?: context?.getMetadataValue("expo.modules.updates.ENABLE_EXPO_UPDATES_PROTOCOL_V0_COMPATIBILITY_MODE") ?: false,
115
+ disableAntiBrickingMeasures = getDisableAntiBrickingMeasures(context, overrideMap)
103
116
  )
104
117
 
105
118
  val codeSigningConfiguration: CodeSigningConfiguration? by lazy {
@@ -128,6 +141,7 @@ data class UpdatesConfiguration(
128
141
  const val UPDATES_CONFIGURATION_LAUNCH_WAIT_MS_KEY = "launchWaitMs"
129
142
  const val UPDATES_CONFIGURATION_HAS_EMBEDDED_UPDATE_KEY = "hasEmbeddedUpdate"
130
143
  const val UPDATES_CONFIGURATION_ENABLE_EXPO_UPDATES_PROTOCOL_V0_COMPATIBILITY_MODE = "enableExpoUpdatesProtocolCompatibilityMode"
144
+ const val UPDATES_CONFIGURATION_DISABLE_ANTI_BRICKING_MEASURES = "disableAntiBrickingMeasures"
131
145
 
132
146
  const val UPDATES_CONFIGURATION_CODE_SIGNING_CERTIFICATE = "codeSigningCertificate"
133
147
  const val UPDATES_CONFIGURATION_CODE_SIGNING_METADATA = "codeSigningMetadata"
@@ -139,12 +153,57 @@ data class UpdatesConfiguration(
139
153
  const val UPDATES_CONFIGURATION_RUNTIME_VERSION_READ_FINGERPRINT_FILE_SENTINEL = "file:fingerprint"
140
154
  private const val FINGERPRINT_FILE_NAME = "fingerprint"
141
155
 
142
- private fun getUpdatesUrl(context: Context?, overrideMap: Map<String, Any>?): Uri? {
156
+ private fun getDisableAntiBrickingMeasures(context: Context?, overrideMap: Map<String, Any>?): Boolean {
157
+ return overrideMap?.readValueCheckingType<Boolean>(UPDATES_CONFIGURATION_DISABLE_ANTI_BRICKING_MEASURES) ?: context?.getMetadataValue("expo.modules.updates.DISABLE_ANTI_BRICKING_MEASURES") ?: false
158
+ }
159
+
160
+ private fun getHasEmbeddedUpdate(
161
+ context: Context?,
162
+ overrideMap: Map<String, Any>?,
163
+ disableAntiBrickingMeasures: Boolean,
164
+ configOverride: UpdatesConfigurationOverride?
165
+ ): Boolean {
166
+ if (disableAntiBrickingMeasures && configOverride != null) {
167
+ return false
168
+ }
169
+ return overrideMap?.readValueCheckingType<Boolean>(UPDATES_CONFIGURATION_HAS_EMBEDDED_UPDATE_KEY)
170
+ ?: context?.getMetadataValue("expo.modules.updates.HAS_EMBEDDED_UPDATE")
171
+ ?: true
172
+ }
173
+
174
+ private fun getUpdateUrl(
175
+ context: Context?,
176
+ overrideMap: Map<String, Any>?,
177
+ disableAntiBrickingMeasures: Boolean,
178
+ configOverride: UpdatesConfigurationOverride?
179
+ ): Uri? {
180
+ if (disableAntiBrickingMeasures) {
181
+ configOverride?.let {
182
+ return it.updateUrl
183
+ }
184
+ }
143
185
  return overrideMap?.readValueCheckingType(UPDATES_CONFIGURATION_UPDATE_URL_KEY)
144
186
  ?: context?.getMetadataValue<String>("expo.modules.updates.EXPO_UPDATE_URL")
145
187
  ?.let { Uri.parse(it) }
146
188
  }
147
189
 
190
+ private fun getRequestHeaders(
191
+ context: Context?,
192
+ overrideMap: Map<String, Any>?,
193
+ disableAntiBrickingMeasures: Boolean,
194
+ configOverride: UpdatesConfigurationOverride?
195
+ ): Map<String, String> {
196
+ if (disableAntiBrickingMeasures) {
197
+ configOverride?.let {
198
+ return it.requestHeaders
199
+ }
200
+ }
201
+ return overrideMap?.readValueCheckingType<Map<String, String>>(UPDATES_CONFIGURATION_REQUEST_HEADERS_KEY)
202
+ ?: (context?.getMetadataValue<String>("expo.modules.updates.UPDATES_CONFIGURATION_REQUEST_HEADERS_KEY") ?: "{}").let {
203
+ UpdatesUtils.getMapFromJSONString(it)
204
+ }
205
+ }
206
+
148
207
  private fun getIsEnabled(context: Context?, overrideMap: Map<String, Any>?): Boolean {
149
208
  return overrideMap?.readValueCheckingType(UPDATES_CONFIGURATION_ENABLED_KEY) ?: context?.getMetadataValue("expo.modules.updates.ENABLED") ?: true
150
209
  }
@@ -154,7 +213,7 @@ data class UpdatesConfiguration(
154
213
 
155
214
  if (context != null && runtimeVersion == UPDATES_CONFIGURATION_RUNTIME_VERSION_READ_FINGERPRINT_FILE_SENTINEL) {
156
215
  return context.assets.open(FINGERPRINT_FILE_NAME).use { stream ->
157
- IOUtils.toString(stream, StandardCharsets.UTF_8)
216
+ stream.bufferedReader(Charsets.UTF_8).use { it.readText() }
158
217
  }
159
218
  }
160
219
 
@@ -166,7 +225,9 @@ data class UpdatesConfiguration(
166
225
  if (!isEnabledConfigSetting) {
167
226
  return UpdatesConfigurationValidationResult.INVALID_NOT_ENABLED
168
227
  }
169
- getUpdatesUrl(context, overrideMap) ?: return UpdatesConfigurationValidationResult.INVALID_MISSING_URL
228
+ val disableAntiBrickingMeasures = getDisableAntiBrickingMeasures(context, overrideMap)
229
+ val configOverride = if (context != null) UpdatesConfigurationOverride.load(context) else null
230
+ getUpdateUrl(context, overrideMap, disableAntiBrickingMeasures, configOverride) ?: return UpdatesConfigurationValidationResult.INVALID_MISSING_URL
170
231
 
171
232
  if (getRuntimeVersion(context, overrideMap).isNullOrEmpty()) {
172
233
  return UpdatesConfigurationValidationResult.INVALID_MISSING_RUNTIME_VERSION
@@ -0,0 +1,57 @@
1
+ package expo.modules.updates
2
+
3
+ import android.content.Context
4
+ import android.net.Uri
5
+ import expo.modules.manifests.core.toMap
6
+ import org.json.JSONObject
7
+
8
+ /**
9
+ * [UpdatesConfiguration] values set at runtime that override build-time configuration.
10
+ */
11
+ data class UpdatesConfigurationOverride(
12
+ val updateUrl: Uri,
13
+ val requestHeaders: Map<String, String>
14
+ ) {
15
+ private fun toJSONObject(): JSONObject {
16
+ return JSONObject().apply {
17
+ put("updateUrl", updateUrl.toString())
18
+ put("requestHeaders", JSONObject(requestHeaders))
19
+ }
20
+ }
21
+
22
+ companion object {
23
+ private const val UPDATES_PREFS_FILE = "dev.expo.updates.prefs"
24
+ private const val UPDATES_PREFS_KEY_UPDATES_CONFIGURATION_OVERRIDE = "updatesConfigOverride"
25
+
26
+ internal fun load(context: Context): UpdatesConfigurationOverride? {
27
+ val configOverride =
28
+ context.getSharedPreferences(UPDATES_PREFS_FILE, Context.MODE_PRIVATE)
29
+ ?.getString(UPDATES_PREFS_KEY_UPDATES_CONFIGURATION_OVERRIDE, null)
30
+ return configOverride?.let {
31
+ fromJSONObject(JSONObject(it))
32
+ }
33
+ }
34
+
35
+ internal fun save(context: Context, configOverride: UpdatesConfigurationOverride?) {
36
+ val prefs = context.getSharedPreferences(UPDATES_PREFS_FILE, Context.MODE_PRIVATE)
37
+ with(prefs.edit()) {
38
+ if (configOverride != null) {
39
+ putString(UPDATES_PREFS_KEY_UPDATES_CONFIGURATION_OVERRIDE, configOverride.toJSONObject().toString())
40
+ } else {
41
+ remove(UPDATES_PREFS_KEY_UPDATES_CONFIGURATION_OVERRIDE)
42
+ }
43
+ apply()
44
+ }
45
+ }
46
+
47
+ private fun fromJSONObject(json: JSONObject): UpdatesConfigurationOverride {
48
+ val requestHeaders = json.getJSONObject("requestHeaders")
49
+ .toMap()
50
+ .mapValues { it.value.toString() }
51
+ return UpdatesConfigurationOverride(
52
+ updateUrl = Uri.parse(json.getString("updateUrl")),
53
+ requestHeaders = requestHeaders
54
+ )
55
+ }
56
+ }
57
+ }
@@ -78,8 +78,11 @@ class UpdatesDevLauncherController(
78
78
  }
79
79
 
80
80
  @get:Synchronized
81
- override val launchAssetFile: String
82
- get() = throw Exception("IUpdatesController.launchAssetFile should not be called in dev client")
81
+ override val launchAssetFile: String?
82
+ get() {
83
+ logger.warn("launchAssetFile should not be called from expo-dev-client build, except for Detox testing")
84
+ return null
85
+ }
83
86
 
84
87
  override val bundleAssetName: String
85
88
  get() = throw Exception("IUpdatesController.bundleAssetName should not be called in dev client")
@@ -363,6 +366,10 @@ class UpdatesDevLauncherController(
363
366
  callback.onFailure(NotAvailableInDevClientException("Updates.setExtraParamAsync() is not supported in development builds."))
364
367
  }
365
368
 
369
+ override fun setUpdateURLAndRequestHeadersOverride(configOverride: UpdatesConfigurationOverride?) {
370
+ throw NotAvailableInDevClientException("Updates.setUpdateURLAndRequestHeadersOverride() is not supported in development builds.")
371
+ }
372
+
366
373
  companion object {
367
374
  private val TAG = UpdatesDevLauncherController::class.java.simpleName
368
375
  }
@@ -1,6 +1,7 @@
1
1
  package expo.modules.updates
2
2
 
3
3
  import android.content.Context
4
+ import android.net.Uri
4
5
  import android.os.AsyncTask
5
6
  import android.os.Bundle
6
7
  import expo.modules.kotlin.Promise
@@ -8,6 +9,8 @@ import expo.modules.kotlin.exception.CodedException
8
9
  import expo.modules.kotlin.exception.Exceptions
9
10
  import expo.modules.kotlin.modules.Module
10
11
  import expo.modules.kotlin.modules.ModuleDefinition
12
+ import expo.modules.kotlin.records.Field
13
+ import expo.modules.kotlin.records.Record
11
14
  import expo.modules.kotlin.types.Enumerable
12
15
  import expo.modules.updates.events.IUpdatesEventManagerObserver
13
16
  import expo.modules.updates.logging.UpdatesErrorCode
@@ -211,6 +214,10 @@ class UpdatesModule : Module(), IUpdatesEventManagerObserver {
211
214
  }
212
215
  }
213
216
  }
217
+
218
+ Function("setUpdateURLAndRequestHeadersOverride") { configOverride: UpdatesConfigurationOverrideParam? ->
219
+ UpdatesController.instance.setUpdateURLAndRequestHeadersOverride(configOverride?.toUpdatesConfigurationOverride())
220
+ }
214
221
  }
215
222
 
216
223
  companion object {
@@ -253,4 +260,13 @@ class UpdatesModule : Module(), IUpdatesEventManagerObserver {
253
260
  override fun onStateMachineContextEvent(context: UpdatesStateContext) {
254
261
  sendEvent(UpdatesJSEvent.StateChange, Bundle().apply { putBundle("context", context.bundle) })
255
262
  }
263
+
264
+ internal data class UpdatesConfigurationOverrideParam(
265
+ @Field val updateUrl: Uri,
266
+ @Field val requestHeaders: Map<String, String>
267
+ ) : Record {
268
+ fun toUpdatesConfigurationOverride(): UpdatesConfigurationOverride {
269
+ return UpdatesConfigurationOverride(updateUrl, requestHeaders)
270
+ }
271
+ }
256
272
  }
@@ -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
@@ -1,8 +1,9 @@
1
1
  package expo.modules.updates.db
2
2
 
3
- import android.net.Uri
4
- import expo.modules.jsonutils.getNullable
3
+ import expo.modules.manifests.core.toMap
5
4
  import expo.modules.updates.UpdatesConfiguration
5
+ import expo.modules.updates.db.dao.JSONDataDao
6
+ import expo.modules.updates.manifest.ManifestMetadata
6
7
  import org.json.JSONObject
7
8
 
8
9
  /**
@@ -27,8 +28,6 @@ import org.json.JSONObject
27
28
  * UPDATES_CONFIGURATION_REQUEST_HEADERS_KEY
28
29
  */
29
30
  object BuildData {
30
- private var staticBuildDataKey = "staticBuildData"
31
-
32
31
  fun ensureBuildDataIsConsistent(
33
32
  updatesConfiguration: UpdatesConfiguration,
34
33
  database: UpdatesDatabase
@@ -39,6 +38,7 @@ object BuildData {
39
38
  setBuildDataInDatabase(database, updatesConfiguration)
40
39
  } else if (!isBuildDataConsistent(updatesConfiguration, buildJSON)) {
41
40
  clearAllUpdatesFromDatabase(database)
41
+ clearManifestMetadataFromDatabase(database)
42
42
  setBuildDataInDatabase(database, updatesConfiguration)
43
43
  }
44
44
  }
@@ -48,28 +48,17 @@ object BuildData {
48
48
  database.updateDao().deleteUpdates(allUpdates)
49
49
  }
50
50
 
51
+ fun clearManifestMetadataFromDatabase(database: UpdatesDatabase) {
52
+ ManifestMetadata.clearMetadataForBuildDataClearOperation(database)
53
+ }
54
+
51
55
  fun isBuildDataConsistent(
52
56
  updatesConfiguration: UpdatesConfiguration,
53
57
  databaseBuildData: JSONObject
54
58
  ): Boolean {
55
- val configBuildData = getBuildDataFromConfig(updatesConfiguration)
56
-
57
- val updateUrlKey = UpdatesConfiguration.UPDATES_CONFIGURATION_UPDATE_URL_KEY
58
- val requestHeadersKey = UpdatesConfiguration.UPDATES_CONFIGURATION_REQUEST_HEADERS_KEY
59
-
60
- // check equality of the two JSONObjects. The build data object is string valued with the
61
- // exception of "requestHeaders" which is a string valued object.
62
- return mutableListOf<Boolean>().apply {
63
- add(databaseBuildData.get(updateUrlKey).let { Uri.parse(it.toString()) } == configBuildData.get(updateUrlKey))
64
-
65
- // loop through keys from both requestHeaders objects.
66
- for (key in configBuildData.getJSONObject(requestHeadersKey).keys()) {
67
- add(databaseBuildData.getJSONObject(requestHeadersKey).getNullable<String>(key) == configBuildData.getJSONObject(requestHeadersKey).getNullable(key))
68
- }
69
- for (key in databaseBuildData.getJSONObject(requestHeadersKey).keys()) {
70
- add(databaseBuildData.getJSONObject(requestHeadersKey).getNullable<String>(key) == configBuildData.getJSONObject(requestHeadersKey).getNullable(key))
71
- }
72
- }.all { it }
59
+ val configBuildData = defaultBuildData + getBuildDataFromConfig(updatesConfiguration).toMap()
60
+ val dbBuildData = defaultBuildData + databaseBuildData.toMap()
61
+ return configBuildData == dbBuildData
73
62
  }
74
63
 
75
64
  fun setBuildDataInDatabase(
@@ -78,14 +67,14 @@ object BuildData {
78
67
  ) {
79
68
  val buildDataJSON = getBuildDataFromConfig(updatesConfiguration)
80
69
  database.jsonDataDao()?.setJSONStringForKey(
81
- staticBuildDataKey,
70
+ JSONDataDao.JSONDataKey.STATIC_BUILD_DATA,
82
71
  buildDataJSON.toString(),
83
72
  updatesConfiguration.scopeKey
84
73
  )
85
74
  }
86
75
 
87
76
  fun getBuildDataFromDatabase(database: UpdatesDatabase, scopeKey: String): JSONObject? {
88
- val buildJSONString = database.jsonDataDao()?.loadJSONStringForKey(staticBuildDataKey, scopeKey)
77
+ val buildJSONString = database.jsonDataDao()?.loadJSONStringForKey(JSONDataDao.JSONDataKey.STATIC_BUILD_DATA, scopeKey)
89
78
  return if (buildJSONString == null) null else JSONObject(buildJSONString)
90
79
  }
91
80
 
@@ -94,9 +83,17 @@ object BuildData {
94
83
  for ((key, value) in updatesConfiguration.requestHeaders) put(key, value)
95
84
  }
96
85
  val buildData = JSONObject().apply {
97
- put(UpdatesConfiguration.UPDATES_CONFIGURATION_UPDATE_URL_KEY, updatesConfiguration.updateUrl)
86
+ put(UpdatesConfiguration.UPDATES_CONFIGURATION_UPDATE_URL_KEY, updatesConfiguration.updateUrl.toString())
98
87
  put(UpdatesConfiguration.UPDATES_CONFIGURATION_REQUEST_HEADERS_KEY, requestHeadersJSON)
88
+ put(UpdatesConfiguration.UPDATES_CONFIGURATION_HAS_EMBEDDED_UPDATE_KEY, updatesConfiguration.hasEmbeddedUpdate)
99
89
  }
100
90
  return buildData
101
91
  }
92
+
93
+ /**
94
+ * Fallback data specifically for migration while database data doesn't have these keys
95
+ */
96
+ private val defaultBuildData = mapOf(
97
+ UpdatesConfiguration.UPDATES_CONFIGURATION_HAS_EMBEDDED_UPDATE_KEY to true
98
+ )
102
99
  }
@@ -12,6 +12,13 @@ import java.util.*
12
12
  */
13
13
  @Dao
14
14
  abstract class JSONDataDao {
15
+ enum class JSONDataKey(val key: String) {
16
+ STATIC_BUILD_DATA("staticBuildData"),
17
+ EXTRA_PARAMS("extraParams"),
18
+ MANIFEST_SERVER_DEFINED_HEADERS("serverDefinedHeaders"),
19
+ MANIFEST_FILTERS("manifestFilters")
20
+ }
21
+
15
22
  @Query("SELECT * FROM json_data WHERE `key` = :key AND scope_key = :scopeKey ORDER BY last_updated DESC LIMIT 1;")
16
23
  protected abstract fun loadJSONDataForKeyInternal(key: String, scopeKey: String): List<JSONDataEntity>
17
24
 
@@ -21,11 +28,14 @@ abstract class JSONDataDao {
21
28
  @Query("DELETE FROM json_data WHERE `key` = :key AND scope_key = :scopeKey;")
22
29
  protected abstract fun deleteJSONDataForKeyInternal(key: String, scopeKey: String)
23
30
 
31
+ @Query("DELETE FROM json_data WHERE `key` IN (:keys)")
32
+ protected abstract fun deleteJSONDataForKeysForAllScopeKeysInternal(keys: List<String>)
33
+
24
34
  /**
25
35
  * for public use
26
36
  */
27
- fun loadJSONStringForKey(key: String, scopeKey: String): String? {
28
- val rows = loadJSONDataForKeyInternal(key, scopeKey)
37
+ fun loadJSONStringForKey(key: JSONDataKey, scopeKey: String): String? {
38
+ val rows = loadJSONDataForKeyInternal(key.key, scopeKey)
29
39
  return if (rows.isEmpty()) {
30
40
  null
31
41
  } else {
@@ -34,25 +44,30 @@ abstract class JSONDataDao {
34
44
  }
35
45
 
36
46
  @Transaction
37
- open fun setJSONStringForKey(key: String, value: String, scopeKey: String) {
38
- deleteJSONDataForKeyInternal(key, scopeKey)
39
- insertJSONDataInternal(JSONDataEntity(key, value, Date(), scopeKey))
47
+ open fun setJSONStringForKey(key: JSONDataKey, value: String, scopeKey: String) {
48
+ deleteJSONDataForKeyInternal(key.key, scopeKey)
49
+ insertJSONDataInternal(JSONDataEntity(key.key, value, Date(), scopeKey))
40
50
  }
41
51
 
42
52
  @Transaction
43
- open fun setMultipleFields(fields: Map<String, String>, scopeKey: String) {
53
+ open fun setMultipleFields(fields: Map<JSONDataKey, String>, scopeKey: String) {
44
54
  val iterator = fields.entries.iterator()
45
55
  while (iterator.hasNext()) {
46
56
  val entry = iterator.next()
47
- deleteJSONDataForKeyInternal(entry.key, scopeKey)
48
- insertJSONDataInternal(JSONDataEntity(entry.key, entry.value, Date(), scopeKey))
57
+ deleteJSONDataForKeyInternal(entry.key.key, scopeKey)
58
+ insertJSONDataInternal(JSONDataEntity(entry.key.key, entry.value, Date(), scopeKey))
49
59
  }
50
60
  }
51
61
 
52
62
  @Transaction
53
- open fun updateJSONStringForKey(key: String, scopeKey: String, updater: (previousValue: String?) -> String) {
63
+ open fun updateJSONStringForKey(key: JSONDataKey, scopeKey: String, updater: (previousValue: String?) -> String) {
54
64
  val previousValue = loadJSONStringForKey(key, scopeKey)
55
- deleteJSONDataForKeyInternal(key, scopeKey)
56
- insertJSONDataInternal(JSONDataEntity(key, updater(previousValue), Date(), scopeKey))
65
+ deleteJSONDataForKeyInternal(key.key, scopeKey)
66
+ insertJSONDataInternal(JSONDataEntity(key.key, updater(previousValue), Date(), scopeKey))
67
+ }
68
+
69
+ @Transaction
70
+ open fun deleteJSONDataForKeysForAllScopeKeys(keys: List<JSONDataKey>) {
71
+ deleteJSONDataForKeysForAllScopeKeysInternal(keys.map { it.key })
57
72
  }
58
73
  }
@@ -8,7 +8,7 @@ import com.facebook.react.bridge.ReactMarker.MarkerListener
8
8
  import com.facebook.react.bridge.ReactMarkerConstants
9
9
  import com.facebook.react.devsupport.ReleaseDevSupportManager
10
10
  import com.facebook.react.devsupport.interfaces.DevSupportManager
11
- import expo.modules.rncompatibility.ReactNativeFeatureFlags
11
+ import expo.modules.rncompatibility.IReactNativeFeatureFlagsProvider
12
12
  import expo.modules.updates.logging.UpdatesErrorCode
13
13
  import expo.modules.updates.logging.UpdatesLogger
14
14
  import java.lang.ref.WeakReference
@@ -27,7 +27,8 @@ import java.lang.ref.WeakReference
27
27
  * and so there is no more need to trigger the error recovery pipeline.
28
28
  */
29
29
  class ErrorRecovery(
30
- private val logger: UpdatesLogger
30
+ private val logger: UpdatesLogger,
31
+ private val reactNativeFeatureFlagsProvider: IReactNativeFeatureFlagsProvider
31
32
  ) {
32
33
  internal val handlerThread = HandlerThread("expo-updates-error-recovery")
33
34
  internal lateinit var handler: Handler
@@ -97,7 +98,7 @@ class ErrorRecovery(
97
98
  }
98
99
 
99
100
  private fun registerErrorHandler(devSupportManager: DevSupportManager) {
100
- if (ReactNativeFeatureFlags.enableBridgelessArchitecture) {
101
+ if (reactNativeFeatureFlagsProvider.enableBridgelessArchitecture) {
101
102
  registerErrorHandlerImplBridgeless()
102
103
  } else {
103
104
  registerErrorHandlerImplBridge(devSupportManager)
@@ -130,7 +131,7 @@ class ErrorRecovery(
130
131
  }
131
132
 
132
133
  private fun unregisterErrorHandler() {
133
- if (ReactNativeFeatureFlags.enableBridgelessArchitecture) {
134
+ if (reactNativeFeatureFlagsProvider.enableBridgelessArchitecture) {
134
135
  unregisterErrorHandlerImplBridgeless()
135
136
  } else {
136
137
  unregisterErrorHandlerImplBridge()
@@ -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) {
@@ -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)