Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions packages/expo-dev-launcher/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@

### 🎉 New features

- Full native interface for updates. ([#42981](https://github.com/expo/expo/pull/42981) by [@douglowder](https://github.com/douglowder))

### 🐛 Bug fixes

- fixes to error handling ([#42873](https://github.com/expo/expo/pull/42873) by [@vonovak](https://github.com/vonovak))
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ import expo.modules.devlauncher.services.DependencyInjection
import expo.modules.kotlin.weak
import expo.modules.manifests.core.Manifest
import expo.modules.updatesinterface.UpdatesInterface
import expo.modules.updatesinterface.UpdatesDevLauncherInterface
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
Expand Down Expand Up @@ -146,7 +147,7 @@ class DevLauncherController private constructor(
val manifestParser = DevLauncherManifestParser(httpClient, parsedUrl, installationIDHelper.getOrCreateInstallationID(context))
val appIntent = createAppIntent()

updatesInterface?.reset()
(updatesInterface as UpdatesDevLauncherInterface?)?.reset()

val appLoaderFactory = DevLauncherAppLoaderFactory(
context,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ package expo.modules.devlauncher.helpers

import android.content.Context
import android.net.Uri
import expo.modules.updatesinterface.UpdatesInterface
import expo.modules.updatesinterface.UpdatesDevLauncherInterface
import org.json.JSONObject
import java.lang.Exception
import java.util.*
Expand All @@ -11,16 +11,16 @@ import kotlin.coroutines.resume
import kotlin.coroutines.resumeWithException
import kotlin.coroutines.suspendCoroutine

suspend fun UpdatesInterface.loadUpdate(
suspend fun UpdatesDevLauncherInterface.loadUpdate(
configuration: HashMap<String, Any>,
context: Context,
shouldContinue: (manifest: JSONObject) -> Boolean
): UpdatesInterface.Update =
): UpdatesDevLauncherInterface.Update =
suspendCoroutine { cont ->
this.fetchUpdateWithConfiguration(
configuration,
object : UpdatesInterface.UpdateCallback {
override fun onSuccess(update: UpdatesInterface.Update?) {
object : UpdatesDevLauncherInterface.UpdateCallback {
override fun onSuccess(update: UpdatesDevLauncherInterface.Update?) {
// if the update is null, we previously aborted the fetch, so we've already resumed
update?.let { cont.resume(update) }
}
Expand All @@ -32,7 +32,7 @@ suspend fun UpdatesInterface.loadUpdate(
return if (shouldContinue(manifest)) {
true
} else {
cont.resume(object : UpdatesInterface.Update {
cont.resume(object : UpdatesDevLauncherInterface.Update {
override val manifest: JSONObject = manifest
override val launchAssetPath: String
get() = throw Exception("Tried to access launch asset path for a manifest that was not loaded")
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import expo.modules.devlauncher.helpers.createUpdatesConfigurationWithUrl
import expo.modules.devlauncher.helpers.loadUpdate
import expo.modules.devlauncher.launcher.manifest.DevLauncherManifestParser
import expo.modules.manifests.core.Manifest
import expo.modules.updatesinterface.UpdatesDevLauncherInterface
import expo.modules.updatesinterface.UpdatesInterface

class DevLauncherAppLoaderFactory(
Expand Down Expand Up @@ -36,7 +37,7 @@ class DevLauncherAppLoaderFactory(
null
} else {
val configurationCandidate = createUpdatesConfigurationWithUrl(url, projectUrl, runtimeVersion, installationIDHelper.getOrCreateInstallationID(context))
if (it.isValidUpdatesConfiguration(configurationCandidate)) {
if ((it as UpdatesDevLauncherInterface).isValidUpdatesConfiguration(configurationCandidate)) {
configurationCandidate
} else {
null
Expand All @@ -51,7 +52,7 @@ class DevLauncherAppLoaderFactory(
}
DevLauncherLocalAppLoader(manifest!!, appHost, context, controller)
} else {
val update = updatesInterface!!.loadUpdate(validConfiguration, context) {
val update = (updatesInterface as UpdatesDevLauncherInterface?)!!.loadUpdate(validConfiguration, context) {
manifest = Manifest.fromManifestJson(it) // TODO: might be able to pass actual manifest object in here
return@loadUpdate !manifest!!.isUsingDeveloperTool()
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import expo.modules.core.interfaces.ReactNativeHostHandler
import expo.modules.devlauncher.DevLauncherController
import java.lang.ref.WeakReference
import expo.modules.updatesinterface.UpdatesControllerRegistry
import expo.modules.updatesinterface.UpdatesDevLauncherInterface
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
Expand Down Expand Up @@ -45,7 +46,7 @@ class DevLauncherReactNativeHostHandler(context: Context) : ReactNativeHostHandl
CoroutineScope(Dispatchers.Main).launch {
UpdatesControllerRegistry.controller?.get()?.let {
DevLauncherController.instance.updatesInterface = it
it.updatesInterfaceCallbacks = WeakReference(DevLauncherController.instance)
(it as UpdatesDevLauncherInterface).updatesInterfaceCallbacks = WeakReference(DevLauncherController.instance)
}
}
}
Expand Down
2 changes: 1 addition & 1 deletion packages/expo-dev-launcher/ios/EXDevLauncherController.h
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@ NS_ASSUME_NONNULL_BEGIN
@property (nonatomic, weak) EXAppContext * _Nullable appContext;
@property (nonatomic, strong) EXDevLauncherPendingDeepLinkRegistry *pendingDeepLinkRegistry;
@property (nonatomic, strong) EXDevLauncherRecentlyOpenedAppsRegistry *recentlyOpenedAppsRegistry;
@property (nonatomic, strong) id<EXUpdatesExternalInterface> updatesInterface;
@property (nonatomic, strong) id<EXUpdatesDevLauncherInterface> updatesInterface;

+ (instancetype)sharedInstance;

Expand Down
8 changes: 4 additions & 4 deletions packages/expo-dev-launcher/ios/EXDevLauncherController.m
Original file line number Diff line number Diff line change
Expand Up @@ -289,8 +289,8 @@ - (BOOL)_handleExternalDeepLink:(NSURL *)url options:(NSDictionary *)options

- (nullable NSURL *)sourceUrl
{
if (_shouldPreferUpdatesInterfaceSourceUrl && _updatesInterface && ((id<EXUpdatesExternalInterface>)_updatesInterface).launchAssetURL) {
return ((id<EXUpdatesExternalInterface>)_updatesInterface).launchAssetURL;
if (_shouldPreferUpdatesInterfaceSourceUrl && _updatesInterface && ((id<EXUpdatesDevLauncherInterface>)_updatesInterface).launchAssetURL) {
return ((id<EXUpdatesDevLauncherInterface>)_updatesInterface).launchAssetURL;
}
return _sourceUrl;
}
Expand Down Expand Up @@ -425,7 +425,7 @@ - (void)loadApp:(NSURL *)url
// do nothing for now
} success:^(NSDictionary * _Nullable manifest) {
if (manifest) {
launchExpoApp(((id<EXUpdatesExternalInterface>)self->_updatesInterface).launchAssetURL, [EXManifestsManifestFactory manifestForManifestJSON:manifest]);
launchExpoApp(((id<EXUpdatesDevLauncherInterface>)self->_updatesInterface).launchAssetURL, [EXManifestsManifestFactory manifestForManifestJSON:manifest]);
}
} error:onError];
};
Expand Down Expand Up @@ -692,7 +692,7 @@ -(NSDictionary *)getUpdatesConfig: (nullable NSDictionary *) constants
return updatesConfig;
}

- (void)updatesExternalInterfaceDidRequestRelaunch:(id<EXUpdatesExternalInterface> _Nonnull)updatesExternalInterface {
- (void)updatesExternalInterfaceDidRequestRelaunch:(id<EXUpdatesDevLauncherInterface> _Nonnull)updatesExternalInterface {
NSURL * _Nullable appUrl = self.appManifestURLWithFallback;
if (!appUrl) {
return;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -92,7 +92,7 @@ public class ExpoDevLauncherReactDelegateHandler: ExpoReactDelegateHandler, EXDe
self.reactDelegate = reactDelegate
self.launchOptions = launchOptions

if let sharedController = UpdatesControllerRegistry.sharedInstance.controller {
if let sharedController = UpdatesControllerRegistry.sharedInstance.controller as? UpdatesDevLauncherInterface {
// for some reason the swift compiler and bridge are having issues here
EXDevLauncherController.sharedInstance().updatesInterface = sharedController
sharedController.updatesExternalInterfaceDelegate = EXDevLauncherController.sharedInstance()
Expand Down
6 changes: 3 additions & 3 deletions packages/expo-updates-interface/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@

### 🎉 New features

- Full native interface for updates. ([#42981](https://github.com/expo/expo/pull/42981) by [@douglowder](https://github.com/douglowder))

### 🐛 Bug fixes

### 💡 Others
Expand All @@ -16,9 +18,7 @@ _This version does not introduce any user-facing changes._

## 55.1.0 — 2026-01-22

### 🎉 New features

- Full native interface for updates. ([#41527](https://github.com/expo/expo/pull/41527) by [@douglowder](https://github.com/douglowder))
_This version does not introduce any user-facing changes._

## 55.0.0 — 2026-01-21

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,5 +4,4 @@ import java.lang.ref.WeakReference

object UpdatesControllerRegistry {
var controller: WeakReference<UpdatesInterface>? = null
var metricsController: WeakReference<UpdatesMetricsInterface>? = null
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,44 @@ package expo.modules.updatesinterface
import android.net.Uri
import org.json.JSONObject
import java.lang.ref.WeakReference
import java.util.UUID

/**
* Interface for modules that depend on expo-updates for loading production updates but do not want
* to depend on expo-updates or delegate control to the singleton UpdatesController.
*
* All updates controllers implement this protocol
*/
interface UpdatesInterface {
/**
* Whether updates is enabled
*/
val isEnabled: Boolean get() = false

/**
* These properties are set when updates is enabled, or the dev client is running
*/
val runtimeVersion: String?
val updateUrl: Uri?

/**
* These properties are only set when updates is enabled
*/
val launchedUpdateId: UUID? get() = null
val embeddedUpdateId: UUID? get() = null
val launchAssetPath: String? get() = null

/**
* User code or third party modules can add a listener that will be called
* on updates state machine transitions (only when updates is enabled)
*/
fun subscribeToUpdatesStateChanges(listener: UpdatesStateChangeListener): UpdatesStateChangeSubscription
}

/**
* Implemented only by the dev client updates controller.
*/
interface UpdatesDevLauncherInterface : UpdatesInterface {
interface UpdateCallback {
fun onFailure(e: Exception?)
fun onSuccess(update: Update?)
Expand All @@ -32,7 +64,19 @@ interface UpdatesInterface {
fun reset()
fun fetchUpdateWithConfiguration(configuration: HashMap<String, Any>, callback: UpdateCallback)
fun isValidUpdatesConfiguration(configuration: HashMap<String, Any>): Boolean
}

val runtimeVersion: String?
val updateUrl: Uri?
interface UpdatesInterfaceCallbacks {
fun onRequestRelaunch()
}

interface UpdatesStateChangeListener {
fun updatesStateDidChange(event: Map<String, Any>)
}

interface UpdatesStateChangeSubscription {
/*
* Call this to remove the subscription and stop receiving state change events
*/
fun remove()
}

This file was deleted.

This file was deleted.

Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,8 @@

import Foundation

@objc(EXUpdatesControllerRegistry)
@objcMembers
public final class UpdatesControllerRegistry: NSObject {
public weak var controller: UpdatesExternalInterface?
public weak var metricsController: UpdatesExternalMetricsInterface?
public weak var controller: UpdatesInterface?

public static let sharedInstance = UpdatesControllerRegistry()
}
Original file line number Diff line number Diff line change
Expand Up @@ -15,11 +15,38 @@ public typealias UpdatesProgressBlock = (_ successfulAssetCount: UInt, _ failedA
public typealias UpdatesManifestBlock = (_ manifest: [String: Any]) -> Bool

/**
* Protocol for modules that depend on expo-updates for loading production updates but do not want
* to depend on expo-updates or delegate control to the singleton EXUpdatesAppController.
* All updates controllers implement this protocol, which provides information on the running
* updates system.
*/
@objc(EXUpdatesExternalInterface)
public protocol UpdatesExternalInterface {
@objc(EXUpdatesInterface)
public protocol UpdatesInterface {
/*
* Whether updates is enabled
*/
@objc var isEnabled: Bool { get }
/*
* These properties are set when updates is enabled, or the dev client is running
*/
@objc var runtimeVersion: String? { get }
@objc var updateURL: URL? { get }
/*
* These properties are only set when updates is enabled
*/
@objc var launchedUpdateId: UUID? { get }
@objc var embeddedUpdateId: UUID? { get }
@objc var launchAssetPath: String? { get }
/*
* User code or third party modules can add a listener that will be called
* on updates state machine transitions (only when updates is enabled)
*/
@objc func subscribeToUpdatesStateChanges(_ listener: any UpdatesStateChangeListener) -> UpdatesStateChangeSubscription
}

/**
* Implemented only by the dev client updates controller.
*/
@objc(EXUpdatesDevLauncherInterface)
public protocol UpdatesDevLauncherInterface: UpdatesInterface {
@objc weak var updatesExternalInterfaceDelegate: (any UpdatesExternalInterfaceDelegate)? { get set }
@objc var launchAssetURL: URL? { get }

Expand All @@ -44,16 +71,18 @@ public protocol UpdatesExternalInterface {
*/
@objc(EXUpdatesExternalInterfaceDelegate)
public protocol UpdatesExternalInterfaceDelegate {
@objc func updatesExternalInterfaceDidRequestRelaunch(_ updatesExternalInterface: UpdatesExternalInterface)
@objc func updatesExternalInterfaceDidRequestRelaunch(_ updatesExternalInterface: UpdatesDevLauncherInterface)
}

/**
* Protocol for use by the expo-app-metrics library
*/
@objc(EXUpdatesExternalMetricsInterface)
public protocol UpdatesExternalMetricsInterface {
@objc var runtimeVersion: String? { get }
@objc var updateURL: URL? { get }
@objc var launchedUpdateId: UUID? { get }
@objc var embeddedUpdateId: UUID? { get }
@objc(EXUpdatesStateChangeListener)
public protocol UpdatesStateChangeListener {
func updatesStateDidChange(_ event: [String: Any])
}

@objc(EXUpdatesStateChangeSubscription)
public protocol UpdatesStateChangeSubscription {
/*
* Call this to remove the subscription and stop receiving state change events
*/
func remove()
}
2 changes: 2 additions & 0 deletions packages/expo-updates/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@

### 🎉 New features

- Full native interface for updates. ([#42981](https://github.com/expo/expo/pull/42981) by [@douglowder](https://github.com/douglowder))

### 🐛 Bug fixes

- [IOS] Fix optional value handling for asset hash in ExpoUpdatesUpdate. ([#43093](https://github.com/expo/expo/pull/43093) by [@billysutomo](https://github.com/billysutomo))
Expand Down
Loading
Loading