diff --git a/IonicPortals/build.gradle.kts b/IonicPortals/build.gradle.kts index 119a5e6..c0feab8 100644 --- a/IonicPortals/build.gradle.kts +++ b/IonicPortals/build.gradle.kts @@ -50,6 +50,7 @@ dependencies { implementation(kotlin("reflect")) api("com.capacitorjs:core:[8.0.0,9.0.0)") + api("io.ionic:liveupdateprovider:0.1.0-alpha.2") compileOnly("io.ionic:liveupdates:0.5.5") implementation("org.jetbrains.kotlinx:kotlinx-coroutines-android:1.7.3") diff --git a/IonicPortals/src/main/kotlin/io/ionic/portals/Portal.kt b/IonicPortals/src/main/kotlin/io/ionic/portals/Portal.kt index f5f7abe..5ea2ecc 100644 --- a/IonicPortals/src/main/kotlin/io/ionic/portals/Portal.kt +++ b/IonicPortals/src/main/kotlin/io/ionic/portals/Portal.kt @@ -1,9 +1,15 @@ package io.ionic.portals import android.content.Context +import android.util.Log import com.getcapacitor.Plugin import io.ionic.liveupdates.LiveUpdate import io.ionic.liveupdates.LiveUpdateManager +import io.ionic.liveupdateprovider.LiveUpdateProviderError +import io.ionic.liveupdateprovider.LiveUpdateProviderManager +import io.ionic.liveupdateprovider.LiveUpdateProviderSyncCallback +import io.ionic.liveupdateprovider.LiveUpdateProviderSyncResult +import java.io.File /** * A class representing a Portal that contains information about the web content to load and any @@ -25,6 +31,21 @@ import io.ionic.liveupdates.LiveUpdateManager * @property name the name of the Portal */ class Portal(val name: String) { + /** + * The live update provider for a [Portal]. + */ + sealed class LiveUpdateProvider { + /** + * Uses Ionic Live Updates to sync and locate the latest web application assets. + */ + data class Ionic(val liveUpdateConfig: LiveUpdate) : LiveUpdateProvider() + + /** + * Uses an external live update provider to sync and locate the latest web application assets. + */ + data class Provider(val liveUpdateManager: LiveUpdateProviderManager) : LiveUpdateProvider() + } + /** * Capacitor [Plugin] registered with the Portal. */ @@ -73,15 +94,16 @@ class Portal(val name: String) { var devMode: Boolean = true /** - * A LiveUpdate config, if live updates is being used. + * The live update source responsible for locating and syncing the latest web application assets. + * + * Use [LiveUpdateProvider.Ionic] for Ionic Live Updates, or [LiveUpdateProvider.Provider] for + * an external provider that conforms to the Live Update Provider SDK. */ - var liveUpdateConfig: LiveUpdate? = null + var liveUpdateProvider: LiveUpdateProvider? = null set(value) { field = value - if (value != null) { - if(value.assetPath == null) { - value.assetPath = this.startDir - } + if (value is LiveUpdateProvider.Ionic && value.liveUpdateConfig.assetPath == null) { + value.liveUpdateConfig.assetPath = this.startDir } } @@ -90,6 +112,52 @@ class Portal(val name: String) { */ var liveUpdateOnAppLoad: Boolean = true + /** + * The directory of the latest synced web application assets for this portal. + * Returns null when no live update source is configured or no sync has completed. + */ + fun latestAppDirectory(context: Context): File? { + return when (val provider = liveUpdateProvider) { + is LiveUpdateProvider.Ionic -> LiveUpdateManager.getLatestAppDirectory(context, provider.liveUpdateConfig.appId) + is LiveUpdateProvider.Provider -> provider.liveUpdateManager.latestAppDirectory + null -> null + } + } + + /** + * Sync the external live update provider configured for this Portal. + * + * This only applies to Portals configured with [LiveUpdateProvider.Provider]. Portals configured + * with [LiveUpdateProvider.Ionic] should continue to use Ionic Live Updates APIs, such as + * [LiveUpdateManager.sync]. + * + * Example usage (kotlin): + * ```kotlin + * portal.syncProvider(callback) + * ``` + * + * Example usage (java): + * ```java + * portal.syncProvider(callback); + * ``` + * + * @param callback optional callback invoked with the provider sync result or failure. + */ + @JvmOverloads + fun syncProvider(callback: LiveUpdateProviderSyncCallback? = null) { + when (val provider = liveUpdateProvider) { + is LiveUpdateProvider.Provider -> provider.liveUpdateManager.sync(callback) + is LiveUpdateProvider.Ionic -> callback?.onFailure( + LiveUpdateProviderError.SyncFailed( + "Portal is configured for Ionic Live Updates. Use LiveUpdateManager.sync to sync Ionic Live Updates." + ) + ) + null -> callback?.onFailure( + LiveUpdateProviderError.SyncFailed("No external live update provider is configured for this Portal.") + ) + } + } + /** * Add a Capacitor [Plugin] to be loaded with this Portal. * @@ -308,7 +376,7 @@ class PortalBuilder(val name: String) { private var initialContext: Any? = null private var portalFragmentType: Class = PortalFragment::class.java private var onCreate: (portal: Portal) -> Unit = {} - private var liveUpdateConfig: LiveUpdate? = null + private var liveUpdateProvider: Portal.LiveUpdateProvider? = null private var devMode: Boolean = true internal constructor(name: String, onCreate: (portal: Portal) -> Unit) : this(name) { @@ -526,31 +594,31 @@ class PortalBuilder(val name: String) { } /** - * Set the [LiveUpdate] config if using the Live Updates SDK with Portals. + * Set the [LiveUpdate] config if using Ionic Live Updates with Portals. * * Example usage (kotlin): * ```kotlin * val liveUpdateConfig = LiveUpdate("appId", "production") - * builder = builder.setLiveUpdateConfig(liveUpdateConfig) + * builder = builder.setLiveUpdateConfig(context, liveUpdateConfig) * ``` * * Example usage (java): * ```java * LiveUpdate liveUpdateConfig = new LiveUpdate("appId", "production"); - * builder = builder.setLiveUpdateConfig(liveUpdateConfig); + * builder = builder.setLiveUpdateConfig(context, liveUpdateConfig); * ``` * - * @param context the Android [Context] used with Live Update configuration - * @param liveUpdateConfig the Live Update config object - * @param updateOnAppLoad if a Live Update sync should occur as soon as the Portal loads - * @return the instance of the PortalBuilder with the Live Update config set + * @param context the Android [Context] used with Ionic Live Updates configuration. + * @param liveUpdateConfig the Ionic Live Updates config object. + * @param updateOnAppLoad whether to start an Ionic Live Updates sync as the Portal is configured. + * @return the instance of the PortalBuilder with the Ionic Live Updates config set. */ @JvmOverloads fun setLiveUpdateConfig(context: Context, liveUpdateConfig: LiveUpdate, updateOnAppLoad: Boolean = true): PortalBuilder { - this.liveUpdateConfig = liveUpdateConfig if(liveUpdateConfig.assetPath == null) { liveUpdateConfig.assetPath = this._startDir ?: this.name } + this.liveUpdateProvider = Portal.LiveUpdateProvider.Ionic(liveUpdateConfig) LiveUpdateManager.initialize(context) LiveUpdateManager.cleanVersions(context, liveUpdateConfig.appId) @@ -561,6 +629,47 @@ class PortalBuilder(val name: String) { return this } + /** + * Set an external live update provider manager to be used with the Portal. + * + * Example usage (kotlin): + * ```kotlin + * builder = builder.setLiveUpdateProviderManager(providerManager) + * ``` + * + * Example usage (java): + * ```java + * builder = builder.setLiveUpdateProviderManager(providerManager); + * ``` + * + * To trigger a provider sync after the Portal is created, use [Portal.syncProvider]. + * + * @param liveUpdateManager the external live update provider manager. + * @param updateOnAppLoad whether to start an external provider sync as the Portal is configured. + * @return the instance of the PortalBuilder with the external live update provider manager set. + */ + @JvmOverloads + fun setLiveUpdateProviderManager( + liveUpdateManager: LiveUpdateProviderManager, + updateOnAppLoad: Boolean = true + ): PortalBuilder { + this.liveUpdateProvider = Portal.LiveUpdateProvider.Provider(liveUpdateManager) + if (updateOnAppLoad) { + liveUpdateManager.sync( + callback = object : LiveUpdateProviderSyncCallback { + override fun onSuccess(result: LiveUpdateProviderSyncResult) { + Log.d("PortalBuilder", "Live Update sync complete. Latest app dir: ${liveUpdateManager.latestAppDirectory}") + } + + override fun onFailure(error: LiveUpdateProviderError.SyncFailed) { + Log.e("PortalBuilder", "Live Update sync failed: ${error.message}") + } + } + ) + } + return this + } + /** * Set development mode on the Portal which will look for a server URL set by the Portals CLI. * This is set to true by default but can be turned off manually if desired. @@ -597,7 +706,7 @@ class PortalBuilder(val name: String) { portal.addAssetMaps(assetMaps) portal.initialContext = this.initialContext portal.portalFragmentType = this.portalFragmentType - portal.liveUpdateConfig = this.liveUpdateConfig + portal.liveUpdateProvider = this.liveUpdateProvider portal.devMode = this.devMode onCreate(portal) return portal diff --git a/IonicPortals/src/main/kotlin/io/ionic/portals/PortalFragment.kt b/IonicPortals/src/main/kotlin/io/ionic/portals/PortalFragment.kt index 2b73fdb..861d3d6 100644 --- a/IonicPortals/src/main/kotlin/io/ionic/portals/PortalFragment.kt +++ b/IonicPortals/src/main/kotlin/io/ionic/portals/PortalFragment.kt @@ -13,7 +13,6 @@ import androidx.annotation.NonNull import androidx.fragment.app.Fragment import androidx.fragment.app.viewModels import com.getcapacitor.* -import io.ionic.liveupdates.LiveUpdateManager import org.json.JSONException import org.json.JSONObject import java.io.File @@ -270,8 +269,8 @@ open class PortalFragment : Fragment { * If Live Updates is used and the web content was updated, the new content will be loaded. */ fun reload() { - if(portal?.liveUpdateConfig != null) { - val latestLiveUpdateFiles = LiveUpdateManager.getLatestAppDirectory(requireContext(), portal?.liveUpdateConfig?.appId!!) + if(portal?.liveUpdateProvider != null) { + val latestLiveUpdateFiles = portal?.latestAppDirectory(requireContext()) if (latestLiveUpdateFiles != null) { if (liveUpdateFiles == null || liveUpdateFiles!!.path != latestLiveUpdateFiles.path) { liveUpdateFiles = latestLiveUpdateFiles @@ -326,8 +325,8 @@ open class PortalFragment : Fragment { .addPluginInstances(initialPluginInstances) .addWebViewListeners(webViewListeners) - if (portal?.liveUpdateConfig != null) { - liveUpdateFiles = LiveUpdateManager.getLatestAppDirectory(requireContext(), portal?.liveUpdateConfig?.appId!!) + if (portal?.liveUpdateProvider != null) { + liveUpdateFiles = portal?.latestAppDirectory(requireContext()) bridgeBuilder = if (liveUpdateFiles != null) { if (config == null) { val configFile = File(liveUpdateFiles!!.path + "/capacitor.config.json") @@ -516,4 +515,4 @@ open class PortalFragment : Fragment { } } } -} \ No newline at end of file +} diff --git a/IonicPortals/src/main/kotlin/io/ionic/portals/PortalManager.kt b/IonicPortals/src/main/kotlin/io/ionic/portals/PortalManager.kt index 58a2969..389ba41 100644 --- a/IonicPortals/src/main/kotlin/io/ionic/portals/PortalManager.kt +++ b/IonicPortals/src/main/kotlin/io/ionic/portals/PortalManager.kt @@ -83,8 +83,8 @@ object PortalManager { /** * Removes the Portal from the Portal Manager. The Portal will be returned if it was present. If not, null is returned. - * Note: if the Portal uses Live Updates and registered an instance on creation, the Live Update instance for the app - * is not removed. + * Note: if the Portal uses Ionic Live Updates and registered an instance on creation, the Ionic + * Live Updates instance for the app is not removed. * * @param name the name of the Portal to remove */ @@ -246,4 +246,4 @@ object PortalManager { internal fun isRegisteredError(): Boolean { return registeredError } -} \ No newline at end of file +} diff --git a/LiveUpdates.md b/LiveUpdates.md new file mode 100644 index 0000000..09353f3 --- /dev/null +++ b/LiveUpdates.md @@ -0,0 +1,92 @@ +# Live Updates + +Portals can load updated web assets from Ionic Live Updates or from an external live update provider. Each Portal has one live update source, represented by `Portal.liveUpdateProvider`. + +## Ionic Live Updates + +Use Ionic Live Updates with the existing `setLiveUpdateConfig` builder API. + +```kotlin +val liveUpdateConfig = LiveUpdate("appId", "production") + +PortalManager.newPortal("help") + .setStartDir("webapp") + .setLiveUpdateConfig(context, liveUpdateConfig) + .create() +``` + +```java +LiveUpdate liveUpdateConfig = new LiveUpdate("appId", "production"); + +PortalManager.newPortal("help") + .setStartDir("webapp") + .setLiveUpdateConfig(context, liveUpdateConfig) + .create(); +``` + +`setLiveUpdateConfig` configures the Portal with `Portal.LiveUpdateProvider.Ionic(liveUpdateConfig)`, initializes Ionic Live Updates, registers the live update instance, and optionally starts an Ionic Live Updates sync. + +Manual Ionic Live Updates syncs should continue to use the Ionic Live Updates SDK. + +```java +LiveUpdateManager.sync(context, new String[] { "appId" }); +``` + +## External Providers + +Use an external provider by passing a `LiveUpdateProviderManager`. + +```kotlin +val providerManager: LiveUpdateProviderManager = getProviderManager() + +val helpPortal = PortalManager.newPortal("help") + .setStartDir("webapp") + .setLiveUpdateProviderManager(providerManager, updateOnAppLoad = false) + .create() + +helpPortal.syncProvider(callback) +``` + +```java +LiveUpdateProviderManager providerManager = getProviderManager(); + +Portal helpPortal = PortalManager.newPortal("help") + .setStartDir("webapp") + .setLiveUpdateProviderManager(providerManager, false) + .create(); + +helpPortal.syncProvider(callback); +``` + +`setLiveUpdateProviderManager` configures the Portal with `Portal.LiveUpdateProvider.Provider(providerManager)`. Android Portals does not use a provider registry; the app creates the provider manager and passes that instance to the Portal. + +After a provider sync completes, reload the Portal view or fragment to load the synced assets. + +```java +helpPortal.syncProvider(callback); +portalFragment.reload(); +``` + +## Migration Notes + +`Portal.liveUpdateConfig` has been replaced by `Portal.liveUpdateProvider`. + +Builder-based Ionic Live Updates setup still uses `setLiveUpdateConfig`. + +```java +PortalManager.newPortal("help") + .setLiveUpdateConfig(context, liveUpdateConfig) + .create(); +``` + +Direct `Portal` property usage should wrap the config in the Ionic live update provider case. + +```kotlin +portal.liveUpdateProvider = Portal.LiveUpdateProvider.Ionic(liveUpdateConfig) +``` + +External providers should use the provider case. + +```kotlin +portal.liveUpdateProvider = Portal.LiveUpdateProvider.Provider(providerManager) +``` diff --git a/README.md b/README.md index 0eeceef..495acec 100644 --- a/README.md +++ b/README.md @@ -33,6 +33,10 @@ See our docs to [get started with Portals](https://ionic.io/docs/portals/getting The Ionic Portals library for Android and iOS requires a license key to use. Once you have integrated Portals into your project, login to your ionic account to get a key. See our doc on [how to register and get your Portals license key](https://ionic.io/docs/portals/how-to/get-a-product-key) and refer to the [Android](https://ionic.io/docs/portals/getting-started/android) or [iOS](https://ionic.io/docs/portals/getting-started/iOS) getting started guides to see where to add your key. +## Live Updates + +Portals can load updated web assets from Ionic Live Updates or from an external live update provider. See [LiveUpdates.md](./LiveUpdates.md) for setup examples and migration notes. + ## FAQ ### What is the pricing for Portals use? @@ -57,4 +61,4 @@ The test projects within the repository will only work with a valid Portals key. portals_key=YOUR_PORTALS_KEY ``` -Note: This file is in the `.gitignore` and is not committed to repos by default. \ No newline at end of file +Note: This file is in the `.gitignore` and is not committed to repos by default.