Skip to content
Merged
Show file tree
Hide file tree
Changes from 35 commits
Commits
Show all changes
36 commits
Select commit Hold shift + click to select a range
8606a51
Add M2 implementation plan (iOS support)
michelutke Jul 20, 2026
622d270
Add iOS targets, Ktor Darwin, and KeyValueStore/AppInfo actuals to up…
michelutke Jul 20, 2026
7165335
Add iOS actuals for shake, screenshot, url, and foreground observer
michelutke Jul 20, 2026
0ac5f2d
Add iOS targets and Skia image codec to updraft-ui-compose
michelutke Jul 20, 2026
25d39ab
Add iOS feedback view controller and event auto-wiring
michelutke Jul 21, 2026
a30d491
Replace app/ with CMP sample (androidApp + iosApp), raise Gradle heap…
michelutke Jul 21, 2026
9b6bb37
Add XCFramework export for updraft-core
michelutke Jul 21, 2026
c7d518f
Extend CI to feature branches with iOS job, publish all modules from …
michelutke Jul 21, 2026
2eca502
Document iOS setup and Swift integration
michelutke Jul 21, 2026
ad33ac9
Use macos-15 runners for Xcode 16 project format
michelutke Jul 21, 2026
158a888
Add CADisableMinimumFrameDurationOnPhone to fix Compose iOS launch crash
michelutke Jul 21, 2026
0b09033
Update M2 status doc: macos-15, verification findings, resolved M1 re…
michelutke Jul 21, 2026
f217b02
Load sample Updraft keys from local.properties instead of hardcoding
michelutke Jul 22, 2026
ec8edd2
Fix runtime crash: apply Compose compiler to updraft-sdk, wire sample…
michelutke Jul 22, 2026
ec1c9cf
Restyle Compose feedback UI to match legacy design
michelutke Jul 22, 2026
9c91dd2
Restyle dropdown menu white and point sample at production API
michelutke Jul 22, 2026
e15cc56
Send device_uudid and navigation_stack with feedback, drop dead stagi…
michelutke Jul 22, 2026
aec7f37
Add pluggable navigation stack provider for feedback uploads
michelutke Jul 22, 2026
5e00061
Make navigation stack reporting optional via sendNavigationStack setting
michelutke Jul 22, 2026
2716a33
Trigger iOS feedback on user screenshot like the legacy SDK
michelutke Jul 22, 2026
1a48775
Catch iOS shake via UIWindow motionEnded hook
michelutke Jul 22, 2026
03635c0
Document device verification round: bugs fixed, features added, open …
michelutke Jul 22, 2026
68817e7
Bump sample to versionCode 6 (uploaded to Updraft for update-dialog t…
michelutke Jul 22, 2026
ff6dfad
Document update-feature verification: Android E2E, iOS by construction
michelutke Jul 22, 2026
1700c40
Rebrand repository as the multiplatform Updraft SDK
michelutke Jul 22, 2026
6148641
Add CMP migration overview: before/after, library map, architecture
michelutke Jul 22, 2026
0fd9d24
Optimize CI: concurrency cancel, konan cache, setup-gradle, docs path…
michelutke Jul 22, 2026
dfcf6b0
Harden publish workflow: verify before publishing, guard duplicate ve…
michelutke Jul 22, 2026
5f47e69
Document decision: Compose UI ships for Views-based Android apps too
michelutke Jul 22, 2026
459ae0f
Bump CI and publish workflows to JDK 21
michelutke Jul 22, 2026
d2f1d71
Document Kotlin version requirements per consumer type
michelutke Jul 22, 2026
f076cfa
Re-enable Loco string sync, targeting shared compose-resources
michelutke Jul 22, 2026
7c10e08
Add AGP 9 upgrade to M3 scope with rationale for deferring past M2
michelutke Jul 22, 2026
d327698
Canonicalize strings via Loco round-trip
michelutke Jul 23, 2026
3f11ae9
Render update-dialog age from create_at as relative time
michelutke Jul 23, 2026
985bb7a
Link AGP 9 migration issue from M3 scope
michelutke Jul 23, 2026
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
62 changes: 48 additions & 14 deletions .github/workflows/build.yml
Original file line number Diff line number Diff line change
@@ -1,26 +1,60 @@
name: Build
name: CI

on:
push:
branches: [ "main" ]
pull_request:
branches: [ "main" ]
branches: [ "main", "feature/**" ]
paths-ignore:
- "**.md"
- "docs/**"

jobs:
build:
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true

jobs:
android:
runs-on: ubuntu-latest
timeout-minutes: 25

steps:
- uses: actions/checkout@v3
- name: set up JDK 17
uses: actions/setup-java@v3
- uses: actions/checkout@v4
- name: set up JDK 21
uses: actions/setup-java@v4
with:
java-version: '17'
java-version: '21'
distribution: 'temurin'
cache: gradle
- name: Set up Gradle
uses: gradle/actions/setup-gradle@v4
with:
validate-wrappers: true

- name: Grant execute permission for gradlew
run: chmod +x gradlew
- name: Build with Gradle
run: ./gradlew build
run: ./gradlew build

ios:
runs-on: macos-15
timeout-minutes: 35

env:
GRADLE_OPTS: -Dorg.gradle.jvmargs=-Xmx3g -Dkotlin.daemon.jvmargs=-Xmx3g

steps:
- uses: actions/checkout@v4
- name: set up JDK 21
uses: actions/setup-java@v4
with:
java-version: '21'
distribution: 'temurin'
- name: Set up Gradle
uses: gradle/actions/setup-gradle@v4
- name: Cache Kotlin/Native toolchain
uses: actions/cache@v4
with:
path: ~/.konan
key: konan-${{ runner.os }}-${{ hashFiles('gradle/libs.versions.toml') }}
restore-keys: konan-${{ runner.os }}-

- name: Build and test with Gradle
run: ./gradlew :updraft-core:iosSimulatorArm64Test :updraft-ui-compose:iosSimulatorArm64Test :updraft-core:assembleUpdraftCoreXCFramework
- name: Build iOS sample app
run: xcodebuild -project sample/iosApp/iosApp.xcodeproj -scheme iosApp -sdk iphonesimulator -configuration Debug build CODE_SIGNING_ALLOWED=NO
58 changes: 40 additions & 18 deletions .github/workflows/publish.yml
Original file line number Diff line number Diff line change
Expand Up @@ -4,44 +4,66 @@ on:
push:
branches: [ production ]

concurrency:
group: publish
cancel-in-progress: false

jobs:
publish:
runs-on: ubuntu-latest
runs-on: macos-15
timeout-minutes: 60
environment: maven-central
permissions:
contents: write
env:
GRADLE_OPTS: -Dorg.gradle.jvmargs=-Xmx3g -Dkotlin.daemon.jvmargs=-Xmx3g
steps:
- uses: actions/checkout@v3
- uses: actions/checkout@v4
- name: set up JDK 21
uses: actions/setup-java@v3
uses: actions/setup-java@v4
with:
java-version: '21'
distribution: 'temurin'
cache: gradle
- name: Set up Gradle
uses: gradle/actions/setup-gradle@v4
with:
validate-wrappers: true
- name: Cache Kotlin/Native toolchain
uses: actions/cache@v4
with:
path: ~/.konan
key: konan-${{ runner.os }}-${{ hashFiles('gradle/libs.versions.toml') }}
restore-keys: konan-${{ runner.os }}-

- name: Extract version from gradle.properties
id: get_version
run: echo "version=$(grep VERSION_NAME updraft-sdk/gradle.properties | cut -d'=' -f2)" >> $GITHUB_OUTPUT

- name: Grant execute permission for gradlew
run: chmod +x gradlew
- name: Fail if version already released
run: |
if git ls-remote --tags origin "refs/tags/${{ steps.get_version.outputs.version }}" | grep -q .; then
echo "::error::Version ${{ steps.get_version.outputs.version }} is already tagged — bump VERSION_NAME before publishing."
exit 1
fi

- name: Verify (build + all tests, both platforms)
run: ./gradlew build iosSimulatorArm64Test

- name: Publish to Maven Central Portal
run: ./gradlew :updraft-sdk:publish --no-configuration-cache
run: ./gradlew publishAllPublicationsToMavenCentralRepository --no-configuration-cache
env:
ORG_GRADLE_PROJECT_mavenCentralUsername: ${{ secrets.SONATYPE_NEXUS_USERNAME }}
ORG_GRADLE_PROJECT_mavenCentralPassword: ${{ secrets.SONATYPE_NEXUS_PASSWORD }}
ORG_GRADLE_PROJECT_signingInMemoryKey: ${{ secrets.SIGNING_PRIVATE_KEY }}
ORG_GRADLE_PROJECT_signingInMemoryKeyPassword: ${{ secrets.SIGNING_PASSWORD }}

- name: Extract version from gradle.properties
id: get_version
run: echo "version=$(grep VERSION_NAME updraft-sdk/gradle.properties | cut -d'=' -f2)" >> $GITHUB_OUTPUT

- name: Create GitHub Release
uses: actions/create-release@v1
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
uses: softprops/action-gh-release@v2
with:
tag_name: ${{ steps.get_version.outputs.version }}
release_name: ${{ steps.get_version.outputs.version }}
name: ${{ steps.get_version.outputs.version }}
body: |
Automated release ${{ steps.get_version.outputs.version }}

Published to:
- Maven Central
draft: false
prerelease: false
152 changes: 144 additions & 8 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,23 +1,50 @@
![Updraft: Mobile App Distribution](updraft.png)

[![Maven Central](https://maven-badges.sml.io/sonatype-central/com.appswithlove.updraft/updraft-sdk/badge.svg)](https://maven-badges.sml.io/sonatype-central/com.appswithlove.updraft/updraft-sdk)
[![GitHub license](https://img.shields.io/badge/license-MIT-lightgrey.svg)](https://raw.githubusercontent.com/appswithlove/updraft-sdk-ios/master/LICENSE)
[![GitHub license](https://img.shields.io/badge/license-MIT-lightgrey.svg)](LICENSE)
[![Bluesky](https://img.shields.io/badge/Bluesky-@appswithlove.bsky.social-blue.svg?style=flat)]([https://twitter.com/GetUpdraft](https://bsky.app/profile/appswithlove.bsky.social))


# Updraft SDK

Updraft SDK for Android
Updraft SDK for Android & iOS — built with Kotlin Multiplatform and Compose Multiplatform.

Updraft is a super easy app delivery tool that allows you to simply and quickly distribute your app. It's super useful during beta-testing or if you want to deliver an app without going through the app store review processes. Your users get a link and are guided through the installation process with in a comprehensive web-app. Updraft works with Android and iOS apps and easily integrates with your IDE.
The SDK adds additional features to apps that are delivered with Updraft: Auto-update for your distributed apps and most importantly the collection of user feedback.
The SDK adds additional features to apps that are delivered with Updraft: Auto-update for your distributed apps and most importantly the collection of user feedback (shake or screenshot to report, annotate, send).

One codebase serves every kind of app: native Android (Views or Compose), Kotlin/Compose Multiplatform, and pure-Swift iOS apps via an XCFramework. This repository was formerly `updraft-sdk-android`; since 2.0.0 it is the single home of the Updraft SDK for both platforms and supersedes [`updraft-sdk-ios`](https://github.com/appswithlove/updraft-sdk-ios).

Updraft is built by App Agencies [Apps with love](https://appswithlove.com/) and [Moqod](https://moqod.com/). Learn more at [getupdraft.com](https://getupdraft.com/) or follow the latest news on [twitter](https://twitter.com/GetUpdraft).


## Requirements

- minSdkVersion >=23
- iOS 14.0+ (for `updraft-core` / `updraft-ui-compose` iOS targets)

### Kotlin version compatibility

The SDK is built with Kotlin 2.2 and Compose Multiplatform 1.9. What your project needs depends on how you consume it:

| Your app | Required Kotlin |
| --- | --- |
| Java-only Android app | none — Kotlin metadata is not read at runtime |
| Kotlin Android app | **Kotlin ≥ 2.1** (the Kotlin compiler reads metadata up to one minor version ahead) |
| KMP / Compose Multiplatform app | **Kotlin ≥ 2.2** (klib consumption requires a matching compiler) |
| Swift app via `UpdraftCore.xcframework` | none — compiled binary, no Kotlin toolchain involved |

If your Kotlin version is too old, the build fails at compile time (never at runtime) with one of these:

```
Module 'updraft-core' was compiled with an incompatible version of Kotlin.
The binary version of its metadata is 2.2.0, expected version is 2.1.0.
```

```
KLIB resolver: Could not find "updraft-core" ... compiled with a newer Kotlin compiler
```

**Fix:** upgrade your project's Kotlin (and for CMP apps, Compose Multiplatform) to the versions above — or stay on Updraft SDK 1.x until you can.

## Installation

Expand Down Expand Up @@ -70,7 +97,9 @@ kotlin {
}
```

> iOS targets are not published yet — they land in a follow-up milestone (M2), which also adds an XCFramework and deprecates `updraft-sdk-ios`.
`updraft-core` and `updraft-ui-compose` publish `iosArm64`/`iosSimulatorArm64`/`iosX64` targets alongside `androidTarget`, so the same dependency works from a KMP or Compose Multiplatform `commonMain` source set on both platforms. Pure-Swift consumers that don't want Kotlin/Compose tooling use the `UpdraftCore.xcframework` instead — see [Swift integration](#swift-integration) below. `updraft-sdk` (the all-in-one artifact) is Android-only.

`updraft-sdk-ios` is superseded by this SDK once 2.0.0 ships. Archiving that repo and adding a final README banner there are release-time actions, tracked in [`docs/kmp-migration-m1-status.md`](docs/kmp-migration-m1-status.md).

## Setup

Expand All @@ -84,7 +113,7 @@ class App : Application() {
UpdraftSettings(
appKey = APP_KEY,
sdkKey = SDK_KEY,
baseUrl = UpdraftSettings.BASE_URL_STAGING, // Optional base url for updraft
// baseUrl defaults to the Updraft production API; override only for self-hosted instances
logLevel = LogLevel.Debug, // Optional log level
showFeedbackAlert = true, // Optional set if should show start alert
feedbackEnabled = true, // force disabling feedback if needed
Expand Down Expand Up @@ -117,6 +146,63 @@ A prompt is shown to the user to inform him of the change of state of the feedba
If enabled, the user is explained how he can give feedback.
User can take a screenshot to give a feedback.

### Navigation stack

Each feedback upload includes the app's navigation stack, captured at the moment
feedback is triggered (e.g. on shake). By default the SDK reports the Android
activity stack or the iOS view controller chain. Single-activity apps (Compose,
Navigation Component) should plug in their navigation library via
`Updraft.navigationStackProvider` — return screen names ordered root to top:

**Jetpack Compose Navigation**

```kotlin
val navController = rememberNavController()
LaunchedEffect(navController) {
Updraft.navigationStackProvider = {
listOfNotNull(navController.currentBackStackEntry?.destination?.route)
}
}
```

Or track the full back stack yourself with a destination listener:

```kotlin
val breadcrumbs = mutableListOf<String>()
navController.addOnDestinationChangedListener { _, destination, _ ->
destination.route?.let { route ->
breadcrumbs.remove(route)
breadcrumbs.add(route)
}
}
Updraft.navigationStackProvider = { breadcrumbs.toList() }
```

**Fragments (Navigation Component)**

```kotlin
Updraft.navigationStackProvider = {
navController.currentBackStack.value.mapNotNull { it.destination.label?.toString() }
}
```

**SwiftUI (iOS)**

```swift
UpdraftKt.navigationStackProvider = { ["Home", "Settings"] } // from your router state
```

Setting the provider to `null` restores the platform default. Updraft's own
screens are always excluded.

The whole feature is optional: to not send any navigation information at all
(e.g. for privacy reasons), disable it in the settings — the provider and the
platform default are then never invoked:

```kotlin
UpdraftSettings(appKey = ..., sdkKey = ..., sendNavigationStack = false)
```

## Advanced setup: Logging

To check if data is send properly to Updraft and also see some additional SDK log data in the console, you can set different log levels.
Expand All @@ -135,7 +221,7 @@ UpdraftSettings(

## Migrating from 1.x

Version 2.0.0 rebuilds the SDK on Kotlin Multiplatform. `updraft-sdk` is still a drop-in Android dependency and behaves the same at runtime, but the setup API changed:
Version 2.0.0 rebuilds the SDK on Kotlin Multiplatform. `updraft-sdk` is still a drop-in Android dependency and behaves the same at runtime, but the setup API changed. Before migrating, check the [Kotlin version compatibility](#kotlin-version-compatibility) table — projects on Kotlin < 2.1 should stay on 1.x until they upgrade.

| 1.x | 2.0.0 |
| --- | --- |
Expand Down Expand Up @@ -208,12 +294,62 @@ fun FeedbackRoute(screenshotPng: ByteArray?, onClose: () -> Unit) {
}
```

> iOS targets for `updraft-core` and `updraft-ui-compose` are not available yet — they're planned for milestone M2 alongside an XCFramework and iOS sample app.
### iOS

Both setups above work unchanged in `iosMain` — `Updraft.start`, `Updraft.events`, `Updraft.setFeedbackUiPresenter`, `Updraft.sendFeedback`, etc. are the same `commonMain` API on iOS as on Android. There's no iOS-specific initialization step; call `Updraft.start(...)` once from your shared Kotlin entry point (e.g. the function your `iOSApp.swift` calls on launch).

Compose Multiplatform apps that added `updraft-ui-compose` get the built-in feedback UI wired up for free on iOS via `UpdraftIos.autoWire()` — it presents the update/feedback dialogs and the feedback screen on top of the current key window, no `UpdraftEventHost` needed:

```kotlin
// commonMain or iosMain, called once from your app's iOS entry point
fun startUpdraft() {
Updraft.start(UpdraftSettings(appKey = APP_KEY, sdkKey = SDK_KEY))
UpdraftIos.autoWire()
}
```

Shake detection uses `CMMotionManager` (accelerometer), which needs no `Info.plist` permission entry.

### Swift integration

`updraft-core` builds an `UpdraftCore.xcframework` for pure-Swift consumers that don't want to pull in Kotlin/Compose tooling:

```
./gradlew :updraft-core:assembleUpdraftCoreXCFramework
```

The framework is written to `updraft-core/build/XCFrameworks/release/UpdraftCore.xcframework`. Drag it into an Xcode project or wrap it as an SPM binary target; how it's distributed (e.g. published alongside a release, hosted as a zip) is a release-time decision, out of scope for M2.

`UpdraftCore.xcframework` exposes the same `commonMain` API as above — `Updraft`, `UpdraftSettings`, `Updraft.events`, etc. — callable from Swift:

```swift
import UpdraftCore

Updraft.shared.start(
settings: UpdraftSettings(
appKey: APP_KEY,
sdkKey: SDK_KEY,
baseUrl: UpdraftSettings.companion.BASE_URL_PROD,
logLevel: .error,
showFeedbackAlert: true,
feedbackEnabled: true,
storeRelease: false
)
)
```

Kotlin default parameter values aren't exposed to the generated Objective-C/Swift header, so every `UpdraftSettings` argument must be passed explicitly from Swift (unlike Kotlin callers, which can rely on the defaults shown in [Setup](#setup)).

Since this framework only wraps `updraft-core`, it does not include the built-in Compose feedback UI (`updraft-ui-compose`, incl. `UpdraftFeedbackViewController`) — a pure-Swift, non-Compose app builds its own feedback screen and drives it via `Updraft.setFeedbackUiPresenter` / `Updraft.sendFeedback`, the same way an `updraft-core`-only Android/KMP app does. `UpdraftFeedbackViewController` and `UpdraftIos.autoWire()` are only reachable from a Kotlin/Compose Multiplatform app's own shared Kotlin code, as in the [`iOS` section](#ios) above.

## Local Development

In order to locally develop this plugin, the sample project can be used for easy testing. Additionally, the gradle task `publishToMavenLocal` allows to install the current version to Maven Local.

### Strings / Loco

UI strings live in `updraft-ui-compose/src/commonMain/composeResources/values*/strings.xml` (en/de) and are shared by Android and iOS. They are managed on [Loco](https://localise.biz); `./gradlew :updraft-ui-compose:updateLoco` re-fetches them into the compose-resources directory (same `strings.xml` format as classic Android resources). The task needs `updraft.locoApiKey=<key>` in `local.properties` and **overwrites local edits** — string changes must be made in Loco first, then pulled. Adding a new key: add it to the Loco project (en + de), run `updateLoco`, reference it via `Res.string.<key>`.

## Release

In order to release a new version of this plugin, create a new release on GitHub and the [pipeline](.github/workflows/publish.yml) will automatically publish the new version to Maven Central.
Pushing to the `production` branch triggers the [publish workflow](.github/workflows/publish.yml), which publishes `updraft-core`, `updraft-ui-compose`, and `updraft-sdk` to Maven Central.
Loading
Loading