Add Chatwoot KMP SDK#3
Open
rohittp0 wants to merge 10 commits into
Open
Conversation
… 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>
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 For identity, |
- 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>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
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 theChatPageViewController()wrapper for Swift consumers — with everything elseinternal.CI is already wired up:
build.ymlruns./gradlew build(all targets + tests) on macOS.publish.ymlpublishes to Maven Central only when theversion = "…"line changes insdk/build.gradle.ktsonmaster, then builds the XCFramework, attaches it to asdk-vX.Y.ZGitHub release, and rewritesPackage.swiftfor 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:
Verify the
com.chatwootnamespace on the Maven Central Portal (Sonatype). The artifacts are published undercom.chatwoot.android, so the namespace has to be claimed/verified by an org-owned account. Docs: Register a namespace.Add the following repository secrets (Settings → Secrets and variables → Actions). These map directly to the env vars consumed by
publish.ymlvia the vanniktech maven-publish plugin:MAVEN_CENTRAL_USERNAMEMAVEN_CENTRAL_PASSWORDSIGNING_KEYSIGNING_KEY_IDSIGNING_KEY_PASSWORDGPG 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!