Skip to content

Add Chatwoot KMP SDK#3

Open
rohittp0 wants to merge 10 commits into
chatwoot:masterfrom
Lascade-Co:master
Open

Add Chatwoot KMP SDK#3
rohittp0 wants to merge 10 commits into
chatwoot:masterfrom
Lascade-Co:master

Conversation

@rohittp0

@rohittp0 rohittp0 commented Jun 12, 2026

Copy link
Copy Markdown

Summary

This PR adds a Kotlin Multiplatform (Android + iOS) chat SDK for Chatwoot, published as com.chatwoot.android:sdk. The public surface is intentionally tiny — ChatPage(...) (Compose Multiplatform), Chatwoot.configure(), and the ChatPageViewController() wrapper for Swift consumers — with everything else internal.

CI is already wired up:

  • build.yml runs ./gradlew build (all targets + tests) on macOS.
  • publish.yml publishes to Maven Central only when the version = "…" line changes in sdk/build.gradle.kts on master, then builds the XCFramework, attaches it to a sdk-vX.Y.Z GitHub release, and rewrites Package.swift for SPM consumers.

Action needed from maintainers

Publishing depends on a few things that only the Chatwoot org can set up. Whenever you have a moment, could you please:

  1. Verify the com.chatwoot namespace on the Maven Central Portal (Sonatype). The artifacts are published under com.chatwoot.android, so the namespace has to be claimed/verified by an org-owned account. Docs: Register a namespace.

  2. Add the following repository secrets (Settings → Secrets and variables → Actions). These map directly to the env vars consumed by publish.yml via the vanniktech maven-publish plugin:

    Secret Purpose
    MAVEN_CENTRAL_USERNAME Central Portal user token username
    MAVEN_CENTRAL_PASSWORD Central Portal user token password
    SIGNING_KEY ASCII-armored GPG private key (in-memory)
    SIGNING_KEY_ID GPG key ID
    SIGNING_KEY_PASSWORD GPG key passphrase

    GPG signing is required by Maven Central — see Sonatype's GPG guide for generating and distributing the key.

Until these are in place the publish job will fail as expected; everything else (build + tests) runs green. Happy to jump on a call or adjust anything if a different setup works better for you. Thanks for taking a look!

rohittp0 and others added 7 commits June 12, 2026 14:00
… iOS

Kotlin Multiplatform module (com.chatwoot.android:sdk) exposing
ChatPage(show, onFinish, styleConfig) backed by the Chatwoot website-widget
API: HTML bootstrap session parsing, widget REST endpoints, and live
messages over the ActionCable websocket with presence keepalive and
backoff reconnect. Includes Android and iOS sample apps, XCFramework +
SPM distribution, version-bump-triggered publish CI, and the verified
protocol contract in CONTEXT.md (the upstream wiki is stale).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Send and receive image, video, audio and generic file attachments in the
Compose ChatPage.

Send: multipart POST to /api/v1/widget/messages with message[attachments][]
(verified against the Chatwoot widget source) as a caption-less message;
optimistic local bubble with upload progress, reconciled with the server
message (websocket echo deduped by id). First-message-as-attachment uses a
multipart POST /conversations + refetch.

Receive: MessageDto gains an attachments array; both REST history and the
message.created/updated websocket events flow through unchanged. Attachment-only
messages (blank content) are no longer dropped by toChatMessage.

Render: images inline (Coil 3), video plays on tap, audio as a voice-note row
(play/pause + seek + duration), files open externally. Video/audio playback and
url opening are expect/actual — Media3 on Android, AVPlayer on iOS. The file
picker uses FileKit. Voice-note recording is deferred to Phase 2.

Docs: CONTEXT.md records the attachment wire contract; ADR 0003 the media stack.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Host apps can now identify the contact instead of leaving it anonymous.
New imperative API on the Chatwoot singleton:

- setUser(identifier, name, email, phoneNumber, avatarUrl, customAttributes,
  identifierHash) — flushed to the server right after bootstrap and on later
  changes, routing to PATCH /widget/contact/set_user when an identifier is
  present and PATCH /widget/contact otherwise.
- setCustomAttributes(map) — merge inbox-defined attributes.
- reset() — clear identity + persisted session on logout.

Identity validation stays server-side: identifierHash is host-supplied
(HMAC-SHA256 of the identifier with the per-inbox secret); the SDK never
holds the secret. A changed identifier clears the stored session so the next
bootstrap creates a fresh contact, preventing cross-user conversation leakage.

TokenStore now persists the active identifier alongside the cw_conversation
token and can clear a session. Adds ContactRequest DTO and WidgetApi
setUser/updateContact calls.

Docs: CONTEXT.md glossary + endpoints, ADR 0004, README, sample app.
Tests: ContactRequest serialization, set_user vs contact routing (MockEngine),
TokenStore round-trip/clear (MapSettings).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Public surface now includes Chatwoot.setUser/setCustomAttributes/reset; note
the identity flush flow and server-side HMAC invariant, the active-identifier
in TokenStore, and refresh the ADR list (now four).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Record and send voice notes from the chat input bar, reusing the Phase 1
attachment pipeline end to end.

UX: when the text field is empty the send arrow becomes a 🎤 button (📎 file
picker stays). Tapping it turns the input row into an inline recording bar
(live timer • cancel ✕ • send ➤); send stops the recorder and uploads, cancel
discards. The clip is wrapped as a PickedFile(audio/mp4) and routed through the
existing ChatViewModel.sendAttachment path, so it shows the optimistic bubble
and renders via the existing AudioAttachment player.

Recording is native expect/actual: MediaRecorder (AAC/m4a) on Android,
AVAudioRecorder on iOS. Mic permission is requested at first tap; on denial the
mic button is silently hidden (no error). RECORD_AUDIO is declared in the SDK
manifest (auto-merged); iOS hosts must add NSMicrophoneUsageDescription —
documented in the README. ADR 0003 updated.

Also pins Coil to 3.4.0 and FileKit to 0.13.0: their latest releases ship
Kotlin/Native klibs built with the 2.4.0 compiler, whose ABI the project's
pinned Kotlin 2.3.21 cannot consume (iOS link fails otherwise). This bundles the
in-flight Kotlin 2.3.21 pin (Hilt metadata compatibility) that the dep
downgrade depends on.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@muhsin-k

Copy link
Copy Markdown
Member

@rohittp0 I checked the endpoints against Chatwoot’s current widget API.

Most of the endpoint choices look correct and match what the web widget uses:

One issue: attachment-first conversations should not use POST /api/v1/widget/conversations. A ttachment uploads should always go through: POST /api/v1/widget/messages.

For identity, /contact and /contact/set_user are the right endpoints. Please also make sure set_user parses widget_auth_token from the response and updates the active/persisted X-Auth-Token when present, since the server can return a new token when the identified contact changes.

rohittp0 and others added 3 commits June 16, 2026 14:18
- Attachments: upload the picked file's real platform MIME (FileKit
  mimeType()) instead of guessing from a frequently-empty extension, so
  Chatwoot classifies images/videos correctly (they previously arrived as
  generic files in both the app and the Chatwoot web UI). Also parse the
  widget API's integer `file_type` enum (image=0, audio=1, video=2, file=3, …)
  via a tolerant serializer so received/echoed attachments render inline.
- Layout: stop double-counting the IME inset (safeDrawing already maxes the
  keyboard) which pushed the whole screen up while typing; the header now
  paints behind the status bar, removing the gap above it; cap the input
  field at 5 lines.
- Trim surrounding whitespace and trailing blank lines from message content
  (sent and received).
- Auto-scroll to the newest message on receive/reconcile, not just on count
  changes.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Attachment uploads now always go through POST /api/v1/widget/messages,
never POST /conversations — verified against app.chatwoot.com that the
endpoint lazily creates the conversation and returns a fully parseable
Message (int message_type + populated attachments). The attachment-first
branch and createConversationWithAttachment are gone; the optimistic
bubble reconciles directly from the response (no refetch).

set_user now parses widget_auth_token from the response and, when the
server mints a fresh session JWT (identifying merged/swapped the
contact), adopts it as the new active+persisted X-Auth-Token. session is
now the single source of truth for the token: flushIdentity and the
history refetch read it at call time rather than from a captured copy,
so later REST calls follow the contact the server resolved. The realtime
channel can't follow (no new pubsub_token) — documented.

Docs: refreshed CONTEXT.md REST table + attachment note, amended ADR 0004.
Tests: setUser token parse (present/absent) + repository attachment-first
and token-adoption coverage.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
build.yml has been red on master since the SDK landed: linking the
iOS-simulator test binary (linkDebugTestIosSimulatorArm64) fails with an
undefined _OBJC_CLASS_$_UIViewLayoutRegion and a missing auto-linked private
framework UIUtilities, coming from compose.ui:ui-uikit.

Root cause is the runner's Xcode, not our code. macos-latest still resolves to
macos-15 / Xcode 16.4, whose iOS SDK can't resolve the UIKit symbols Compose
Multiplatform 1.11 emits. The same task links cleanly on Xcode 26 locally. Pin
both CI workflows to macos-26 (default Xcode 26.2) to match.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants