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
1 change: 1 addition & 0 deletions IonicPortals.podspec
Original file line number Diff line number Diff line change
Expand Up @@ -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
8 changes: 5 additions & 3 deletions Package.swift
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
// swift-tools-version:5.5
// swift-tools-version:5.6

import PackageDescription

Expand All @@ -14,15 +14,17 @@ 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(
name: "IonicPortals",
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(
Expand Down
1 change: 0 additions & 1 deletion Sources/IonicPortals/AssetMap.swift
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,6 @@
//

import Foundation
import IonicLiveUpdates

public struct AssetMap {
/// The name to index the asset map by.
Expand Down
54 changes: 54 additions & 0 deletions Sources/IonicPortals/IonicPortals.docc/LiveUpdates.md
Original file line number Diff line number Diff line change
@@ -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.
5 changes: 2 additions & 3 deletions Sources/IonicPortals/IonicPortals.docc/Portal.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -26,8 +26,7 @@

### Live Updates

- ``liveUpdateConfig``
- ``liveUpdateManager``
- ``liveUpdateProvider``

### Initial Application State

Expand Down
112 changes: 100 additions & 12 deletions Sources/IonicPortals/Portal+LiveUpdates.swift
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -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
Expand All @@ -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<Result<LiveUpdateManager.SyncResult, any Error>>

/// Alias for a parallel sequence of external Live Update provider synchronization results
public typealias ParallelLiveUpdateProviderSyncGroup = ParallelAsyncSequence<Result<any SyncResult, any Error>>

extension ParallelLiveUpdateSyncGroup {
init(_ portals: [Portal]) {
work = portals.map { portal in
Expand All @@ -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<T>: AsyncSequence {
public typealias Element = Iterator.Element
Expand Down
63 changes: 33 additions & 30 deletions Sources/IonicPortals/Portal.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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)
)
}
}
}

Expand All @@ -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,
Expand All @@ -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
}
}

Expand Down Expand Up @@ -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)
}
}
}

Expand All @@ -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)
Expand Down
Loading
Loading