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 '}
+ case $link in #(
+ /*) app_path=$link ;; #(
+ *) app_path=$APP_HOME$link ;;
+ esac
+done
+
+# This is normally unused
+# shellcheck disable=SC2034
+APP_BASE_NAME=${0##*/}
+# Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036)
+APP_HOME=$( cd -P "${APP_HOME:-./}" > /dev/null && printf '%s\n' "$PWD" ) || exit
+
+# Use the maximum available, or set MAX_FD != -1 to use that value.
+MAX_FD=maximum
+
+warn () {
+ echo "$*"
+} >&2
+
+die () {
+ echo
+ echo "$*"
+ echo
+ exit 1
+} >&2
+
+# OS specific support (must be 'true' or 'false').
+cygwin=false
+msys=false
+darwin=false
+nonstop=false
+case "$( uname )" in #(
+ CYGWIN* ) cygwin=true ;; #(
+ Darwin* ) darwin=true ;; #(
+ MSYS* | MINGW* ) msys=true ;; #(
+ NONSTOP* ) nonstop=true ;;
+esac
+
+CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
+
+
+# Determine the Java command to use to start the JVM.
+if [ -n "$JAVA_HOME" ] ; then
+ if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
+ # IBM's JDK on AIX uses strange locations for the executables
+ JAVACMD=$JAVA_HOME/jre/sh/java
+ else
+ JAVACMD=$JAVA_HOME/bin/java
+ fi
+ if [ ! -x "$JAVACMD" ] ; then
+ die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
+
+Please set the JAVA_HOME variable in your environment to match the
+location of your Java installation."
+ fi
+else
+ JAVACMD=java
+ if ! command -v java >/dev/null 2>&1
+ then
+ die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
+
+Please set the JAVA_HOME variable in your environment to match the
+location of your Java installation."
+ fi
+fi
+
+# Increase the maximum file descriptors if we can.
+if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then
+ case $MAX_FD in #(
+ max*)
+ # In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked.
+ # shellcheck disable=SC2039,SC3045
+ MAX_FD=$( ulimit -H -n ) ||
+ warn "Could not query maximum file descriptor limit"
+ esac
+ case $MAX_FD in #(
+ '' | soft) :;; #(
+ *)
+ # In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked.
+ # shellcheck disable=SC2039,SC3045
+ ulimit -n "$MAX_FD" ||
+ warn "Could not set maximum file descriptor limit to $MAX_FD"
+ esac
+fi
+
+# Collect all arguments for the java command, stacking in reverse order:
+# * args from the command line
+# * the main class name
+# * -classpath
+# * -D...appname settings
+# * --module-path (only if needed)
+# * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables.
+
+# For Cygwin or MSYS, switch paths to Windows format before running java
+if "$cygwin" || "$msys" ; then
+ APP_HOME=$( cygpath --path --mixed "$APP_HOME" )
+ CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" )
+
+ JAVACMD=$( cygpath --unix "$JAVACMD" )
+
+ # Now convert the arguments - kludge to limit ourselves to /bin/sh
+ for arg do
+ if
+ case $arg in #(
+ -*) false ;; # don't mess with options #(
+ /?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath
+ [ -e "$t" ] ;; #(
+ *) false ;;
+ esac
+ then
+ arg=$( cygpath --path --ignore --mixed "$arg" )
+ fi
+ # Roll the args list around exactly as many times as the number of
+ # args, so each arg winds up back in the position where it started, but
+ # possibly modified.
+ #
+ # NB: a `for` loop captures its iteration list before it begins, so
+ # changing the positional parameters here affects neither the number of
+ # iterations, nor the values presented in `arg`.
+ shift # remove old arg
+ set -- "$@" "$arg" # push replacement arg
+ done
+fi
+
+
+# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
+DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
+
+# Collect all arguments for the java command:
+# * DEFAULT_JVM_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments,
+# and any embedded shellness will be escaped.
+# * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be
+# treated as '${Hostname}' itself on the command line.
+
+set -- \
+ "-Dorg.gradle.appname=$APP_BASE_NAME" \
+ -classpath "$CLASSPATH" \
+ org.gradle.wrapper.GradleWrapperMain \
+ "$@"
+
+# Stop when "xargs" is not available.
+if ! command -v xargs >/dev/null 2>&1
+then
+ die "xargs is not available"
+fi
+
+# Use "xargs" to parse quoted args.
+#
+# With -n1 it outputs one arg per line, with the quotes and backslashes removed.
+#
+# In Bash we could simply go:
+#
+# readarray ARGS < <( xargs -n1 <<<"$var" ) &&
+# set -- "${ARGS[@]}" "$@"
+#
+# but POSIX shell has neither arrays nor command substitution, so instead we
+# post-process each arg (as a line of input to sed) to backslash-escape any
+# character that might be a shell metacharacter, then use eval to reverse
+# that process (while maintaining the separation between arguments), and wrap
+# the whole thing up as a single "set" statement.
+#
+# This will of course break if any of these variables contains a newline or
+# an unmatched quote.
+#
+
+eval "set -- $(
+ printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" |
+ xargs -n1 |
+ sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' |
+ tr '\n' ' '
+ )" '"$@"'
+
+exec "$JAVACMD" "$@"
diff --git a/kotlin/Examples/IDKitKmpSampleApp/gradlew.bat b/kotlin/Examples/IDKitKmpSampleApp/gradlew.bat
new file mode 100644
index 00000000..9b42019c
--- /dev/null
+++ b/kotlin/Examples/IDKitKmpSampleApp/gradlew.bat
@@ -0,0 +1,94 @@
+@rem
+@rem Copyright 2015 the original author or authors.
+@rem
+@rem Licensed under the Apache License, Version 2.0 (the "License");
+@rem you may not use this file except in compliance with the License.
+@rem You may obtain a copy of the License at
+@rem
+@rem https://www.apache.org/licenses/LICENSE-2.0
+@rem
+@rem Unless required by applicable law or agreed to in writing, software
+@rem distributed under the License is distributed on an "AS IS" BASIS,
+@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+@rem See the License for the specific language governing permissions and
+@rem limitations under the License.
+@rem
+@rem SPDX-License-Identifier: Apache-2.0
+@rem
+
+@if "%DEBUG%"=="" @echo off
+@rem ##########################################################################
+@rem
+@rem Gradle startup script for Windows
+@rem
+@rem ##########################################################################
+
+@rem Set local scope for the variables with windows NT shell
+if "%OS%"=="Windows_NT" setlocal
+
+set DIRNAME=%~dp0
+if "%DIRNAME%"=="" set DIRNAME=.
+@rem This is normally unused
+set APP_BASE_NAME=%~n0
+set APP_HOME=%DIRNAME%
+
+@rem Resolve any "." and ".." in APP_HOME to make it shorter.
+for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi
+
+@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
+set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m"
+
+@rem Find java.exe
+if defined JAVA_HOME goto findJavaFromJavaHome
+
+set JAVA_EXE=java.exe
+%JAVA_EXE% -version >NUL 2>&1
+if %ERRORLEVEL% equ 0 goto execute
+
+echo. 1>&2
+echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 1>&2
+echo. 1>&2
+echo Please set the JAVA_HOME variable in your environment to match the 1>&2
+echo location of your Java installation. 1>&2
+
+goto fail
+
+:findJavaFromJavaHome
+set JAVA_HOME=%JAVA_HOME:"=%
+set JAVA_EXE=%JAVA_HOME%/bin/java.exe
+
+if exist "%JAVA_EXE%" goto execute
+
+echo. 1>&2
+echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 1>&2
+echo. 1>&2
+echo Please set the JAVA_HOME variable in your environment to match the 1>&2
+echo location of your Java installation. 1>&2
+
+goto fail
+
+:execute
+@rem Setup the command line
+
+set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
+
+
+@rem Execute Gradle
+"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %*
+
+:end
+@rem End local scope for the variables with windows NT shell
+if %ERRORLEVEL% equ 0 goto mainEnd
+
+:fail
+rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
+rem the _cmd.exe /c_ return code!
+set EXIT_CODE=%ERRORLEVEL%
+if %EXIT_CODE% equ 0 set EXIT_CODE=1
+if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE%
+exit /b %EXIT_CODE%
+
+:mainEnd
+if "%OS%"=="Windows_NT" endlocal
+
+:omega
diff --git a/kotlin/Examples/IDKitKmpSampleApp/iosApp/IDKitKmpSampleApp/ContentView.swift b/kotlin/Examples/IDKitKmpSampleApp/iosApp/IDKitKmpSampleApp/ContentView.swift
new file mode 100644
index 00000000..3cffe7b9
--- /dev/null
+++ b/kotlin/Examples/IDKitKmpSampleApp/iosApp/IDKitKmpSampleApp/ContentView.swift
@@ -0,0 +1,118 @@
+import SwiftUI
+import SampleShared
+
+/// Bridges the shared Kotlin `SampleController` (StateFlow) into SwiftUI.
+@MainActor
+final class SampleViewModel: ObservableObject {
+ @Published private(set) var state = SampleUiState(
+ appId: "", rpId: "", action: "", signal: "",
+ environment: .production, preset: .device,
+ connectorUrl: nil, isLoading: false, logs: ""
+ )
+
+ private let controller = SampleController()
+
+ init() {
+ state = controller.state.value as! SampleUiState
+ controller.watchState { [weak self] newState in
+ self?.state = newState
+ }
+ }
+
+ deinit {
+ controller.dispose()
+ }
+
+ func setAction(_ value: String) { controller.setAction(value: value) }
+ func setSignal(_ value: String) { controller.setSignal(value: value) }
+ func setEnvironment(_ value: SampleEnvironment) { controller.setEnvironment(value: value) }
+ func setPreset(_ value: SamplePreset) { controller.setPreset(value: value) }
+ func generateRequest() { controller.generateRequest() }
+ func handleDeepLink(_ url: URL) { controller.handleDeepLink(url: url.absoluteString) }
+}
+
+struct ContentView: View {
+ @StateObject private var model = SampleViewModel()
+ // Qualified: the shared framework also exports IDKit's `Environment` enum.
+ @SwiftUI.Environment(\.openURL) private var openURL
+
+ var body: some View {
+ NavigationView {
+ Form {
+ Section("Request") {
+ HStack {
+ Text("App ID")
+ Spacer()
+ Text(model.state.appId)
+ .font(.footnote.monospaced())
+ .foregroundColor(.secondary)
+ }
+ HStack {
+ Text("RP ID")
+ Spacer()
+ Text(model.state.rpId)
+ .font(.footnote.monospaced())
+ .foregroundColor(.secondary)
+ }
+ TextField("Action", text: Binding(
+ get: { model.state.action },
+ set: { model.setAction($0) }
+ ))
+ TextField("Signal", text: Binding(
+ get: { model.state.signal },
+ set: { model.setSignal($0) }
+ ))
+ Picker("Environment", selection: Binding(
+ get: { model.state.environment },
+ set: { model.setEnvironment($0) }
+ )) {
+ Text("production").tag(SampleEnvironment.production)
+ Text("staging").tag(SampleEnvironment.staging)
+ }
+ Picker("Preset", selection: Binding(
+ get: { model.state.preset },
+ set: { model.setPreset($0) }
+ )) {
+ Text("orb").tag(SamplePreset.orb)
+ Text("secure document").tag(SamplePreset.secureDocument)
+ Text("document").tag(SamplePreset.document)
+ Text("device").tag(SamplePreset.device)
+ Text("selfie check").tag(SamplePreset.selfieCheck)
+ Text("identity check").tag(SamplePreset.identityCheck)
+ }
+ }
+
+ Section {
+ Button(model.state.isLoading ? "Generating..." : "Generate Connector URL") {
+ model.generateRequest()
+ }
+ .disabled(model.state.isLoading)
+ }
+
+ if let connectorUrl = model.state.connectorUrl {
+ Section("Connector URL") {
+ Button("Open Connector URL") {
+ if let url = URL(string: connectorUrl) {
+ openURL(url)
+ }
+ }
+ Text(connectorUrl)
+ .font(.footnote.monospaced())
+ .textSelection(.enabled)
+ }
+ }
+
+ Section("Logs") {
+ Text(model.state.logs.isEmpty ? "No logs yet." : model.state.logs)
+ .font(.footnote.monospaced())
+ .frame(maxWidth: .infinity, minHeight: 180, alignment: .topLeading)
+ .textSelection(.enabled)
+ }
+ }
+ .navigationTitle("IDKit KMP Sample")
+ }
+ .onOpenURL { url in
+ model.handleDeepLink(url)
+ }
+ }
+}
diff --git a/kotlin/Examples/IDKitKmpSampleApp/iosApp/IDKitKmpSampleApp/IDKitKmpSampleApp.swift b/kotlin/Examples/IDKitKmpSampleApp/iosApp/IDKitKmpSampleApp/IDKitKmpSampleApp.swift
new file mode 100644
index 00000000..9767bd6c
--- /dev/null
+++ b/kotlin/Examples/IDKitKmpSampleApp/iosApp/IDKitKmpSampleApp/IDKitKmpSampleApp.swift
@@ -0,0 +1,10 @@
+import SwiftUI
+
+@main
+struct IDKitKmpSampleApp: App {
+ var body: some Scene {
+ WindowGroup {
+ ContentView()
+ }
+ }
+}
diff --git a/kotlin/Examples/IDKitKmpSampleApp/iosApp/IDKitKmpSampleApp/Info.plist b/kotlin/Examples/IDKitKmpSampleApp/iosApp/IDKitKmpSampleApp/Info.plist
new file mode 100644
index 00000000..8cd93591
--- /dev/null
+++ b/kotlin/Examples/IDKitKmpSampleApp/iosApp/IDKitKmpSampleApp/Info.plist
@@ -0,0 +1,37 @@
+
+
+
+
+ CFBundleDevelopmentRegion
+ en
+ CFBundleDisplayName
+ IDKit KMP Sample
+ CFBundleExecutable
+ $(EXECUTABLE_NAME)
+ CFBundleIdentifier
+ $(PRODUCT_BUNDLE_IDENTIFIER)
+ CFBundleInfoDictionaryVersion
+ 6.0
+ CFBundleName
+ $(PRODUCT_NAME)
+ CFBundlePackageType
+ APPL
+ CFBundleShortVersionString
+ $(MARKETING_VERSION)
+ CFBundleURLTypes
+
+
+ CFBundleURLName
+ org.worldcoin.idkit.kmp.sample.callback
+ CFBundleURLSchemes
+
+ idkitkmpsample
+
+
+
+ CFBundleVersion
+ $(CURRENT_PROJECT_VERSION)
+ UILaunchScreen
+
+
+
diff --git a/kotlin/Examples/IDKitKmpSampleApp/iosApp/build-shared-framework.sh b/kotlin/Examples/IDKitKmpSampleApp/iosApp/build-shared-framework.sh
new file mode 100755
index 00000000..3240d9e8
--- /dev/null
+++ b/kotlin/Examples/IDKitKmpSampleApp/iosApp/build-shared-framework.sh
@@ -0,0 +1,45 @@
+#!/bin/sh
+# Xcode build phase: builds the Kotlin shared framework for the current
+# Xcode configuration/SDK via Gradle's embedAndSignAppleFrameworkForXcode.
+#
+# Xcode's environment has no JAVA_HOME and a minimal PATH, so locate a
+# Gradle-compatible JDK (17-21) across common install locations first.
+set -eu
+
+java_major() {
+ "$1/bin/java" -version 2>&1 | head -n 1 | sed -E 's/.*version "([0-9]+).*/\1/'
+}
+
+resolve_jdk() {
+ for candidate in \
+ "${JAVA_HOME:-}" \
+ "$(/usr/libexec/java_home -v 17 2>/dev/null || true)" \
+ "$(/usr/libexec/java_home -v 21 2>/dev/null || true)" \
+ /opt/homebrew/opt/openjdk@17/libexec/openjdk.jdk/Contents/Home \
+ /usr/local/opt/openjdk@17/libexec/openjdk.jdk/Contents/Home \
+ /opt/homebrew/opt/openjdk@21/libexec/openjdk.jdk/Contents/Home \
+ /usr/local/opt/openjdk@21/libexec/openjdk.jdk/Contents/Home \
+ "$HOME/Applications/Android Studio.app/Contents/jbr/Contents/Home" \
+ "/Applications/Android Studio.app/Contents/jbr/Contents/Home"; do
+ [ -n "$candidate" ] && [ -x "$candidate/bin/java" ] || continue
+ major="$(java_major "$candidate" || true)"
+ case "$major" in
+ 17|18|19|20|21)
+ echo "$candidate"
+ return 0
+ ;;
+ esac
+ done
+ return 1
+}
+
+if JDK="$(resolve_jdk)"; then
+ export JAVA_HOME="$JDK"
+ echo "Using JAVA_HOME=$JAVA_HOME"
+else
+ echo "error: No JDK 17-21 found for Gradle. Install one (e.g. brew install openjdk@17)" >&2
+ exit 1
+fi
+
+cd "$SRCROOT/.."
+exec ./gradlew :shared:embedAndSignAppleFrameworkForXcode
diff --git a/kotlin/Examples/IDKitKmpSampleApp/iosApp/project.yml b/kotlin/Examples/IDKitKmpSampleApp/iosApp/project.yml
new file mode 100644
index 00000000..9feef824
--- /dev/null
+++ b/kotlin/Examples/IDKitKmpSampleApp/iosApp/project.yml
@@ -0,0 +1,52 @@
+name: IDKitKmpSampleApp
+options:
+ minimumXcodeGenVersion: 2.38.0
+ deploymentTarget:
+ iOS: 15.0
+targets:
+ IDKitKmpSampleApp:
+ type: application
+ platform: iOS
+ deploymentTarget: 15.0
+ sources:
+ - path: IDKitKmpSampleApp
+ settings:
+ base:
+ PRODUCT_BUNDLE_IDENTIFIER: org.worldcoin.idkit.kmp.sample
+ INFOPLIST_FILE: IDKitKmpSampleApp/Info.plist
+ SWIFT_VERSION: 5.9
+ CURRENT_PROJECT_VERSION: 1
+ MARKETING_VERSION: 1.0
+ CODE_SIGN_STYLE: Automatic
+ DEVELOPMENT_TEAM: ""
+ FRAMEWORK_SEARCH_PATHS: $(inherited) $(SRCROOT)/../shared/build/xcode-frameworks/$(CONFIGURATION)/$(SDK_NAME)
+ OTHER_LDFLAGS: $(inherited) -framework SampleShared
+ ENABLE_USER_SCRIPT_SANDBOXING: "NO"
+ preBuildScripts:
+ - script: |
+ "$SRCROOT/build-shared-framework.sh"
+ name: Build Kotlin Shared Framework
+ basedOnDependencyAnalysis: false
+schemes:
+ IDKitKmpSampleApp:
+ build:
+ targets:
+ IDKitKmpSampleApp: all
+ preActions:
+ - script: |
+ REPO_ROOT="${SRCROOT}/../../../.."
+ LIB_SIM="${REPO_ROOT}/target/aarch64-apple-ios-sim/release/libidkit_kmp.a"
+ LIB_DEVICE="${REPO_ROOT}/target/aarch64-apple-ios/release/libidkit_kmp.a"
+
+ if [ ! -f "${LIB_SIM}" ] && [ ! -f "${LIB_DEVICE}" ]; then
+ echo "error: IDKit KMP native artifacts not found." >&2
+ echo "error: Run the following from the repo root, then build again:" >&2
+ echo "error: bash scripts/build-kotlin.sh" >&2
+ exit 1
+ fi
+ name: Check IDKit KMP Dependencies
+ settingsTarget: IDKitKmpSampleApp
+ run:
+ config: Debug
+ test:
+ config: Debug
diff --git a/kotlin/Examples/IDKitKmpSampleApp/settings.gradle.kts b/kotlin/Examples/IDKitKmpSampleApp/settings.gradle.kts
new file mode 100644
index 00000000..e0a6d4bb
--- /dev/null
+++ b/kotlin/Examples/IDKitKmpSampleApp/settings.gradle.kts
@@ -0,0 +1,22 @@
+pluginManagement {
+ repositories {
+ google()
+ mavenCentral()
+ gradlePluginPortal()
+ }
+}
+
+dependencyResolutionManagement {
+ repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS)
+ repositories {
+ google()
+ mavenCentral()
+ }
+}
+
+rootProject.name = "IDKitKmpSampleApp"
+
+include(":shared")
+include(":androidApp")
+include(":idkit")
+project(":idkit").projectDir = file("../../idkit")
diff --git a/kotlin/Examples/IDKitKmpSampleApp/shared/build.gradle.kts b/kotlin/Examples/IDKitKmpSampleApp/shared/build.gradle.kts
new file mode 100644
index 00000000..daf1d65e
--- /dev/null
+++ b/kotlin/Examples/IDKitKmpSampleApp/shared/build.gradle.kts
@@ -0,0 +1,55 @@
+plugins {
+ id("org.jetbrains.kotlin.multiplatform")
+ id("org.jetbrains.kotlin.plugin.serialization")
+ id("com.android.library")
+}
+
+kotlin {
+ jvmToolchain(17)
+
+ androidTarget()
+
+ listOf(iosArm64(), iosSimulatorArm64(), iosX64()).forEach { target ->
+ target.binaries.framework {
+ baseName = "SampleShared"
+ isStatic = true
+ // Export the SDK so Swift sees IDKit types through this framework.
+ export(project(":idkit"))
+ }
+ }
+
+ sourceSets {
+ commonMain {
+ dependencies {
+ api(project(":idkit"))
+ implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.10.2")
+ implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:1.9.0")
+ implementation("io.ktor:ktor-client-core:3.5.1")
+ }
+ }
+ androidMain {
+ dependencies {
+ implementation("io.ktor:ktor-client-okhttp:3.5.1")
+ }
+ }
+ iosMain {
+ dependencies {
+ implementation("io.ktor:ktor-client-darwin:3.5.1")
+ }
+ }
+ }
+}
+
+android {
+ namespace = "com.worldcoin.idkit.kmpsample.shared"
+ compileSdk = 35
+
+ defaultConfig {
+ minSdk = 23
+ }
+
+ compileOptions {
+ sourceCompatibility = JavaVersion.VERSION_17
+ targetCompatibility = JavaVersion.VERSION_17
+ }
+}
diff --git a/kotlin/Examples/IDKitKmpSampleApp/shared/src/commonMain/kotlin/com/worldcoin/idkit/kmpsample/shared/SampleController.kt b/kotlin/Examples/IDKitKmpSampleApp/shared/src/commonMain/kotlin/com/worldcoin/idkit/kmpsample/shared/SampleController.kt
new file mode 100644
index 00000000..63a76804
--- /dev/null
+++ b/kotlin/Examples/IDKitKmpSampleApp/shared/src/commonMain/kotlin/com/worldcoin/idkit/kmpsample/shared/SampleController.kt
@@ -0,0 +1,253 @@
+package com.worldcoin.idkit.kmpsample.shared
+
+import com.worldcoin.idkit.DocumentType
+import com.worldcoin.idkit.Environment
+import com.worldcoin.idkit.IDKit
+import com.worldcoin.idkit.IDKitRequest
+import com.worldcoin.idkit.IDKitRequestConfig
+import com.worldcoin.idkit.IDKitStatus
+import com.worldcoin.idkit.IdentityAttribute
+import com.worldcoin.idkit.Preset
+import com.worldcoin.idkit.RpContext
+import com.worldcoin.idkit.deviceLegacy
+import com.worldcoin.idkit.documentLegacy
+import com.worldcoin.idkit.identityCheck
+import com.worldcoin.idkit.orbLegacy
+import com.worldcoin.idkit.secureDocumentLegacy
+import com.worldcoin.idkit.selfieCheckLegacy
+import com.worldcoin.idkit.statusFlow
+import io.ktor.client.HttpClient
+import io.ktor.client.request.post
+import io.ktor.client.request.setBody
+import io.ktor.client.statement.bodyAsText
+import io.ktor.http.ContentType
+import io.ktor.http.contentType
+import io.ktor.http.isSuccess
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.Job
+import kotlinx.coroutines.SupervisorJob
+import kotlinx.coroutines.cancel
+import kotlinx.coroutines.flow.MutableStateFlow
+import kotlinx.coroutines.flow.asStateFlow
+import kotlinx.coroutines.flow.update
+import kotlinx.coroutines.launch
+import kotlinx.coroutines.withTimeoutOrNull
+import kotlinx.serialization.SerialName
+import kotlinx.serialization.Serializable
+import kotlinx.serialization.json.Json
+import kotlinx.serialization.json.JsonObject
+import kotlinx.serialization.json.buildJsonObject
+import kotlinx.serialization.json.put
+import kotlin.time.Duration.Companion.milliseconds
+
+enum class SampleEnvironment(val label: String) {
+ PRODUCTION(label = "production"),
+ STAGING(label = "staging"),
+}
+
+enum class SamplePreset(val label: String) {
+ ORB(label = "orb"),
+ SECURE_DOCUMENT(label = "secure document"),
+ DOCUMENT(label = "document"),
+ DEVICE(label = "device"),
+ SELFIE_CHECK(label = "selfie check"),
+ IDENTITY_CHECK(label = "identity check"),
+ ;
+
+ internal fun toPreset(signal: String): Preset = when (this) {
+ ORB -> orbLegacy(signal = signal)
+ SECURE_DOCUMENT -> secureDocumentLegacy(signal = signal)
+ DOCUMENT -> documentLegacy(signal = signal)
+ DEVICE -> deviceLegacy(signal = signal)
+ SELFIE_CHECK -> selfieCheckLegacy(signal = signal)
+ IDENTITY_CHECK -> identityCheck(
+ attributes = listOf(
+ IdentityAttribute.MinimumAge(value = 21u),
+ IdentityAttribute.Nationality(value = "JPN"),
+ IdentityAttribute.DocumentType(value = DocumentType.PASSPORT),
+ ),
+ )
+ }
+}
+
+data class SampleUiState(
+ val appId: String = "app_d8bbd5341f16fb97a61e644b7e169c0e",
+ val rpId: String = "rp_7b4f23dd5fb2a826",
+ val action: String = "test-action",
+ val signal: String = "signal",
+ val environment: SampleEnvironment = SampleEnvironment.PRODUCTION,
+ val preset: SamplePreset = SamplePreset.DEVICE,
+ val connectorUrl: String? = null,
+ val isLoading: Boolean = false,
+ val logs: String = "",
+)
+
+@Serializable
+private data class SignaturePayload(
+ val sig: String,
+ val nonce: String,
+ @SerialName(value = "created_at") val createdAt: Long,
+ @SerialName(value = "expires_at") val expiresAt: Long,
+)
+
+/**
+ * Shared verification flow driven by both the Compose and SwiftUI UIs:
+ * fetch an RP signature from the demo backend, create an IDKit request,
+ * expose the connector URL, poll for the proof, and verify it server-side.
+ */
+class SampleController {
+ private val signatureEndpoint = "https://idkit-js-example.vercel.app/api/rp-signature"
+ private val verifyEndpoint = "https://idkit-js-example.vercel.app/api/verify-proof"
+ private val returnToUrl = "idkitkmpsample://callback"
+
+ private val scope = CoroutineScope(context = SupervisorJob() + Dispatchers.Main)
+ private val http = HttpClient()
+ private val json = Json { ignoreUnknownKeys = true }
+
+ private val _state = MutableStateFlow(value = SampleUiState())
+ val state = _state.asStateFlow()
+
+ private var pendingRequest: IDKitRequest? = null
+ private var pollJob: Job? = null
+
+ fun setAction(value: String) = _state.update { it.copy(action = value) }
+ fun setSignal(value: String) = _state.update { it.copy(signal = value) }
+ fun setEnvironment(value: SampleEnvironment) = _state.update { it.copy(environment = value) }
+ fun setPreset(value: SamplePreset) = _state.update { it.copy(preset = value) }
+
+ /**
+ * Callback-based observation for SwiftUI (StateFlow generics erase in ObjC).
+ * Observation lasts until [dispose] cancels the controller scope.
+ */
+ fun watchState(block: (SampleUiState) -> Unit) {
+ scope.launch { state.collect { block(it) } }
+ }
+
+ fun generateRequest() {
+ val snapshot = _state.value
+ scope.launch {
+ _state.update { it.copy(isLoading = true) }
+ try {
+ log("Fetching RP signature from $signatureEndpoint")
+ val signature = fetchSignaturePayload(snapshot.action)
+
+ val config = IDKitRequestConfig(
+ appId = snapshot.appId,
+ action = snapshot.action,
+ rpContext = RpContext(
+ rpId = snapshot.rpId,
+ nonce = signature.nonce,
+ createdAt = signature.createdAt.toULong(),
+ expiresAt = signature.expiresAt.toULong(),
+ signature = signature.sig,
+ ),
+ actionDescription = "KMP sample",
+ allowLegacyProofs = false,
+ requireUserPresence = false,
+ returnTo = returnToUrl,
+ environment = when (snapshot.environment) {
+ SampleEnvironment.PRODUCTION -> Environment.PRODUCTION
+ SampleEnvironment.STAGING -> Environment.STAGING
+ },
+ )
+
+ val request = IDKit.request(config).preset(snapshot.preset.toPreset(snapshot.signal))
+
+ pendingRequest?.close()
+ pendingRequest = request
+ _state.update { it.copy(connectorUrl = request.connectorURI) }
+ log("Using preset: ${snapshot.preset.label}")
+ log("Generated request ID: ${request.requestId}")
+ log("Configured return_to callback: $returnToUrl")
+ startPolling(request, reason = "request generation")
+ } catch (error: Throwable) {
+ log("Error: ${error.message ?: error::class.simpleName}")
+ } finally {
+ _state.update { it.copy(isLoading = false) }
+ }
+ }
+ }
+
+ fun handleDeepLink(url: String) {
+ log("Received deep link callback: $url")
+ val request = pendingRequest
+ if (request == null) {
+ log("No pending request found. Generate a connector URL first.")
+ return
+ }
+ if (pollJob?.isActive == true) {
+ log("Polling already running for request ${request.requestId}.")
+ return
+ }
+ startPolling(request, reason = "deep link callback")
+ }
+
+ fun dispose() {
+ pendingRequest?.close()
+ http.close()
+ scope.cancel()
+ }
+
+ private fun startPolling(request: IDKitRequest, reason: String) {
+ pollJob?.cancel()
+ log("Started polling for request ${request.requestId} (trigger: $reason).")
+ pollJob = scope.launch {
+ val finished = withTimeoutOrNull(timeout = 180_000.milliseconds) {
+ request.statusFlow(pollIntervalMs = 2_000u).collect { status ->
+ when (status) {
+ IDKitStatus.WaitingForConnection -> log("Waiting for World App to connect...")
+ IDKitStatus.AwaitingConfirmation -> log("Awaiting user confirmation...")
+ is IDKitStatus.Confirmed -> {
+ pendingRequest = null
+ request.close()
+ log("Proof confirmed. Calling verify endpoint: $verifyEndpoint")
+ try {
+ log("Verify response: ${verifyProof(resultJson = status.result.rawJson)}")
+ } catch (error: Throwable) {
+ log("Verify request failed: ${error.message ?: error::class.simpleName}")
+ }
+ }
+
+ is IDKitStatus.Failed -> log("Proof completion failed: ${status.error.rawValue}")
+ is IDKitStatus.NetworkingError -> log("Networking error (${status.error.rawValue}), retrying...")
+ }
+ }
+ }
+ if (finished == null) {
+ log("Proof completion failed: timeout")
+ }
+ }
+ }
+
+ private suspend fun fetchSignaturePayload(action: String): SignaturePayload {
+ val response = http.post(urlString = signatureEndpoint) {
+ contentType(ContentType.Application.Json)
+ setBody(buildJsonObject { put("action", action) }.toString())
+ }
+ val body = response.bodyAsText()
+ check(value = response.status.isSuccess()) { "Backend request failed (${response.status.value}): $body" }
+ return json.decodeFromString(deserializer = SignaturePayload.serializer(), string = body)
+ }
+
+ private suspend fun verifyProof(resultJson: String): String {
+ val payload = buildJsonObject {
+ put("rp_id", _state.value.rpId)
+ put(
+ key = "devPortalPayload",
+ element = json.decodeFromString(deserializer = JsonObject.serializer(), string = resultJson),
+ )
+ }
+ val response = http.post(urlString = verifyEndpoint) {
+ contentType(ContentType.Application.Json)
+ setBody(payload.toString())
+ }
+ val body = response.bodyAsText()
+ check(value = response.status.isSuccess()) { "Verify failed (${response.status.value}): $body" }
+ return body
+ }
+
+ private fun log(message: String) {
+ _state.update { it.copy(logs = it.logs + "$message\n") }
+ }
+}
diff --git a/kotlin/Examples/IDKitSampleApp/app/build.gradle.kts b/kotlin/Examples/IDKitSampleApp/app/build.gradle.kts
index 0060a993..1419c0df 100644
--- a/kotlin/Examples/IDKitSampleApp/app/build.gradle.kts
+++ b/kotlin/Examples/IDKitSampleApp/app/build.gradle.kts
@@ -1,6 +1,7 @@
plugins {
id("com.android.application")
- kotlin("android")
+ id("org.jetbrains.kotlin.android")
+ id("org.jetbrains.kotlin.plugin.compose")
}
android {
@@ -30,18 +31,10 @@ android {
targetCompatibility = JavaVersion.VERSION_17
}
- kotlinOptions {
- jvmTarget = "17"
- }
-
buildFeatures {
compose = true
}
- composeOptions {
- kotlinCompilerExtensionVersion = "1.5.14"
- }
-
packaging {
resources {
excludes += "/META-INF/{AL2.0,LGPL2.1}"
@@ -49,8 +42,12 @@ android {
}
}
+kotlin {
+ jvmToolchain(17)
+}
+
dependencies {
- implementation(project(":bindings"))
+ implementation(project(":idkit"))
implementation("androidx.core:core-ktx:1.15.0")
implementation("androidx.activity:activity-compose:1.10.1")
@@ -58,7 +55,7 @@ dependencies {
implementation("androidx.compose.ui:ui-tooling-preview:1.7.8")
implementation("androidx.compose.material3:material3:1.3.1")
- implementation("org.jetbrains.kotlinx:kotlinx-coroutines-android:1.8.1")
+ implementation("org.jetbrains.kotlinx:kotlinx-coroutines-android:1.10.2")
implementation("com.squareup.okhttp3:okhttp:4.12.0")
debugImplementation("androidx.compose.ui:ui-tooling:1.7.8")
diff --git a/kotlin/Examples/IDKitSampleApp/app/src/main/java/com/worldcoin/idkit/sample/MainActivity.kt b/kotlin/Examples/IDKitSampleApp/app/src/main/java/com/worldcoin/idkit/sample/MainActivity.kt
index 5c0f6352..2359c96f 100644
--- a/kotlin/Examples/IDKitSampleApp/app/src/main/java/com/worldcoin/idkit/sample/MainActivity.kt
+++ b/kotlin/Examples/IDKitSampleApp/app/src/main/java/com/worldcoin/idkit/sample/MainActivity.kt
@@ -30,9 +30,13 @@ import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.text.font.FontFamily
import androidx.compose.ui.unit.dp
+import com.worldcoin.idkit.DocumentType
+import com.worldcoin.idkit.Environment
import com.worldcoin.idkit.IDKit
import com.worldcoin.idkit.IDKitRequest
import com.worldcoin.idkit.IDKitRequestConfig
+import com.worldcoin.idkit.IdentityAttribute
+import com.worldcoin.idkit.RpContext
import com.worldcoin.idkit.documentLegacy
import com.worldcoin.idkit.idkitResultToJson
import com.worldcoin.idkit.deviceLegacy
@@ -54,10 +58,6 @@ import okhttp3.Request
import okhttp3.RequestBody.Companion.toRequestBody
import org.json.JSONArray
import org.json.JSONObject
-import uniffi.idkit_core.DocumentType
-import uniffi.idkit_core.Environment
-import uniffi.idkit_core.IdentityAttribute
-import uniffi.idkit_core.RpContext
class MainActivity : ComponentActivity() {
private val model = SampleModel()
@@ -328,6 +328,8 @@ private class SampleModel {
fun clear() {
scope.cancel()
+ pendingRequest?.close()
+ pendingRequest = null
}
fun setAppForeground(isForeground: Boolean) {
@@ -374,6 +376,7 @@ private class SampleModel {
completionJob?.cancel()
connectorURI = request.connectorURI
+ pendingRequest?.close()
pendingRequest = request
deepLinkReceivedForPendingRequest = false
@@ -456,12 +459,18 @@ private class SampleModel {
log("Verify response: $verifyResult")
} catch (error: Throwable) {
log("Verify request failed: ${error.message ?: error::class.simpleName}")
+ } finally {
+ request.close()
}
return@launch
}
is com.worldcoin.idkit.IDKitStatus.Failed -> {
log("Proof completion failed: ${status.error.rawValue}")
+ if (pendingRequest === request) {
+ pendingRequest = null
+ }
+ request.close()
return@launch
}
diff --git a/kotlin/Examples/IDKitSampleApp/build.gradle.kts b/kotlin/Examples/IDKitSampleApp/build.gradle.kts
index 0cfeec76..2cdbc869 100644
--- a/kotlin/Examples/IDKitSampleApp/build.gradle.kts
+++ b/kotlin/Examples/IDKitSampleApp/build.gradle.kts
@@ -1,4 +1,8 @@
plugins {
- id("com.android.application") version "8.7.3" apply false
- kotlin("android") version "1.9.24" apply false
+ id("org.jetbrains.kotlin.multiplatform") version "2.3.21" apply false
+ id("org.jetbrains.kotlin.android") version "2.3.21" apply false
+ id("org.jetbrains.kotlin.plugin.serialization") version "2.3.21" apply false
+ id("org.jetbrains.kotlin.plugin.compose") version "2.3.21" apply false
+ id("com.android.application") version "8.11.2" apply false
+ id("com.android.library") version "8.11.2" apply false
}
diff --git a/kotlin/Examples/IDKitSampleApp/gradle.properties b/kotlin/Examples/IDKitSampleApp/gradle.properties
index d2418ac5..61ed6adc 100644
--- a/kotlin/Examples/IDKitSampleApp/gradle.properties
+++ b/kotlin/Examples/IDKitSampleApp/gradle.properties
@@ -1,4 +1,6 @@
-org.gradle.jvmargs=-Xmx2g -Dfile.encoding=UTF-8
+# Version used when building :idkit from source (kotlin/gradle.properties owns the release version).
+version=5.0.0
+
+org.gradle.jvmargs=-Xmx4g -Dfile.encoding=UTF-8
android.useAndroidX=true
kotlin.code.style=official
-version=4.0.0
diff --git a/kotlin/Examples/IDKitSampleApp/settings.gradle.kts b/kotlin/Examples/IDKitSampleApp/settings.gradle.kts
index a0a4ca48..6e416418 100644
--- a/kotlin/Examples/IDKitSampleApp/settings.gradle.kts
+++ b/kotlin/Examples/IDKitSampleApp/settings.gradle.kts
@@ -17,5 +17,5 @@ dependencyResolutionManagement {
rootProject.name = "IDKitSampleApp"
include(":app")
-include(":bindings")
-project(":bindings").projectDir = file("../../bindings")
+include(":idkit")
+project(":idkit").projectDir = file("../../idkit")
diff --git a/kotlin/README.md b/kotlin/README.md
index 7b081775..211962c8 100644
--- a/kotlin/README.md
+++ b/kotlin/README.md
@@ -1,261 +1,112 @@
# IDKit Kotlin SDK
-Kotlin SDK for World ID verification, backed by the Rust core via UniFFI.
-
-## Installation
-
-The Kotlin SDK is published to Maven Central as `com.worldcoin:idkit` — once a version is released there, add `mavenCentral()` to your repositories and depend on it with no authentication. Release builds are also published to GitHub Packages; dev builds (`X.Y.Z-dev.`) are published there only.
-
-GitHub Packages requires authentication for Maven downloads, even for public packages.
-Create a token with `read:packages` and expose it through environment variables.
-
-```kotlin
-dependencyResolutionManagement {
- repositories {
- mavenCentral()
- maven {
- url = uri("https://maven.pkg.github.com/worldcoin/idkit")
- credentials {
- username = System.getenv("GITHUB_ACTOR")
- password = System.getenv("GITHUB_TOKEN")
- }
- }
- }
-}
-```
-
-For local integration testing, build the Kotlin artifacts, publish them to `mavenLocal()`, and add `mavenLocal()` to the consuming app repositories:
-
-```bash
-bash scripts/build-kotlin.sh
-./kotlin/Examples/IDKitSampleApp/gradlew -p kotlin :bindings:publishToMavenLocal
-```
-
-Then add `mavenLocal()` to the consuming app repositories:
+World ID SDK for Kotlin Multiplatform — one Kotlin API for **Android and iOS**, backed by the same Rust core as every other IDKit SDK. Plain Android apps consume it as a regular AAR; KMP projects use it from `commonMain`.
```kotlin
-dependencyResolutionManagement {
- repositories {
- mavenLocal()
- google()
- mavenCentral()
- }
-}
-```
-
-Then add the dependency:
-
-```kotlin
-implementation("com.worldcoin:idkit:")
-```
-
-## Local setup
-
-From repo root:
-
-```bash
-bash scripts/build-kotlin.sh
-```
-
-This builds Rust artifacts, regenerates UniFFI Kotlin bindings, and copies native libraries used by the Kotlin module.
-
-## Canonical Kotlin API
-
-- Entry points:
- - `IDKit.request(config: IDKitRequestConfig)`
- - `IDKit.createSession(config: IDKitSessionConfig)`
- - `IDKit.proveSession(sessionId: String, config: IDKitSessionConfig)`
-- Request object:
- - `connectorURI: String`
- - `requestId: String`
- - `pollStatusOnce(): IDKitStatus`
- - `pollUntilCompletion(options: IDKitPollOptions): IDKitCompletionResult`
-- Hashing:
- - `IDKit.hashSignal(signal: String)`
- - `IDKit.hashSignal(signal: ByteArray)`
-
-## Quickstart
-
-```kotlin
-import com.worldcoin.idkit.CredentialRequest
-import com.worldcoin.idkit.IDKit
-import com.worldcoin.idkit.IDKitPollOptions
-import com.worldcoin.idkit.IDKitRequestConfig
-import com.worldcoin.idkit.IDKitCompletionResult
-import com.worldcoin.idkit.IdentityAttribute
-import com.worldcoin.idkit.selfieCheckLegacy
-import com.worldcoin.idkit.identityCheck
-import com.worldcoin.idkit.orbLegacy
-import com.worldcoin.idkit.deviceLegacy
-import uniffi.idkit_core.DocumentType
-import uniffi.idkit_core.Environment
-import uniffi.idkit_core.RpContext
-
-val rpContext = RpContext(
- rpId = "rp_1234567890abcdef",
- nonce = backendNonce,
- createdAt = backendCreatedAt,
- expiresAt = backendExpiresAt,
- signature = backendSig,
-)
-
val config = IDKitRequestConfig(
- appId = "app_staging_1234567890abcdef",
- action = "login",
- rpContext = rpContext,
- actionDescription = "Log in",
- bridgeUrl = null,
- allowLegacyProofs = false,
- requireUserPresence = false,
- overrideConnectBaseUrl = null,
- returnTo = null,
- environment = Environment.STAGING,
+ appId = "app_...",
+ action = "my-action",
+ rpContext = RpContext(rpId = "rp_...", nonce = nonce, createdAt = createdAt, expiresAt = expiresAt, signature = sig),
+ returnTo = "myapp://callback",
)
-val request = IDKit
- .request(config)
- .preset(orbLegacy(signal = "user-123"))
+val request = IDKit.request(config).preset(orbLegacy(signal = "my-signal"))
+openWorldApp(request.connectorURI)
-println("Connector URL: ${request.connectorURI}")
-
-when (val completion = request.pollUntilCompletion(IDKitPollOptions())) {
- is IDKitCompletionResult.Success -> println("Verified: ${completion.result.protocolVersion}")
- is IDKitCompletionResult.Failure -> println("Failed: ${completion.error.rawValue}")
+when (val completion = request.pollUntilCompletion()) {
+ is IDKitCompletionResult.Success -> verifyOnBackend(completion.result.rawJson)
+ is IDKitCompletionResult.Failure -> handle(completion.error)
}
+request.close()
```
-For orb-or-device legacy verification, use:
+## Installation
-```kotlin
-val request = IDKit
- .request(config)
- .preset(deviceLegacy(signal = "user-123"))
-```
+The SDK is published to Maven Central as `com.worldcoin:idkit` — add `mavenCentral()` to your repositories and depend on it with no authentication. Release builds are also published to GitHub Packages; dev builds (`X.Y.Z-dev.`) are published there only (GitHub Packages requires a token with `read:packages` even for public packages).
-For selfie-check verification, use:
+Plain Android app or a KMP project's `commonMain` — same coordinates either way:
```kotlin
-val request = IDKit
- .request(config)
- .preset(selfieCheckLegacy(signal = "user-123"))
+dependencies {
+ implementation("com.worldcoin:idkit:")
+}
```
-For document-based identity attestation, use:
+Pure-iOS (Swift-only) apps should prefer the [Swift SDK](../swift), which has first-class Swift types.
-```kotlin
-val request = IDKit
- .request(config)
- .preset(
- identityCheck(
- attributes = listOf(
- IdentityAttribute.MinimumAge(21u),
- IdentityAttribute.Nationality("JPN"),
- IdentityAttribute.DocumentType(DocumentType.PASSPORT),
- ),
- ),
- )
-```
+### Migrating from 4.x
-## Credential request options parity
+5.0.0 replaces the UniFFI/JNA Android-only implementation with the Kotlin Multiplatform one. Coordinates (`com.worldcoin:idkit`) and package (`com.worldcoin.idkit`) are unchanged, but there are breaking API changes:
-```kotlin
-import com.worldcoin.idkit.CredentialRequest
-import com.worldcoin.idkit.CredentialRequestOptions
-import uniffi.idkit_core.CredentialType
+- `IDKitBuilder.preset(...)` / `.constraints(...)` are now `suspend` (they open the bridge connection; 4.x did this blocking).
+- Call `IDKitRequest.close()` when done with a request to release the native handle (safe to call twice).
+- Types that previously leaked from `uniffi.idkit_core.*` (`RpContext`, `Environment`, `DocumentType`, `IdentityAttribute`, `ConstraintNode`, …) now live in `com.worldcoin.idkit` — update imports.
-val orb = CredentialRequest(
- CredentialType.ORB,
- options = CredentialRequestOptions(
- signal = "user-123",
- genesisIssuedAtMin = 1_700_000_000u,
- expiresAtMin = 1_800_000_000u,
- ),
-)
-```
+## Architecture
-## Session flow example
+The SDK calls the Rust core **directly** through a small hand-written C ABI — it does not use UniFFI-generated bindings:
-```kotlin
-val sessionRequest = IDKit
- .createSession(sessionConfig)
- .constraints(anyOf(CredentialRequest(CredentialType.ORB)))
-
-val completion = sessionRequest.pollUntilCompletion()
```
-
-## Android sample app
-
-A runnable Android sample exists at:
-
-- `kotlin/Examples/IDKitSampleApp`
-
-See `kotlin/Examples/IDKitSampleApp/README.md` for run steps.
-
-## Migration notes (`IdKit` -> `IDKit`)
-
-This release removes the legacy `IdKit` entrypoint and uses canonical `IDKit` naming.
-
-- `IdKit.request(...)` -> `IDKit.request(...)`
-- old raw `IdKitBuilder` wrapper usage -> canonical `IDKitBuilder`
-- old raw status/result wrappers -> `IDKitStatus` and `IDKitCompletionResult`
-
-## Local verification loop
-
-```bash
-bash scripts/build-kotlin.sh
+ commonMain (kotlin/idkit)
+ public API + poll loop + status/error mapping
+ kotlinx-serialization DTOs for the JSON boundary
+ │
+ internal expect object NativeBridge (10 fns)
+ ┌────────────┴────────────┐
+ androidMain iosMain
+ JNA direct mapping Kotlin/Native cinterop
+ libidkit_kmp.so libidkit_kmp.a (static)
+ └────────────┬────────────┘
+ rust/kmp-ffi (extern "C", JSON in/out)
+ │
+ rust/core (idkit-core)
```
-If Gradle is available locally:
+Why this shape:
-```bash
-gradle -p kotlin bindings:test
-```
+- **Why not generate KMP bindings from UniFFI?** The Rust core uses UniFFI 0.31; no Kotlin Multiplatform binding generator supports it (Gobley, the maintained one, targets UniFFI 0.29.x, and the compiled-metadata formats are incompatible). Downgrading the workspace's UniFFI would regenerate the shipping Swift SDK bindings and couple future core upgrades to a third-party release cadence.
+- **The C ABI** (`rust/kmp-ffi`, header at `rust/kmp-ffi/include/idkit_kmp.h`) passes JSON both ways and reuses the serde codecs the core already has. Every function returns an `{"ok": ...}` / `{"err": {code, message}}` envelope; panics are caught and converted to envelopes (never unwind across FFI); requests are opaque handles so double-free is a no-op; network-bound calls have a bounded 30s deadline and run off the main thread. It is independent of UniFFI versioning by construction.
+- **Distinct native library name** (`libidkit_kmp` vs the UniFFI toolchain's `libidkit`) keeps host test artifacts and the Swift SDK's build products from colliding.
-## Publishing
+## Building
-On production releases the Kotlin release workflow publishes to GitHub Packages and uploads a signed artifact to Maven Central (the first release awaits manual confirmation in the Central Portal before going live — see below). The GitHub Packages path uses GitHub's package credentials and can also be run locally:
+Native artifacts are never committed; build them first from the repo root:
```bash
-./kotlin/Examples/IDKitSampleApp/gradlew -p kotlin :bindings:publish
+bash scripts/build-kotlin.sh # host lib + Android ABIs (Docker/cargo-ndk) + iOS static libs
+SKIP_ANDROID=1 bash scripts/build-kotlin.sh # macOS host + iOS only (no Docker/NDK needed)
```
-Without `-Pidkit.publish.mavenCentral=true`, this does not configure Maven Central upload or signing tasks.
+Outputs:
-For local integration testing, publish to the local Maven repository with `:bindings:publishToMavenLocal` as described under [Installation](#installation).
+- `target/release/libidkit_kmp.{dylib,so}` — host library for JVM unit tests
+- `kotlin/idkit/src/androidMain/jniLibs//libidkit_kmp.so` — Android (gitignored)
+- `target//release/libidkit_kmp.a` — iOS, referenced by the cinterop config
-To publish to Maven Central from a local machine that already has credentials, keep the secrets in `~/.gradle/gradle.properties`:
+Android cross-builds use the `kmp-android-release` cargo profile (`panic = "unwind"`) — **not** `android-release` — because the FFI layer's `catch_unwind` must be able to convert panics into error envelopes instead of aborting the host app.
-```properties
-mavenCentralUsername=
-mavenCentralPassword=
-signing.keyId=
-signing.password=
-signing.secretKeyRingFile=/path/to/secring.gpg
-```
-
-Then explicitly enable the Central publishing path for that Gradle invocation:
+Then:
```bash
-./kotlin/Examples/IDKitSampleApp/gradlew -p kotlin \
- -Pidkit.publish.mavenCentral=true \
- :bindings:publishToMavenCentral
+cd kotlin
+./gradlew :idkit:assemble # all targets enabled on this host
+./gradlew :idkit:testReleaseUnitTest # commonTest on the host JVM (JNA → host lib)
+./gradlew :idkit:iosSimulatorArm64Test # commonTest on the iOS simulator (cinterop, statically linked)
+./gradlew :idkit:publishToMavenLocal # a guard task verifies the native artifacts for enabled targets
```
-To upload and release from the Central Portal deployment in one command, run:
+Requires JDK 17+, the Android SDK (`local.properties` or `ANDROID_HOME`), and Xcode on macOS for the iOS targets. On Linux the iOS targets are disabled automatically; **publishing to a remote repository is macOS-only** (the build fails it elsewhere, because the upload would otherwise be missing the iOS variants).
-```bash
-./kotlin/Examples/IDKitSampleApp/gradlew -p kotlin \
- -Pidkit.publish.mavenCentral=true \
- :bindings:publishAndReleaseToMavenCentral
-```
+## API notes
-On production releases the workflow runs the upload-only `:bindings:publishToMavenCentral` step automatically (not `publishAndReleaseToMavenCentral`), using the Sonatype and GPG signing credentials stored as `production` environment secrets. The first release uploads to the Central Portal for manual confirmation before going live; a follow-up change switches it to fully automatic.
+- `IDKitBuilder.preset(...)` / `.constraints(...)` are `suspend` and open the bridge connection over the network.
+- Call `IDKitRequest.close()` when done with a request to release the native handle (safe to call twice; the samples do it after the terminal status).
+- `IDKitResult.rawJson` is the untouched result JSON from the core — POST it verbatim to backend verification endpoints so unmodeled fields survive.
+- `IDKit.hashSignal(String)` follows the JS `hashSignal` semantics; use the `ByteArray` overload for binary signals (including any with interior NUL bytes).
+- Session and invite-code APIs are not exposed yet ("TODO: Re-enable when World ID 4.0 is live").
+- Kotlin and AGP versions are pinned in `kotlin/build.gradle.kts`; upgrade them in lockstep (Kotlin/Native ↔ Xcode compatibility matters here).
-## Troubleshooting
+## Example apps
-- `connection_failed`:
- - Check bridge URL/network and backend-generated RP context values.
-- `timeout`:
- - Increase `IDKitPollOptions(timeoutMs = ...)` or verify user completed flow in World App.
-- `cancelled`:
- - The polling coroutine was cancelled by the host app.
+- [`Examples/IDKitSampleApp`](Examples/IDKitSampleApp) — plain Android app (Jetpack Compose) consuming the SDK the way an Android-only integrator would.
+- [`Examples/IDKitKmpSampleApp`](Examples/IDKitKmpSampleApp) — KMP app: shared verification flow (Ktor + this SDK) driven by two native UIs, Jetpack Compose on Android and SwiftUI on iOS. See its README for run instructions (the iOS Xcode project is generated with XcodeGen, not checked in).
diff --git a/kotlin/bindings/build.gradle.kts b/kotlin/bindings/build.gradle.kts
deleted file mode 100644
index c9e4f21b..00000000
--- a/kotlin/bindings/build.gradle.kts
+++ /dev/null
@@ -1,166 +0,0 @@
-import com.vanniktech.maven.publish.AndroidSingleVariantLibrary
-import org.gradle.api.publish.maven.MavenPublication
-import org.gradle.api.publish.maven.tasks.PublishToMavenLocal
-import org.gradle.api.publish.maven.tasks.PublishToMavenRepository
-import org.gradle.jvm.tasks.Jar
-
-plugins {
- id("com.android.library")
- kotlin("android")
- id("com.vanniktech.maven.publish.base") version "0.34.0"
-}
-
-val libraryGroup = "com.worldcoin"
-val libraryArtifactId = "idkit"
-
-// Allow callers to exercise the Maven publication with an explicit artifact version.
-val libraryVersion = System.getenv("PKG_VERSION")?.takeIf { it.isNotBlank() }
- ?: project.version.toString().takeIf { it.isNotBlank() && it != "unspecified" }
- ?: throw GradleException("Could not find version in kotlin/gradle.properties")
-
-val enableMavenCentralPublishing = providers.gradleProperty("idkit.publish.mavenCentral")
- .map(String::toBoolean)
- .orElse(false)
-
-val emptyJavadocJar by tasks.registering(Jar::class) {
- archiveClassifier.set("javadoc")
-}
-
-val requiredNativeAbis = listOf("arm64-v8a", "armeabi-v7a", "x86", "x86_64")
-val verifyKotlinNativeLibraries by tasks.registering {
- group = "verification"
- description = "Verifies that Kotlin publishing includes native IDKit libraries for every Android ABI."
-
- doLast {
- val missingLibraries = requiredNativeAbis.map { abi ->
- abi to layout.projectDirectory.file("src/main/jniLibs/$abi/libidkit.so").asFile
- }.filter { (_, library) ->
- !library.isFile || library.length() == 0L
- }
-
- if (missingLibraries.isNotEmpty()) {
- val missing = missingLibraries.joinToString(separator = "\n") { (abi, library) ->
- "- $abi: ${library.relativeTo(projectDir)}"
- }
- throw GradleException(
- "Missing native libraries required for publishing:\n$missing\n" +
- "Run `bash scripts/build-kotlin.sh` from the repository root before publishing.",
- )
- }
- }
-}
-
-group = libraryGroup
-version = libraryVersion
-
-android {
- namespace = "com.worldcoin.idkit"
- compileSdk = 35
-
- buildFeatures {
- buildConfig = true
- }
-
- defaultConfig {
- minSdk = 23
- buildConfigField("String", "IDKIT_PACKAGE_VERSION", "\"$libraryVersion\"")
- }
-
- compileOptions {
- sourceCompatibility = JavaVersion.VERSION_17
- targetCompatibility = JavaVersion.VERSION_17
- }
-
- kotlinOptions {
- jvmTarget = "17"
- }
-
- testOptions {
- unitTests.all { test ->
- val rustLibDir = project.projectDir.resolve("../../target/release").canonicalPath
- test.jvmArgs("-Djna.library.path=$rustLibDir")
- }
- }
-}
-
-dependencies {
- implementation("net.java.dev.jna:jna:5.14.0@aar")
- implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.8.1")
- implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:1.7.1")
- implementation(kotlin("stdlib"))
-
- testImplementation(kotlin("test"))
- // The @aar variant doesn't bundle libjnidispatch — use the plain JVM jar for unit tests
- testImplementation("net.java.dev.jna:jna:5.14.0")
-}
-
-mavenPublishing {
- configure(
- AndroidSingleVariantLibrary(
- variant = "release",
- sourcesJar = true,
- publishJavadocJar = false,
- ),
- )
-
- coordinates(libraryGroup, libraryArtifactId, libraryVersion)
-
- pom {
- name.set("IDKit Kotlin")
- description.set("Kotlin bindings for IDKit backed by the Rust core")
- url.set("https://github.com/worldcoin/idkit")
- licenses {
- license {
- name.set("MIT License")
- url.set("https://opensource.org/licenses/MIT")
- }
- }
- developers {
- developer {
- id.set("worldcoin")
- name.set("Worldcoin")
- }
- }
- scm {
- connection.set("scm:git:https://github.com/worldcoin/idkit.git")
- developerConnection.set("scm:git:ssh://git@github.com/worldcoin/idkit.git")
- url.set("https://github.com/worldcoin/idkit")
- }
- }
-
- if (enableMavenCentralPublishing.get()) {
- publishToMavenCentral()
- signAllPublications()
- }
-}
-
-publishing {
- repositories {
- maven {
- name = "GitHubPackages"
- url = uri("https://maven.pkg.github.com/worldcoin/idkit")
- credentials {
- username = providers.environmentVariable("GITHUB_ACTOR")
- .orElse(providers.environmentVariable("GITHUB_USER"))
- .orNull
- password = providers.environmentVariable("GITHUB_TOKEN").orNull
- }
- }
- }
-}
-
-tasks.withType().configureEach {
- dependsOn(verifyKotlinNativeLibraries)
-}
-
-tasks.withType().configureEach {
- dependsOn(verifyKotlinNativeLibraries)
-}
-
-afterEvaluate {
- publishing {
- publications.withType().configureEach {
- artifact(emptyJavadocJar)
- }
- }
-}
diff --git a/kotlin/bindings/src/main/kotlin/com/worldcoin/idkit/IdKit.kt b/kotlin/bindings/src/main/kotlin/com/worldcoin/idkit/IdKit.kt
deleted file mode 100644
index b422357a..00000000
--- a/kotlin/bindings/src/main/kotlin/com/worldcoin/idkit/IdKit.kt
+++ /dev/null
@@ -1,438 +0,0 @@
-package com.worldcoin.idkit
-
-import kotlinx.coroutines.CancellationException
-import kotlinx.coroutines.delay
-import kotlinx.coroutines.ensureActive
-// TODO: Re-enable when World ID 4.0 is live
-// import kotlinx.serialization.json.JsonPrimitive
-// import kotlinx.serialization.json.buildJsonObject
-import kotlin.coroutines.coroutineContext
-import uniffi.idkit_core.AppError
-// TODO: Re-enable when World ID 4.0 is live
-// import uniffi.idkit_core.ConstraintNode
-// import uniffi.idkit_core.CredentialRequest
-// import uniffi.idkit_core.CredentialType
-import uniffi.idkit_core.IdKitBuilder
-import uniffi.idkit_core.IdKitRequestConfig as NativeIDKitRequestConfig
-import uniffi.idkit_core.IdKitRequestWrapper
-import uniffi.idkit_core.IdKitResult
-import uniffi.idkit_core.IdKitSessionConfig as NativeIDKitSessionConfig
-import uniffi.idkit_core.Preset
-import uniffi.idkit_core.Signal
-import uniffi.idkit_core.StatusWrapper
-// TODO: Re-enable when World ID 4.0 is live
-// import uniffi.idkit_core.createSession as nativeCreateSession
-// import uniffi.idkit_core.credentialToString
-import uniffi.idkit_core.hashSignalFfi
-import uniffi.idkit_core.idkitResultFromJson as nativeIdkitResultFromJson
-import uniffi.idkit_core.idkitResultToJson as nativeIdkitResultToJson
-// TODO: Re-enable when World ID 4.0 is live
-// import uniffi.idkit_core.proveSession as nativeProveSession
-import uniffi.idkit_core.request as nativeRequest
-
-typealias IDKitResult = IdKitResult
-typealias RpContext = uniffi.idkit_core.RpContext
-typealias Environment = uniffi.idkit_core.Environment
-typealias DocumentType = uniffi.idkit_core.DocumentType
-typealias IdentityAttribute = uniffi.idkit_core.IdentityAttribute
-typealias ConnectUrlMode = uniffi.idkit_core.ConnectUrlMode
-
-private const val SDK_PACKAGE_NAME = "idkit_kotlin"
-
-/** Typed projection of the bridge request payload, exposed for building test fixtures. */
-typealias BridgeRequestPayload = uniffi.idkit_core.BridgeRequestPayloadWrapper
-/** Protocol-level proof request inside a [BridgeRequestPayload]. */
-typealias ProofRequest = uniffi.idkit_core.ProofRequestWrapper
-/** A per-credential request line item inside a [ProofRequest]. */
-typealias CredentialRequestItem = uniffi.idkit_core.CredentialRequestWrapper
-
-data class IDKitRequestConfig(
- val appId: String,
- val action: String,
- val rpContext: RpContext,
- val actionDescription: String? = null,
- val bridgeUrl: String? = null,
- val allowLegacyProofs: Boolean = false,
- val requireUserPresence: Boolean = false,
- val overrideConnectBaseUrl: String? = null,
- val returnTo: String? = null,
- val environment: Environment? = null,
- val connectUrlMode: ConnectUrlMode? = null,
-) {
- internal fun toNative(): NativeIDKitRequestConfig =
- NativeIDKitRequestConfig(
- appId = appId,
- packageName = SDK_PACKAGE_NAME,
- packageVersion = IDKit.version,
- action = action,
- rpContext = rpContext,
- actionDescription = actionDescription,
- bridgeUrl = bridgeUrl,
- allowLegacyProofs = allowLegacyProofs,
- requireUserPresence = requireUserPresence,
- overrideConnectBaseUrl = overrideConnectBaseUrl,
- returnTo = returnTo,
- environment = environment,
- connectUrlMode = connectUrlMode,
- )
-}
-
-data class IDKitSessionConfig(
- val appId: String,
- val rpContext: RpContext,
- val actionDescription: String? = null,
- val bridgeUrl: String? = null,
- val requireUserPresence: Boolean = false,
- val overrideConnectBaseUrl: String? = null,
- val returnTo: String? = null,
- val environment: Environment? = null,
-) {
- internal fun toNative(): NativeIDKitSessionConfig =
- NativeIDKitSessionConfig(
- appId = appId,
- packageName = SDK_PACKAGE_NAME,
- packageVersion = IDKit.version,
- rpContext = rpContext,
- actionDescription = actionDescription,
- bridgeUrl = bridgeUrl,
- requireUserPresence = requireUserPresence,
- overrideConnectBaseUrl = overrideConnectBaseUrl,
- returnTo = returnTo,
- environment = environment,
- )
-}
-
-class IDKitClientError(message: String) : IllegalArgumentException(message)
-
-enum class IDKitErrorCode(val rawValue: String) {
- USER_REJECTED("user_rejected"),
- VERIFICATION_REJECTED("verification_rejected"),
- CREDENTIAL_UNAVAILABLE("credential_unavailable"),
- WORLD_ID_4_NOT_AVAILABLE("world_id_4_not_available"),
- WORLD_ID_3_NOT_AVAILABLE("world_id_3_not_available"),
- MALFORMED_REQUEST("malformed_request"),
- INVALID_NETWORK("invalid_network"),
- INCLUSION_PROOF_PENDING("inclusion_proof_pending"),
- INCLUSION_PROOF_FAILED("inclusion_proof_failed"),
- UNEXPECTED_RESPONSE("unexpected_response"),
- CONNECTION_FAILED("connection_failed"),
- MAX_VERIFICATIONS_REACHED("max_verifications_reached"),
- FAILED_BY_HOST_APP("failed_by_host_app"),
- USER_PRESENCE_FAILED("user_presence_failed"),
- INVALID_RP_SIGNATURE("invalid_rp_signature"),
- NULLIFIER_REPLAYED("nullifier_replayed"),
- DUPLICATE_NONCE("duplicate_nonce"),
- UNKNOWN_RP("unknown_rp"),
- INACTIVE_RP("inactive_rp"),
- TIMESTAMP_TOO_OLD("timestamp_too_old"),
- TIMESTAMP_TOO_FAR_IN_FUTURE("timestamp_too_far_in_future"),
- INVALID_TIMESTAMP("invalid_timestamp"),
- RP_SIGNATURE_EXPIRED("rp_signature_expired"),
- IDENTITY_ATTRIBUTES_NOT_MATCHED("identity_attributes_not_matched"),
- GENERIC_ERROR("generic_error"),
- TIMEOUT("timeout"),
- CANCELLED("cancelled");
-
- internal companion object {
- fun from(error: AppError): IDKitErrorCode = when (error) {
- AppError.USER_REJECTED -> USER_REJECTED
- AppError.VERIFICATION_REJECTED -> VERIFICATION_REJECTED
- AppError.CREDENTIAL_UNAVAILABLE -> CREDENTIAL_UNAVAILABLE
- AppError.WORLD_ID4_NOT_AVAILABLE -> WORLD_ID_4_NOT_AVAILABLE
- AppError.WORLD_ID3_NOT_AVAILABLE -> WORLD_ID_3_NOT_AVAILABLE
- AppError.MALFORMED_REQUEST -> MALFORMED_REQUEST
- AppError.INVALID_NETWORK -> INVALID_NETWORK
- AppError.INCLUSION_PROOF_PENDING -> INCLUSION_PROOF_PENDING
- AppError.INCLUSION_PROOF_FAILED -> INCLUSION_PROOF_FAILED
- AppError.UNEXPECTED_RESPONSE -> UNEXPECTED_RESPONSE
- AppError.CONNECTION_FAILED -> CONNECTION_FAILED
- AppError.MAX_VERIFICATIONS_REACHED -> MAX_VERIFICATIONS_REACHED
- AppError.FAILED_BY_HOST_APP -> FAILED_BY_HOST_APP
- AppError.USER_PRESENCE_FAILED -> USER_PRESENCE_FAILED
- AppError.INVALID_RP_SIGNATURE -> INVALID_RP_SIGNATURE
- AppError.NULLIFIER_REPLAYED -> NULLIFIER_REPLAYED
- AppError.DUPLICATE_NONCE -> DUPLICATE_NONCE
- AppError.UNKNOWN_RP -> UNKNOWN_RP
- AppError.INACTIVE_RP -> INACTIVE_RP
- AppError.TIMESTAMP_TOO_OLD -> TIMESTAMP_TOO_OLD
- AppError.TIMESTAMP_TOO_FAR_IN_FUTURE -> TIMESTAMP_TOO_FAR_IN_FUTURE
- AppError.INVALID_TIMESTAMP -> INVALID_TIMESTAMP
- AppError.RP_SIGNATURE_EXPIRED -> RP_SIGNATURE_EXPIRED
- AppError.IDENTITY_ATTRIBUTES_NOT_MATCHED -> IDENTITY_ATTRIBUTES_NOT_MATCHED
- AppError.GENERIC_ERROR -> GENERIC_ERROR
- }
- }
-}
-
-sealed interface IDKitStatus {
- data object WaitingForConnection : IDKitStatus
- data object AwaitingConfirmation : IDKitStatus
- data class Confirmed(val result: IDKitResult) : IDKitStatus
- data class Failed(val error: IDKitErrorCode) : IDKitStatus
- data class NetworkingError(val error: IDKitErrorCode) : IDKitStatus
-}
-
-sealed interface IDKitCompletionResult {
- data class Success(val result: IDKitResult) : IDKitCompletionResult
- data class Failure(val error: IDKitErrorCode) : IDKitCompletionResult
-}
-
-data class IDKitPollOptions(
- val pollIntervalMs: ULong = 1_000u,
- val timeoutMs: ULong = 900_000u,
-)
-
-// TODO: Re-enable when World ID 4.0 is live
-// data class CredentialRequestOptions(
-// val signal: String? = null,
-// val genesisIssuedAtMin: ULong? = null,
-// val expiresAtMin: ULong? = null,
-// )
-
-class IDKitBuilder internal constructor(
- private val inner: IdKitBuilder,
-) {
- fun constraints(constraints: uniffi.idkit_core.ConstraintNode): IDKitRequest =
- IDKitRequest(inner.constraints(constraints))
-
- fun preset(preset: Preset): IDKitRequest =
- IDKitRequest(inner.preset(preset))
-}
-
-class IDKitRequest internal constructor(
- private val connectorUriValue: String,
- private val requestIdValue: String,
- private val pollStatusProvider: suspend () -> IDKitStatus,
-) {
- internal constructor(inner: IdKitRequestWrapper) : this(
- connectorUriValue = inner.connectUrl(),
- requestIdValue = inner.requestId(),
- pollStatusProvider = { mapStatus(inner.pollStatusOnce()) },
- )
-
- val connectorURI: String
- get() = connectorUriValue
-
- val requestId: String
- get() = requestIdValue
-
- suspend fun pollStatusOnce(): IDKitStatus = pollStatusProvider()
-
- suspend fun pollUntilCompletion(
- options: IDKitPollOptions = IDKitPollOptions(),
- ): IDKitCompletionResult {
- val pollIntervalMs = options.pollIntervalMs.coerceAtLeast(1u)
- val startedAt = System.currentTimeMillis()
-
- try {
- while (true) {
- coroutineContext.ensureActive()
-
- if (System.currentTimeMillis() - startedAt >= options.timeoutMs.toLong()) {
- return IDKitCompletionResult.Failure(IDKitErrorCode.TIMEOUT)
- }
-
- when (val status = pollStatusOnce()) {
- is IDKitStatus.Confirmed -> return IDKitCompletionResult.Success(status.result)
- is IDKitStatus.Failed -> return IDKitCompletionResult.Failure(status.error)
- is IDKitStatus.NetworkingError -> delay(pollIntervalMs.toLong())
- IDKitStatus.AwaitingConfirmation,
- IDKitStatus.WaitingForConnection -> delay(pollIntervalMs.toLong())
- }
- }
- } catch (_: CancellationException) {
- return IDKitCompletionResult.Failure(IDKitErrorCode.CANCELLED)
- }
- }
-
- internal companion object {
- internal fun forTesting(
- connectorURI: String,
- requestId: String,
- pollStatusProvider: suspend () -> IDKitStatus,
- ): IDKitRequest = IDKitRequest(connectorURI, requestId, pollStatusProvider)
-
- internal fun mapStatus(status: StatusWrapper): IDKitStatus = when (status) {
- StatusWrapper.WaitingForConnection -> IDKitStatus.WaitingForConnection
- StatusWrapper.AwaitingConfirmation -> IDKitStatus.AwaitingConfirmation
- is StatusWrapper.Confirmed -> IDKitStatus.Confirmed(status.result)
- is StatusWrapper.Failed -> IDKitStatus.Failed(IDKitErrorCode.from(status.error))
- is StatusWrapper.NetworkingError -> IDKitStatus.NetworkingError(IDKitErrorCode.from(status.error))
- }
- }
-}
-
-object IDKit {
- val version: String = BuildConfig.IDKIT_PACKAGE_VERSION
-
- fun request(config: IDKitRequestConfig): IDKitBuilder {
- require(config.appId.isNotBlank()) { "app_id is required" }
- require(config.action.isNotBlank()) { "action is required" }
- return IDKitBuilder(nativeRequest(config.toNative()))
- }
-
- /**
- * Builds the bridge request payload from a preset without opening a network
- * connection. Intended for building test fixtures.
- */
- fun createBridgePayloadFromPresets(
- config: IDKitRequestConfig,
- preset: Preset,
- ): BridgeRequestPayload =
- nativeRequest(config.toNative()).bridgeRequestPayloadFromPreset(preset)
-
- /**
- * Builds the bridge request payload from custom constraints without opening a
- * network connection. Intended for building test fixtures.
- */
- fun createBridgePayloadFromConstraints(
- config: IDKitRequestConfig,
- constraints: uniffi.idkit_core.ConstraintNode,
- ): BridgeRequestPayload =
- nativeRequest(config.toNative()).bridgeRequestPayload(constraints)
-
- // TODO: Re-enable when World ID 4.0 is live
- // fun createSession(config: IDKitSessionConfig): IDKitBuilder {
- // require(config.appId.isNotBlank()) { "app_id is required" }
- // return IDKitBuilder(nativeCreateSession(config.toNative()))
- // }
-
- // fun proveSession(sessionId: String, config: IDKitSessionConfig): IDKitBuilder {
- // require(sessionId.isNotBlank()) { "session_id is required" }
- // require(config.appId.isNotBlank()) { "app_id is required" }
- // return IDKitBuilder(nativeProveSession(sessionId, config.toNative()))
- // }
-
- fun hashSignal(signal: String): String = hashSignalFfi(Signal.fromString(signal))
-
- fun hashSignal(signal: ByteArray): String = hashSignalFfi(Signal.fromBytes(signal))
-}
-
-// TODO: Re-enable when World ID 4.0 is live
-// private fun credentialRequestFromOptions(
-// type: CredentialType,
-// options: CredentialRequestOptions,
-// ): CredentialRequest {
-// val payload = buildJsonObject {
-// put("type", JsonPrimitive(credentialToString(type)))
-// options.signal?.let { put("signal", JsonPrimitive(it)) }
-// options.genesisIssuedAtMin?.let { put("genesis_issued_at_min", JsonPrimitive(it.toLong())) }
-// options.expiresAtMin?.let { put("expires_at_min", JsonPrimitive(it.toLong())) }
-// }
-// return CredentialRequest.fromJson(payload.toString())
-// }
-
-// fun CredentialRequest(type: CredentialType, signal: String? = null): CredentialRequest =
-// CredentialRequest.withStringSignal(type, signal)
-
-// fun CredentialRequest(type: CredentialType, abiEncodedSignal: ByteArray): CredentialRequest =
-// CredentialRequest(type, Signal.fromBytes(abiEncodedSignal))
-
-// fun CredentialRequest(type: CredentialType, options: CredentialRequestOptions): CredentialRequest {
-// if (options.genesisIssuedAtMin == null && options.expiresAtMin == null) {
-// return CredentialRequest.withStringSignal(type, options.signal)
-// }
-//
-// if (options.expiresAtMin == null) {
-// return CredentialRequest.withGenesisMin(type, options.signal?.let { Signal.fromString(it) }, options.genesisIssuedAtMin!!)
-// }
-//
-// if (options.genesisIssuedAtMin == null) {
-// return CredentialRequest.withExpiresAtMin(type, options.signal?.let { Signal.fromString(it) }, options.expiresAtMin)
-// }
-//
-// return credentialRequestFromOptions(type, options)
-// }
-
-// fun anyOf(vararg items: CredentialRequest): ConstraintNode =
-// ConstraintNode.any(items.map { ConstraintNode.item(it) })
-
-// fun anyOf(items: List): ConstraintNode =
-// ConstraintNode.any(items.map { ConstraintNode.item(it) })
-
-// fun anyOfNodes(vararg nodes: ConstraintNode): ConstraintNode =
-// ConstraintNode.any(nodes.toList())
-
-// fun anyOfNodes(nodes: List): ConstraintNode =
-// ConstraintNode.any(nodes)
-
-// fun allOf(vararg items: CredentialRequest): ConstraintNode =
-// ConstraintNode.all(items.map { ConstraintNode.item(it) })
-
-// fun allOf(items: List): ConstraintNode =
-// ConstraintNode.all(items.map { ConstraintNode.item(it) })
-
-// fun allOfNodes(vararg nodes: ConstraintNode): ConstraintNode =
-// ConstraintNode.all(nodes.toList())
-
-// fun allOfNodes(nodes: List): ConstraintNode =
-// ConstraintNode.all(nodes)
-
-// fun enumerateOf(vararg items: CredentialRequest): ConstraintNode =
-// enumerateOfNodes(items.map { ConstraintNode.item(it) })
-
-// fun enumerateOf(items: List): ConstraintNode =
-// enumerateOfNodes(items.map { ConstraintNode.item(it) })
-
-// fun enumerateOfNodes(vararg nodes: ConstraintNode): ConstraintNode =
-// enumerateOfNodes(nodes.toList())
-
-// fun enumerateOfNodes(nodes: List): ConstraintNode {
-// val nodesJson = nodes.joinToString(separator = ",") { it.toJson() }
-// return ConstraintNode.fromJson("""{"enumerate":[${nodesJson}]}""")
-// }
-
-/**
- * Returns the orb legacy preset.
- *
- * This preset only returns World ID 3.0 proofs. Use it for compatibility with older IDKit versions.
- */
-fun orbLegacy(signal: String? = null): Preset = Preset.OrbLegacy(signal = signal)
-
-/**
- * Returns the secure document legacy preset.
- *
- * This preset only returns World ID 3.0 proofs. Use it for compatibility with older IDKit versions.
- */
-fun secureDocumentLegacy(signal: String? = null): Preset =
- Preset.SecureDocumentLegacy(signal = signal)
-
-/**
- * Returns the document legacy preset.
- *
- * This preset only returns World ID 3.0 proofs. Use it for compatibility with older IDKit versions.
- */
-fun documentLegacy(signal: String? = null): Preset = Preset.DocumentLegacy(signal = signal)
-
-/**
- * Returns the device legacy preset.
- *
- * This preset only returns World ID 3.0 proofs. Use it for compatibility with older IDKit versions.
- */
-fun deviceLegacy(signal: String? = null): Preset = Preset.DeviceLegacy(signal = signal)
-
-/**
- * Returns the selfie check legacy preset.
- *
- * This preset only returns World ID 3.0 proofs. Use it for compatibility with older IDKit versions.
- * Preview: Selfie Check is currently in preview. Contact us if you need it enabled.
- */
-fun selfieCheckLegacy(signal: String? = null): Preset = Preset.SelfieCheckLegacy(signal = signal)
-
-/**
- * Returns the identity check preset.
- */
-fun identityCheck(attributes: List, legacySignal: String? = null): Preset =
- Preset.IdentityCheck(attributes = attributes, legacySignal = legacySignal)
-
-fun idkitResultToJson(result: IDKitResult): String = nativeIdkitResultToJson(result)
-
-fun idkitResultFromJson(json: String): IDKitResult = nativeIdkitResultFromJson(json)
-
-fun hashSignal(signal: Signal): String = hashSignalFfi(signal)
-
-val ProofRequest.credentialIdentifiers: List
- get() = proofRequests.map { it.identifier }
-
-val BridgeRequestPayload.credentialIdentifiers: List
- get() = proofRequest?.credentialIdentifiers.orEmpty()
diff --git a/kotlin/bindings/src/main/kotlin/com/worldcoin/idkit/KotlinCompat.kt b/kotlin/bindings/src/main/kotlin/com/worldcoin/idkit/KotlinCompat.kt
deleted file mode 100644
index 1c5ae402..00000000
--- a/kotlin/bindings/src/main/kotlin/com/worldcoin/idkit/KotlinCompat.kt
+++ /dev/null
@@ -1,52 +0,0 @@
-package com.worldcoin.idkit
-
-import kotlinx.coroutines.delay
-import kotlinx.coroutines.flow.Flow
-import kotlinx.coroutines.flow.flow
-import kotlin.time.Duration
-import kotlin.time.Duration.Companion.seconds
-import uniffi.idkit_core.Signal
-
-val Signal.data: ByteArray
- get() = this.asBytes()
-
-val Signal.string: String?
- get() = this.asString()
-
-// ─────────────────────────────────────────────────────────────────────────────
-// Canonical Status Extensions
-// ─────────────────────────────────────────────────────────────────────────────
-
-/**
- * Flow-based status helper for IDKitRequest.
- *
- * @param pollInterval How long to wait between polls.
- */
-fun IDKitRequest.statusFlow(pollInterval: Duration = 3.seconds): Flow = flow {
- var last: IDKitStatus? = null
-
- while (true) {
- val current = pollStatusOnce()
- // Networking errors are silently retried, consistent with pollUntilCompletion
- if (current != last && current !is IDKitStatus.NetworkingError) {
- last = current
- emit(current)
- }
-
- when (current) {
- is IDKitStatus.Confirmed,
- is IDKitStatus.Failed -> return@flow
- is IDKitStatus.NetworkingError,
- IDKitStatus.AwaitingConfirmation,
- IDKitStatus.WaitingForConnection -> {
- delay(pollInterval)
- }
- }
- }
-}
-
-/**
- * Convenience accessor for the IDKitResult when status is Confirmed.
- */
-val IDKitStatus.Confirmed.idkitResult: IDKitResult
- get() = this.result
diff --git a/kotlin/bindings/src/test/kotlin/com/worldcoin/idkit/IDKitTests.kt b/kotlin/bindings/src/test/kotlin/com/worldcoin/idkit/IDKitTests.kt
deleted file mode 100644
index 3a1fab8a..00000000
--- a/kotlin/bindings/src/test/kotlin/com/worldcoin/idkit/IDKitTests.kt
+++ /dev/null
@@ -1,465 +0,0 @@
-package com.worldcoin.idkit
-
-import kotlinx.coroutines.CancellationException
-import kotlinx.coroutines.runBlocking
-import kotlin.test.Test
-import kotlin.test.assertEquals
-import kotlin.test.assertNotEquals
-import kotlin.test.assertNull
-import kotlin.test.assertTrue
-import uniffi.idkit_core.AppError
-import uniffi.idkit_core.ConnectUrlMode
-// TODO: Re-enable when World ID 4.0 is live
-// import uniffi.idkit_core.CredentialType
-import uniffi.idkit_core.DocumentType
-import uniffi.idkit_core.Environment
-import uniffi.idkit_core.IdentityAttribute
-import uniffi.idkit_core.Preset
-import uniffi.idkit_core.ResponseItem
-import uniffi.idkit_core.RpContext
-import uniffi.idkit_core.StatusWrapper
-import uniffi.idkit_core.VerificationLevel
-import uniffi.idkit_core.ConstraintKindWrapper
-import uniffi.idkit_core.ConstraintNode
-import uniffi.idkit_core.CredentialRequest
-import uniffi.idkit_core.CredentialType
-
-class IDKitTests {
- private fun sampleResult(
- sessionId: String? = null,
- userPresenceCompleted: Boolean = false,
- ): IDKitResult =
- IDKitResult(
- protocolVersion = "4.0",
- nonce = "0x1234",
- action = if (sessionId == null) "login" else null,
- actionDescription = "Sample action",
- sessionId = sessionId,
- responses = emptyList(),
- userPresenceCompleted = userPresenceCompleted,
- environment = "production",
- identityAttested = null,
- integrityBundle = null,
- )
-
- private fun sampleRpContext(): RpContext {
- val signature = "0x" + "00".repeat(64) + "1b"
- return RpContext(
- rpId = "rp_1234567890abcdef",
- nonce = "0x0000000000000000000000000000000000000000000000000000000000000001",
- createdAt = 1_700_000_000u,
- expiresAt = 1_700_003_600u,
- signature = signature,
- )
- }
-
- @Test
- fun `IDKit entrypoints expose canonical builders`() {
- val requestConfig = IDKitRequestConfig(
- appId = "app_staging_1234567890abcdef",
- action = "login",
- rpContext = sampleRpContext(),
- actionDescription = null,
- bridgeUrl = null,
- allowLegacyProofs = false,
- requireUserPresence = false,
- overrideConnectBaseUrl = null,
- returnTo = null,
- environment = Environment.STAGING,
- connectUrlMode = ConnectUrlMode.DEFAULT,
- )
-
- // TODO: Re-enable when World ID 4.0 is live
- // val sessionConfig = IDKitSessionConfig(
- // appId = "app_staging_1234567890abcdef",
- // rpContext = sampleRpContext(),
- // actionDescription = null,
- // bridgeUrl = null,
- // requireUserPresence = false,
- // overrideConnectBaseUrl = null,
- // returnTo = null,
- // environment = Environment.STAGING,
- // )
-
- IDKit.request(requestConfig)
- // TODO: Re-enable when World ID 4.0 is live
- // IDKit.createSession(sessionConfig)
- // IDKit.proveSession("0x01", sessionConfig)
- }
-
- @Test
- fun `bridge request payload exposes identity check contract fields`() {
- val config = IDKitRequestConfig(
- appId = "app_staging_1234567890abcdef",
- action = "test-action",
- rpContext = sampleRpContext(),
- actionDescription = "Identity check",
- bridgeUrl = null,
- allowLegacyProofs = false,
- requireUserPresence = true,
- overrideConnectBaseUrl = null,
- returnTo = "idkitsample://callback",
- environment = Environment.STAGING,
- connectUrlMode = null,
- )
-
- val preset = identityCheck(
- attributes = listOf(
- IdentityAttribute.MinimumAge(21u),
- IdentityAttribute.Nationality("JPN"),
- ),
- )
-
- val payload = IDKit.createBridgePayloadFromPresets(config, preset)
-
- assertEquals("app_staging_1234567890abcdef", payload.appId)
- assertEquals("idkit_kotlin", payload.packageName)
- assertEquals(IDKit.version, payload.packageVersion)
- assertEquals("test-action", payload.action)
- assertEquals("Identity check", payload.actionDescription)
- assertEquals(VerificationLevel.DOCUMENT, payload.verificationLevel)
- assertEquals(true, payload.requireUserPresence)
- assertEquals(true, payload.allowLegacyProofs)
- assertEquals("idkitsample://callback", payload.returnToUrl)
- assertEquals(Environment.STAGING, payload.environment)
- assertNull(payload.timestamp)
-
- val attributes = payload.identityAttributes!!
- assertEquals(
- listOf(
- IdentityAttribute.MinimumAge(21u),
- IdentityAttribute.Nationality("JPN"),
- ),
- attributes,
- )
-
- val proofRequest = payload.proofRequest!!
- assertEquals(1u, proofRequest.version)
- assertEquals("uniqueness", proofRequest.proofType)
- assertEquals("rp_1234567890abcdef", proofRequest.rpId)
- assertEquals(1_700_000_000u, proofRequest.createdAt)
- assertEquals(1_700_003_600u, proofRequest.expiresAt)
- assertTrue(proofRequest.id.isNotEmpty())
-
- val constraints = proofRequest.constraints!!
- assertEquals(ConstraintKindWrapper.ANY, constraints.kind())
- assertEquals(2, constraints.children().size)
- assertEquals(ConstraintKindWrapper.TYPE, constraints.children()[0].kind())
- assertEquals("passport", constraints.children()[0].identifier())
- assertEquals(ConstraintKindWrapper.TYPE, constraints.children()[1].kind())
- assertEquals("mnc", constraints.children()[1].identifier())
-
- assertEquals(listOf("passport", "mnc"), proofRequest.credentialIdentifiers)
- }
-
- @Test
- fun `bridge request payload from constraints exposes passport or mnc`() {
- val config = IDKitRequestConfig(
- appId = "app_staging_1234567890abcdef",
- action = "test-action",
- rpContext = sampleRpContext(),
- actionDescription = "Identity check",
- bridgeUrl = null,
- allowLegacyProofs = false,
- requireUserPresence = false,
- overrideConnectBaseUrl = null,
- returnTo = null,
- environment = Environment.STAGING,
- connectUrlMode = null,
- )
-
- val constraints = ConstraintNode.any(
- nodes = listOf(
- ConstraintNode.item(
- request = CredentialRequest.withStringSignal(
- credentialType = CredentialType.PASSPORT,
- signal = null,
- ),
- ),
- ConstraintNode.item(
- request = CredentialRequest.withStringSignal(
- credentialType = CredentialType.MNC,
- signal = null,
- ),
- ),
- ),
- )
-
- val payload = IDKit.createBridgePayloadFromConstraints(config, constraints)
-
- assertNull(payload.identityAttributes)
-
- val proofRequest = payload.proofRequest!!
- val payloadConstraints = proofRequest.constraints!!
- assertEquals(ConstraintKindWrapper.ANY, payloadConstraints.kind())
- assertEquals(listOf("passport", "mnc"), proofRequest.credentialIdentifiers)
- }
-
- @Test
- fun `status mapping covers all canonical variants`() {
- val result = sampleResult()
-
- assertEquals(
- IDKitStatus.WaitingForConnection,
- IDKitRequest.mapStatus(StatusWrapper.WaitingForConnection),
- )
- assertEquals(
- IDKitStatus.AwaitingConfirmation,
- IDKitRequest.mapStatus(StatusWrapper.AwaitingConfirmation),
- )
- assertEquals(
- IDKitStatus.Confirmed(result),
- IDKitRequest.mapStatus(StatusWrapper.Confirmed(result)),
- )
- assertEquals(
- IDKitStatus.Failed(IDKitErrorCode.INVALID_NETWORK),
- IDKitRequest.mapStatus(StatusWrapper.Failed(AppError.INVALID_NETWORK)),
- )
- assertEquals(
- IDKitStatus.Failed(IDKitErrorCode.USER_PRESENCE_FAILED),
- IDKitRequest.mapStatus(StatusWrapper.Failed(AppError.USER_PRESENCE_FAILED)),
- )
- assertEquals(
- IDKitStatus.Failed(IDKitErrorCode.INVALID_RP_SIGNATURE),
- IDKitRequest.mapStatus(StatusWrapper.Failed(AppError.INVALID_RP_SIGNATURE)),
- )
- assertEquals(
- IDKitStatus.Failed(IDKitErrorCode.NULLIFIER_REPLAYED),
- IDKitRequest.mapStatus(StatusWrapper.Failed(AppError.NULLIFIER_REPLAYED)),
- )
- assertEquals(
- IDKitStatus.Failed(IDKitErrorCode.DUPLICATE_NONCE),
- IDKitRequest.mapStatus(StatusWrapper.Failed(AppError.DUPLICATE_NONCE)),
- )
- assertEquals(
- IDKitStatus.Failed(IDKitErrorCode.UNKNOWN_RP),
- IDKitRequest.mapStatus(StatusWrapper.Failed(AppError.UNKNOWN_RP)),
- )
- assertEquals(
- IDKitStatus.Failed(IDKitErrorCode.INACTIVE_RP),
- IDKitRequest.mapStatus(StatusWrapper.Failed(AppError.INACTIVE_RP)),
- )
- assertEquals(
- IDKitStatus.Failed(IDKitErrorCode.TIMESTAMP_TOO_OLD),
- IDKitRequest.mapStatus(StatusWrapper.Failed(AppError.TIMESTAMP_TOO_OLD)),
- )
- assertEquals(
- IDKitStatus.Failed(IDKitErrorCode.TIMESTAMP_TOO_FAR_IN_FUTURE),
- IDKitRequest.mapStatus(StatusWrapper.Failed(AppError.TIMESTAMP_TOO_FAR_IN_FUTURE)),
- )
- assertEquals(
- IDKitStatus.Failed(IDKitErrorCode.INVALID_TIMESTAMP),
- IDKitRequest.mapStatus(StatusWrapper.Failed(AppError.INVALID_TIMESTAMP)),
- )
- assertEquals(
- IDKitStatus.Failed(IDKitErrorCode.RP_SIGNATURE_EXPIRED),
- IDKitRequest.mapStatus(StatusWrapper.Failed(AppError.RP_SIGNATURE_EXPIRED)),
- )
- assertEquals(
- IDKitStatus.NetworkingError(IDKitErrorCode.CONNECTION_FAILED),
- IDKitRequest.mapStatus(StatusWrapper.NetworkingError(AppError.CONNECTION_FAILED)),
- )
- }
-
- @Test
- fun `pollUntilCompletion success path`() = runBlocking {
- val statuses = ArrayDeque(
- listOf(
- IDKitStatus.WaitingForConnection,
- IDKitStatus.AwaitingConfirmation,
- IDKitStatus.Confirmed(sampleResult()),
- ),
- )
-
- val request = IDKitRequest.forTesting(
- connectorURI = "https://world.org/verify?t=wld",
- requestId = "7a6ff287-c95f-4330-b3de-9447f77ca3f9",
- ) {
- statuses.removeFirstOrNull() ?: IDKitStatus.WaitingForConnection
- }
-
- val completion = request.pollUntilCompletion(IDKitPollOptions(pollIntervalMs = 1u, timeoutMs = 1_000u))
- assertEquals(IDKitCompletionResult.Success(sampleResult()), completion)
- }
-
- @Test
- fun `pollUntilCompletion timeout path`() = runBlocking {
- val request = IDKitRequest.forTesting(
- connectorURI = "https://world.org/verify?t=wld",
- requestId = "7a6ff287-c95f-4330-b3de-9447f77ca3f9",
- ) {
- IDKitStatus.WaitingForConnection
- }
-
- val completion = request.pollUntilCompletion(IDKitPollOptions(pollIntervalMs = 5u, timeoutMs = 20u))
- assertEquals(IDKitCompletionResult.Failure(IDKitErrorCode.TIMEOUT), completion)
- }
-
- @Test
- fun `pollUntilCompletion cancellation path`() = runBlocking {
- val request = IDKitRequest.forTesting(
- connectorURI = "https://world.org/verify?t=wld",
- requestId = "7a6ff287-c95f-4330-b3de-9447f77ca3f9",
- ) {
- throw CancellationException("test cancellation")
- }
-
- val completion = request.pollUntilCompletion(IDKitPollOptions(pollIntervalMs = 200u, timeoutMs = 10_000u))
- assertEquals(IDKitCompletionResult.Failure(IDKitErrorCode.CANCELLED), completion)
- }
-
- @Test
- fun `pollUntilCompletion recovers from networking errors`() = runBlocking {
- val statuses = ArrayDeque(
- listOf(
- IDKitStatus.WaitingForConnection,
- IDKitStatus.NetworkingError(IDKitErrorCode.CONNECTION_FAILED),
- IDKitStatus.NetworkingError(IDKitErrorCode.CONNECTION_FAILED),
- IDKitStatus.AwaitingConfirmation,
- IDKitStatus.Confirmed(sampleResult()),
- ),
- )
-
- val request = IDKitRequest.forTesting(
- connectorURI = "https://world.org/verify?t=wld",
- requestId = "7a6ff287-c95f-4330-b3de-9447f77ca3f9",
- ) {
- statuses.removeFirstOrNull() ?: IDKitStatus.WaitingForConnection
- }
-
- val completion = request.pollUntilCompletion(IDKitPollOptions(pollIntervalMs = 1u, timeoutMs = 1_000u))
- assertEquals(IDKitCompletionResult.Success(sampleResult()), completion)
- }
-
- @Test
- fun `pollUntilCompletion app failure path`() = runBlocking {
- val request = IDKitRequest.forTesting(
- connectorURI = "https://world.org/verify?t=wld",
- requestId = "7a6ff287-c95f-4330-b3de-9447f77ca3f9",
- ) {
- IDKitStatus.Failed(IDKitErrorCode.USER_REJECTED)
- }
-
- val completion = request.pollUntilCompletion(IDKitPollOptions(pollIntervalMs = 1u, timeoutMs = 1_000u))
- assertEquals(IDKitCompletionResult.Failure(IDKitErrorCode.USER_REJECTED), completion)
- }
-
- @Test
- fun `hashSignal string and bytes overloads are deterministic`() {
- val raw = "test-signal"
- val hashFromString = IDKit.hashSignal(raw)
- val hashFromBytes = IDKit.hashSignal(raw.toByteArray())
-
- assertEquals(hashFromString, hashFromBytes)
- assertTrue(hashFromString.startsWith("0x"))
- assertTrue(hashFromString.isNotEmpty())
- }
-
- // TODO: Re-enable when World ID 4.0 is live
- // @Test
- // fun `CredentialRequest signal-only options`() {
- // val request = CredentialRequest(
- // CredentialType.ORB,
- // options = CredentialRequestOptions(signal = "user-123"),
- // )
- //
- // assertEquals(CredentialType.ORB, request.credentialType())
- // assertEquals("user-123", request.getSignalBytes()!!.toString(Charsets.UTF_8))
- // assertEquals(null, request.genesisIssuedAtMin())
- // assertEquals(null, request.expiresAtMin())
- // }
-
- // @Test
- // fun `CredentialRequest genesis-only options`() {
- // val request = CredentialRequest(
- // CredentialType.ORB,
- // options = CredentialRequestOptions(genesisIssuedAtMin = 1_700_000_000u),
- // )
- //
- // assertEquals(1_700_000_000u, request.genesisIssuedAtMin())
- // assertEquals(null, request.expiresAtMin())
- // }
-
- // @Test
- // fun `CredentialRequest expiry-only options`() {
- // val request = CredentialRequest(
- // CredentialType.ORB,
- // options = CredentialRequestOptions(expiresAtMin = 1_800_000_000u),
- // )
- //
- // assertEquals(null, request.genesisIssuedAtMin())
- // assertEquals(1_800_000_000u, request.expiresAtMin())
- // }
-
- // @Test
- // fun `CredentialRequest combined options`() {
- // val request = CredentialRequest(
- // CredentialType.ORB,
- // options = CredentialRequestOptions(
- // signal = "user-123",
- // genesisIssuedAtMin = 1_700_000_000u,
- // expiresAtMin = 1_800_000_000u,
- // ),
- // )
- //
- // assertEquals(CredentialType.ORB, request.credentialType())
- // assertEquals("user-123", request.getSignalBytes()!!.toString(Charsets.UTF_8))
- // assertEquals(1_700_000_000u, request.genesisIssuedAtMin())
- // assertEquals(1_800_000_000u, request.expiresAtMin())
- // }
-
- @Test
- fun `legacy preset helpers remain available`() {
- val orb = orbLegacy(signal = "x")
- val secureDoc = secureDocumentLegacy(signal = "y")
- val doc = documentLegacy(signal = "z")
- val device = deviceLegacy(signal = "d")
- val face = selfieCheckLegacy(signal = "f")
-
- assertTrue(orb is Preset.OrbLegacy)
- assertTrue(secureDoc is Preset.SecureDocumentLegacy)
- assertTrue(doc is Preset.DocumentLegacy)
- assertTrue(device is Preset.DeviceLegacy)
- assertTrue(face is Preset.SelfieCheckLegacy)
- assertEquals("x", (orb).signal)
- assertEquals("y", (secureDoc).signal)
- assertEquals("z", (doc).signal)
- assertEquals("d", (device).signal)
- assertEquals("f", (face).signal)
- }
-
- @Test
- fun `identityCheck helper exposes canonical preset`() {
- val attributes = listOf(
- IdentityAttribute.MinimumAge(21u),
- IdentityAttribute.Nationality("JPN"),
- IdentityAttribute.DocumentType(DocumentType.PASSPORT),
- )
-
- val preset = identityCheck(attributes = attributes)
-
- assertTrue(preset is Preset.IdentityCheck)
- assertEquals(attributes, preset.attributes)
- assertNull(preset.legacySignal)
- }
-
- @Test
- fun `identityCheck helper preserves legacySignal`() {
- val attributes = listOf(IdentityAttribute.MinimumAge(18u))
-
- val preset = identityCheck(attributes = attributes, legacySignal = "my-signal")
-
- assertTrue(preset is Preset.IdentityCheck)
- assertEquals("my-signal", preset.legacySignal)
- }
-
- @Test
- fun `idkit result json helpers roundtrip`() {
- val input = sampleResult()
- val json = idkitResultToJson(input)
- val output = idkitResultFromJson(json)
-
- assertEquals(input, output)
- assertNotEquals("", json)
- }
-}
diff --git a/kotlin/build.gradle.kts b/kotlin/build.gradle.kts
index f769d3a7..94129360 100644
--- a/kotlin/build.gradle.kts
+++ b/kotlin/build.gradle.kts
@@ -1,4 +1,5 @@
plugins {
- id("com.android.library") version "8.7.3" apply false
- kotlin("android") version "1.9.24" apply false
+ id("org.jetbrains.kotlin.multiplatform") version "2.3.21" apply false
+ id("org.jetbrains.kotlin.plugin.serialization") version "2.3.21" apply false
+ id("com.android.library") version "8.11.2" apply false
}
diff --git a/kotlin/gradle.properties b/kotlin/gradle.properties
index e627dfe9..7f99d9d7 100644
--- a/kotlin/gradle.properties
+++ b/kotlin/gradle.properties
@@ -1 +1,11 @@
-version=4.0.5
+version=5.0.0
+
+org.gradle.jvmargs=-Xmx4g -Dfile.encoding=UTF-8
+android.useAndroidX=true
+kotlin.code.style=official
+# Apple targets can only build on macOS hosts; elsewhere they are disabled so
+# Android-only work (CI on Linux, plain Android consumers) still builds.
+kotlin.native.ignoreDisabledTargets=true
+# iosMain is shared across iosArm64/iosSimulatorArm64/iosX64 and uses the
+# idkit_kmp cinterop; commonization makes the cinterop API visible there.
+kotlin.mpp.enableCInteropCommonization=true
diff --git a/kotlin/gradle/wrapper/gradle-wrapper.jar b/kotlin/gradle/wrapper/gradle-wrapper.jar
new file mode 100644
index 0000000000000000000000000000000000000000..980502d167d3610f88fa03b2f717935189d9fbcf
GIT binary patch
literal 43739
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