CapacitorWatchEnhanced - Bidirectional Messaging Guide
December 2, 2025 · View on GitHub
What's New in v1.0.0
This fork of @capacitor/watch adds full bidirectional messaging between your iPhone app and Apple Watch, enabling Watch → Phone communication that was missing in the original plugin.
Key Enhancements
✅ NEW Event Listeners - JavaScript can now receive messages from the watch
✅ Reply Handler Support - Interactive request/response patterns
✅ Standard WatchConnectivity Methods - sendMessage(), updateApplicationContext(), transferUserInfo()
✅ Connection Status - getInfo() to check watch pairing and reachability
✅ Backward Compatible - All existing @capacitor/watch code still works
Installation
cd /path/to/CapacitorWatchEnhanced/packages/capacitor-plugin
pnpm install
pnpm run build
Then in your Capacitor app:
npm install file:///path/to/CapacitorWatchEnhanced/packages/capacitor-plugin
npx cap sync ios
Breaking Changes from @capacitor/watch
None! This plugin is 100% backward compatible. Existing code using updateWatchUI(), updateWatchData(), and the runCommand listener will continue to work.
New API Reference
Outbound Methods (Phone → Watch)
sendMessage()
Send interactive message to watch (requires watch to be reachable).
import { Watch } from '@macsupport/capacitor-watch-enhanced';
try {
const result = await Watch.sendMessage({
message: {
type: 'getStatus',
detail: 'full'
}
});
console.log('Watch replied:', result.reply);
} catch (error) {
console.error('Watch not reachable:', error);
}
When to use: Interactive requests when watch is actively connected Requires: Watch must be reachable (isReachable = true) Behavior: Returns promise with watch's reply or error
updateApplicationContext()
Update application context (latest-value-only, background-capable).
await Watch.updateApplicationContext({
data: {
currentPage: 'dogs',
selectedDrugs: ['acepromazine', 'butorphanol'],
lastUpdated: Date.now()
}
});
When to use: Syncing current app state Requires: Nothing - always works Behavior: Only most recent value delivered, previous values overwritten
transferUserInfo()
Transfer user info to watch (background-capable queue).
const result = await Watch.transferUserInfo({
userInfo: {
calculationId: '123',
drugResults: [...],
timestamp: Date.now()
}
});
console.log('Transfer queued:', result.isTransferring);
When to use: Important data that must not be lost Requires: Nothing - always works Behavior: All transfers queued and delivered in order
getInfo()
Get watch connectivity status.
const info = await Watch.getInfo();
if (info.isSupported && info.isPaired && info.isWatchAppInstalled) {
console.log('✅ Watch app is installed');
if (info.isReachable) {
console.log('✅ Can send messages immediately');
} else {
console.log('⚠️ Watch not reachable - messages will be queued');
}
}
Returns:
isSupported- WatchConnectivity available on deviceisPaired- Apple Watch is paired with iPhoneisWatchAppInstalled- Your watch app is installedisReachable- Watch is actively connected (can use sendMessage)activationState- 0=notActivated, 1=inactive, 2=activated
Inbound Event Listeners (Watch → Phone)
messageReceived
Listen for simple messages from watch (no reply expected).
import { Watch } from '@macsupport/capacitor-watch-enhanced';
// Set up listener
const listener = await Watch.addListener('messageReceived', async (message) => {
console.log('📱 Received from watch:', message);
if (message.type === 'requestPresetList') {
await handlePresetListRequest();
}
if (message.type === 'loadPresetAndCalculate') {
const { pageName, weight, unit } = message.payload;
await handleLoadPreset(pageName, weight, unit);
}
});
// Later, remove listener
await listener.remove();
Triggered by: Watch calling sendMessage() or transferUserInfo()
Use case: One-way messages from watch
messageReceivedWithReply
Listen for messages from watch that expect a reply.
Watch.addListener('messageReceivedWithReply', async (message) => {
console.log('📱 Received message with reply handler:', message);
// Extract callback ID and process message
const { _replyCallbackId, type, payload } = message;
// Process request
const result = await processWatchRequest(type, payload);
// Send reply back to watch
await Watch.replyToMessage({
callbackId: _replyCallbackId,
reply: {
status: 'success',
data: result
}
});
});
Triggered by: Watch calling sendMessage(_:replyHandler:)
Use case: Interactive request/response patterns
Important: Must call replyToMessage() to send response
applicationContextReceived
Listen for application context updates from watch.
Watch.addListener('applicationContextReceived', (context) => {
console.log('📱 Received context from watch:', context);
// Handle watch state sync
});
userInfoReceived
Listen for user info transfers from watch.
Watch.addListener('userInfoReceived', (userInfo) => {
console.log('📱 Received user info from watch:', userInfo);
// Handle queued data from watch
});
reachabilityChanged
Listen for watch reachability changes.
Watch.addListener('reachabilityChanged', (data) => {
if (data.isReachable) {
console.log('✅ Watch is now reachable - can send messages');
} else {
console.log('⚠️ Watch not reachable - messages will be queued');
}
});
Reachable = Watch is actively connected, can use sendMessage()
Not Reachable = Messages will be queued for later delivery
activationStateChanged
Listen for WatchConnectivity session activation state changes.
Watch.addListener('activationStateChanged', (data) => {
console.log('Session state:', data.state);
// 0 = notActivated, 1 = inactive, 2 = activated
if (data.error) {
console.error('Activation error:', data.error);
}
});
VetDrugs Integration Example
Based on your use case, here's how to integrate with your existing watchHomeIntegration.js:
// src/js/watch/watchHomeIntegration.js
import { Watch } from '@macsupport/capacitor-watch-enhanced';
import { Capacitor } from '@capacitor/core';
import { getSavedPages } from '../drugServices.js';
export async function initializeWatchHomeListeners(context) {
if (!Capacitor.isNativePlatform()) {
console.log('⌚ Skipping watch - not on native platform');
return;
}
// Check if watch is available
const info = await Watch.getInfo();
if (!info.isSupported || !info.isPaired || !info.isWatchAppInstalled) {
console.log('⌚ Watch app not available');
return;
}
console.log('⌚ Initializing watch message listeners');
// Listen for preset list requests
Watch.addListener('messageReceived', async (message) => {
console.log('⌚ Message from watch:', message);
if (message.type === 'requestPresetList') {
await handleRequestPresetList();
}
if (message.type === 'loadPresetAndCalculate') {
await handleLoadPreset(message.payload, context);
}
if (message.type === 'updateWeightOnly') {
await handleWeightUpdate(message.payload, context);
}
});
// Listen for reachability changes
Watch.addListener('reachabilityChanged', (data) => {
console.log('⌚ Watch reachability:', data.isReachable);
});
console.log('✅ Watch listeners initialized');
}
async function handleRequestPresetList() {
console.log('⌚ Handling preset list request');
const savedPages = await getSavedPages();
const watchPageList = savedPages.map(page => ({
id: page.id,
name: page.name,
drugCount: page.drugs?.length || 0
}));
// Send to watch via application context
await Watch.updateApplicationContext({
data: {
type: 'presetPageList',
pages: watchPageList,
totalPages: watchPageList.length,
lastUpdated: Date.now()
}
});
console.log(`✅ Sent ${watchPageList.length} pages to watch`);
}
async function handleLoadPreset(payload, context) {
const { pageName, weight, unit } = payload;
console.log(`⌚ Loading "${pageName}" with ${weight}${unit}`);
// Load the page
await context.loadSelection(pageName);
await new Promise(resolve => setTimeout(resolve, 500));
// Set weight
context.setWeight(weight, unit);
await new Promise(resolve => setTimeout(resolve, 300));
// Get results
const selectedItems = context.getSelectedItems();
const calculatedDoses = selectedItems.map(item => ({
drugName: item.name,
dose: item.calculatedDose,
route: item.route,
frequency: item.frequency
}));
// Send results to watch
await Watch.updateApplicationContext({
data: {
type: 'calculationResults',
payload: {
pageName,
weight: `${weight} ${unit}`,
drugCount: calculatedDoses.length,
drugs: calculatedDoses,
timestamp: Date.now()
}
}
});
console.log(`✅ Sent ${calculatedDoses.length} drug calculations to watch`);
}
Then in your home.svelte:
import { onMount } from 'svelte';
import { initializeWatchHomeListeners } from '../js/watch/watchHomeIntegration.js';
onMount(() => {
// Initialize watch listeners with home page context
initializeWatchHomeListeners({
loadSelection: (name) => loadSelection(name),
setWeight: (weight, unit) => setWeight(weight, unit),
getSelectedItems: () => getSelectedItems()
});
});
Communication Patterns
Pattern 1: One-Way (Watch → Phone)
Watch sends data, phone processes it.
Watch Side:
WCSession.default.sendMessage(["type": "action"], replyHandler: nil)
Phone Side:
Watch.addListener('messageReceived', (message) => {
if (message.type === 'action') {
// Handle action
}
});
Pattern 2: Request/Response (Watch ↔ Phone)
Watch sends request, waits for phone's reply.
Watch Side:
WCSession.default.sendMessage(
["type": "getStatus"],
replyHandler: { reply in
print("Phone replied: \(reply)")
}
)
Phone Side:
Watch.addListener('messageReceivedWithReply', async (message) => {
const { _replyCallbackId, type } = message;
if (type === 'getStatus') {
const status = await getAppStatus();
await Watch.replyToMessage({
callbackId: _replyCallbackId,
reply: { status: status }
});
}
});
Pattern 3: Background Data Sync (Phone → Watch)
Phone sends data to watch even when watch is not actively running.
Phone Side:
// Latest-value-only (overwrites previous)
await Watch.updateApplicationContext({
data: { currentState: 'updated' }
});
// Or queued delivery (all transfers delivered)
await Watch.transferUserInfo({
userInfo: { importantData: [...] }
});
Watch Side:
// Receives via WCSessionDelegate
func session(_ session: WCSession, didReceiveApplicationContext: [String: Any]) {
// Handle context update
}
Debugging
Console Logs
The plugin adds detailed console logs with emoji prefixes:
📱- Phone-side logs⌚- Watch-related operations⚠️- Warnings or errors✅- Success messages
Check Watch Status
const info = await Watch.getInfo();
console.log('Watch Status:', {
supported: info.isSupported,
paired: info.isPaired,
installed: info.isWatchAppInstalled,
reachable: info.isReachable,
state: info.activationState
});
Common Issues
Issue: Messages not received on phone
Solution: Check that Watch.addListener() is called BEFORE watch sends messages
Issue: "Watch not reachable" error
Solution: Use updateApplicationContext() or transferUserInfo() instead of sendMessage()
Issue: Reply handler timeout on watch
Solution: Make sure phone calls replyToMessage() within watch's timeout (usually 10 seconds)
Migration from Custom Plugin
If you were using a custom VetDrugsWatchPlugin, replace:
// OLD (custom plugin)
import { VetDrugsWatch } from './VetDrugsWatchPlugin';
VetDrugsWatch.addListener('messageReceived', handler);
// NEW (enhanced plugin)
import { Watch } from '@macsupport/capacitor-watch-enhanced';
Watch.addListener('messageReceived', handler);
The message format and behavior are identical.
Technical Details
Architecture
-
WatchPlugin.swift - Main Capacitor plugin class
- Registers with Capacitor plugin system
- Exposes methods to JavaScript
- Manages reply handler callbacks
-
CapWatchDelegate.swift - WCSessionDelegate implementation
- Receives messages from WatchConnectivity
- Forwards events to JavaScript via
notifyListeners() - Maintains weak reference to plugin
-
definitions.ts - TypeScript interfaces
- Type-safe method signatures
- Event listener types
- JSDoc documentation
Thread Safety
- All WatchConnectivity delegate methods forward to JavaScript on main thread
- Reply handlers are stored in thread-safe dictionary
- Weak reference prevents retain cycles
Memory Management
- Plugin holds weak reference to delegate
- Reply handlers automatically removed after use
- Event listeners can be removed via
listener.remove()
Contributing
Found a bug or want to add features? Open an issue or PR at: https://github.com/macsupport/CapacitorWatchEnhanced
License
MIT (same as original @capacitor/watch)
Credits
Enhanced by VetCalculators Based on @capacitor/watch by Ionic Team