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:
| Concept | What it is |
|---|---|
apiKey | Your 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 queue | Submissions are queued in Capacitor Preferences and retried automatically on reconnect or app foreground — this happens for every call, with no extra code. |
| Theme | A single theme object, referenced by nearly every widget, applied as CSS custom properties. |
Getting Started
Requirements
| Capacitor | 5.0 or later (6.x recommended) |
| Ionic Framework | 7.x or 8.x (React, Angular, or Vue) |
| iOS | 14.0+ |
| Android | API 22+ (Android 5.1+) |
| Node.js | 18+ (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
npm install @swake/ionic
# or
yarn add @swake/ionic
# or
pnpm add @swake/ionic
Install the required Capacitor plugins (peer dependencies):
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:
npm install @capacitor/filesystem @capacitor/camera
| Plugin | Required | Purpose |
|---|---|---|
@capacitor/core | Yes | Capacitor runtime |
@capacitor/device | Yes | Native OS, model, and version info |
@capacitor/network | Yes | Connectivity change events for offline queue |
@capacitor/preferences | Yes | Persistent key-value storage for queue |
@capacitor/app | Yes | App version info and foreground/background events |
@capacitor/filesystem | Optional | Read native file URIs (for nativeFileToFile) |
@capacitor/camera | Optional | Take 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)
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)
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)
<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
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);
},
});
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.
| Adapter | Capacitor Plugin | What it does |
|---|---|---|
| Storage | @capacitor/preferences | Replaces 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/app | Reports accurate native OS name, OS version, device model, and app version instead of parsing the user agent string. |
| Queue Events | @capacitor/network + @capacitor/app | Flushes 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.
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',
},
});
| Parameter | Type | Description |
|---|---|---|
userId | string | Your app's unique user ID (required) |
email | string | User's email. Preserved if omitted on subsequent calls. |
name | string | User's display name. Preserved if omitted on subsequent calls. |
metadata | Record<string, string> | Custom key-value pairs. Fully replaced on each call — send the complete set. |
Clearing identity on logout
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.
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
});
| Option | Type | Default |
|---|---|---|
defaultType | 'bug' | 'feature' | 'question' | 'bug' |
showTypeSelector | boolean | true |
title | string | "What's on your mind?" |
placeholder | string | 'Tell us more...' |
theme | SwakeWebTheme | global theme |
Custom trigger with Ionic components
Hide the built-in FAB and trigger the form from an Ionic button:
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.
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 }],
});
| Parameter | Type | Description |
|---|---|---|
uri | string | Native file URI, blob URL, or HTTP URL |
fileName | string (optional) | Override the filename (defaults to last path segment) |
The helper handles three URI types:
| URI type | How 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.
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);
| Parameter | Type | Description |
|---|---|---|
type | 'bug' | 'feature' | 'question' | Submission category (required) |
title | string | Brief summary (required) |
description | string | Detailed description |
attachments | { file: File }[] | File objects — use nativeFileToFile() for native URIs |
customFields | Record<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.
// Open the feedback history panel
Swake.showFeedbackHistory();
Programmatic access
Fetch history data directly to build your own UI with Ionic components:
// 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.
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
// 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.
// 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
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.
// 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).
// 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.
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
| Token | Default | Description |
|---|---|---|
primaryColor | #6366F1 | Buttons, active states, FAB background |
successColor | #10B981 | Success checkmark, resolved badges |
dangerColor | #EF4444 | Error borders, destructive actions |
backgroundColor | #FFFFFF | Modal / panel background |
surfaceColor | #F9FAFB | Input fields, secondary surfaces |
overlayColor | rgba(0,0,0,0.45) | Backdrop behind modals |
textColor | #111827 | Primary text |
textSecondary | #6B7280 | Labels, placeholders, metadata |
borderColor | #E5E7EB | Input borders, dividers |
borderRadius | 8 | Corner radius for inputs and buttons |
fontFamily | System default | Font family applied to all text |
Per-call overrides
showFeedbackForm() accepts an optional theme that merges on top of the global theme.
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.
Breadcrumbs
Breadcrumbs capture a trail of user actions leading up to a feedback submission. They are stored in a ring buffer (default 50 entries, max 100) and automatically attached to every submission.
Auto-captured events
| Category | Config key | What it captures |
|---|---|---|
navigation | captureNavigation | pushState, popstate (SPA route changes) |
network | captureNetwork | fetch() and XMLHttpRequest calls |
console | captureConsole | console.warn and console.error |
click | captureClicks | Element click events with CSS selector |
Manual breadcrumbs
Swake.addBreadcrumb({
category: 'custom',
message: 'User completed onboarding',
data: { step: 'final', durationMs: 4500 },
});
// Read the current trail
const trail = Swake.getBreadcrumbs();
// Clear the buffer
Swake.clearBreadcrumbs();
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.
| Event | Source | Behaviour |
|---|---|---|
| Network restored | @capacitor/network | Queue flushed automatically |
| App foregrounded | @capacitor/app | Queue flushed automatically |
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.
- Sign in to the Swake portal and create a project (or open an existing one).
- In Project settings → API keys, get existing or create a key and copy it — it's shown once and starts with
ep_live_. - 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:
| Value | Where to find it | Example |
|---|---|---|
apiKey | Project settings → API keys | ep_live_… |
baseUrl | API endpoint — optional, only needed when self-hosting or routing through your own backend | https://api.swake.io |
boardSlug | Voting 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.
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.
| Method | Description |
|---|---|
| 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
| Export | Description |
|---|---|
offlineQueue | Direct access to the offline queue (size, flush). |
IonicInitOptions | Type alias for WebInitOptions. |