Your Capacitor app keeps its web screen while conversation transport and widget presentation remain native.
Install and sync the published Capacitor packages
For a new host, initialize Capacitor once, install matching Capacitor platform packages and install the DobroDesk wrapper and shared protocol package at the same release version. Add each native platform once, then synchronize the projects.
# Run once for a new host.
npx cap init "Your app" com.example.app --web-dir=www
npm install @capacitor/core@8.5.3 @capacitor/cli@8.5.3 @capacitor/ios@8.5.3 @capacitor/android@8.5.3 @dobrodesk/capacitor-sdk@0.2.0 @dobrodesk/mobile-protocol@0.2.0
npx cap add ios --packagemanager CocoaPods
npx cap add android
npx cap syncRegister the bundled Android repository
Add the Capacitor wrapper's local Maven path from the Android host settings. The wrapper applies org.jetbrains.kotlin.android without choosing a version, so declare the Kotlin and Android plugins before the generated include lines. The tested minimum Capacitor 6.2.1 host uses Android Gradle Plugin 8.13.2, Kotlin Gradle Plugin 2.2.0, Gradle 8.14.3 and JDK 17. The current Capacitor 8.5.3 generated host uses AGP 8.13.0, Kotlin 2.2.20 and JDK 21. Keep the versions generated by the installed Capacitor release. PREFER_SETTINGS keeps the official Capacitor Android module's project repository compatible while the DobroDesk artifacts resolve from the installed package.
pluginManagement {
repositories {
google()
mavenCentral()
gradlePluginPortal()
}
}
plugins {
id 'com.android.application' version '8.13.0' apply false
id 'com.android.library' version '8.13.0' apply false
id 'org.jetbrains.kotlin.android' version '2.2.20' apply false
}
import org.gradle.api.initialization.resolve.RepositoriesMode
dependencyResolutionManagement {
repositoriesMode.set(RepositoriesMode.PREFER_SETTINGS)
repositories {
google()
mavenCentral()
maven { url = uri("$rootDir/../node_modules/@dobrodesk/capacitor-sdk/native/maven") }
}
}
// Keep the generated include and apply-from lines below this block.Set the native platform requirements
The published wrapper requires iOS 15.1 or newer and Android API 26 or newer. Both tested Android profiles use compile SDK 36 and minSdk 26. The minimum Capacitor 6.2.1 profile uses JDK 17. For that profile, set distributionUrl in android/gradle/wrapper/gradle-wrapper.properties to https://services.gradle.org/distributions/gradle-8.14.3-all.zip. Current Capacitor 8.5.3 hosts use JDK 21 with the generated compatible wrapper and tuple. Set the iOS deployment target before pod install and raise the generated Android minSdkVersion from 24 to 26.
# ios/App/Podfile
platform :ios, "15.1"
# android/variables.gradle
minSdkVersion = 26
compileSdkVersion = 36
# android/gradle/wrapper/gradle-wrapper.properties for Capacitor 6.2.1
distributionUrl=https\://services.gradle.org/distributions/gradle-8.14.3-all.zipConfigure and present the native wrapper
The wrapper supports Capacitor 6 through 8. After package installation, native synchronization and repository setup, provide the credentialProvider from your host application.
The host web screen remains a normal Capacitor WebView, but the DobroDesk widget and transport are native. Do not load the web widget renderer inside the WebView as a substitute for this integration.
import { DobroDeskCapacitorClient } from "@dobrodesk/capacitor-sdk";
import type { DobroDeskSdkConfig } from "@dobrodesk/mobile-protocol";
const config: DobroDeskSdkConfig = {
apiOrigin: "https://dobrodesk.com",
integrationId: "wgt_0123456789abcdef0123456789abcdef",
accountScope: currentCustomer.id,
credentialProvider: async () => ({
authorizationToken: await loadDobroDeskToken(),
}),
};
const client = new DobroDeskCapacitorClient(config);
await client.presentWidget();Mount inline or use a custom trigger
mountWidget attaches the native widget to a connected HTMLElement in the Capacitor host. Give the container explicit dimensions before mounting so the WebView layout does not jump while the native surface authenticates and loads content.
A host button can call setVisible on the one returned handle. Keep the same client and handle across route visibility changes. Pass the typed onError callback when the host needs its own recovery notice. The helper removes its button listener and calls dispose during cleanup, releasing the embedded owner and native listeners without resetting the session or draft. Avoid clipped resize containers, scroll views and rotated or transformed host slots because the native surface needs a stable rectangular host.
mountWidget is included in the published Mobile SDK 0.2.0 package. Use it for a bounded native surface and call await handle.dispose() when the host removes the container.
import type {
DobroDeskCapacitorClient,
DobroDeskEmbeddedWidget,
} from "@dobrodesk/capacitor-sdk";
type SupportPanelOptions = Readonly<{
onError?: (error: unknown) => void;
}>;
export async function mountSupportPanel(
client: DobroDeskCapacitorClient,
{ onError }: SupportPanelOptions = {},
) {
const supportSlot = document.querySelector<HTMLElement>("#support-slot");
const helpButton = document.querySelector<HTMLButtonElement>("#help-button");
if (
!supportSlot ||
!helpButton ||
!supportSlot.isConnected ||
!helpButton.isConnected
) {
throw new Error("Connect #support-slot and #help-button before mounting");
}
Object.assign(supportSlot.style, { width: "100%", height: "560px" });
helpButton.setAttribute("aria-controls", "support-slot");
let visible = true;
let mountedHandle: DobroDeskEmbeddedWidget | undefined;
const toggle = () => {
visible = !visible;
mountedHandle?.setVisible(visible);
helpButton.setAttribute("aria-expanded", String(visible));
};
const onClose = () => {
visible = false;
helpButton.setAttribute("aria-expanded", "false");
};
helpButton.addEventListener("click", toggle);
try {
mountedHandle = await client.mountWidget(
supportSlot,
{ locale: "en", view: "home" },
{ onClose, onError },
);
mountedHandle.setVisible(true);
helpButton.setAttribute("aria-expanded", "true");
} catch (error) {
helpButton.removeEventListener("click", toggle);
onError?.(error);
throw error;
}
if (!mountedHandle) throw new Error("Support widget did not mount");
const handle = mountedHandle;
return {
handle,
dispose: async () => {
helpButton.removeEventListener("click", toggle);
await handle.dispose();
},
};
}Keep hybrid and native lifecycles separate
The host-provided credentialProvider must call a backend endpoint that returns a short-lived customer identity JWT. It must not expose a support API token or signing secret in the web bundle. Call closeWidget when the host route is destroyed, resetSession when the customer discards a conversation and logout before account changes.
The native presenter honors the operating system's reduced-motion setting when no override is supplied. Keep host page transitions independent so a WebView route change cannot create a second native widget.
Handle loading, detachment and accessibility
The native surface renders message loading, assistant waiting and retry states. Use onError for a recoverable host notice and keep the container mounted while an operation is pending. The host WebView should show only surrounding page state rather than a duplicate send spinner.
Call setVisible(false) when a route is temporarily hidden and setVisible(true) after the same container is connected again. Call dispose when the screen or account ends. The native widget follows system reduced-motion and animation scale preferences, while the host owns keyboard insets, focus order and accessible names.
- Create one handle per client and guard mountWidget while an embedded handle is active.
- Use setVisible(false) for temporary detachment and setVisible(true) after reattachment.
- Call dispose and logout only for a real screen or account end. Do not reset a session because an inline container was removed.
Verify the Capacitor flow
- Run npx cap doctor after npx cap sync, then open the widget from the web screen. Confirm that presentation is native and that no second web widget iframe appears.
- Exercise start, resume, send with a stable idempotency key, refresh and flushOutbox while switching network availability.
- Call logout before changing the signed customer and confirm that the next customer cannot inherit the previous native session.