Skip to content
Open
Show file tree
Hide file tree
Changes from 1 commit
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
5 changes: 5 additions & 0 deletions .changeset/tall-doors-clean.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@capawesome/capacitor-live-update': minor
---

feat: integrate Ionic Live Update Provider SDK
27 changes: 23 additions & 4 deletions packages/live-update/CapawesomeCapacitorLiveUpdate.podspec
Original file line number Diff line number Diff line change
Expand Up @@ -10,11 +10,30 @@ Pod::Spec.new do |s|
s.homepage = package['repository']['url']
s.author = package['author']
s.source = { :git => package['repository']['url'], :tag => s.version.to_s }
s.source_files = 'ios/Plugin/**/*.{swift,h,m,c,cc,mm,cpp}'
s.ios.deployment_target = '15.0'
s.dependency 'Capacitor'
s.dependency 'Alamofire', '~> 5.10.2'
s.dependency 'ZIPFoundation', '~> 0.9.0'
s.swift_version = '5.1'
s.static_framework = true

s.default_subspec = 'Default'

# Default subspec — plain plugin, no Ionic Live Update Provider SDK integration.
s.subspec 'Default' do |ss|
ss.source_files = 'ios/Plugin/**/*.{swift,h,m,c,cc,mm,cpp}'
ss.dependency 'Capacitor'
ss.dependency 'Alamofire', '~> 5.10.2'
ss.dependency 'ZIPFoundation', '~> 0.9.0'
end

# Opt-in subspec that pulls in the Ionic Live Update Provider SDK and registers
# the Capawesome provider with `LiveUpdateProviderRegistry` on plugin load.
s.subspec 'IonicProvider' do |ss|
ss.source_files = 'ios/Plugin/**/*.{swift,h,m,c,cc,mm,cpp}'
ss.dependency 'Capacitor'
ss.dependency 'Alamofire', '~> 5.10.2'
ss.dependency 'ZIPFoundation', '~> 0.9.0'
ss.dependency 'LiveUpdateProvider', '0.1.0-alpha.2'
ss.pod_target_xcconfig = {
'OTHER_SWIFT_FLAGS' => '$(inherited) -DCAPAWESOME_INCLUDE_IONIC_PROVIDER'
}
end
end
26 changes: 21 additions & 5 deletions packages/live-update/Package.swift
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
// swift-tools-version: 5.9
// swift-tools-version: 6.1
import PackageDescription

let package = Package(
Expand All @@ -9,10 +9,17 @@ let package = Package(
name: "CapawesomeCapacitorLiveUpdate",
targets: ["LiveUpdatePlugin"])
],
traits: [
.trait(
name: "IonicProvider",
description: "Enables registration with the Ionic Live Update Provider SDK."
)
],
dependencies: [
.package(url: "https://github.com/ionic-team/capacitor-swift-pm.git", from: "8.0.0"),
.package(url: "https://github.com/Alamofire/Alamofire.git", .upToNextMinor(from: "5.10.2")),
.package(url: "https://github.com/weichsel/ZIPFoundation.git", .upToNextMinor(from: "0.9.0"))
.package(url: "https://github.com/weichsel/ZIPFoundation.git", .upToNextMinor(from: "0.9.0")),
.package(url: "https://github.com/ionic-team/live-update-provider-sdk.git", exact: "0.1.0-alpha.2")
],
targets: [
.target(
Expand All @@ -21,12 +28,21 @@ let package = Package(
.product(name: "Capacitor", package: "capacitor-swift-pm"),
.product(name: "Cordova", package: "capacitor-swift-pm"),
.product(name: "Alamofire", package: "Alamofire"),
.product(name: "ZIPFoundation", package: "ZIPFoundation")
.product(name: "ZIPFoundation", package: "ZIPFoundation"),
.product(
name: "LiveUpdateProvider",
package: "live-update-provider-sdk",
condition: .when(traits: ["IonicProvider"])
)
],
path: "ios/Plugin"),
path: "ios/Plugin",
swiftSettings: [
.define("CAPAWESOME_INCLUDE_IONIC_PROVIDER", .when(traits: ["IonicProvider"]))
]),
.testTarget(
name: "LiveUpdatePluginTests",
dependencies: ["LiveUpdatePlugin"],
path: "ios/PluginTests")
]
],
swiftLanguageModes: [.v5]
)
115 changes: 115 additions & 0 deletions packages/live-update/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,8 @@ We are proud to offer one of the most complete and feature-rich Capacitor plugin
- 📡 **Update Lifecycle Events**: Track download progress, react to bundle changes, and detect reloads with auto-blocking of rolled back bundles.
- 🏠 **Self-Hosted Bundles**: Download bundles from any URL, no Capawesome Cloud dependency required.
- 🏷️ **Custom Properties**: Associate custom key-value metadata with bundles via Capawesome Cloud.
- 🌀 **Ionic Portals Support**: Power live updates for [Ionic Portals](https://ionic.io/docs/portals) via the [Ionic Live Update Provider SDK](https://github.com/ionic-team/live-update-provider-sdk).
- 🧩 **Federated Capacitor Support**: Power live updates for [Federated Capacitor](https://ionic.io/docs/portals/for-capacitor/overview) shells via the [Ionic Live Update Provider SDK](https://github.com/ionic-team/live-update-provider-sdk).
- 🔒 **Security**: Verify the authenticity and integrity of the bundle using a public key.
- ⚔️ **Battle-Tested**: Used in more than 1,000 projects to update apps on more than 20,000,000 devices.
- 🌐 **Open Source**: Licensed under the MIT License.
Expand Down Expand Up @@ -139,6 +141,119 @@ Add the `NSPrivacyAccessedAPICategoryUserDefaults` dictionary key to your [Priva

We recommend to declare [`CA92.1`](https://developer.apple.com/documentation/bundleresources/privacy_manifest_files/describing_use_of_required_reason_api#4278401) as the reason for accessing the [`UserDefaults`](https://developer.apple.com/documentation/foundation/userdefaults) API.

## Ionic Live Update Provider SDK integration

This plugin can register itself as a provider for the [Ionic Live Update Provider SDK](https://github.com/ionic-team/live-update-provider-sdk), so that **Ionic Portals** and **Federated Capacitor** apps can use Capawesome Cloud to deliver live updates.

The Ionic SDK is an **optional native dependency**: you only pay the install footprint when you opt in. The plugin's registered provider id is `capawesome`.

### Android

Set the `capawesomeCapacitorLiveUpdateIncludeIonicProvider` variable to `true` in your app's `variables.gradle` file:

```diff
ext {
+ capawesomeCapacitorLiveUpdateIncludeIonicProvider = true // Default: false
}
```

When enabled, the plugin pulls in `io.ionic:liveupdateprovider` and registers the Capawesome provider with `LiveUpdateProviderRegistry` automatically when the Capacitor plugin loads.

### iOS

#### CocoaPods

Add the `IonicProvider` subspec to your app's `Podfile`:

```diff
target 'App' do
capacitor_pods
# Add your Pods here
+ pod 'CapawesomeCapacitorLiveUpdate/IonicProvider', :path => '../../node_modules/@capawesome/capacitor-live-update'
end
```

The `IonicProvider` subspec adds the `LiveUpdateProvider` pod and sets the `CAPAWESOME_INCLUDE_IONIC_PROVIDER` Swift compile flag, which compiles in the Ionic provider classes.

#### Swift Package Manager

Enable the `IonicProvider` package trait in your `capacitor.config.json` (or `capacitor.config.ts`):

```json
{
"experimental": {
"ios": {
"spm": {
"swiftToolsVersion": "6.1",
"packageTraits": {
"@capawesome/capacitor-live-update": ["IonicProvider"]
}
}
}
}
}
```

**Attention**: SPM trait support requires Capacitor CLI 8.3.0+ and Xcode 16.3+ (Swift 6.1+).

### Provider configuration

When Federated Capacitor or Portals creates a manager via this provider, it passes a configuration map. V1 accepts the following keys:

| Key | Type | Required | Description |
| ------------ | ------ | -------- | -------------------------------------------------------------------------------------------------------- |
| `managerKey` | string | Yes | Scopes per-manager persisted state. Use a stable, unique key per shell / portal. |
| `appId` | string | No | Capawesome Cloud app UUID. Falls back to the plugin-wide `appId` from `capacitor.config.json`. |
| `channel` | string | No | Channel to sync. Falls back to the plugin-wide `defaultChannel` from `capacitor.config.json`. |

Comment thread
robingenz marked this conversation as resolved.
All other settings (`serverDomain`, `publicKey`, `httpTimeout`, etc.) come from the plugin-wide capacitor config. To set the per-device `customId`, call `LiveUpdate.setCustomId({ customId })` from JavaScript once at app startup.

### Federated Capacitor usage

Registration is automatic — the provider is registered on plugin load. In your Federated Capacitor configuration, reference the provider id:

```ts
liveUpdateConfig: {
providerId: 'capawesome',
providerConfig: {
managerKey: 'my-shell',
appId: '6e351b4f-69a7-415e-a057-4567df7ffe94',
channel: 'production',
},
}
```

### Portals usage

Construct the manager directly in your host app and hand it to your Portal:

```swift
import CapawesomeCapacitorLiveUpdate
import LiveUpdateProvider

let provider = LiveUpdateProviderRegistry.shared.resolve("capawesome")
let manager = try provider?.createManager(config: [
"managerKey": "my-portal",
"appId": "6e351b4f-69a7-415e-a057-4567df7ffe94",
"channel": "production",
])
// hand `manager` to your Portal config
```

```kotlin
import io.ionic.liveupdateprovider.LiveUpdateProviderRegistry

val provider = LiveUpdateProviderRegistry.resolve("capawesome")
val manager = provider?.createManager(applicationContext, mapOf(
"managerKey" to "my-portal",
"appId" to "6e351b4f-69a7-415e-a057-4567df7ffe94",
"channel" to "production",
))
// hand `manager` to your Portal config
```

**Note**: Provider mode and the standalone JavaScript sync flow share the same on-disk bundle storage but use isolated state pointers (`managerKey`-scoped). Pick one mode per app to avoid surprises.

## Configuration

<docgen-config>
Expand Down
7 changes: 7 additions & 0 deletions packages/live-update/android/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@ ext {
androidxEspressoCoreVersion = project.hasProperty('androidxEspressoCoreVersion') ? rootProject.ext.androidxEspressoCoreVersion : '3.7.0'
okhttp3Version = project.hasProperty('okhttp3Version') ? rootProject.ext.okhttp3Version : '5.3.2'
zip4jVersion = project.hasProperty('zip4jVersion') ? rootProject.ext.zip4jVersion : '2.11.5'
capawesomeCapacitorLiveUpdateIncludeIonicProvider = project.hasProperty('capawesomeCapacitorLiveUpdateIncludeIonicProvider') ? rootProject.ext.capawesomeCapacitorLiveUpdateIncludeIonicProvider : false
ionicLiveUpdateProviderVersion = project.hasProperty('ionicLiveUpdateProviderVersion') ? rootProject.ext.ionicLiveUpdateProviderVersion : '0.1.0-alpha.2'
}

buildscript {
Expand Down Expand Up @@ -56,6 +58,11 @@ dependencies {
implementation "androidx.appcompat:appcompat:$androidxAppCompatVersion"
implementation "net.lingala.zip4j:zip4j:$zip4jVersion"
implementation "com.squareup.okhttp3:okhttp:$okhttp3Version"
if (capawesomeCapacitorLiveUpdateIncludeIonicProvider) {
implementation "io.ionic:liveupdateprovider:$ionicLiveUpdateProviderVersion"
} else {
compileOnly "io.ionic:liveupdateprovider:$ionicLiveUpdateProviderVersion"
}
testImplementation "junit:junit:$junitVersion"
androidTestImplementation "androidx.test.ext:junit:$androidxJunitVersion"
androidTestImplementation "androidx.test.espresso:espresso-core:$androidxEspressoCoreVersion"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -298,6 +298,17 @@ public void getCurrentBundle(@NonNull NonEmptyCallback<GetCurrentBundleResult> c
callback.success(result);
}

/**
* @return The on-disk directory of a downloaded bundle, or `null` if the bundle does not exist.
*/
@Nullable
public File getBundleDirectory(@NonNull String bundleId) {
if (!hasBundleById(bundleId)) {
return null;
}
return buildBundleDirectoryFor(bundleId);
}

public void getCustomId(@NonNull NonEmptyCallback callback) {
String customId = preferences.getCustomId();
GetCustomIdResult result = new GetCustomIdResult(customId);
Expand Down Expand Up @@ -972,13 +983,14 @@ private void fetchLatestBundleInternal(
@NonNull NonEmptyCallback<GetLatestBundleResponse> callback
) {
try {
String appId = options.getAppId() == null ? getAppId() : options.getAppId();
String channel = options.getChannel() == null ? getChannel() : options.getChannel();
String url = new HttpUrl.Builder()
.scheme("https")
.host(config.getServerDomain())
.addPathSegment("v1")
.addPathSegment("apps")
.addPathSegment(getAppId())
.addPathSegment(appId)
.addPathSegment("bundles")
.addPathSegment("latest")
Comment thread
robingenz marked this conversation as resolved.
.addQueryParameter("appVersionCode", getVersionCodeAsString())
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@
import io.capawesome.capacitorjs.plugins.liveupdate.interfaces.EmptyCallback;
import io.capawesome.capacitorjs.plugins.liveupdate.interfaces.NonEmptyCallback;
import io.capawesome.capacitorjs.plugins.liveupdate.interfaces.Result;
import io.capawesome.capacitorjs.plugins.liveupdate.providers.ionic.LiveUpdateIonicProviderRegistration;

@CapacitorPlugin(name = "LiveUpdate")
public class LiveUpdatePlugin extends Plugin {
Expand Down Expand Up @@ -71,6 +72,10 @@ public void load() {
try {
config = getLiveUpdateConfig();
implementation = new LiveUpdate(config, this);
// Register the Ionic Live Update Provider when the optional SDK is on the classpath.
if (LiveUpdateIonicProviderRegistration.isAvailable()) {
LiveUpdateIonicProviderRegistration.tryRegister(implementation);
}
Comment thread
robingenz marked this conversation as resolved.
} catch (Exception exception) {
Logger.error(TAG, exception.getMessage(), exception);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,17 +6,32 @@

public class FetchLatestBundleOptions {

@Nullable
private final String appId;

@Nullable
private final String channel;

public FetchLatestBundleOptions(@NonNull PluginCall call) {
this.appId = null;
this.channel = call.getString("channel", null);
}

public FetchLatestBundleOptions(@Nullable String channel) {
this.appId = null;
this.channel = channel;
}

public FetchLatestBundleOptions(@Nullable String appId, @Nullable String channel) {
this.appId = appId;
this.channel = channel;
}

@Nullable
public String getAppId() {
return appId;
}

@Nullable
public String getChannel() {
return channel;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,36 @@ public FetchLatestBundleResult(
this.signature = signature;
}

@Nullable
public ArtifactType getArtifactType() {
return artifactType;
}

@Nullable
public String getBundleId() {
return bundleId;
}

@Nullable
public String getChecksum() {
return checksum;
}

@Nullable
public JSONObject getCustomProperties() {
return customProperties;
}

@Nullable
public String getDownloadUrl() {
return downloadUrl;
}

@Nullable
public String getSignature() {
return signature;
}

@NonNull
public JSObject toJSObject() {
JSObject result = new JSObject();
Expand Down
Loading
Loading