Skip to content
Open
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
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
15 changes: 15 additions & 0 deletions packages/live-update/CapawesomeCapacitorLiveUpdate.podspec
Original file line number Diff line number Diff line change
Expand Up @@ -17,4 +17,19 @@ Pod::Spec.new do |s|
s.dependency 'ZIPFoundation', '~> 0.9.0'
s.swift_version = '5.1'
s.static_framework = true
s.default_subspec = 'Default'

s.subspec 'Default' do |ss|
# Default subspec
end

# Opt-in subspec that pulls in the Ionic Live Update Provider SDK and
# compiles in the provider/manager classes guarded by
# `#if CAPAWESOME_INCLUDE_IONIC_PROVIDER`.
s.subspec 'IonicProvider' do |ss|
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]
)
117 changes: 117 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,121 @@ 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.

**Attention**: The Ionic Live Update Provider SDK requires `minSdkVersion` **24** or higher. If your app targets a lower minimum, bump `minSdkVersion` in your `variables.gradle` when opting in.

### 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,17 @@ private void fetchLatestBundleInternal(
@NonNull NonEmptyCallback<GetLatestBundleResponse> callback
) {
try {
String appId = options.getAppId() == null ? getAppId() : options.getAppId();
if (appId == null || appId.isEmpty()) {
throw new Exception(LiveUpdatePlugin.ERROR_APP_ID_MISSING);
}
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 All @@ -40,6 +41,7 @@ public class LiveUpdatePlugin extends Plugin {
public static final String VERSION = "8.2.0";
public static final String SHARED_PREFERENCES_NAME = "CapawesomeLiveUpdate"; // DO NOT CHANGE
public static final String ERROR_APP_ID_MISSING = "appId must be configured.";
public static final String ERROR_BUNDLE_DIRECTORY_NOT_FOUND = "Bundle directory could not be resolved.";
public static final String ERROR_BUNDLE_EXISTS = "bundle already exists.";
public static final String ERROR_BUNDLE_ID_MISSING = "bundleId must be provided.";
public static final String ERROR_BUNDLE_INDEX_HTML_MISSING = "The bundle does not contain an index.html file.";
Expand All @@ -48,7 +50,9 @@ public class LiveUpdatePlugin extends Plugin {
public static final String ERROR_CHECKSUM_MISMATCH = "Checksum mismatch.";
public static final String ERROR_CUSTOM_ID_MISSING = "customId must be provided.";
public static final String ERROR_DOWNLOAD_FAILED = "Bundle could not be downloaded.";
public static final String ERROR_DOWNLOAD_URL_MISSING = "Bundle does not have a valid download URL.";
public static final String ERROR_HTTP_TIMEOUT = "Request timed out.";
public static final String ERROR_MANAGER_KEY_MISSING = "managerKey must be provided.";
public static final String ERROR_URL_MISSING = "url must be provided.";
public static final String ERROR_SIGNATURE_VERIFICATION_FAILED = "Signature verification failed.";
public static final String ERROR_PUBLIC_KEY_INVALID = "Invalid public key.";
Expand All @@ -71,6 +75,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