SDK Reference/Ionic / Capacitor
Framework
@swake/ionic

Ionic / Capacitor

Drop-in replacement for @swake/web in Ionic/Capacitor apps. Provides accurate native device info, persistent storage, and network-aware queue flushing.

Overview

@swake/ionic is a drop-in replacement for @swake/web in Capacitor projects. It wraps the full web SDK and adds native adapters for device info, persistent storage, and network events, plus one extra helper — nativeFileToFile() — for converting native camera/filesystem files into attachments.

A few concepts come up throughout this guide:

ConceptWhat it is
apiKeyYour project's key, the only required option to Swake.init().
identify()Links feedback, surveys, and notifications to a user. Almost everything downstream depends on it.
Offline queueSubmissions are queued in Capacitor Preferences and retried automatically on reconnect or app foreground — this happens for every call, with no extra code.
ThemeA single theme object, referenced by nearly every widget, applied as CSS custom properties.

Getting Started

Requirements

Capacitor5.0 or later (6.x recommended)
Ionic Framework7.x or 8.x (React, Angular, or Vue)
iOS14.0+
AndroidAPI 22+ (Android 5.1+)
Node.js18+ (for build tooling)

@swake/ionic is a drop-in replacement for @swake/web in Capacitor projects. It wraps the full web SDK and adds native adapters for device info, persistent storage, network events, and file handling.


Installation

bash
npm install @swake/ionic
# or
yarn add @swake/ionic
# or
pnpm add @swake/ionic

Install the required Capacitor plugins (peer dependencies):

bash
npm install @capacitor/core @capacitor/device @capacitor/network \
  @capacitor/preferences @capacitor/app

# Sync native projects
npx cap sync

Optional: native file attachments

If you plan to attach photos or files from the device, also install:

bash
npm install @capacitor/filesystem @capacitor/camera
PluginRequiredPurpose
@capacitor/coreYesCapacitor runtime
@capacitor/deviceYesNative OS, model, and version info
@capacitor/networkYesConnectivity change events for offline queue
@capacitor/preferencesYesPersistent key-value storage for queue
@capacitor/appYesApp version info and foreground/background events
@capacitor/filesystemOptionalRead native file URIs (for nativeFileToFile)
@capacitor/cameraOptionalTake photos or pick from gallery

Quick Start

Import Swake from @swake/ionic and call await Swake.init(). The UI widgets (trigger FAB, feedback form, survey modals) are included automatically — no extra import needed.

React (Ionic)

App.tsx
import { useEffect, useState } from 'react';
import { setupIonicReact, IonApp } from '@ionic/react';
import Swake from '@swake/ionic';

setupIonicReact();

function App() {
  const [ready, setReady] = useState(false);

  useEffect(() => {
    const setup = async () => {
      await Swake.init({
        apiKey: 'ep_live_your_api_key',
        appVersion: '1.0.0',
        trigger: { position: 'bottom-right' },
      });
      setReady(true);
    };
    void setup();

    return () => { Swake.destroy(); };
  }, []);

  if (!ready) return null;

  return <IonApp>{/* your routes */}</IonApp>;
}

Angular (Ionic)

app.component.ts
import { Component, OnInit, OnDestroy } from '@angular/core';
import Swake from '@swake/ionic';

@Component({ selector: 'app-root', templateUrl: 'app.component.html' })
export class AppComponent implements OnInit, OnDestroy {
  async ngOnInit() {
    await Swake.init({
      apiKey: 'ep_live_your_api_key',
      appVersion: '1.0.0',
      trigger: { position: 'bottom-right' },
    });
  }

  ngOnDestroy() {
    Swake.destroy();
  }
}

Vue (Ionic)

App.vue
<script setup lang="ts">
import { onMounted, onUnmounted } from 'vue';
import Swake from '@swake/ionic';

onMounted(async () => {
  await Swake.init({
    apiKey: 'ep_live_your_api_key',
    appVersion: '1.0.0',
    trigger: { position: 'bottom-right' },
  });
});

onUnmounted(() => {
  Swake.destroy();
});
</script>

Key difference from @swake/web: Swake.init() is async in the Ionic SDK. It must be awaited because it pre-loads Capacitor adapters (storage, device info, network listeners) before initialising the underlying web SDK.

Full init options

typescript
await Swake.init({
  apiKey: 'ep_live_your_api_key',        // required
  baseUrl: 'https://api.swake.io',       // optional — override for self-hosted or a proxy
  debug: false,                          // verbose console logs
  appVersion: '1.0.0',                   // shown per submission in portal
  autoCapture: {
    browserInfo: true,                   // attach device/browser info
  },
  surveys: {
    enabled: true,                       // fetch & show eligible surveys
    autoShow: true,                      // auto-show after identify()
    delay: 3000,                         // ms before auto-show
    cooldown: 86_400_000,                // ms between surveys (default: 24h)
  },
  breadcrumbs: {
    enabled: true,                       // attach breadcrumb trail
    maxEntries: 50,                      // ring buffer size (max 100)
    captureNavigation: true,             // route changes
    captureNetwork: true,                // fetch/XHR calls
    captureConsole: true,                // console.warn/error
    captureClicks: true,                 // element click events
  },
  trigger: {
    position: 'bottom-right',            // FAB position
    icon: 'chat',                        // 'feedback' | 'bug' | 'idea'
    label: 'Feedback',
    hidden: false,
    zIndex: 999999,
    style: {
      backgroundColor: '#059669',
      size: 56,
      borderRadius: 28,
      shadow: true,
    },
  },
  theme: { /* see Theme Customization section */ },
  onLimitReachedMode: 'default_ui',      // 'silent' | 'callback_only'
  onLimitReached: (event) => {
    console.warn(event.limitKey, event.message);
  },
});
Full reference:

Capacitor Adapters

When you call await Swake.init(), the Ionic SDK automatically configures three native adapters before the web SDK initialises. You don't need to set these up manually.

AdapterCapacitor PluginWhat it does
Storage@capacitor/preferencesReplaces localStorage with Capacitor Preferences. Pre-loads all SDK keys into an in-memory cache for synchronous reads, with async write-through to native storage.
Device Info@capacitor/device + @capacitor/appReports accurate native OS name, OS version, device model, and app version instead of parsing the user agent string.
Queue Events@capacitor/network + @capacitor/appFlushes the offline queue when network connectivity is restored or the app returns to the foreground (replaces browser online / visibilitychange events).

If any Capacitor plugin is missing or throws, the SDK gracefully falls back to the browser-based default (localStorage, UA parsing, online events).


Identifying Users

Call Swake.identify() after your user logs in. This links all feedback, survey responses, and notifications to that user.

typescript
import Swake from '@swake/ionic';

await Swake.identify({
  userId: 'user_123',         // required — your app's user ID
  email: 'jane@example.com',  // optional
  name: 'Jane Smith',         // optional
  metadata: {                 // optional — arbitrary key-value pairs
    plan: 'pro',
    company: 'Acme Corp',
  },
});
ParameterTypeDescription
userIdstringYour app's unique user ID (required)
emailstringUser's email. Preserved if omitted on subsequent calls.
namestringUser's display name. Preserved if omitted on subsequent calls.
metadataRecord<string, string>Custom key-value pairs. Fully replaced on each call — send the complete set.

Clearing identity on logout

typescript
Swake.clearIdentity();

Wipes the stored user ID and clears the offline queue. Always call this when the user logs out.


Feedback

Feedback Form

Open the built-in feedback form widget. The form includes a type selector (Bug / Idea / Help), title and description fields, and a drag-and-drop file zone. The UI is rendered in a Shadow DOM and won't conflict with Ionic styles.

typescript
import Swake from '@swake/ionic';

// Default form
Swake.showFeedbackForm();

// With options
Swake.showFeedbackForm({
  defaultType: 'feature',           // pre-select type
  showTypeSelector: true,           // show/hide type toggle (default: true)
  title: "What's on your mind?",    // header text
  placeholder: 'Tell us more...',   // textarea placeholder
  theme: { primaryColor: '#8B5CF6' }, // per-call theme override
});
OptionTypeDefault
defaultType'bug' | 'feature' | 'question''bug'
showTypeSelectorbooleantrue
titlestring"What's on your mind?"
placeholderstring'Tell us more...'
themeSwakeWebThemeglobal theme

Custom trigger with Ionic components

Hide the built-in FAB and trigger the form from an Ionic button:

typescript
import { IonButton, IonIcon } from '@ionic/react';
import { chatbubbleOutline } from 'ionicons/icons';
import Swake from '@swake/ionic';

function FeedbackButton() {
  return (
    <IonButton onClick={() => Swake.showFeedbackForm({ defaultType: 'bug' })}>
      <IonIcon slot="start" icon={chatbubbleOutline} />
      Report a Bug
    </IonButton>
  );
}

// In your init config, hide the trigger:
// trigger: { hidden: true }

Native File Attachments

Capacitor Camera and Filesystem plugins return native file URIs that can't be used directly as File objects. The nativeFileToFile() helper converts them.

typescript
import Swake, { nativeFileToFile } from '@swake/ionic';
import { Camera, CameraResultType } from '@capacitor/camera';

// Take a photo
const photo = await Camera.getPhoto({
  resultType: CameraResultType.Uri,
  quality: 80,
});

// Convert the native URI to a File object
const file = await nativeFileToFile(photo.webPath ?? photo.path!);

// Submit with the attachment
await Swake.submitFeedback({
  type: 'bug',
  title: 'Screenshot attached',
  attachments: [{ file }],
});
ParameterTypeDescription
uristringNative file URI, blob URL, or HTTP URL
fileNamestring (optional)Override the filename (defaults to last path segment)

The helper handles three URI types:

URI typeHow it's processed
blob: or http(s)://Fetched directly via fetch()
Native file URI (file:///)Read via @capacitor/filesystem, base64 decoded

@capacitor/filesystem must be installed for native file URIs. If it's missing, nativeFileToFile() throws a helpful error message with installation instructions.


Submitting Feedback

Submit feedback programmatically. Device info and breadcrumbs are automatically attached.

typescript
import Swake from '@swake/ionic';

const submission = await Swake.submitFeedback({
  type: 'bug',                                      // 'bug' | 'feature' | 'question'
  title: 'App crashes on photo upload',              // required
  description: 'Reproducible on iPhone 15 Pro.',     // optional
  attachments: [                                      // optional — File objects
    { file: screenshotFile },
  ],
});

console.log('Created:', submission.id);
ParameterTypeDescription
type'bug' | 'feature' | 'question'Submission category (required)
titlestringBrief summary (required)
descriptionstringDetailed description
attachments{ file: File }[]File objects — use nativeFileToFile() for native URIs
customFieldsRecord<string, string>Project custom field values as a { fieldId: value } map — fetch definitions with getCustomFields(); required fields enforced

Feedback History

Show a panel listing the current user's past submissions with status badges and a detail view with replies (including auto-translated team responses). Requires the user to be identified.

typescript
// Open the feedback history panel
Swake.showFeedbackHistory();

Programmatic access

Fetch history data directly to build your own UI with Ionic components:

typescript
// Lightweight list (id, title, status, type, createdAt)
const { submissions, hasMore, cursor } = await Swake.getHistory({ limit: 20 });

// Full submission detail with comments + attachments
const submission = await Swake.getSubmission('submission_id');

// Paginated submission list with more fields
const result = await Swake.listSubmissions({ limit: 10 });

Trigger Button (FAB)

A floating action button appears automatically after init. Configure it via the trigger option. The FAB is rendered in a Shadow DOM and won't conflict with Ionic's own FAB components.

typescript
await Swake.init({
  apiKey: 'ep_live_...',
  trigger: {
    position: 'bottom-right',     // 'bottom-left' | 'top-right' | 'top-left'
    icon: 'chat',                 // 'feedback' | 'bug' | 'idea'
    label: 'Feedback',
    hidden: false,
    zIndex: 999999,
    style: {
      backgroundColor: '#059669',
      size: 56,                   // diameter in px
      borderRadius: 28,
      shadow: true,
    },
  },
});

Show / hide at runtime

typescript
// Hide the FAB (e.g. during onboarding)
Swake.setTriggerVisible(false);

// Show it again
Swake.setTriggerVisible(true);

// Programmatic open / close
Swake.open();    // opens the feedback form
Swake.close();   // closes any open widget

Engagement

Surveys & NPS

Swake can show surveys, NPS polls, and multiple-choice polls to your users. Surveys are created in the Swake portal, and the SDK handles targeting, display, and response submission.

typescript
// Configure at init
await Swake.init({
  apiKey: 'ep_live_...',
  surveys: {
    enabled: true,        // default: true
    autoShow: true,       // auto-show after identify()
    delay: 3000,          // ms before auto-show
    cooldown: 86_400_000, // ms between surveys (default: 24h)
  },
});

// Manual control
await Swake.checkSurveys();              // re-check eligibility
await Swake.showSurvey('survey_abc123'); // show specific survey (ignores cooldown)

Event listeners

typescript
Swake.onSurveyShown((event) => {
  console.log('Survey shown:', event.id, event.type);
});

Swake.onSurveyCompleted((event) => {
  console.log('Completed:', event.survey.id, 'answers:', event.response.answers);
});

Swake.onSurveyDismissed((event) => {
  console.log('Dismissed:', event.id);
});

Voting Board

Let users browse and vote on public board items (feature requests, roadmap items). Fetching items does not require identification, but voting does.

typescript
// Fetch board items
const { items, hasMore } = await Swake.getBoardItems('public-roadmap', {
  sort: 'votes',      // 'votes' | 'recent'
  status: 'open',     // 'open' | 'in_progress' | 'resolved' | 'closed'
  page: 1,
});

// Cast a vote (requires identify)
const { status, voteCount } = await Swake.vote('public-roadmap', 'item_id');

// Remove a vote
const result = await Swake.removeVote('public-roadmap', 'item_id');

Notifications

Notifications

The SDK provides in-app notifications for status changes and team replies. The trigger FAB shows an unread badge automatically (polled every 60 seconds).

typescript
// Get unread count
const count = await Swake.getUnreadCount();

// List notifications
const { notifications, pagination } = await Swake.listNotifications({
  unreadOnly: true,
  limit: 20,
  cursor: undefined,
});

// Mark as read
await Swake.markNotificationRead('notification_id');

// Mark all as read
await Swake.markAllNotificationsRead();

Customization

Theme Customization

Pass a theme object in Swake.init() to brand the trigger button, feedback form, survey modal, and feedback history panel. Theme tokens are applied as CSS custom properties on the Shadow DOM root.

typescript
await Swake.init({
  apiKey: 'ep_live_your_api_key',
  theme: {
    primaryColor: '#059669',       // buttons, active states, FAB
    successColor: '#10B981',       // success states
    dangerColor: '#EF4444',        // error states
    backgroundColor: '#FFFFFF',    // panel backgrounds
    surfaceColor: '#F8FAFC',       // cards, input backgrounds
    overlayColor: 'rgba(15,23,42,0.4)', // backdrop
    textColor: '#0F172A',          // primary text
    textSecondary: '#64748B',      // muted text
    borderColor: '#E2E8F0',        // borders, dividers
    borderRadius: 10,              // px
    fontFamily: 'Inter, sans-serif',
  },
});

Available tokens

TokenDefaultDescription
primaryColor#6366F1Buttons, active states, FAB background
successColor#10B981Success checkmark, resolved badges
dangerColor#EF4444Error borders, destructive actions
backgroundColor#FFFFFFModal / panel background
surfaceColor#F9FAFBInput fields, secondary surfaces
overlayColorrgba(0,0,0,0.45)Backdrop behind modals
textColor#111827Primary text
textSecondary#6B7280Labels, placeholders, metadata
borderColor#E5E7EBInput borders, dividers
borderRadius8Corner radius for inputs and buttons
fontFamilySystem defaultFont family applied to all text

Per-call overrides

showFeedbackForm() accepts an optional theme that merges on top of the global theme.

typescript
Swake.showFeedbackForm({
  theme: { primaryColor: '#0EA5E9' },
});

Shadow DOM isolation: Swake widgets are scoped via CSS custom properties (--swake-primary, --swake-bg, etc.) and never leak into your Ionic styles or vice versa.



Offline

Offline Queue

When the device is offline, submissions are queued in Capacitor Preferences and automatically retried when connectivity returns or the app comes back to the foreground. This is a significant improvement over the web SDK's localStorage-based queue.

EventSourceBehaviour
Network restored@capacitor/networkQueue flushed automatically
App foregrounded@capacitor/appQueue flushed automatically
typescript
import { offlineQueue } from '@swake/ionic';

// Check queue size (e.g. show a badge)
const pending = offlineQueue.size();

// Force a flush attempt
await offlineQueue.flush();

Swake.clearIdentity() clears the offline queue. Always call it on logout to prevent queued items from being sent after a different user logs in.


AI Setup

Using Claude Code

Prefer to let an AI coding agent do the wiring? Run Claude Code from the root of your Ionic + Capacitor project and paste the prompt below. It reads this page as the source of truth, detects your stack, asks you for your API key and anything else it needs, then wires up @swake/ionic, init(), identify(), the widgets, and theming — in one shot.

Prerequisites

Before you run the prompt, set up your project in the Swake portal and grab the values the agent will ask for — it only writes placeholders to your env files, so you supply the real values yourself.

  1. Sign in to the Swake portal and create a project (or open an existing one).
  2. In Project settings → API keys, get existing or create a key and copy it — it's shown once and starts with ep_live_.
  3. If you want the voting board, create a board and copy its slug from the board's URL / settings.

Have these ready when the agent prompts you — the same values you'd otherwise wire up by hand:

ValueWhere to find itExample
apiKeyProject settings → API keysep_live_…
baseUrlAPI endpoint — optional, only needed when self-hosting or routing through your own backendhttps://api.swake.io
boardSlugVoting board settings — optional; passed to getBoardItems() / vote(), not init()public-roadmap

The prompt is additive and safe: a Swake or network error must never break your app's boot or existing behavior. The agent pauses and asks before installing packages or writing secrets, and it never guesses APIs that aren't in these docs. It also asks whether to keep your ep_live_ key server-side behind your own backend — the recommended setup for any app that has one, since the key never reaches the browser and the submitting user can't be spoofed.

The prompt

Run Claude Code from your project root and paste this. It targets Ionic / Capacitor specifically, and re-reads this guide before it writes anything, so it stays correct as the SDK evolves.

Claude prompt
Fully integrate the Swake feedback SDK (feedback forms, bug reports, surveys/NPS, in-app notifications, voting boards) into this Ionic + Capacitor app. Treat it as additive: a Swake or network failure must NEVER break boot or any existing behavior. Never invent or fake an API that is not in the docs.

1. READ THE DOCS FIRST — they are the source of truth for the package, the Capacitor peers, init/identify and native setup. Fetch with a browser User-Agent (a plain fetch returns 403): curl -sL -A "Mozilla/5.0" "https://swake.io/docs/sdk?framework=ionic". Then detect the Capacitor major version and which UI framework Ionic is running (React, Angular, or Vue) and say which you found; if there is no Capacitor here, STOP and use the matching web guide instead.
2. ASK BEFORE DECIDING ANYTHING (AskUserQuestion): (a) the API key (ep_live_...), an optional baseUrl, and a voting-board slug if they want the board — written as PLACEHOLDERS ONLY into env files and the .example alongside, never a real key; (b) confirmation to install the required Capacitor peers @capacitor/core, @capacitor/app, @capacitor/device, @capacitor/network and @capacitor/preferences, plus @capacitor/filesystem only if they want native file attachments and @capacitor/camera only if they want in-app photo capture. Install nothing that was not approved, then run npx cap sync.
3. INSTALL — add @swake/ionic, NOT @swake/web. It wraps the full web SDK and adds native adapters for storage, device info and network events. The widget layer is already imported for you, so do NOT add an '@swake/web/ui' import — doing so is redundant.
4. INIT EXACTLY ONCE AND AWAIT IT — Swake.init() is ASYNC in this package (unlike @swake/web) because it preloads the Capacitor adapters before the underlying web SDK starts; a missing await means later calls run against an uninitialised SDK. Keep a module-level "let initPromise: Promise<void> | null" and have every caller await the SAME promise: the SDK exposes no isInitialized(), every method throws "[Swake] SDK not initialized" before init(), and a second init() starts a second offline-queue flush interval without clearing the first. Call it from AppComponent.ngOnInit (Angular), a root useEffect (React), or onMounted in App.vue (Vue), and Swake.destroy() on teardown. Set trigger: { hidden: true } if the app will open feedback from its own Ionic button.
5. IDENTITY — after init resolves, call Swake.identify({ userId, email, name, metadata }) on EVERY launch that has a session, not just on the login event: identity is held in memory only, so an app restart loses it. userId must be your stable internal id, and every metadata value must be a string. On logout call Swake.clearIdentity() inside a try/catch — it throws if init never ran.
6. NATIVE & POLISH — for attachments use nativeFileToFile(uri) from @swake/ionic to turn a Camera or Filesystem URI into a File, then pass it as attachments: [{ file }]. Add the iOS Info.plist usage strings for camera and photo library only for the plugins you actually installed. Drive Swake.setTheme() from the app's light/dark toggle, and gate init/identify by route so an auto-shown survey never lands on onboarding or auth pages.
7. VERIFY & REPORT — funnel every SDK call through one wrapper with try/catch. Then verify on a real iOS AND Android build, not just the browser: a real submission with an attachment appears in the Swake dashboard, queued feedback flushes after the device goes offline and back online, and no survey shows during onboarding. Note that every Capacitor adapter fails soft to its browser default, so a missing plugin degrades quietly rather than throwing — check the dashboard shows real native device info, which is the visible proof the adapters are actually wired. Report files changed, manual steps left, and assumptions made.

Reference

Full API Reference

@swake/ionic exposes the same API as @swake/web with one change: init() is async. It also adds the nativeFileToFile() helper. Click any method for its full signature, parameters, and an example.

MethodDescription
Initialise the SDK with Capacitor adapters. Must be awaited.
Link the current user. Returns a Promise.
Clear user identity and offline queue.
Replace the global UI theme at runtime.
Submit feedback programmatically (with optional customFields). Returns the created Submission.
Fetch the project's custom field definitions for a custom feedback UI.
Open the built-in feedback form widget.
Open the feedback history panel.
Open the feedback form (alias).
Close any open widget.
Show or hide the trigger FAB.
List the current user's submissions.
Get full submission detail with comments + attachments.
Edit a submission you own.
Unpublish a submission you own (one-way).
Lightweight submission history.
Get unread notification count.
List notifications with pagination.
Mark a single notification as read.
Mark all notifications as read.
Re-check survey eligibility and show the first match.
Show a specific survey (ignores cooldown).
Register survey-shown event listener.
Register survey-completed event listener.
Register survey-dismissed event listener.
Fetch voting board items.
Cast a vote on a board item.
Remove a vote from a board item.
Add a custom breadcrumb to the trail.
Return a snapshot of the breadcrumb trail.
Clear the breadcrumb ring buffer.
Register plan-limit event listener.
Tear down all listeners, timers, and UI.
Convert a native file URI to a Web File object.

Ionic-specific exports

ExportDescription
offlineQueueDirect access to the offline queue (size, flush).
IonicInitOptionsType alias for WebInitOptions.