npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@kailyai/react-native-sdk

v1.1.7

Published

React Native SDK for Kaily AI chatbot with dynamic tool registration and WebView-based integration

Readme

Kaily React Native SDK

npm version

A complete React Native SDK for Kaily AI chatbot integration with WebView-based AI functionality, dynamic tool registration, and comprehensive event handling.

Features

  • 🚀 Easy Integration - Simple initialization with minimal configuration
  • 🛠️ Dynamic Tool Registration - Register custom tools that the AI can execute
  • 📱 Cross-Platform - Works on both iOS and Android
  • 🎨 Customizable UI - Fully customizable appearance and themes
  • 🔊 Voice Support - Built-in voice input and text-to-speech capabilities
  • 📎 File Attachments - Native camera, gallery, and document picker with HEIC→JPEG transcode
  • 📡 Event System - Comprehensive event handling with EventEmitter
  • 🔐 User Management - Built-in user authentication and context management
  • 📦 TypeScript Support - Full TypeScript types for enhanced development

Installation

Current Version: 1.1.7

npm install @kailyai/react-native-sdk

or

yarn add @kailyai/react-native-sdk

Note: The SDK automatically installs its required dependencies (react-native-webview and eventemitter3).

File Attachments — Optional Peer Dependencies

If your copilot is configured to accept user attachments (surfaces.mobile.config.attachments enabled in the admin console), install these peer dependencies so the SDK can open the native camera / gallery / document picker and transcode iOS HEIC photos to JPEG:

npm install react-native-image-picker @react-native-documents/picker @bam.tech/react-native-image-resizer

If you skip this install and attachments are enabled, file pickers will fail at pick-time. The SDK does not enable attachments by itself — the operator controls the toggle server-side and the in-WebView widget hides its attachment button automatically when the toggle is off.

iOS Setup

  1. Install iOS native dependencies:

Since the SDK uses react-native-webview which requires native modules, you need to install iOS pods:

cd ios
pod install
cd ..
  1. Add permissions to your ios/YourApp/Info.plist:
<!-- Voice features -->
<key>NSMicrophoneUsageDescription</key>
<string>This app uses the microphone for voice interactions with Kaily AI assistant.</string>

<key>NSSpeechRecognitionUsageDescription</key>
<string>This app uses speech recognition for voice commands with Kaily AI assistant.</string>

<!-- File attachments (camera + photo library) -->
<key>NSCameraUsageDescription</key>
<string>Used to attach photos to your conversation.</string>

<key>NSPhotoLibraryUsageDescription</key>
<string>Used to attach images from your library to your conversation.</string>

⚠️ All four NSXxxUsageDescription keys above are mandatory once attachments or voice are enabled server-side. iOS crashes the app with NSInvalidArgumentException on the first call to the matching API — there is no fallback. The "Open Camera" button inside the widget routes through AVCaptureSession, so NSCameraUsageDescription is required even if your host code never touches a camera API directly.

Android Setup

Add the following permissions to your android/app/src/main/AndroidManifest.xml:

<!-- Networking + voice features -->
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.RECORD_AUDIO" />
<uses-permission android:name="android.permission.MODIFY_AUDIO_SETTINGS" />

<!-- File attachments (required if attachments are enabled in the admin console) -->
<uses-permission android:name="android.permission.CAMERA" />

<!-- Photo library access:
     Android 13+ (API 33+): granular media permissions.
     Android 14+ (API 34+): optional partial-access permission for "Allow only selected photos".
     Android <= 12 (API 32-): legacy READ_EXTERNAL_STORAGE, scoped via maxSdkVersion. -->
<uses-permission android:name="android.permission.READ_MEDIA_IMAGES" />
<uses-permission android:name="android.permission.READ_MEDIA_VIDEO" />
<uses-permission android:name="android.permission.READ_MEDIA_VISUAL_USER_SELECTED" />
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"
    android:maxSdkVersion="32" />

<!-- Mark camera as optional so the app stays installable on devices without one -->
<uses-feature android:name="android.hardware.camera" android:required="false" />

⚠️ Declare every permission you use. Skip the CAMERA line and PermissionsAndroid.request(CAMERA) returns "denied" silently — no system dialog appears — and the SDK emits attachment:error with reason: 'permission'. Looks like an SDK bug, but Android's rule is that an undeclared permission can never be granted at runtime. Same trap with RECORD_AUDIO for the in-widget microphone path.

That's it! The SDK automatically keeps the chat input above the keyboard on both iOS and Android — no additional configuration needed. (See Keyboard avoidance if you ever need to tweak it.)

Quick Start

import React, { useState, useEffect } from 'react';
import { View, TouchableOpacity, Text } from 'react-native';
import {
  KailySDK,
  KailyWidget,
  KailyTool,
  KailyConfig,
} from '@kailyai/react-native-sdk';

export default function App() {
  const [isInitialized, setIsInitialized] = useState(false);
  const [showChat, setShowChat] = useState(false);
  const sdk = KailySDK.getInstance();

  useEffect(() => {
    initializeSDK();
  }, []);

  const initializeSDK = async () => {
    try {
      const config: KailyConfig = {
        token: 'your-kaily-token-here',
        debugMode: true,
        appearance: {
          primaryColor: '#007AFF',
          title: 'AI Assistant',
        },
      };

      await sdk.initialize(config);
      await registerTools();
      setIsInitialized(true);
    } catch (error) {
      console.error('Failed to initialize:', error);
    }
  };

  const registerTools = async () => {
    const greetTool = new KailyTool({
      name: 'greet_user',
      description: 'Greet the user with a personalized message',
      parameters: [
        {
          name: 'name',
          type: 'string',
          description: 'User name',
          required: true,
        },
      ],
      handler: async (params) => {
        return {
          success: true,
          data: { message: `Hello, ${params.name}!` },
        };
      },
    });

    await sdk.registerTool(greetTool);
  };

  return (
    <View style={{ flex: 1 }}>
      <TouchableOpacity onPress={() => setShowChat(!showChat)}>
        <Text>Toggle Chat</Text>
      </TouchableOpacity>

      {showChat && isInitialized && (
        <KailyWidget
          config={sdk.currentConfig!}
          bridge={sdk.getBridge()}
          style={{ flex: 1 }}
        />
      )}
    </View>
  );
}

Core Concepts

SDK Initialization

The SDK follows a singleton pattern. Initialize once in your app:

const sdk = KailySDK.getInstance();

const config: KailyConfig = {
  token: 'your-token',
  debugMode: true,
  user: {
    id: 'user_123',
    name: 'John Doe',
    email: '[email protected]',
    token: 'optional-user-token',
  },
  context: {
    current_page: 'home',
    user_tier: 'premium',
  },
  appearance: {
    primaryColor: '#007AFF',
    backgroundColor: '#FFFFFF',
    title: 'My Assistant',
    showHeader: true,
  },
  voiceConfig: {
    enabled: true,
    ttsEnabled: true,
    language: 'en-US',
  },
};

await sdk.initialize(config);

Custom Script URL

By default the SDK loads the production Kaily script. To point to a different environment or a self-hosted script, pass kailyUrl in the config:

const config: KailyConfig = {
  token: 'your-token',
  kailyUrl: '',
};

If kailyUrl is not provided, the SDK uses the default production URL.

Tool Registration

Tools allow the AI to execute native functions:

const addToCartTool = new KailyTool({
  name: 'add_to_cart',
  description: 'Add a product to the shopping cart',
  parameters: [
    {
      name: 'product_id',
      type: 'string',
      required: true,
    },
    {
      name: 'quantity',
      type: 'number',
      defaultValue: 1,
    },
  ],
  handler: async (params) => {
    // Your logic here
    return {
      success: true,
      data: { cartCount: 5 },
    };
  },
});

// Register single tool
await sdk.registerTool(addToCartTool);

// Register multiple tools
await sdk.registerTools([tool1, tool2, tool3]);

// Unregister tool
await sdk.unregisterTool('add_to_cart');

Event Handling

Listen to events from the AI:

const eventStream = sdk.getEventStream();

eventStream.on('event', (event: KailyEvent) => {
  console.log('Event:', event.type, event.data);
});

// Listen to specific events
eventStream.on(KailyEventType.ConversationLoaded, (event) => {
  console.log('Chat loaded!');
});

eventStream.on(KailyEventType.UserMessage, (event) => {
  console.log('User said:', event.data?.message);
});

eventStream.on(KailyEventType.Error, (event) => {
  console.error('Error:', event.data?.message);
});

User Management

// Set user
await sdk.setUser({
  id: 'user_123',
  name: 'John Doe',
  email: '[email protected]',
  token: 'user-auth-token',
  attributes: {
    tier: 'premium',
  },
});

// Unset user
await sdk.unsetUser();

// Notify login success
await sdk.notifyLoginSuccess();

// Logout
await sdk.logout();

Context Management

Keep the AI informed about app state:

// Set context
await sdk.setContext({
  current_page: 'checkout',
  cart_count: 3,
  cart_total: 149.99,
  user_tier: 'premium',
});

// Update context when state changes
useEffect(() => {
  if (sdk.isInitialized()) {
    sdk.setContext({
      current_page: pageName,
      cart_count: cartItems.length,
    });
  }
}, [cartItems, pageName]);

// Unset context
await sdk.unsetContext();

Sending Messages

// Send a message programmatically
await sdk.sendMessage('What products do you recommend?');

// Send custom event
await sdk.sendGenericEvent('custom_event', {
  action: 'button_clicked',
  value: 'checkout',
});

UI Actions

Control the Kaily UI programmatically:

// Toggle the context menu
await sdk.toggleContextMenu();

## API Reference

### KailySDK

Main SDK class (singleton):

```typescript
// Get instance
const sdk = KailySDK.getInstance();

// Initialize
await sdk.initialize(config: KailyConfig): Promise<void>

// Check if initialized
sdk.isInitialized(): boolean

// Tool management
await sdk.registerTool(tool: KailyTool): Promise<void>
await sdk.registerTools(tools: KailyTool[]): Promise<void>
await sdk.unregisterTool(toolName: string): Promise<void>
sdk.getRegisteredTools(): KailyTool[]
await sdk.addFrontendTools(tools: KailyTool[]): Promise<void>
await sdk.removeFrontendTools(toolNames: string[]): Promise<void>
await sdk.removeAllTools(): Promise<void>

// User management
await sdk.setUser(user: KailyUser): Promise<void>
await sdk.unsetUser(): Promise<void>
await sdk.notifyLoginSuccess(): Promise<void>
await sdk.logout(): Promise<void>

// Context management
await sdk.setContext(context: Record<string, any>): Promise<void>
await sdk.unsetContext(): Promise<void>

// Messaging
await sdk.sendMessage(message: string): Promise<void>
await sdk.sendEvent(event: KailyEvent): Promise<void>
await sdk.sendGenericEvent(eventType: string, data: Record<string, any>): Promise<void>

// Event streams
sdk.getEventStream(): EventEmitter
sdk.getToolCallStream(): EventEmitter

// Getters
sdk.currentConfig: KailyConfig | null
sdk.currentUser: KailyUser | null
sdk.currentContext: Record<string, any> | null
sdk.hasUser: boolean

// Lifecycle
sdk.dispose(): void

// UI Actions
await sdk.toggleContextMenu(): Promise<void>

KailyWidget

React component for displaying the chat interface:

<KailyWidget
  config={config}
  bridge={sdk.getBridge()}
  onConversationLoaded={() => console.log('Loaded')}
  onConversationFailedToLoad={(error) => console.error(error)}
  onDeepLinkReceived={(url) => console.log(url)}
  onTelemetryEvent={(type, data) => console.log(type, data)}
  onClose={() => setShowChat(false)}
  onEvent={(event) => console.log(event)}
  style={{ flex: 1 }}
/>

Keyboard avoidance

You don't need to do anything. By default the SDK keeps the chat input above the keyboard on both iOS and Android. No AndroidManifest or KeyboardAvoidingView setup.

The optional keyboardAvoidance prop is only for fixing two specific problems:

1. There's an empty gap between the input and the keyboard. Your app is also moving the screen for the keyboard (a KeyboardAvoidingView wrapper, or windowSoftInputMode="adjustResize"), so it gets pushed up twice. Tell the SDK to stand down and let your app handle it:

<KailyWidget config={config} bridge={sdk.getBridge()} style={{ flex: 1 }}
  keyboardAvoidance={{ mode: 'none' }}
/>

2. The gap above the keyboard is too small / too big. Tune it. spacing adds space; offset removes it (useful if a tab bar already sits below the widget, or on Android 15 where the measured keyboard height runs large):

<KailyWidget config={config} bridge={sdk.getBridge()} style={{ flex: 1 }}
  keyboardAvoidance={{ spacing: 24, offset: 0 }}
/>

That's it. Full option reference:

| Field | Default | What it does | | --- | --- | --- | | mode | 'auto' | 'auto' = SDK handles the keyboard. 'none' = SDK does nothing, your app handles it. | | spacing | 16 | Pixels of breathing room added above the keyboard. | | offset | 0 | Pixels removed from the keyboard height (for a tab bar / safe-area below the widget). |

⚠️ Upgrading from an older version? The SDK now handles iOS too (it used to be Android-only). If your app already wraps <KailyWidget> in a KeyboardAvoidingView, you'll see the "empty gap" above — fix it with keyboardAvoidance={{ mode: 'none' }}.

Configuration Types

KailyConfig

interface KailyConfig {
  token: string; // Required
  kailyUrl?: string; // Custom script URL
  baseUrl?: string;
  widgetUrl?: string;
  user?: KailyUser;
  appearance?: KailyAppearance;
  voiceConfig?: KailyVoiceConfig;
  debugMode?: boolean;
  enableTelemetry?: boolean;
  context?: Record<string, any>;
  headers?: Record<string, string>;
  loadTimeoutMs?: number;
  enableJsDebugging?: boolean;
  userAgent?: string;
  allowExternalNavigation?: boolean;
  callbackUrls?: string[];
  initParams?: Record<string, any>;
}

KeyboardAvoidanceConfig

type KeyboardAvoidanceMode = 'auto' | 'none';

interface KeyboardAvoidanceConfig {
  mode?: KeyboardAvoidanceMode; // default 'auto'
  spacing?: number; // default 16
  offset?: number; // default 0
}

KailyUser

interface KailyUser {
  id: string; // Required
  name?: string;
  email?: string;
  phone?: string;
  avatarUrl?: string;
  attributes?: Record<string, any>;
  session?: string;
  token?: string;
}

KailyAppearance

interface KailyAppearance {
  primaryColor?: string;
  backgroundColor?: string;
  textColor?: string;
  secondaryTextColor?: string;
  userMessageColor?: string;
  botMessageColor?: string;
  borderRadius?: number;
  customCss?: string;
  fontFamily?: string;
  fontSize?: number;
  title?: string;
  subtitle?: string;
  logoUrl?: string;
  showHeader?: boolean;
  showTimestamps?: boolean;
  darkMode?: boolean;
  themeMode?: 'light' | 'dark' | 'auto';
}

Event Types

enum KailyEventType {
  ConversationLoaded = 'conversationLoaded',
  ConversationFailedToLoad = 'conversationFailedToLoad',
  DeepLinkReceived = 'deepLinkReceived',
  Telemetry = 'telemetry',
  Close = 'close',
  ToolCall = 'toolCall',
  ToolResult = 'toolResult',
  UserMessage = 'userMessage',
  BotMessage = 'botMessage',
  Error = 'error',
  AuthChanged = 'authChanged',
  ContextUpdated = 'contextUpdated',
  VoiceInputStarted = 'voiceInputStarted',
  VoiceInputStopped = 'voiceInputStopped',
  TtsStarted = 'ttsStarted',
  TtsCompleted = 'ttsCompleted',
  WidgetResized = 'widgetResized',
  Navigation = 'navigation',
  Custom = 'custom',
}

Error Handling

import {
  KailyException,
  KailyInitializationException,
  KailyConfigurationException,
  KailyToolException,
  KailyWebViewException,
} from '@kailyai/react-native-sdk';

try {
  await sdk.initialize(config);
} catch (error) {
  if (error instanceof KailyConfigurationException) {
    console.error('Configuration error:', error.field, error.message);
  } else if (error instanceof KailyInitializationException) {
    console.error('Initialization error:', error.message);
  } else {
    console.error('Unknown error:', error);
  }
}

Permissions

Request microphone permission for voice features:

import { PermissionHelper } from '@kailyai/react-native-sdk';

const granted = await PermissionHelper.requestMicrophonePermission();
if (!granted) {
  Alert.alert('Permission Required', 'Microphone permission is needed for voice features', [
    { text: 'Cancel' },
    { text: 'Open Settings', onPress: () => PermissionHelper.openAppSettings() },
  ]);
}

File Attachments

When surfaces.mobile.config.attachments is enabled for your copilot, the in-WebView widget renders an attachment button. The SDK transparently bridges those clicks into the host OS's native picker — there is no host-app code to write to wire it up. The contract:

| Concern | Owner | | --- | --- | | Tagging requests as the mobile surface | SDK (via WebView URL param + instanceType: 'mobile') | | Opening the camera / gallery / document picker | SDK (via react-native-image-picker + @react-native-documents/picker) | | HEIC → JPEG transcode (iOS camera default) | SDK (via @bam.tech/react-native-image-resizer) | | 10 MB / MIME / extension preflight | SDK + Widget (defense-in-depth) | | /v1/storage/init and PUT to GCS | In-WebView widget | | Sending copilot:query { files } over the socket | In-WebView widget | | Hiding the UI button when the toggle is off | In-WebView widget |

Allowed file types (server-side client-files namespace)

  • Images: JPEG, PNG, WebP (HEIC/HEIF auto-transcoded to JPEG on iOS)
  • Audio: MP3, WebM
  • Documents: PDF
  • Max size: 10 MB per file

Observing attachment outcomes

Subscribe to onEvent to react to picker outcomes:

<KailyWidget
  config={config}
  bridge={bridge}
  onEvent={(event) => {
    switch (event.type) {
      case 'attachment:picked':
        console.log('Files attached:', event.data?.names);
        break;
      case 'attachment:rejected':
        console.warn('Rejected:', event.data?.reason, event.data?.name);
        break;
      case 'attachment:error':
        console.warn('Picker error:', event.data?.reason);
        break;
    }
  }}
/>

reason values: mime, size, ext, permission, io, unknown. Cancellation is intentionally silent — the user dismissed the picker.

The attachment:picked, attachment:rejected, and attachment:error event payloads also include a target field — one of 'camera' | 'photo-library' | 'files' — so you can show a precise CTA when permission was denied:

onEvent={(event) => {
  if (event.type === 'attachment:error' && event.data?.reason === 'permission') {
    const target = event.data.target as 'camera' | 'photo-library' | 'files';
    Alert.alert(
      `Allow ${target === 'camera' ? 'camera' : 'photo'} access`,
      'Open Settings to grant permission.',
      [
        { text: 'Cancel', style: 'cancel' },
        { text: 'Open Settings', onPress: () => PermissionHelper.openAppSettings() },
      ]
    );
  }
}}

Permission handling — two paths

Tap "Upload Image", "Upload Files", or "Open Camera" inside the widget and one of two permission flows runs:

| User action | Permission surface | How it's granted | |---|---|---| | Upload Image / Upload Files | OS-level CAMERA / photo library via the native picker | SDK calls PermissionHelper.* before launching the picker (host can override via permissionGate) | | Open Camera (in-widget HTML camera via getUserMedia) | WebView page-level camera + OS-level CAMERA | Android: RNCWebChromeClient auto-grants the page when the host has the OS-level grant. iOS: WKWebView surfaces the system camera dialog (uses NSCameraUsageDescription). |

Default behavior (zero host code)

With the manifest / Info.plist entries above in place, the SDK:

  1. Calls PermissionHelper.requestCameraPermission() before launching the native camera. On grant → launchCamera. On deny → emits attachment:error{reason:'permission', target:'camera'}.
  2. Calls PermissionHelper.requestPhotoLibraryPermission() before launching the photo library. On Android 13+ this is a no-op — react-native-image-picker uses the permission-less system photo picker.
  3. For the in-HTML camera button (getUserMedia): the SDK injects a JS bridge that intercepts navigator.permissions.query({name:'camera'}) and routes it through PermissionHelper.requestCameraPermission() so the widget sees a real 'granted' / 'denied' verdict instead of the WebView's default 'prompt' dead end. The subsequent getUserMedia call is granted by the platform's WebView (see table above).

Optional: host-side override (permissionGate)

If you want a branded rationale screen, want to reuse a permission grant from elsewhere in your app, or have compliance constraints, pass a permissionGate prop:

import { KailyWidget, PermissionHelper, type AttachmentPermissionGate } from '@kailyai/react-native-sdk';

const gate: AttachmentPermissionGate = {
  // Override the camera path. Photo library still uses the SDK default.
  camera: async () => {
    const accepted = await showBrandedCameraRationale();
    if (!accepted) return false;
    return PermissionHelper.requestCameraPermission();
  },
};

<KailyWidget config={config} bridge={bridge} permissionGate={gate} />

Partial overrides are supported — provide camera, photoLibrary, both, or neither. Missing keys fall through to the SDK default.

Default frontend attachment tools (LLM-invokable)

When you mount KailyWidget, the SDK auto-registers three default frontend tools that the LLM can invoke from chat to trigger the matching native picker:

| Tool name | Effect | Parameters | | --- | --- | --- | | open_camera | Launch the device camera | none | | open_image_library | Open the photo library | multiple?: boolean (default true) | | open_file_picker | Open the system file picker | accept?: string (default '*/*'), multiple?: boolean (default true) |

Each tool runs the SDK's permission check (same code path as the input-shim picker — permissionGate overrides still apply), opens the matching native picker, and feeds picked files back through the existing upload pipeline (the WebView's __kaily_deliver_files callback). The tool result returned to the LLM is a lightweight ack — { success: true, data: { delivered: true, count: N } } on pick, { success: true, data: { cancelled: true } } on cancel, or { success: false, error: '<reason>' } on permission / IO / validation failure.

Override a default tool via the attachmentTools prop (replaces or merges with the SDK default for the given name):

import { KailyWidget, KailyTool } from '@kailyai/react-native-sdk';

<KailyWidget
  config={config}
  bridge={bridge}
  attachmentTools={{
    open_camera: {
      description: 'Custom description shown to the LLM',
      // handler is optional — omit to keep the SDK default behaviour
      handler: async () => {
        // your custom flow here
        return { success: true, data: { delivered: true, count: 0 } };
      },
    },
  }}
/>

If you register your own tool with the same name before mounting the widget, the SDK leaves it alone — the widget never overwrites a host-owned registration. The supported override surface is the attachmentTools prop above.

Permission responsibility for full overrides — if you replace the default handler, the SDK's permission gate is bypassed. Either call PermissionHelper.requestCameraPermission() (or the equivalent) yourself, or set permissionGate on the widget so the SDK still runs the check before any default tool that you haven't fully overridden.

Opt out of default tools entirely via the disableDefaultAttachmentTools prop. When true, the SDK skips auto-registering all three default tools. attachmentTools overrides are also ignored in this mode — the host is responsible for any tool registration via KailySDK.getInstance().registerTool(...).

<KailyWidget
  config={config}
  bridge={bridge}
  disableDefaultAttachmentTools
/>

Use this when you want a completely custom attachment tool surface, or when the defaults conflict with your UX. Defaults remain on whenever the prop is unset or false.

Example App

Example App

See the example repository for a comprehensive demo app that showcases:

  • SDK initialization with error handling
  • Shopping cart with multiple tools
  • Event logging
  • Context updates
  • User management
  • Full UI implementation

To run the example, please follow the instructions in the example repository.

Troubleshooting

WebView not loading

  • Ensure you have react-native-webview installed
  • Check your network connection
  • Verify your Kaily token is valid
  • Enable debugMode: true to see detailed logs

Tools not executing

  • Ensure SDK is initialized before registering tools
  • Check tool handler returns proper KailyToolResult format
  • Enable debug mode to see tool call logs
  • Verify tool names match between registration and AI calls

Events not firing

  • Ensure you subscribe to events after SDK initialization
  • Check you're listening to the correct event types
  • Verify WebView has loaded successfully

Keyboard Overlapping Chat Input

The SDK keeps the input above the keyboard on both iOS and Android by default (keyboardAvoidance.mode === 'auto') — no AndroidManifest configuration needed. If you still experience issues:

  1. Ensure the KailyWidget has a bounded flex: 1 parent — style={{ flex: 1 }} and not nested inside a ScrollView or fixed-height box (the frame must be able to shrink).
  2. Seeing a gap above the keyboard? Your app is double-compensating. Remove your own KeyboardAvoidingView / windowSoftInputMode=adjustResize, or set keyboardAvoidance={{ mode: 'none' }} and let your app own it.
  3. On Android 15 / targetSdkVersion 35 (edge-to-edge), the measured keyboard height can include navigation-bar insets — use keyboardAvoidance={{ offset }} to compensate.
  4. Tune the gap with keyboardAvoidance={{ spacing }} (default 16).

Deep Links Not Working on Android

The SDK automatically handles deep link interception on Android. No additional configuration needed. If links are still redirecting:

  1. Ensure you're using the latest version of the SDK
  2. Check that the onDeepLinkReceived callback is set on your KailyWidget
  3. Enable debug mode and check console logs for interception messages

TypeScript Support

This SDK is written in TypeScript and includes full type definitions. No additional @types packages needed.

License

MIT

Support

For issues, questions, or contributions, please visit our GitHub repository.