diff --git a/IonicPortals.podspec b/IonicPortals.podspec index a807597..b1e165f 100644 --- a/IonicPortals.podspec +++ b/IonicPortals.podspec @@ -11,5 +11,6 @@ Pod::Spec.new do |s| s.source_files = 'Sources/IonicPortals/**/*.swift' s.dependency 'Capacitor', '~> 8.0.0' s.dependency 'IonicLiveUpdates', '>= 0.5.0', '< 0.6.0' + s.dependency 'LiveUpdateProvider', '~> 0.1.0-alpha.2' s.swift_version = '5.7' end diff --git a/Package.swift b/Package.swift index 93f70bd..c47f6c1 100644 --- a/Package.swift +++ b/Package.swift @@ -1,4 +1,4 @@ -// swift-tools-version:5.5 +// swift-tools-version:5.6 import PackageDescription @@ -14,7 +14,8 @@ let package = Package( dependencies: [ .package(url: "https://github.com/ionic-team/capacitor-swift-pm", .upToNextMajor(from: "8.0.0")), .package(url: "https://github.com/ionic-team/ionic-live-updates-releases", "0.5.0"..<"0.6.0"), - .package(url: "https://github.com/pointfreeco/swift-clocks", .upToNextMajor(from: "1.0.2")) + .package(url: "https://github.com/pointfreeco/swift-clocks", .upToNextMajor(from: "1.0.2")), + .package(url: "https://github.com/ionic-team/live-update-provider-sdk", exact: "0.1.0-alpha.2") ], targets: [ .target( @@ -22,7 +23,8 @@ let package = Package( dependencies: [ .product(name: "Capacitor", package: "capacitor-swift-pm"), .product(name: "Cordova", package: "capacitor-swift-pm"), - .product(name: "IonicLiveUpdates", package: "ionic-live-updates-releases") + .product(name: "IonicLiveUpdates", package: "ionic-live-updates-releases"), + .product(name: "LiveUpdateProvider", package: "live-update-provider-sdk") ] ), .testTarget( diff --git a/Sources/IonicPortals/AssetMap.swift b/Sources/IonicPortals/AssetMap.swift index 4358b92..9948647 100644 --- a/Sources/IonicPortals/AssetMap.swift +++ b/Sources/IonicPortals/AssetMap.swift @@ -6,7 +6,6 @@ // import Foundation -import IonicLiveUpdates public struct AssetMap { /// The name to index the asset map by. diff --git a/Sources/IonicPortals/IonicPortals.docc/LiveUpdates.md b/Sources/IonicPortals/IonicPortals.docc/LiveUpdates.md new file mode 100644 index 0000000..9a41053 --- /dev/null +++ b/Sources/IonicPortals/IonicPortals.docc/LiveUpdates.md @@ -0,0 +1,54 @@ +# Live Updates + +Configure a portal with Ionic Live Updates or an external live update provider. + +## Ionic Live Updates + +Use ``Portal/LiveUpdateProvider/ionic(liveUpdateManager:liveUpdateConfig:)`` with a `LiveUpdate` configuration: + +```swift +import IonicLiveUpdates +import IonicPortals + +let portal = Portal( + name: "checkout", + liveUpdateProvider: .ionic( + liveUpdateConfig: LiveUpdate( + appId: "checkout-app", + channel: "production" + ) + ) +) +``` + +Call ``Portal/sync()`` to synchronize a portal configured with Ionic Live Updates: + +```swift +let result = try await portal.sync() +print(result.source) +``` + +## External Providers + +Use ``Portal/LiveUpdateProvider/provider(liveUpdateManager:)`` with a manager that conforms to `LiveUpdateManaging` from the Live Update Provider SDK: + +```swift +import IonicPortals +import LiveUpdateProvider + +let portal = Portal( + name: "checkout", + liveUpdateProvider: .provider(liveUpdateManager: manager) +) +``` + +Call ``Portal/syncProvider()`` to synchronize a portal configured with an external live update provider: + +```swift +let result = try await portal.syncProvider() +print(result) +``` + +When a `PortalUIView` reloads, Portals uses ``Portal/latestAppDirectory`` to switch the web view to newly activated assets. + +Objective-C apps can continue to configure Ionic Live Updates with `setLiveUpdateConfiguration(appId:channel:syncImmediately:)`. That method does not replace an existing external provider. diff --git a/Sources/IonicPortals/IonicPortals.docc/Portal.md b/Sources/IonicPortals/IonicPortals.docc/Portal.md index 8e1109f..ca0d28c 100644 --- a/Sources/IonicPortals/IonicPortals.docc/Portal.md +++ b/Sources/IonicPortals/IonicPortals.docc/Portal.md @@ -4,7 +4,7 @@ ### Create a Portal -- ``init(name:startDir:index:devModeEnabled:bundle:initialContext:assetMaps:plugins:liveUpdateManager:liveUpdateConfig:)`` +- ``init(name:startDir:index:devModeEnabled:bundle:initialContext:assetMaps:plugins:liveUpdateProvider:)`` - ``init(stringLiteral:)`` ### Web App Location @@ -26,8 +26,7 @@ ### Live Updates -- ``liveUpdateConfig`` -- ``liveUpdateManager`` +- ``liveUpdateProvider`` ### Initial Application State diff --git a/Sources/IonicPortals/Portal+LiveUpdates.swift b/Sources/IonicPortals/Portal+LiveUpdates.swift index b6d218e..50a8672 100644 --- a/Sources/IonicPortals/Portal+LiveUpdates.swift +++ b/Sources/IonicPortals/Portal+LiveUpdates.swift @@ -1,23 +1,64 @@ +import Foundation import IonicLiveUpdates +import LiveUpdateProvider extension Portal { - /// Error thrown if a ``liveUpdateConfig`` is not present on a ``Portal`` when ``sync()`` is called. + /// The live update provider for a ``Portal``. + public enum LiveUpdateProvider { + /// Uses Ionic Live Updates to sync and locate the latest web application assets. + /// + /// Portals configured with this case are synchronized with ``Portal/sync()``. + case ionic( + /// The `LiveUpdateManager` responsible for locating the latest source for the web application. + liveUpdateManager: LiveUpdateManager = .shared, + /// The `LiveUpdate` configuration used to determine the location of updated application assets. + liveUpdateConfig: LiveUpdate) + + /// Uses an external live update provider to sync and locate the latest web application assets. + /// + /// Portals configured with this case are synchronized with ``Portal/syncProvider()``. + case provider(liveUpdateManager: any LiveUpdateManaging) + } + + /// Error thrown when a portal is not configured with the live update provider type required by the sync method. public struct LiveUpdateNotConfigured: Error {} - /// Syncs the ``liveUpdateConfig`` if present - /// - Returns: The result of the synchronization operation - /// - Throws: If the portal has no ``liveUpdateConfig``, a ``LiveUpdateNotConfigured`` error will be thrown. - /// Any errors thrown from ``liveUpdateManager`` will be propogated. + /// Syncs a portal configured with ``Portal/LiveUpdateProvider/ionic(liveUpdateManager:liveUpdateConfig:)``. + /// + /// Use this method for Ionic Live Updates. To sync a portal configured with + /// ``Portal/LiveUpdateProvider/provider(liveUpdateManager:)``, call ``syncProvider()``. + /// - Returns: The Ionic Live Updates synchronization result. + /// - Throws: ``LiveUpdateNotConfigured`` if the portal is not configured with the Ionic Live Updates provider. + /// Any errors thrown from Ionic Live Updates will be propagated. public func sync() async throws -> LiveUpdateManager.SyncResult { - if let liveUpdateConfig { - return try await liveUpdateManager.sync(appId: liveUpdateConfig.appId) - } else { + guard case .ionic(let manager, let config) = liveUpdateProvider else { throw LiveUpdateNotConfigured() } + + return try await manager.sync(appId: config.appId) } - /// Synchronizes the ``liveUpdateConfig``s of the provided ``Portal``s in parallel - /// - Parameter portals: The ``Portal``s to ``sync()`` + /// Syncs a portal configured with ``Portal/LiveUpdateProvider/provider(liveUpdateManager:)``. + /// + /// Use this method for external live update providers. To sync a portal configured with + /// ``Portal/LiveUpdateProvider/ionic(liveUpdateManager:liveUpdateConfig:)``, call ``sync()``. + /// - Returns: The external provider's synchronization result. + /// - Throws: ``LiveUpdateNotConfigured`` if the portal is not configured with an external live update provider. + /// Any errors thrown from the live update provider will be propagated. + public func syncProvider() async throws -> any SyncResult { + guard case .provider(let manager) = liveUpdateProvider else { + throw LiveUpdateNotConfigured() + } + + return try await manager.sync() + } + + /// Synchronizes portals configured with Ionic Live Updates in parallel. + /// + /// Each portal must be configured with + /// ``Portal/LiveUpdateProvider/ionic(liveUpdateManager:liveUpdateConfig:)``. + /// Use ``syncProvider(_:)`` for portals configured with external live update providers. + /// - Parameter portals: The ``Portal``s to synchronize with ``Portal/sync()`` /// - Returns: A ``ParallelLiveUpdateSyncGroup`` of the results of each call to ``Portal/sync()`` /// /// Usage @@ -30,10 +71,37 @@ extension Portal { public static func sync(_ portals: [Portal]) -> ParallelLiveUpdateSyncGroup { .init(portals) } + + /// Synchronizes portals configured with external live update providers in parallel. + /// + /// Each portal must be configured with ``Portal/LiveUpdateProvider/provider(liveUpdateManager:)``. + /// Use ``sync(_:)`` for portals configured with Ionic Live Updates. + /// - Parameter portals: The ``Portal``s to synchronize with ``Portal/syncProvider()`` + /// - Returns: A ``ParallelLiveUpdateProviderSyncGroup`` of the results of each call to ``Portal/syncProvider()`` + public static func syncProvider(_ portals: [Portal]) -> ParallelLiveUpdateProviderSyncGroup { + .init(portals) + } + + /// The directory of the latest synced web application assets for this portal. + /// Returns `nil` if no live update provider is configured or no sync has occurred. + public var latestAppDirectory: URL? { + switch liveUpdateProvider { + case .ionic(let manager, let config): + return manager.latestAppDirectory(for: config.appId) + case .provider(let manager): + return manager.latestAppDirectory + case .none: + return nil + } + } } extension Array where Element == Portal { - /// Synchronizes the ``Portal/liveUpdateConfig`` for the elements in the array + /// Synchronizes portals configured with Ionic Live Updates in parallel. + /// + /// Each portal must be configured with + /// ``Portal/LiveUpdateProvider/ionic(liveUpdateManager:liveUpdateConfig:)``. + /// Use ``syncProvider()`` for portals configured with external live update providers. /// - Returns: A ``ParallelLiveUpdateSyncGroup`` of the results of each call to ``Portal/sync()`` /// /// Usage @@ -46,11 +114,23 @@ extension Array where Element == Portal { public func sync() -> ParallelLiveUpdateSyncGroup { .init(self) } + + /// Synchronizes portals configured with external live update providers in parallel. + /// + /// Each portal must be configured with ``Portal/LiveUpdateProvider/provider(liveUpdateManager:)``. + /// Use ``sync()`` for portals configured with Ionic Live Updates. + /// - Returns: A ``ParallelLiveUpdateProviderSyncGroup`` of the results of each call to ``Portal/syncProvider()`` + public func syncProvider() -> ParallelLiveUpdateProviderSyncGroup { + .init(self) + } } -/// Alias for a parallel sequence of Live Update synchronization results +/// Alias for a parallel sequence of Ionic Live Updates synchronization results public typealias ParallelLiveUpdateSyncGroup = ParallelAsyncSequence> +/// Alias for a parallel sequence of external Live Update provider synchronization results +public typealias ParallelLiveUpdateProviderSyncGroup = ParallelAsyncSequence> + extension ParallelLiveUpdateSyncGroup { init(_ portals: [Portal]) { work = portals.map { portal in @@ -59,6 +139,14 @@ extension ParallelLiveUpdateSyncGroup { } } +extension ParallelLiveUpdateProviderSyncGroup { + init(_ portals: [Portal]) { + work = portals.map { portal in + { await Result(catching: portal.syncProvider) } + } + } +} + /// A sequence that executes its tasks in parallel and yields their results as they complete public struct ParallelAsyncSequence: AsyncSequence { public typealias Element = Iterator.Element diff --git a/Sources/IonicPortals/Portal.swift b/Sources/IonicPortals/Portal.swift index 4125300..75b7213 100644 --- a/Sources/IonicPortals/Portal.swift +++ b/Sources/IonicPortals/Portal.swift @@ -28,17 +28,15 @@ public struct Portal { /// Any Capacitor plugins to load on the ``Portal`` public var plugins: [Plugin] - /// The `LiveUpdateManager` responsible for locating the latest source for the web application - public var liveUpdateManager: LiveUpdateManager - - /// The `LiveUpdate` configuration used to determine the location of updated application assets. - public var liveUpdateConfig: LiveUpdate? = nil { + /// The ``Portal/LiveUpdateProvider`` responsible for locating the latest source for the web application. + public var liveUpdateProvider: LiveUpdateProvider? { didSet { - guard let liveUpdateConfig = liveUpdateConfig else { return } - try? liveUpdateManager.add( - liveUpdateConfig, - existingCacheUrl: bundle.url(forResource: startDir, withExtension: nil) - ) + if case .ionic(let manager, let config) = liveUpdateProvider { + try? manager.add( + config, + existingCacheUrl: bundle.url(forResource: startDir, withExtension: nil) + ) + } } } @@ -55,11 +53,10 @@ public struct Portal { /// - index: The initial file to load in the Portal. Defaults to `index.html`. /// - devModeEnabled: Enables web developers to override the Portal content in debug builds. Defaults to `true`. /// - bundle: The `Bundle` that contains the web application. Defaults to `Bundle.main`. - /// - plugins: Any ``Plugin``s to load. Defautls to `[]`. /// - initialContext: Any initial state required by the web application. Defaults to `[:]`. /// - assetMaps: Any ``AssetMap``s needed to share assets with the ``Portal``. Defaults to `[]`. - /// - liveUpdateManager: The `LiveUpdateManager` responsible for locating the source source for the web application. Defaults to `LiveUpdateManager.shared`. - /// - liveUpdateConfig: The `LiveUpdate` configuration used to determine to location of updated application assets. Defaults to `nil`. + /// - plugins: Any ``Plugin``s to load. Defautls to `[]`. + /// - liveUpdateProvider: The ``Portal/LiveUpdateProvider`` responsible for locating and syncing the latest web application assets. Defaults to `nil`. public init( name: String, startDir: String? = nil, @@ -69,25 +66,24 @@ public struct Portal { initialContext: JSObject = [:], assetMaps: [AssetMap] = [], plugins: [Plugin] = [], - liveUpdateManager: LiveUpdateManager = .shared, - liveUpdateConfig: LiveUpdate? = nil + liveUpdateProvider: LiveUpdateProvider? = nil ) { self.name = name self.startDir = startDir ?? name - self.devModeEnabled = devModeEnabled self.index = index - self.initialContext = initialContext + self.devModeEnabled = devModeEnabled self.bundle = bundle + self.initialContext = initialContext + self.assetMaps = assetMaps self.plugins = plugins - self.liveUpdateManager = liveUpdateManager - self.liveUpdateConfig = liveUpdateConfig - if let liveUpdateConfig = liveUpdateConfig { - try? liveUpdateManager.add( - liveUpdateConfig, + self.liveUpdateProvider = liveUpdateProvider + + if case .ionic(let manager, let config) = liveUpdateProvider { + try? manager.add( + config, existingCacheUrl: bundle.url(forResource: self.startDir, withExtension: nil) ) } - self.assetMaps = assetMaps } } @@ -233,13 +229,21 @@ extension Portal { self.portal = portal } - /// Configures the `LiveUpdate` configuration + /// Sets the ``Portal/LiveUpdateProvider/ionic(liveUpdateManager:liveUpdateConfig:)`` configuration for this portal. /// - Parameters: - /// - appId: The AppFlow id of the web application associated with the ``IONPortal`` - /// - channel: The AppFlow channel to check for updates from. - /// - syncImmediately: Whether to immediately sync with AppFlow to check for updates. + /// - appId: The Appflow id of the web application associated with the ``IONPortal`` + /// - channel: The Appflow channel to check for updates from. + /// - syncImmediately: Whether to immediately sync with Appflow to check for updates. + /// - Note: This method has no effect if an external live update provider is already configured. @objc public func setLiveUpdateConfiguration(appId: String, channel: String, syncImmediately: Bool) { - portal.liveUpdateConfig = LiveUpdate(appId: appId, channel: channel, syncOnAdd: syncImmediately) + if case .provider = portal.liveUpdateProvider { return } + + let config = LiveUpdate(appId: appId, channel: channel, syncOnAdd: syncImmediately) + if case .ionic(let manager, _) = portal.liveUpdateProvider { + portal.liveUpdateProvider = .ionic(liveUpdateManager: manager, liveUpdateConfig: config) + } else { + portal.liveUpdateProvider = .ionic(liveUpdateConfig: config) + } } } @@ -254,8 +258,7 @@ extension IONPortal { let portal = Portal( name: name, startDir: startDir, - initialContext: initialContext.flatMap { JSTypes.coerceDictionaryToJSObject($0) } ?? [:], - liveUpdateConfig: nil + initialContext: initialContext.flatMap { JSTypes.coerceDictionaryToJSObject($0) } ?? [:] ) self.init(portal: portal) diff --git a/Sources/IonicPortals/PortalView/PortalUIView.swift b/Sources/IonicPortals/PortalView/PortalUIView.swift index 6d917cf..1a0ac4f 100644 --- a/Sources/IonicPortals/PortalView/PortalUIView.swift +++ b/Sources/IonicPortals/PortalView/PortalUIView.swift @@ -2,7 +2,6 @@ import Foundation import WebKit import UIKit import Capacitor -import IonicLiveUpdates import SwiftUI /// A UIKit UIView to display ``Portal`` content @@ -85,10 +84,7 @@ public class PortalUIView: UIView { private func initView () { if PortalsRegistrationManager.shared.isRegistered { - if let liveUpdateConfig = portal.liveUpdateConfig { - self.liveUpdatePath = portal.liveUpdateManager.latestAppDirectory(for: liveUpdateConfig.appId) - } - + self.liveUpdatePath = portal.latestAppDirectory addPinnedSubview(webView) } else { let showRegistrationError = PortalsRegistrationManager.shared.registrationState == .error @@ -310,8 +306,7 @@ extension PortalUIView { } /// Reloads the underlying `WKWebView` @objc public func reload() { - if let liveUpdate = portal.liveUpdateConfig, - let latestAppPath = portal.liveUpdateManager.latestAppDirectory(for: liveUpdate.appId), + if let latestAppPath = portal.latestAppDirectory, liveUpdatePath == nil || liveUpdatePath?.path != latestAppPath.path { liveUpdatePath = latestAppPath return setServerBasePath(path: latestAppPath.path)