Перейти к содержимому

Android

A Kotlin library with Jetpack Compose UI, and an Activity for apps without it — the same conversation and the same inbox, talking to the same public API the website widget does.

1. Install

minSdk 24. The library's manifest already declares the INTERNET permission, and the only image loading it does is its own — no image library is pulled in.

kotlin
// build.gradle.kts, with the repository you publish to in settings.gradle.kts
dependencies {
    implementation("ru.fanyagin.helpwing:helpwing-android:0.1.0")
}

2. Configure the project

Both values are public — the key is the same pk_… the website snippet carries, and shipping it inside an APK is expected. Copy yours from the Chat widget screen of your workspace. The API URL is whichever host serves /widget.js.

properties
// gradle.properties, or wherever your build keeps config. Both values are public.
helpwingApiUrl=https://api.helpwing.app
helpwingProjectKey=pk_your_project_key

Nothing needs adding to the allowed origins list: an app sends no origin at all. What makes a conversation trustworthy is identity verification, further down.

3. Initialise once

kotlin
// App.kt
class App : Application() {
    override fun onCreate() {
        super.onCreate()
        Helpwing.initialize(
            this,
            HelpwingSettings(apiUrl = BuildConfig.HELPWING_API_URL, projectKey = BuildConfig.HELPWING_PROJECT_KEY),
        )
    }
}

In your Application, so one client owns the conversation, the polling and the stored token for as long as the process runs. Then put the launcher and the sheet over your content:

kotlin
// MainActivity.kt
setContent {
    HelpwingProvider {
        Box(Modifier.fillMaxSize()) {
            AppNavigation()

            // The floating button, and the sheet it opens. Both optional.
            SupportLauncher()
            SupportSheet()
        }
    }
}

An app without Compose skips that and opens SupportActivity instead:

kotlin
// No Compose: a full-screen chat with a close button.
helpButton.setOnClickListener { Helpwing.open(this) }

4. Or draw it yourself

The launcher and the sheet are conveniences. An app with a Help row in its own settings wants neither — put SupportScreen or SupportChat in your navigation. Being in composition is what tells us the visitor is reading.

kotlin
// A Help row of your own, with the unread badge
@Composable
fun HelpRow(onClick: () -> Unit) {
    val support = rememberSupport()
    Row(onClick, badge = support.unreadCount)
}

// The chat as a destination in your own navigation
@Composable
fun SupportDestination(onBack: () -> Unit) {
    SupportScreen(onClose = onBack)   // or SupportChat() under a header of your own
}

Or skip the components entirely: everything is on rememberSupport(), and the transcript is a plain list. Drawing it some other way? Call PresenceEffect() in it.

kotlin
val support = rememberSupport()

// A message that failed carries the id it was sent with, and retrying is free —
// the server stores one message however many times it arrives.
support.messages
    .filter { it.delivery == Delivery.FAILED }
    .forEach { support.retry(it.clientMessageId!!) }

5. Say who the visitor is

kotlin
// Wherever your auth state changes. Identical identities are ignored.
Helpwing.identify(
    user?.let {
        Identity(
            id = it.id,
            email = it.email,
            name = it.name,
            // Computed by YOUR server. See below.
            userHash = it.supportHash,
            metadata = mapOf("plan" to it.plan),
        )
    },
)

Call it whenever your auth state changes; a call made before initialize is applied once it runs. It reaches a conversation that started before anyone signed in too — the ordinary case — and the conversation moves onto their customer record when they do.

The identity secret never goes in the app

userHash is an HMAC-SHA256 of the user id, keyed with your project's identity secret, and your server computes it. An APK is a zip file anybody can download and read, obfuscated or not. Fetch the hash from your own API alongside the rest of the signed-in user, the way you would a session token. Identifying users has the full recipe.

kotlin
// Signing out on a personal device: the next conversation is nobody's yet.
Helpwing.identify(null)

// Signing out on a shared device: forget this conversation entirely.
Helpwing.reset()

Signing straight in as somebody else needs neither. A conversation belongs to whoever opened it: when the SDK sees one user and then another, the first one's conversation is dropped from the device and the second opens their own.

What it does about a phone

  • It asks every five seconds rather than holding a request open. While the chat is closed it drops to every thirty, so the unread badge stays honest, and while the app is in the background it stops entirely.
  • A message sent twice is stored once. Every send carries an id, so the SDK can retry through a tunnel without your customers seeing double. Opening a conversation is the one exception — a start whose answer was lost is shown as a failed send, and retrying it is the visitor's decision.
  • Unsent messages survive process death. They are kept in SharedPreferences and go out on the next launch, in the order they were written. Closing the screen never cancels a send halfway.
  • A reply is not emailed to somebody who is reading it. While the chat is on screen the SDK says so, and the agent's reply is delivered to the app alone.

What it does not do yet

  • Push notifications. Nothing arrives while the app is closed. Ask for an email address — require_email on the widget settings screen — and a reply written while the visitor is away reaches them there.
  • Attachments from the visitor. No client can send one yet. Files an agent attaches are listed by name.
  • One conversation across two devices. The token belongs to one installation, so a reinstall starts a new conversation.

Checklist

  • Initialise in the Application. Not in an Activity, where a rotation would rebuild the client.
  • The hash comes from your server. Everything in the APK is public, BuildConfig included.
  • Ask for an email address. Until push exists, it is the only way to reach somebody who has closed the app.
NextTauri →A Rust plugin that owns the conversation, and web components for whatever your frontend is.