Encatch
Welcome to Encatch Docs
Mobile & Native SDKs

Compose Multiplatform SDK

Complete integration guide for the Encatch Compose Multiplatform SDK — in-app feedback and survey collection for Compose Multiplatform apps

The Encatch Compose Multiplatform SDK (com.encatch:compose-sdk) lets you collect in-app feedback and surveys from one shared commonMain Compose UI targeting Android and iOS. It depends on the Kotlin Multiplatform SDK (com.encatch:kmp-sdk) for the entire Encatch business-logic API — init, identify, track, show/dismiss forms, submit, sessions, events — and adds the one thing a pure KMP consumer wouldn't need: EncatchInlineForm, a composable that wraps the platform-native inline form view (AndroidView/UIKitView interop — no WebView reimplementation, no third-party dependency).

Under the hood, Encatch calls bridge to the two native Encatch SDKs (the Android SDK on Android, the pure-Swift iOS SDK on iOS) — a thin routing layer, not a third implementation.

No install step

The modal form host installs itself: on iOS when you call Encatch.init(...), on Android the first time EncatchInlineForm composes (via Compose's LocalContext). There is no Application.onCreate setup and no EncatchFormHost.install() call to make.


Overview

  • Package: com.encatch:compose-sdk (Maven Central)
  • Version: 0.1.0
  • Platforms: Android (minSdk 24), iOS (iosArm64, iosSimulatorArm64)
  • Repository: github.com/get-encatch/encatch-android
  • License: MIT

Installation

Add the dependency to your shared module's commonMain source set:

// build.gradle.kts (shared module)
kotlin {
    sourceSets {
        commonMain.dependencies {
            implementation("com.encatch:compose-sdk:0.1.0")
        }
    }
}

This transitively brings in com.encatch:kmp-sdk's Encatch API — no separate dependency needed. A Compose Multiplatform customer adds only compose-sdk.

Modal-only Android apps

On Android the modal form host installs lazily the first time EncatchInlineForm composes. If your app only uses modal forms and never composes EncatchInlineForm, install the host eagerly in Application.onCreate instead: com.encatch.android.EncatchFormHost.install(this) (see the KMP SDK setup notes). On iOS the host always installs inside Encatch.init(...), so nothing is needed either way.


Quick Start

1. Initialization

Call Encatch.init once at app startup — a LaunchedEffect at your root composable is a natural place. It's a suspend function; subsequent calls (identifyUser, showForm, tracking) silently no-op until initialization completes.

import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import com.encatch.sdk.Encatch

@Composable
fun App() {
    LaunchedEffect(Unit) {
        if (!Encatch.isInitialized) {
            Encatch.init("your-api-key")
        }
    }

    // ... your app UI ...
}

Pass an optional EncatchConfig to customize SDK behavior:

import com.encatch.sdk.Encatch
import com.encatch.sdk.EncatchConfig
import com.encatch.sdk.Theme

LaunchedEffect(Unit) {
    Encatch.init(
        "your-api-key",
        EncatchConfig(
            theme = Theme.SYSTEM,
            debugMode = true,
            isFullScreen = false,
            appVersion = "1.2.3",
            onBeforeShowForm = { payload ->
                // Return false to prevent the form from showing
                true
            },
        ),
    )
}

Prop

Type

2. Identify users

Identify the current user. The userName is required (can be a username, email, or unique identifier). Traits and options are optional.

Username format

userName must be an ASCII identifier: 1–50 characters, using only letters A–Z / a–z, digits 0–9, and ., _, @, -. Spaces and non-English characters (Unicode, accented letters, emoji, etc.) are not supported. Use an email address, internal user ID, or ASCII username — for example user@example.com or user_123. To store a display name in another language, pass it as a trait instead (e.g. set = mapOf("display_name" to JsonPrimitive("…"))).

val scope = rememberCoroutineScope()

Button(onClick = {
    scope.launch { Encatch.identifyUser("user@example.com") }
}) { Text("Sign in") }

Trait values are kotlinx.serialization JsonElements — use JsonPrimitive for strings, numbers, and booleans:

import com.encatch.sdk.UserTraits
import kotlinx.serialization.json.JsonPrimitive

scope.launch {
    Encatch.identifyUser(
        "user@example.com",
        traits = UserTraits(
            set = mapOf(
                "name" to JsonPrimitive("Alice"),
                "plan" to JsonPrimitive("team"),
            ),
        ),
    )
}
import com.encatch.sdk.UserTraits
import kotlinx.serialization.json.JsonPrimitive

scope.launch {
    Encatch.identifyUser(
        "user@example.com",
        traits = UserTraits(
            set = mapOf("name" to JsonPrimitive("Alice"), "plan" to JsonPrimitive("team")),
            setOnce = mapOf("firstSeen" to JsonPrimitive("2026-08-05T12:00:00Z")),
            increment = mapOf("loginCount" to 1.0),
            decrement = mapOf("credits" to 5.0),
            unset = listOf("trialEndDate"),
        ),
    )
}

Prop

Type

User traits support the following operations:

OperationTypeDescription
setMap<String, JsonElement>?Set user attributes (overwrites existing values)
setOnceMap<String, JsonElement>?Set user attributes only if they don't already exist
incrementMap<String, Double>?Increment numeric user attributes
decrementMap<String, Double>?Decrement numeric user attributes
unsetList<String>?Remove user attributes

Recommended

Using the secure option with a server-generated signature is recommended to verify that identification requests come from your backend. Keep your secret key on the server only — never expose it in client-side code.

Pass a server-generated HMAC signature so Encatch can validate the request. generatedDateTimeInUtc must be milliseconds since the Unix epoch (the string form of your server's epoch-millis timestamp). When your publishable key has a session timeout, use the same value in HMAC-SHA256(userName + epochMs, secretKey). It is sent as the X-User-Signature-Time header and limits the signature's lifespan.

import com.encatch.sdk.IdentifyOptions
import com.encatch.sdk.SecureOptions

scope.launch {
    Encatch.identifyUser(
        "user@example.com",
        options = IdentifyOptions(
            secure = SecureOptions(
                signature = "your-hmac-signature",
                generatedDateTimeInUtc = "1741867200000", // ms since epoch (2025-03-13T12:00:00Z)
            ),
        ),
    )
}

3. Show a form manually

Show a specific form by slug or ID. If a matching EncatchInlineForm is composed, the form renders inline there; otherwise it presents as a modal overlay.

import androidx.compose.runtime.rememberCoroutineScope
import com.encatch.sdk.Encatch
import kotlinx.coroutines.launch

@Composable
fun FeedbackButton() {
    val scope = rememberCoroutineScope()
    Button(onClick = {
        scope.launch { Encatch.showForm("feedback-form") }
    }) { Text("Give feedback") }
}

Prop

Type

Prop

Type

ResetModeBehavior
ResetMode.ALWAYSReset pre-fill and response data on every form display
ResetMode.ON_COMPLETEReset only after the form is completed
ResetMode.NEVERNever reset response data

Pass caller context when showing a form. Context values use the ContextValue sealed class (StringValue, NumberValue, BooleanValue, DateValue):

import com.encatch.sdk.ContextValue
import com.encatch.sdk.ResetMode
import com.encatch.sdk.ShowFormOptions

scope.launch {
    Encatch.showForm(
        "feedback-form",
        ShowFormOptions(
            reset = ResetMode.ALWAYS,
            context = mapOf(
                "plan" to ContextValue.StringValue("team"),
                "feature" to ContextValue.StringValue("checkout"),
            ),
        ),
    )
}

Other actions


Inline Forms

Inline forms are a way to show Encatch in-app feedback without a modal — the survey renders directly in your layout instead of as a full-screen overlay.

Build Your Own Form UX & UI

If your feedback flow uses a fixed, predictable question set — the same fields and workflow every time — you can build the form with your own composables and submit responses through the SDK. That keeps typography, spacing, colors, and interaction patterns aligned with the rest of your app, so the survey feels like a native screen rather than an embedded web page.

The flow has three parts, all available from commonMain:

  1. Intercept the form with onBeforeShowForm and return false. The ShowFormInterceptorPayload includes formConfigJson — the JSON encoding of the full form configuration (including questionnaireFields), so you can render your own UI from the real form definition.
  2. Render your own Compose UI from the payload.
  3. Submit with buildSubmitRequest + Encatch.submitForm.
import com.encatch.sdk.BuildSubmitRequestOptions
import com.encatch.sdk.Encatch
import com.encatch.sdk.EncatchConfig
import com.encatch.sdk.NativeFormResponse
import com.encatch.sdk.buildSubmitRequest

// 1. Intercept: queue the blocked form as Compose state instead of letting the SDK render it
var blockedForm by mutableStateOf<BlockedForm?>(null)

suspend fun initSdk() {
    Encatch.init(
        "your-api-key",
        EncatchConfig(
            onBeforeShowForm = { payload ->
                if (payload.formId == "my-native-form") {
                    blockedForm = BlockedForm(payload.formId, payload.formConfigJson)
                    false // block the SDK form — we render our own composable
                } else {
                    true
                }
            },
        ),
    )
}

// 2. Render: show your own composable when blockedForm != null
// (parse formConfigJson to build the question list)

// 3. Submit: convert your composable's answers and post them to Encatch
suspend fun submitNativeForm(formConfigurationId: String) {
    val responses = listOf(
        NativeFormResponse("q1", "rating", 5),
        NativeFormResponse("q2", "short_answer", "Great product!"),
        NativeFormResponse("q3", "multiple_choice_multiple", listOf("option-a", "option-b")),
    )

    val requestJson = buildSubmitRequest(
        BuildSubmitRequestOptions(formConfigurationId = formConfigurationId),
        responses,
    )
    Encatch.submitForm(requestJson)
}

NativeFormResponse.value's expected shape depends on the question type: numeric scales (rating, nps, csat, opinion_scale) take a Number or numeric String; text types take String; choice and ranking types take String or List<String>; boolean types (yes_no, consent) take Boolean. All 33 Encatch question types are supported; unknown types fall back to short_answer for forward-compatibility.

Prop

Type


Support

Was this page helpful?