arnacon-controller
v1.6.1
Published
Controller SDK for building webapps that communicate with the Arnacon native app
Maintainers
Readme
arnacon-controller
TypeScript SDK for building web apps that run inside the Arnacon native app (iOS & Android). Communicates with the native layer through a WebView bridge, exposing a promise-based API for data fetching and an event emitter for real-time push notifications.
All native array data is automatically deserialized into typed objects -- you work with message.author and session.sessionName, never positional array indices.
Installation
npm install arnacon-controllerQuick Start
import { Controller } from 'arnacon-controller';
const controller = new Controller({
localId: 'user-wallet-address',
send: Controller.autoDetectBridge(),
});
// IMPORTANT: The native app calls window.top.controller.receiveData(jsonString)
// to push data into the SDK, so you must expose the instance globally.
window.top.controller = controller;Fetching data (promise-based)
const { sessions } = await controller.getRecentSessions(50);
sessions.forEach(s => {
console.log(s.sessionName, s.lastMessageContent, s.unreadCount);
});
const { messages } = await controller.getMessages(sessionId, 20, null, true);
messages.forEach(m => {
console.log(m.author, m.content, m.status);
});Sending commands (fire-and-forget)
controller.sendMessage(sessionId, 'Hello!');
controller.callSession(sessionId);Listening for push events
controller.on('new-message', ({ message }) => {
console.log(message.author, message.content);
});
controller.on('mute-state', ({ mute }) => {
toggleMuteUI(mute);
});Setup
Constructor
new Controller(options: ControllerOptions)| Option | Type | Required | Default | Description |
|--------|------|----------|---------|-------------|
| localId | string | Yes | -- | The local user/device identifier (e.g. wallet address) |
| send | SendFunction | Yes | -- | Transport function that delivers payloads to native |
| requestTimeout | number | No | 10000 | Timeout in ms for promise-based requests |
| onMessage | (data) => void | No | -- | Deprecated. Catch-all callback. Prefer controller.on() |
Bridge Detection
Controller.autoDetectBridge() returns a SendFunction by checking for:
window.webkit.messageHandlers.nativeHandler(iOS)window.AndroidBridge.processAction(Android)
Throws if neither is found. You can also provide a custom transport:
const send = (payload) => myWebSocket.send(JSON.stringify(payload));
const controller = new Controller({ localId: '...', send });URL Parameters
The Arnacon native app passes parameters to the webapp via a hash fragment, not query parameters:
https://example.com/app#screen=MAIN&localId=0x1234Parse them from location.hash:
const params = new URLSearchParams(location.hash.substring(1));
const localId = params.get('localId');
const screen = params.get('screen');Receiving Data from Native
The native app delivers data by calling:
window.top.controller.receiveData(jsonString);The controller routes each incoming message automatically:
- If the body contains a
requestIdmatching a pending request, the corresponding promise resolves. - Otherwise, the
actionfield is emitted as an event to listeners registered viacontroller.on().
Data Models
The SDK automatically deserializes native array data into typed objects.
Message
Returned by getMessages(), and emitted by new-message / message-updated events.
interface Message {
messageId: string;
time: number;
author: string;
content: string;
sessionId: number;
contact: string | null;
status: number; // 0=pending, 1=sent, 2=error, 4=delivered
fileId: unknown | null;
replyTo: {
messageId: string;
author: string;
content: string;
} | null;
isEdited: boolean;
forwardInfo: unknown | null;
}Session
Returned by getRecentSessions().
interface Session {
lastMessageId: string;
lastMessageTime: number;
lastMessageAuthor: string;
lastMessageContent: string;
sessionId: number;
sessionName: string;
unreadCount: number;
fileData: unknown | null;
isGroup: boolean;
}RecentCall
Returned by getRecentCalls().
interface RecentCall {
contact: string;
id: string;
timestamp: number;
status: string; // "incoming", "outgoing", "missed"
}CallLogEntry
Returned by getLogs().
interface CallLogEntry {
contact: string;
timestamp: number;
status: string;
}Data Fetching (Promise-based)
These methods send a request with a unique requestId and return a Promise that resolves when native echoes back the same requestId. Response data is automatically deserialized.
| Method | Returns | Description |
|--------|---------|-------------|
| getRecentSessions(range) | Promise<{ sessions: Session[] }> | Fetch recent chat sessions |
| getMessages(id, range, start, isSession) | Promise<{ messages: Message[] }> | Fetch messages for a session or remote user |
| getRecentCalls() | Promise<{ calls: RecentCall[] }> | Fetch recent call history |
| getLogs(remoteId?) | Promise<{ entries: CallLogEntry[] }> | Fetch call/message logs |
| getAudioDevices() | Promise<{ audioDevices }> | List available audio output devices |
| getSessionName(sessionId) | Promise<{ sessionName }> | Resolve display name for a session |
| getSessionId(remoteId) | Promise<{ sessionId }> | Resolve session ID for a remote user |
| contactPicker(type) | Promise<{ injectRemoteId }> | Open native contact picker |
getMessages parameters
| Parameter | Type | Description |
|-----------|------|-------------|
| id | string | Session ID or remote user ID |
| range | number | Number of messages to fetch |
| start | string \| number \| null | Message ID to start from. Pass null to fetch the latest messages. The SDK strips null values from the payload so native won't receive it. |
| isSession | boolean | true if id is a session ID, false if it's a remote user ID |
const { sessions } = await controller.getRecentSessions(50);
const { messages } = await controller.getMessages(
sessionId, // session or remote ID
20, // number of messages to fetch
null, // null = start from latest
true, // true = sessionId, false = remoteId
);Fire-and-Forget Methods
These methods send a command to native and return void. No response is expected.
Calls
| Method | Description |
|--------|-------------|
| callSession(sessionId) | Start audio call to a session |
| callRemote(remoteId) | Start audio call to a remote user |
| videoCallSession(sessionId) | Start video call to a session |
| videoCallRemote(remoteId) | Start video call to a remote user |
| acceptCall(callId) | Accept an incoming call |
| rejectCall(callId) | Reject an incoming call |
| setMute(callId, muted) | Mute/unmute microphone |
| setHold(callId, held) | Place call on hold or resume |
| switchCamera(callId) | Toggle front/back camera |
| sendDtmf(callId, digit) | Send a DTMF tone |
| transferCall(callId, remoteId) | Transfer call to another user |
| changeAudioDevice(callId, deviceId) | Switch audio output device |
Messaging
| Method | Description |
|--------|-------------|
| sendMessage(sessionId, message) | Send a text message |
| sendReplyMessage(id, message, replyToMessageId, isSession) | Reply to a message |
| sendEditMessage(messageId, newContent) | Edit a sent message |
| sendFileMessage(sessionId, fileId, caption) | Send a file/media message |
| forwardMessage(messageId, content, messageType) | Forward a message |
| deleteMessage(messageId) | Delete a message |
Sessions and Other
| Method | Description |
|--------|-------------|
| updateSessionTimestamp(sessionId) | Mark a session as read |
| deleteSession(sessionId) | Delete a session |
| createGroup(groupData) | Create a new group session |
| camera() | Open the native camera |
| downloadFile(messageId, fileId) | Download a file attachment |
Events
Listen for push events from native using on() and off(). These are unsolicited messages that the native app sends without the web having requested them.
Message events (new-message, message-updated) are automatically deserialized into Message objects.
controller.on('new-message', ({ message }) => {
console.log(message.author, message.content, message.status);
});
const handler = (data) => console.log(data);
controller.on('call-ended', handler);
controller.off('call-ended', handler);Available Events
| Event | Payload | Description |
|-------|---------|-------------|
| new-message | { message: Message } | A new message was sent or received |
| message-updated | { message: Message } | A message status changed (sent, delivered, read) |
| image-picker-result | { imagePickerResult, imageId? } | User finished picking an image |
| receiving-call | { from } | Incoming call |
| ringing | { to } | Outbound call is ringing |
| call-started | { from, videoCall? } | Call connected |
| call-ended | { from } | Call terminated |
| mute-state | { mute } | Mute state changed |
| local-hold-state | { localHold } | Local hold state changed |
| remote-hold-state | { remoteHold } | Remote hold state changed |
| active-calls | { activeCalls } | Active call list updated |
| speaker-state | { speaker } | Audio route changed |
| inject-remote-id | { injectRemoteId } | Contact selected from native picker |
| video-frame | { streamId, frame, width, height } | Video frame data for rendering |
| webrtc-disconnected | {} | WebRTC peer disconnected |
| error | { e } | Error from native layer |
| close-error | {} | Dismiss error UI |
Any action name the native sends that isn't matched by a pending requestId is automatically emitted as an event, so custom events work without any SDK changes.
Native Integration Guide
Outgoing Payloads (Web to Native)
Every call to the native bridge sends a JSON payload:
{
"action": "send-message",
"body": {
"localId": "0x...",
"sessionId": 1,
"content": "Hello"
}
}Promise-based methods include a requestId in the body:
{
"action": "get-recent-sessions",
"body": {
"localId": "0x...",
"range": 50,
"requestId": "m3k7f-2-a9x1"
}
}Note: null values are stripped from the body before sending. For example, getMessages(id, 20, null, true) will not include startMessage in the payload.
Responding to Requests (Native to Web)
For any request that contains a requestId, echo it back in the response body. The action can be anything (e.g. data-retrieved):
controller.receiveData(JSON.stringify({
action: "data-retrieved",
body: {
recentSessions: [...],
requestId: "m3k7f-2-a9x1"
}
}));Pushing Events (Native to Web)
For unsolicited events, use a specific action name and omit requestId:
controller.receiveData(JSON.stringify({
action: "new-message",
body: { message: ["id", 1234567890, "author", "Hello", 1, null, 0, null, null, 0, null] }
}));
controller.receiveData(JSON.stringify({
action: "message-updated",
body: { updatedMessage: ["id", 1234567890, "author", "Hello", 1, null, 4, null, null, 0, null] }
}));
controller.receiveData(JSON.stringify({
action: "receiving-call",
body: { from: "0x..." }
}));Summary of Native Action Names
Request/response (echo requestId):
get-recent-sessions, get-messages, get-recent-calls, get-logs, get-audio-devices, get-session-name, get-session-id, contact-picker
Push events (no requestId):
new-message, message-updated, image-picker-result, receiving-call, ringing, call-started, call-ended, mute-state, local-hold-state, remote-hold-state, active-calls, speaker-state, inject-remote-id, video-frame, webrtc-disconnected, error, close-error
Building from Source
cd arnacon-controller
npm install
npm run buildProduces:
dist/index.js-- CommonJSdist/index.mjs-- ES Moduledist/index.d.ts-- TypeScript declarations
License
MIT
