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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 7 additions & 3 deletions android/build.gradle
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
buildscript {
// Buildscript is evaluated before everything else so we can't use getExtOrDefault
def kotlin_version = rootProject.ext.has("kotlinVersion") ? rootProject.ext.get("kotlinVersion") : project.properties["PortalsReactNative_kotlinVersion"]
def kotlin_version = "2.1.20"

repositories {
mavenLocal()
google()
mavenCentral()
}
Expand Down Expand Up @@ -83,6 +84,7 @@ android {
}

repositories {
mavenLocal()
mavenCentral()
google()
}
Expand All @@ -93,9 +95,11 @@ dependencies {
// For < 0.71, this will be from the local maven repo
// For > 0.71, this will be replaced by `com.facebook.react:react-android:$version` by react gradle plugin
//noinspection GradleDynamicVersion
implementation "io.ionic:live-updates-provider:LOCAL-SNAPSHOT"

implementation "com.facebook.react:react-android"
implementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version"
implementation "io.ionic:portals:0.13.0-rn.1"
// implementation "io.ionic:portals:0.13.0-rn.1"
implementation "io.ionic:portals:LOCAL-SNAPSHOT"
implementation "io.ionic:liveupdates:0.5.+"
}

Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import io.ionic.liveupdates.data.model.FailResult
import io.ionic.liveupdates.data.model.Snapshot
import io.ionic.liveupdates.data.model.SyncResult
import io.ionic.liveupdates.network.SyncCallback
import io.ionic.liveupdatesprovider.LiveUpdatesError
import kotlinx.coroutines.*
import kotlinx.coroutines.channels.awaitClose
import kotlinx.coroutines.flow.callbackFlow
Expand All @@ -21,24 +22,48 @@ internal object LiveUpdatesModule {
)

fun syncOne(appId: String, context: Context, promise: Promise) {
LiveUpdateManager.sync(
context = context,
appId = appId,
callback = object : SyncCallback {
override fun onAppComplete(syncResult: SyncResult) {
promise.resolve(syncResult.toReadableMap())

// Check if there's a custom LiveUpdatesManager for this appId
val customManager = RNPortalManager.getLiveUpdatesManager(appId)

if (customManager != null) {
// Use the custom manager's sync method
customManager.sync(object : io.ionic.liveupdatesprovider.SyncCallback {
override fun onComplete(result: io.ionic.liveupdatesprovider.models.SyncResult) {
// Convert provider SyncResult to the format expected by the bridge
val resultMap = WritableNativeMap()
resultMap.putBoolean("didUpdate", result.didUpdate)
resultMap.putString("appId", appId)
if (result.latestAppDirectory != null) {
resultMap.putString("latestAppDirectory", result.latestAppDirectory?.absolutePath)
}
promise.resolve(resultMap)
}

override fun onAppComplete(failResult: FailResult) {
promise.resolve(failResult.toReadableMap())
override fun onError(error: LiveUpdatesError.SyncFailed) {
promise.reject("SYNC_FAILED", error.message, error.cause)
}
})
} else {
// Fall back to the default LiveUpdateManager
LiveUpdateManager.sync(
context = context,
appId = appId,
callback = object : SyncCallback {
override fun onAppComplete(syncResult: SyncResult) {
promise.resolve(syncResult.toReadableMap())

override fun onSyncComplete() {
// do nothing
}

override fun onAppComplete(failResult: FailResult) {
promise.resolve(failResult.toReadableMap())
}

override fun onSyncComplete() {
// do nothing
}
}
}
)
)
}
}

fun syncSome(appIds: ReadableArray, context: Context, promise: Promise) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,9 @@ import com.facebook.react.bridge.*
import com.getcapacitor.Plugin
import io.ionic.liveupdates.LiveUpdate
import io.ionic.liveupdates.LiveUpdateManager
import io.ionic.liveupdatesprovider.LiveUpdatesManager
import io.ionic.liveupdatesprovider.LiveUpdatesRegistry
import io.ionic.liveupdatesprovider.models.ProviderConfig
import io.ionic.portals.*
import org.json.JSONArray
import org.json.JSONException
Expand Down Expand Up @@ -44,8 +47,18 @@ internal object RNPortalManager {
private lateinit var reactApplicationContext: ReactApplicationContext
private var usesSecureLiveUpdates = false

// Map to store custom LiveUpdatesManager instances by appId
private val liveUpdatesManagers = ConcurrentHashMap<String, io.ionic.liveupdatesprovider.LiveUpdatesManager>()

fun register(key: String) = manager.register(key)

/**
* Get a custom LiveUpdatesManager for the given appId, if one was registered.
*/
fun getLiveUpdatesManager(appId: String): io.ionic.liveupdatesprovider.LiveUpdatesManager? {
return liveUpdatesManagers[appId]
}

fun createPortal(map: ReadableMap): RNPortal? {
val name = map.getString("name") ?: return null
val portalBuilder = PortalBuilder(name)
Expand Down Expand Up @@ -102,20 +115,81 @@ internal object RNPortalManager {

assetMaps.forEach(portalBuilder::addAssetMap)

map.getMap("liveUpdate")
?.let { readableMap ->
val appId = readableMap.getString("appId") ?: return@let null
val channel = readableMap.getString("channel") ?: return@let null
val syncOnAdd = readableMap.getBoolean("syncOnAdd")
Pair(LiveUpdate(appId, channel, usesSecureLiveUpdates), syncOnAdd)
}
?.let { (liveUpdate, updateOnAppLoad) ->
map.getMap("liveUpdate")?.let { liveUpdateMap ->
val appId = liveUpdateMap.getString("appId") ?: return@let
val channel = liveUpdateMap.getString("channel") ?: return@let
val syncOnAdd = liveUpdateMap.getBoolean("syncOnAdd")
val providerId = liveUpdateMap.getString("providerId")
val providerConfigMap = liveUpdateMap.getMap("providerConfig")

// Check if we should use the new LiveUpdatesManagerProvider approach
if (providerId != null && providerConfigMap != null) {
try {
// Get the provider from the registry
val provider = LiveUpdatesRegistry.resolve(providerId)

if (provider == null) {
// Fall back to the traditional LiveUpdate approach
val liveUpdate = LiveUpdate(appId, channel, usesSecureLiveUpdates)
portalBuilder.setLiveUpdateConfig(
context = reactApplicationContext,
liveUpdateConfig = liveUpdate,
updateOnAppLoad = syncOnAdd
)
return@let
}

// Convert the providerConfig ReadableMap to a Map<String, Any>
// Filter out null values to match the expected type
val configDataMap = providerConfigMap.toHashMap()
.filterValues { it != null }
.mapValues { it.value as Any }

// Add appId and channel to the config data
val configData = mutableMapOf<String, Any>(
"appId" to appId,
"channel" to channel,
"providerId" to providerId
)
configData.putAll(configDataMap)

// Create the ProviderConfig
val providerConfig = ProviderConfig(configData)

// Create the manager using the provider
val manager = provider.createManager(
context = reactApplicationContext,
config = providerConfig
)

// Store the manager by appId so it can be retrieved for sync operations
liveUpdatesManagers[appId] = manager

// Set the live updates manager on the portal builder
portalBuilder.setLiveUpdateManager(
context = reactApplicationContext,
liveUpdatesManager = manager,
updateOnAppLoad = syncOnAdd
)
} catch (e: Exception) {
// Fall back to the traditional LiveUpdate approach
val liveUpdate = LiveUpdate(appId, channel, usesSecureLiveUpdates)
portalBuilder.setLiveUpdateConfig(
context = reactApplicationContext,
liveUpdateConfig = liveUpdate,
updateOnAppLoad = syncOnAdd
)
}
} else {
// Fall back to the traditional LiveUpdate approach
val liveUpdate = LiveUpdate(appId, channel, usesSecureLiveUpdates)
portalBuilder.setLiveUpdateConfig(
context = reactApplicationContext,
liveUpdateConfig = liveUpdate,
updateOnAppLoad = updateOnAppLoad
updateOnAppLoad = syncOnAdd
)
}
}

portalBuilder
.addPlugin(PortalsPlugin::class.java)
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
package io.ionic.portals.reactnative.liveupdates

import android.util.Log
import io.ionic.liveupdatesprovider.LiveUpdatesError
import io.ionic.liveupdatesprovider.LiveUpdatesManager
import io.ionic.liveupdatesprovider.SyncCallback
import io.ionic.liveupdatesprovider.models.SyncResult
import java.io.File
import kotlin.concurrent.thread

/**
* Mock implementation of LiveUpdatesManager for testing purposes.
* Simulates async sync behavior with configurable success/failure scenarios.
*/
internal class MockLiveUpdatesManager(
private val appId: String,
private val channel: String,
private val latestAppDir: File,
private val shouldFail: Boolean,
private val failureDetails: String,
private val didUpdate: Boolean
) : LiveUpdatesManager {

companion object {
private const val TAG = "MockLiveUpdatesManager"
private const val SIMULATED_DELAY_MS = 500L
}

override fun sync(callback: SyncCallback?) {
Log.d(TAG, "Starting mock sync for appId: $appId, channel: $channel")

// Simulate async behavior with a background thread
thread {
try {
// Simulate network delay
Thread.sleep(SIMULATED_DELAY_MS)

if (shouldFail) {
Log.d(TAG, "Mock sync failed for appId: $appId - $failureDetails")
val error = LiveUpdatesError.SyncFailed(failureDetails, null)
callback?.onError(error)
} else {
Log.d(
TAG,
"Mock sync completed for appId: $appId, didUpdate: $didUpdate"
)

// Ensure the mock directory exists if didUpdate is true
if (didUpdate && !latestAppDir.exists()) {
latestAppDir.mkdirs()
}

val result = SyncResult(
didUpdate = didUpdate,
latestAppDirectory = if (didUpdate) latestAppDir else null
)
callback?.onComplete(result)
}
} catch (e: InterruptedException) {
Log.e(TAG, "Mock sync interrupted for appId: $appId", e)
val error = LiveUpdatesError.SyncFailed("Sync interrupted", e)
callback?.onError(error)
}
}
}

override fun latestAppDirectory(): File? {
return if (latestAppDir.exists()) latestAppDir else null
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
package io.ionic.portals.reactnative.liveupdates

import android.content.Context
import android.util.Log
import io.ionic.liveupdatesprovider.LiveUpdatesError
import io.ionic.liveupdatesprovider.LiveUpdatesManager
import io.ionic.liveupdatesprovider.LiveUpdatesProvider
import io.ionic.liveupdatesprovider.LiveUpdatesRegistry
import io.ionic.liveupdatesprovider.models.ProviderConfig
import java.io.File

/**
* Mock implementation of LiveUpdatesProvider for testing purposes in React Native.
* This allows testing the live updates API without making actual network requests.
*/
class MockLiveUpdatesProvider private constructor() : LiveUpdatesProvider {
companion object {
private const val TAG = "MockLiveUpdatesProvider"
private const val PROVIDER_ID = "ionic-mock"

val INSTANCE: MockLiveUpdatesProvider by lazy { MockLiveUpdatesProvider() }
private var isRegistered = false

/**
* Initializes and registers the mock provider with the LiveUpdatesRegistry.
* This should be called in the Application onCreate() before any live updates are used.
*/
@JvmStatic
fun initialize() {
if (!isRegistered) {
Log.d(TAG, "Registering MockLiveUpdatesProvider")
LiveUpdatesRegistry.register(INSTANCE)
isRegistered = true
}
}
}

override val id: String
get() = PROVIDER_ID

@Throws(LiveUpdatesError.InvalidConfiguration::class)
override fun createManager(
context: Context,
config: ProviderConfig
): LiveUpdatesManager {
val configData = config.data

// Extract appId (required)
val appId = configData["appId"] as? String
?: throw LiveUpdatesError.InvalidConfiguration(
"Mock provider requires 'appId' in config",
null
)

if (appId.trim().isEmpty()) {
throw LiveUpdatesError.InvalidConfiguration(
"Mock provider requires non-empty 'appId' in config",
null
)
}

// Extract optional parameters
val channel = configData["channel"] as? String ?: "production"
val shouldFail = configData["shouldFail"] as? Boolean ?: false
val failureDetails = configData["failureDetails"] as? String ?: "Mock sync failed"
val didUpdate = configData["didUpdate"] as? Boolean ?: false

// For testing, create a mock directory path
// In a real scenario, this would point to actual app assets
val latestAppDir = File(context.filesDir, "mock_live_updates/$appId/$channel")

Log.d(
TAG,
"Creating MockLiveUpdatesManager for appId: $appId, channel: $channel, shouldFail: $shouldFail"
)

return MockLiveUpdatesManager(
appId = appId,
channel = channel,
latestAppDir = latestAppDir,
shouldFail = shouldFail,
failureDetails = failureDetails,
didUpdate = didUpdate
)
}
}
Loading
Loading