# rn-watch-connect rn-watch-connect is a library for communicating with your companion watchOS app from your Expo and React Native app. It's built for Expo, with the Expo Modules API, on top of Apple's [WatchConnectivity](https://developer.apple.com/documentation/watchconnectivity). # Installation (https://rn-watch-connect.vercel.app/docs/installation) ## Requirements - **Expo SDK 57 or later** (React Native 0.86+) - **iOS 16.4 or later** - **A companion watchOS app** to talk to The library is **iOS only**. On other platforms, its methods fail with `ERR_UNSUPPORTED_PLATFORM` and `watchActivationState` is `"unknown"`. > **On an older Expo SDK, or supporting iOS 15?** > > Stay on `rn-watch-connect@1.x`. Its docs are in the [1.2.0 README](https://github.com/CRIIPI11/rn-watch-connect/blob/v1.2.0/README.md). ## Installation ```bash npm install rn-watch-connect ``` The library has native code, so **rebuild the native app** after installing or upgrading it, for example with `npx expo run:ios` or a new development build. A JS reload isn't enough, and it doesn't run in Expo Go. ## Quick start Send a Message when the Watch is reachable, and listen for Messages from it: ```tsx import { useEventListener } from "expo"; import RnWatchConnect from "rn-watch-connect"; export function WatchPing() { useEventListener(RnWatchConnect, "onMessageReceived", (message) => { console.log("From the Watch:", message); }); async function ping() { if (!RnWatchConnect.isWatchReachable) return; const reply = await RnWatchConnect.sendMessage({ ping: Date.now() }); console.log("Reply:", reply); } return null; // Call ping() from a button. } ``` When something fails, the error has a stable `code`, such as `ERR_WATCH_NOT_REACHABLE`. Match on it, never on `message`. See [Error codes](https://rn-watch-connect.vercel.app/docs/errors.md). # AI agents (https://rn-watch-connect.vercel.app/docs/ai-agents) These docs are also published as plain Markdown, so a coding agent or an AI chat can read them in far fewer tokens than the web pages. Give your agent one of the files below, and it writes code against the real API instead of guessing. ## What's published | File | What it is | Size | | --- | --- | --- | | [`/llms.txt`](https://rn-watch-connect.vercel.app/llms.txt) | The index. The library's description, then one line per page: its Markdown link and the properties, methods and events on it. | Under 1,000 tokens | | [`/llms-full.txt`](https://rn-watch-connect.vercel.app/llms-full.txt) | Every page in one file, in sidebar order. | About 15,000 tokens | | `/docs/.md` | One page as Markdown. For example [`/docs/messages.md`](https://rn-watch-connect.vercel.app/docs/messages.md). | About 500–2,500 tokens each | All three are generated from these docs, so they always match the site. The code examples in them are type-checked against `rn-watch-connect` when the site is built. **Which one to use:** - **`llms.txt`** for agents that can fetch URLs (most coding agents). They read the index, then fetch only the pages they need. - **`llms-full.txt`** when the agent can't fetch more pages, or when you paste the docs into a chat once. - **A single `.md` page** when you're working on one feature, such as [File Transfer](https://rn-watch-connect.vercel.app/docs/file-transfer.md). ## Point your coding agent at the docs Add a short note to the instructions file your agent reads in your app's repo: `AGENTS.md`, `CLAUDE.md`, or your tool's rules file. ```md title="AGENTS.md" ## rn-watch-connect This app talks to its watchOS app with rn-watch-connect. Before writing or changing code that uses it, read https://rn-watch-connect.vercel.app/llms.txt and fetch the pages it links for the APIs you need. Match errors on `code`, never on `message`. ``` The agent now looks up the real signatures, events and Error codes, instead of relying on what it remembers. That matters most for 2.0, which changed APIs from 1.x. ## Use it in a chat To ask ChatGPT, Claude or another assistant about the library: - **Paste a link:** "Read https://rn-watch-connect.vercel.app/llms-full.txt, then help me send a Message to the Watch." - **Paste the page:** every docs page has a **Copy Markdown** button under its title. Paste the result into the chat. - **Open it directly:** the **Open** menu next to Copy Markdown opens the page in ChatGPT, Claude, Cursor or Scira AI, with the page's Markdown link already in the prompt. It also has **View as Markdown**. ## Tips - **Start from the index.** An agent that reads `llms.txt` first fetches only the pages it needs, which keeps its context small. - **Name the version.** These docs are for rn-watch-connect 2.x. If your app is still on 1.x, tell the agent, and point it at [Migrating from 1.x to 2.0](https://rn-watch-connect.vercel.app/docs/migration.md). - **Ask for error handling.** Every failure has a stable `code`. Ask the agent to handle the codes listed on each method's page. See [Error codes](https://rn-watch-connect.vercel.app/docs/errors.md). # Watch status (https://rn-watch-connect.vercel.app/docs/watch-status) These properties tell you what the Session can do right now. Each read returns the current value, and they never throw. | Property | Type | `true` when | | --- | --- | --- | | [`isWatchSupported`](#iswatchsupported) | `boolean` | This iPhone supports WatchConnectivity. | | [`isWatchPaired`](#iswatchpaired) | `boolean` | An Apple Watch is paired with this iPhone. | | [`isWatchAppInstalled`](#iswatchappinstalled) | `boolean` | Your Watch app is installed on the paired Watch. | | [`isWatchReachable`](#iswatchreachable) | `boolean` | Your Watch app can receive a Message right now. | | [`watchActivationState`](#watchactivationstate) | `WatchActivationState` | (A string, not a boolean.) Whether the Session is ready to carry data. | Three of them have an event that fires when the value changes: [`onReachabilityChanged`](#onreachabilitychanged), [`onWatchPairedChanged`](#onwatchpairedchanged) and [`onWatchAppInstallChanged`](#onwatchappinstallchanged). > **Events fire only on a real change** > > A new listener isn't called with the current value. Read the property for the starting value, then > listen for changes. With `useEvent`, pass the property as the initial value: > > ```tsx > import { useEvent } from "expo"; > import RnWatchConnect from "rn-watch-connect"; > > export function ReachabilityBadge() { > const { isWatchReachable } = useEvent(RnWatchConnect, "onReachabilityChanged", { > isWatchReachable: RnWatchConnect.isWatchReachable, > }); > return isWatchReachable ? "Watch reachable" : "Watch not reachable"; > } > ``` > ## `isWatchSupported` ```ts readonly isWatchSupported: boolean ``` `true` when this iPhone supports WatchConnectivity. It's `false` on an iPad, and on platforms other than iOS. Check it before showing any Watch features. **Example** ```ts import RnWatchConnect from "rn-watch-connect"; if (!RnWatchConnect.isWatchSupported) { // Hide your Watch features. } ``` ## `isWatchPaired` ```ts readonly isWatchPaired: boolean ``` `true` when an Apple Watch is paired with this iPhone. Listen to [`onWatchPairedChanged`](#onwatchpairedchanged) for changes. It's `false` on platforms other than iOS. ## `isWatchAppInstalled` ```ts readonly isWatchAppInstalled: boolean ``` `true` when your Watch app is installed on the paired Watch. Listen to [`onWatchAppInstallChanged`](#onwatchappinstallchanged) for changes. It's `false` on platforms other than iOS. ## `isWatchReachable` ```ts readonly isWatchReachable: boolean ``` `true` when your Watch app can receive a Message right now, because it's running and in range. Only [Messages](https://rn-watch-connect.vercel.app/docs/messages.md) and [Data Messages](https://rn-watch-connect.vercel.app/docs/data-messages.md) need it: sending one while it's `false` fails with `ERR_WATCH_NOT_REACHABLE`. [Application Context](https://rn-watch-connect.vercel.app/docs/application-context.md), [User Info](https://rn-watch-connect.vercel.app/docs/user-info.md) and [File Transfers](https://rn-watch-connect.vercel.app/docs/file-transfer.md) are delivered later instead. Listen to [`onReachabilityChanged`](#onreachabilitychanged) for changes. It's `false` on platforms other than iOS. **Example** ```ts import RnWatchConnect from "rn-watch-connect"; if (RnWatchConnect.isWatchReachable) { await RnWatchConnect.sendMessageWithoutReply({ refresh: true }); } else { RnWatchConnect.transferUserInfo({ refresh: true }); } ``` ## `watchActivationState` ```ts readonly watchActivationState: WatchActivationState // "notActivated" | "inactive" | "activated" | "unknown" ``` Whether the Session is ready to carry data. | Value | Meaning | | --- | --- | | `"activated"` | The Session is ready. | | `"inactive"` | The Session is finishing up, for example while the iPhone switches to another Watch. | | `"notActivated"` | The Session isn't activated yet. | | `"unknown"` | WatchConnectivity isn't supported: on an iPhone without it, and on platforms other than iOS. | The Session activates when the library loads. **There's no event for a failed activation**: if `watchActivationState` still isn't `"activated"` shortly after launch, activation failed. **Example** ```ts import RnWatchConnect from "rn-watch-connect"; setTimeout(() => { if (RnWatchConnect.watchActivationState !== "activated") { console.warn("The Session didn't activate:", RnWatchConnect.watchActivationState); } }, 2000); ``` ## `onReachabilityChanged` ```ts onReachabilityChanged: (event: { isWatchReachable: boolean }) => void ``` Fires when [`isWatchReachable`](#iswatchreachable) changes. It fires only on a real change, so read the property for the starting value. **Event** | Field | Type | Description | | --- | --- | --- | | `isWatchReachable` | `boolean` | The new value of `isWatchReachable`. | **Example** ```tsx import { useEventListener } from "expo"; import RnWatchConnect from "rn-watch-connect"; export function ReachabilityLogger() { useEventListener(RnWatchConnect, "onReachabilityChanged", ({ isWatchReachable }) => { console.log("Watch reachable:", isWatchReachable); }); return null; } ``` ## `onWatchPairedChanged` ```ts onWatchPairedChanged: (event: { isWatchPaired: boolean }) => void ``` Fires when [`isWatchPaired`](#iswatchpaired) changes. It fires only on a real change, so read the property for the starting value. **Event** | Field | Type | Description | | --- | --- | --- | | `isWatchPaired` | `boolean` | The new value of `isWatchPaired`. | **Example** ```tsx import { useEvent } from "expo"; import RnWatchConnect from "rn-watch-connect"; export function PairedStatus() { const { isWatchPaired } = useEvent(RnWatchConnect, "onWatchPairedChanged", { isWatchPaired: RnWatchConnect.isWatchPaired, }); return isWatchPaired ? "Watch paired" : "No Watch paired"; } ``` ## `onWatchAppInstallChanged` ```ts onWatchAppInstallChanged: (event: { isWatchAppInstalled: boolean }) => void ``` Fires when [`isWatchAppInstalled`](#iswatchappinstalled) changes. It fires only on a real change, so read the property for the starting value. **Event** | Field | Type | Description | | --- | --- | --- | | `isWatchAppInstalled` | `boolean` | The new value of `isWatchAppInstalled`. | **Example** ```tsx import { useEvent } from "expo"; import RnWatchConnect from "rn-watch-connect"; export function WatchAppStatus() { const { isWatchAppInstalled } = useEvent(RnWatchConnect, "onWatchAppInstallChanged", { isWatchAppInstalled: RnWatchConnect.isWatchAppInstalled, }); return isWatchAppInstalled ? "Watch app installed" : "Install the Watch app"; } ``` # Messages (https://rn-watch-connect.vercel.app/docs/messages) A **Message** is a dictionary sent over the Session for immediate delivery. It only works while the Watch is **reachable**: the Watch app is running and in range. Check [`isWatchReachable`](https://rn-watch-connect.vercel.app/docs/watch-status.md#iswatchreachable) before sending. To send data that waits until the Watch app can take it, use [Application Context](https://rn-watch-connect.vercel.app/docs/application-context.md) or [User Info](https://rn-watch-connect.vercel.app/docs/user-info.md) instead. Messages travel both ways: - **Outbound** (iPhone → Watch): [`sendMessage`](#sendmessage) asks for a Reply, and [`sendMessageWithoutReply`](#sendmessagewithoutreply) doesn't. - **Inbound** (Watch → iPhone): [`onMessageReceived`](#onmessagereceived) fires for a Message that doesn't ask for a Reply, and [`onMessageWithReply`](#onmessagewithreply) fires for one that does. A Message's values must be ones WatchConnectivity can carry: strings, numbers, booleans, arrays and nested objects. Anything else fails with `ERR_PAYLOAD_UNSUPPORTED_TYPES`. To send raw bytes, use [Data Messages](https://rn-watch-connect.vercel.app/docs/data-messages.md). ## `sendMessage` ```ts sendMessage(message: T): Promise ``` Sends a Message to the Watch and waits for its Reply. The Watch app must receive it with a reply handler and call it, in [`session(_:didReceiveMessage:replyHandler:)`](https://developer.apple.com/documentation/watchconnectivity/wcsessiondelegate/session(_:didreceivemessage:replyhandler:)). If it never replies, the Promise rejects with `ERR_MESSAGE_REPLY_TIMED_OUT`. **Type parameters** | Name | Description | | --- | --- | | `T` | The shape of the Message you send. | | `R` | The shape of the Reply you expect. The library doesn't check it at runtime. | `MessagePayload` is `Record`. **Parameters** | Name | Type | Description | | --- | --- | --- | | `message` | `T` | The Message to send. | **Returns** `Promise`, which resolves with the Watch's Reply. **Throws** (the Promise rejects with): | Code | When | | --- | --- | | `ERR_WATCH_NOT_REACHABLE` | The Watch isn't reachable. Nothing was sent. | | `ERR_MESSAGE_REPLY_TIMED_OUT` | The Watch didn't reply in time. | | `ERR_PAYLOAD_UNSUPPORTED_TYPES` | The Message contains a value WatchConnectivity can't carry. | | `ERR_PAYLOAD_TOO_LARGE` | The Message is too large to send. | | `ERR_UNSUPPORTED_PLATFORM` | Called on a platform other than iOS. | WatchConnectivity can fail with other codes too. See [Error codes](https://rn-watch-connect.vercel.app/docs/errors.md) for the full list. **Example** ```ts import RnWatchConnect, { type WatchConnectErrorCode } from "rn-watch-connect"; type Ping = { ping: number }; type Pong = { pong: number }; try { const reply = await RnWatchConnect.sendMessage({ ping: 1 }); console.log(reply.pong); } catch (error) { const code = (error as { code?: WatchConnectErrorCode }).code; if (code === "ERR_WATCH_NOT_REACHABLE") { // Try again when onReachabilityChanged says the Watch is reachable. } } ``` ## `sendMessageWithoutReply` ```ts sendMessageWithoutReply(message: T): Promise ``` Sends a Message to the Watch without asking for a Reply. The Watch app receives it in [`session(_:didReceiveMessage:)`](https://developer.apple.com/documentation/watchconnectivity/wcsessiondelegate/session(_:didreceivemessage:)). **Type parameters** | Name | Description | | --- | --- | | `T` | The shape of the Message you send. | **Parameters** | Name | Type | Description | | --- | --- | --- | | `message` | `T` | The Message to send. | **Returns** `Promise`, which resolves once the Message is handed to the Session. **Throws** (the Promise rejects with): | Code | When | | --- | --- | | `ERR_WATCH_NOT_REACHABLE` | The Watch isn't reachable. Nothing was sent. | | `ERR_UNSUPPORTED_PLATFORM` | Called on a platform other than iOS. | Its error `message` starts with Expo's prefix, "Calling the 'sendMessageWithoutReply' function has failed", so match on `code`. **Example** ```ts import RnWatchConnect from "rn-watch-connect"; if (RnWatchConnect.isWatchReachable) { await RnWatchConnect.sendMessageWithoutReply({ score: 42 }); } ``` ## `onMessageReceived` ```ts onMessageReceived: (message: MessagePayload) => void ``` Fires when an Inbound Message arrives that doesn't ask for a Reply. The listener gets the Message itself. **Example** ```tsx import { useEventListener } from "expo"; import RnWatchConnect from "rn-watch-connect"; export function ScoreListener() { useEventListener(RnWatchConnect, "onMessageReceived", (message) => { console.log("Score from the Watch:", message.score); }); return null; } ``` Outside a component, use `addListener`, and call `remove()` on the subscription when you're done: ```ts import RnWatchConnect from "rn-watch-connect"; const subscription = RnWatchConnect.addListener("onMessageReceived", (message) => { console.log(message); }); subscription.remove(); ``` ## `onMessageWithReply` ```ts onMessageWithReply: (event: MessageWithReplyEvent) => void ``` Fires when an Inbound Message arrives that asks for a Reply. Answer it by calling the event's **Reply function**, `reply`. **Event** | Field | Type | Description | | --- | --- | --- | | `message` | `MessagePayload` | The Message from the Watch. | | `reply` | `(reply: MessagePayload) => Promise` | The Reply function: sends `reply` back to the Watch. | | `replyId` | `string` | **Deprecated.** For [`replyToMessage`](#replytomessage). Call `reply` instead. | Each Message takes one Reply: - If several listeners get the same Message, the first `reply` call wins. Later calls reject. - A Message can stop waiting for its Reply, for example when its last listener is removed. Calling `reply` after that rejects. **`reply` throws** (its Promise rejects with): | Code | When | | --- | --- | | `ERR_REPLY_NOT_PENDING` | This Message was already replied to, or is no longer waiting for a Reply. | **Example** ```tsx import { useEventListener } from "expo"; import RnWatchConnect from "rn-watch-connect"; export function PingResponder() { useEventListener(RnWatchConnect, "onMessageWithReply", ({ message, reply }) => { if ("ping" in message) { reply({ pong: message.ping }); } }); return null; } ``` ## `replyToMessage` > **Deprecated** > > Call the [`reply` function](#onmessagewithreply) from `onMessageWithReply` instead. > `replyToMessage` still works, and will be removed in a future major version. ```ts replyToMessage(replyId: string, reply: MessagePayload): Promise ``` Sends the Reply to an Inbound Message, using the `replyId` from its `onMessageWithReply` event. **Parameters** | Name | Type | Description | | --- | --- | --- | | `replyId` | `string` | The event's `replyId`. | | `reply` | `MessagePayload` | The Reply to send. | **Returns** `Promise`, which resolves once the Reply is sent. **Throws** (the Promise rejects with): | Code | When | | --- | --- | | `ERR_REPLY_NOT_PENDING` | This Message was already replied to, or is no longer waiting for a Reply. | | `ERR_UNSUPPORTED_PLATFORM` | Called on a platform other than iOS. | Its error `message` starts with Expo's prefix, "Calling the 'replyToMessage' function has failed", so match on `code`. **Moving to `reply`** ```tsx // Before (deprecated) useEventListener(RnWatchConnect, "onMessageWithReply", ({ message, replyId }) => { RnWatchConnect.replyToMessage(replyId, { pong: message.ping }); }); // After useEventListener(RnWatchConnect, "onMessageWithReply", ({ message, reply }) => { reply({ pong: message.ping }); }); ``` # Data Messages (https://rn-watch-connect.vercel.app/docs/data-messages) A **Data Message** is a [Message](https://rn-watch-connect.vercel.app/docs/messages.md) whose content is raw bytes instead of a dictionary. Like a Message, it's delivered immediately and only works while the Watch is reachable. Check [`isWatchReachable`](https://rn-watch-connect.vercel.app/docs/watch-status.md#iswatchreachable) before sending. On the JavaScript side the bytes are always a **base64 encoded string**. That applies to what you send, the Replies you get back, the data you receive and the Replies you send. The Watch app gets the decoded bytes as `Data`. - **Outbound** (iPhone → Watch): [`sendDataMessage`](#senddatamessage) asks for a Reply, and [`sendDataMessageWithoutReply`](#senddatamessagewithoutreply) doesn't. - **Inbound** (Watch → iPhone): [`onDataMessageReceived`](#ondatamessagereceived) fires for a Data Message that doesn't ask for a Reply, and [`onDataMessageWithReply`](#ondatamessagewithreply) fires for one that does. A string that isn't valid base64 fails with `ERR_INVALID_BASE64` before anything is sent, and its `message` says what's wrong with it. For text, `btoa` and `atob` encode and decode base64. ## `sendDataMessage` ```ts sendDataMessage(data: string): Promise ``` Sends a Data Message to the Watch and waits for its Reply. The Watch app must receive it with a reply handler and call it, in [`session(_:didReceiveMessageData:replyHandler:)`](https://developer.apple.com/documentation/watchconnectivity/wcsessiondelegate/session(_:didreceivemessagedata:replyhandler:)). If it never replies, the Promise rejects with `ERR_MESSAGE_REPLY_TIMED_OUT`. **Parameters** | Name | Type | Description | | --- | --- | --- | | `data` | `string` | The bytes to send, base64 encoded. | **Returns** `Promise`, which resolves with the Watch's Reply, base64 encoded. **Throws** (the Promise rejects with): | Code | When | | --- | --- | | `ERR_INVALID_BASE64` | `data` isn't valid base64. Nothing was sent. | | `ERR_WATCH_NOT_REACHABLE` | The Watch isn't reachable. Nothing was sent. | | `ERR_MESSAGE_REPLY_TIMED_OUT` | The Watch didn't reply in time. | | `ERR_PAYLOAD_TOO_LARGE` | The data is too large to send. | | `ERR_UNSUPPORTED_PLATFORM` | Called on a platform other than iOS. | WatchConnectivity can fail with other codes too. See [Error codes](https://rn-watch-connect.vercel.app/docs/errors.md) for the full list. **Example** ```ts import RnWatchConnect, { type WatchConnectErrorCode } from "rn-watch-connect"; try { const reply = await RnWatchConnect.sendDataMessage(btoa("Hello from iPhone!")); console.log("Reply:", atob(reply)); } catch (error) { const code = (error as { code?: WatchConnectErrorCode }).code; if (code === "ERR_WATCH_NOT_REACHABLE") { // Try again when onReachabilityChanged says the Watch is reachable. } } ``` ## `sendDataMessageWithoutReply` ```ts sendDataMessageWithoutReply(data: string): Promise ``` Sends a Data Message to the Watch without asking for a Reply. The Watch app receives it in [`session(_:didReceiveMessageData:)`](https://developer.apple.com/documentation/watchconnectivity/wcsessiondelegate/session(_:didreceivemessagedata:)). **Parameters** | Name | Type | Description | | --- | --- | --- | | `data` | `string` | The bytes to send, base64 encoded. | **Returns** `Promise`, which resolves once the Data Message is handed to the Session. **Throws** (the Promise rejects with): | Code | When | | --- | --- | | `ERR_INVALID_BASE64` | `data` isn't valid base64. Nothing was sent. | | `ERR_WATCH_NOT_REACHABLE` | The Watch isn't reachable. Nothing was sent. | | `ERR_UNSUPPORTED_PLATFORM` | Called on a platform other than iOS. | Its error `message` starts with Expo's prefix, "Calling the 'sendDataMessageWithoutReply' function has failed", so match on `code`. **Example** ```ts import RnWatchConnect from "rn-watch-connect"; if (RnWatchConnect.isWatchReachable) { await RnWatchConnect.sendDataMessageWithoutReply(btoa("tap")); } ``` ## `onDataMessageReceived` ```ts onDataMessageReceived: (event: { data: string }) => void ``` Fires when an Inbound Data Message arrives that doesn't ask for a Reply. **Event** | Field | Type | Description | | --- | --- | --- | | `data` | `string` | The bytes from the Watch, base64 encoded. | **Example** ```tsx import { useEventListener } from "expo"; import RnWatchConnect from "rn-watch-connect"; export function DataListener() { useEventListener(RnWatchConnect, "onDataMessageReceived", ({ data }) => { console.log("From the Watch:", atob(data)); }); return null; } ``` Outside a component, use `RnWatchConnect.addListener("onDataMessageReceived", listener)`, and call `remove()` on the subscription when you're done. ## `onDataMessageWithReply` ```ts onDataMessageWithReply: (event: DataMessageWithReplyEvent) => void ``` Fires when an Inbound Data Message arrives that asks for a Reply. Answer it by calling the event's **Reply function**, `reply`. **Event** | Field | Type | Description | | --- | --- | --- | | `data` | `string` | The bytes from the Watch, base64 encoded. | | `reply` | `(reply: string) => Promise` | The Reply function: sends `reply` (base64 encoded) back to the Watch. | | `replyId` | `string` | **Deprecated.** For [`replyToDataMessage`](#replytodatamessage). Call `reply` instead. | Each Data Message takes one Reply: - If several listeners get the same Data Message, the first `reply` call wins. Later calls reject. - A Data Message can stop waiting for its Reply, for example when its last listener is removed. Calling `reply` after that rejects. **`reply` throws** (its Promise rejects with): | Code | When | | --- | --- | | `ERR_REPLY_NOT_PENDING` | This Data Message was already replied to, or is no longer waiting for a Reply. | | `ERR_INVALID_BASE64` | `reply` isn't valid base64. | **Example** ```tsx import { useEventListener } from "expo"; import RnWatchConnect from "rn-watch-connect"; export function EchoResponder() { useEventListener(RnWatchConnect, "onDataMessageWithReply", ({ data, reply }) => { reply(btoa(`Got: ${atob(data)}`)); }); return null; } ``` ## `replyToDataMessage` > **Deprecated** > > Call the [`reply` function](#ondatamessagewithreply) from `onDataMessageWithReply` instead. > `replyToDataMessage` still works, and will be removed in a future major version. ```ts replyToDataMessage(replyId: string, response: string): Promise ``` Sends the Reply to an Inbound Data Message, using the `replyId` from its `onDataMessageWithReply` event. **Parameters** | Name | Type | Description | | --- | --- | --- | | `replyId` | `string` | The event's `replyId`. | | `response` | `string` | The Reply to send, base64 encoded. | **Returns** `Promise`, which resolves once the Reply is sent. **Throws** (the Promise rejects with): | Code | When | | --- | --- | | `ERR_REPLY_NOT_PENDING` | This Data Message was already replied to, or is no longer waiting for a Reply. | | `ERR_INVALID_BASE64` | `response` isn't valid base64. | | `ERR_UNSUPPORTED_PLATFORM` | Called on a platform other than iOS. | Its error `message` starts with Expo's prefix, "Calling the 'replyToDataMessage' function has failed", so match on `code`. **Moving to `reply`** ```tsx // Before (deprecated) useEventListener(RnWatchConnect, "onDataMessageWithReply", ({ data, replyId }) => { RnWatchConnect.replyToDataMessage(replyId, btoa("ok")); }); // After useEventListener(RnWatchConnect, "onDataMessageWithReply", ({ data, reply }) => { reply(btoa("ok")); }); ``` # Application Context (https://rn-watch-connect.vercel.app/docs/application-context) The **Application Context** is a dictionary describing the latest values the other side needs, such as a selected theme or the current score. It's delivered in the background, so the Watch doesn't have to be reachable. Only the most recent one is delivered: each update replaces the previous one. Because an update replaces the whole dictionary, send everything the Watch needs each time, not just what changed. If every value matters, not only the latest, queue [User Info](https://rn-watch-connect.vercel.app/docs/user-info.md) instead. Each side has its own Application Context: - **Outbound** (iPhone → Watch): [`updateApplicationContext`](#updateapplicationcontext) sends one. [`applicationContext`](#applicationcontext) is the last one you sent. - **Inbound** (Watch → iPhone): [`onApplicationContextChanged`](#onapplicationcontextchanged) fires when one arrives. [`receivedApplicationContext`](#receivedapplicationcontext) is the last one received. A dictionary's values must be ones WatchConnectivity can carry: strings, numbers, booleans, arrays and nested objects. ## `updateApplicationContext` ```ts updateApplicationContext>(applicationContext: T): Promise ``` Sends a new Application Context to the Watch, replacing the previous one. The Watch app receives it in [`session(_:didReceiveApplicationContext:)`](https://developer.apple.com/documentation/watchconnectivity/wcsessiondelegate/session(_:didreceiveapplicationcontext:)). **Type parameters** | Name | Description | | --- | --- | | `T` | The shape of the Application Context you send. | **Parameters** | Name | Type | Description | | --- | --- | --- | | `applicationContext` | `T` | The full Application Context. | **Returns** `Promise`, which resolves once the Session has taken the update. Delivery happens later, in the background. **Throws** (the Promise rejects with): | Code | When | | --- | --- | | `ERR_PAYLOAD_UNSUPPORTED_TYPES` | The dictionary contains a value WatchConnectivity can't carry. | | `ERR_PAYLOAD_TOO_LARGE` | The dictionary is too large to send. | | `ERR_UNSUPPORTED_PLATFORM` | Called on a platform other than iOS. | WatchConnectivity can fail with other codes too, such as `ERR_SESSION_NOT_ACTIVATED`, `ERR_DEVICE_NOT_PAIRED` or `ERR_WATCH_APP_NOT_INSTALLED`. See [Error codes](https://rn-watch-connect.vercel.app/docs/errors.md) for the full list. Its error `message` starts with Expo's prefix, "Calling the 'updateApplicationContext' function has failed", so match on `code`. **Example** ```ts import RnWatchConnect from "rn-watch-connect"; type WatchDisplay = { theme: "light" | "dark"; units: "metric" | "imperial" }; await RnWatchConnect.updateApplicationContext({ theme: "dark", units: "metric", }); ``` ## `applicationContext` ```ts readonly applicationContext: Record ``` The last Application Context this iPhone sent with [`updateApplicationContext`](#updateapplicationcontext). It's `{}` when there's none, and on platforms other than iOS. **Example** ```ts import RnWatchConnect from "rn-watch-connect"; // Change one value and send the rest unchanged. await RnWatchConnect.updateApplicationContext({ ...RnWatchConnect.applicationContext, theme: "light", }); ``` ## `receivedApplicationContext` ```ts readonly receivedApplicationContext: Record ``` The last Application Context received from the Watch. It's `{}` when there's none, and on platforms other than iOS. Read it for the starting value, then listen to [`onApplicationContextChanged`](#onapplicationcontextchanged) for new ones. ## `onApplicationContextChanged` ```ts onApplicationContextChanged: (applicationContext: Record) => void ``` Fires when a new Application Context arrives from the Watch. The listener gets the whole dictionary, which replaces the previous one. It fires only when one actually arrives, not when you add a listener. For the starting value, read [`receivedApplicationContext`](#receivedapplicationcontext). **Example** ```tsx import { useEvent } from "expo"; import RnWatchConnect from "rn-watch-connect"; export function WatchTheme() { const context = useEvent( RnWatchConnect, "onApplicationContextChanged", RnWatchConnect.receivedApplicationContext, ); return `Theme on the Watch: ${context.theme ?? "default"}`; } ``` # User Info (https://rn-watch-connect.vercel.app/docs/user-info) A **User Info** is a dictionary queued for background delivery. Unlike [Application Context](https://rn-watch-connect.vercel.app/docs/application-context.md), every User Info is delivered, in order, even when the Watch isn't reachable. Use it when every value matters, such as each completed workout. Each User Info you send is a **Transfer** until it's delivered: - [`transferUserInfo`](#transferuserinfo) starts one and returns its **Transfer snapshot**. - [`outstandingUserInfoTransfers`](#outstandinguserinfotransfers) lists the ones not delivered yet. - [`cancelUserInfoTransfer`](#canceluserinfotransfer) cancels one by its id. - [`onUserInfoTransferFinished`](#onuserinfotransferfinished) fires exactly once per Transfer: when it's delivered, fails or is cancelled. Inbound User Info from the Watch arrives through [`onUserInfoReceived`](#onuserinforeceived). A dictionary's values must be ones WatchConnectivity can carry: strings, numbers, booleans, arrays and nested objects. > **Transfer ids last for one app run** > > After your app restarts, a Transfer that's still outstanding gets a **new id**. Read > `outstandingUserInfoTransfers` at launch to pick them up. Their finish events and > `cancelUserInfoTransfer` use the new ids. ## `transferUserInfo` ```ts transferUserInfo>(userInfo: T): OutstandingUserInfoTransfer ``` Queues a User Info for delivery to the Watch and returns at once. The Watch app receives it in [`session(_:didReceiveUserInfo:)`](https://developer.apple.com/documentation/watchconnectivity/wcsessiondelegate/session(_:didreceiveuserinfo:)). **Type parameters** | Name | Description | | --- | --- | | `T` | The shape of the User Info you send. | **Parameters** | Name | Type | Description | | --- | --- | --- | | `userInfo` | `T` | The User Info to queue. | **Returns** the Transfer snapshot, in the same shape as [`outstandingUserInfoTransfers`](#outstandinguserinfotransfers): | Field | Type | Description | | --- | --- | --- | | `id` | `string` | The Transfer id, valid for the current app run. | | `userInfo` | `Record` | The User Info being sent. | | `isTransferring` | `boolean` | Whether the Transfer is in progress right now. | The type is `OutstandingUserInfoTransfer`. The older name `UserInfoTransfer` is a deprecated alias of it. **Throws** (synchronously): | Code | When | | --- | --- | | `ERR_UNSUPPORTED_PLATFORM` | Called on a platform other than iOS. | Delivery failures don't throw here: they arrive later, in [`onUserInfoTransferFinished`](#onuserinfotransferfinished). **Example** ```ts import RnWatchConnect from "rn-watch-connect"; const transfer = RnWatchConnect.transferUserInfo({ workout: "run", km: 5.2 }); console.log("Queued Transfer", transfer.id); ``` ## `cancelUserInfoTransfer` ```ts cancelUserInfoTransfer(transferId: string): { id: string } ``` Cancels an outstanding User Info Transfer. Its [`onUserInfoTransferFinished`](#onuserinfotransferfinished) event is sent immediately, with `error.code` `"ERR_TRANSFER_CANCELLED"`. **Parameters** | Name | Type | Description | | --- | --- | --- | | `transferId` | `string` | The `id` of an outstanding Transfer, from the current app run. | **Returns** `{ id: string }`: the id of the cancelled Transfer. **Throws** (synchronously): | Code | When | | --- | --- | | `ERR_INVALID_TRANSFER_ID` | `transferId` isn't an outstanding Transfer: it was already delivered, already cancelled, or is from an earlier app run. | | `ERR_UNSUPPORTED_PLATFORM` | Called on a platform other than iOS. | Its error `message` starts with Expo's prefix, "Calling the 'cancelUserInfoTransfer' function has failed", so match on `code`. If the Transfer was delivered just before you cancel, the cancel throws `ERR_INVALID_TRANSFER_ID` and the finish event reports the delivery, with `error: null`. **Example** ```ts import RnWatchConnect, { type WatchConnectErrorCode } from "rn-watch-connect"; const transfer = RnWatchConnect.transferUserInfo({ draft: true }); try { RnWatchConnect.cancelUserInfoTransfer(transfer.id); } catch (error) { const code = (error as { code?: WatchConnectErrorCode }).code; if (code === "ERR_INVALID_TRANSFER_ID") { // Already delivered or cancelled: nothing to do. } } ``` ## `outstandingUserInfoTransfers` ```ts readonly outstandingUserInfoTransfers: OutstandingUserInfoTransfer[] ``` The User Info Transfers that haven't been delivered yet, as Transfer snapshots (`id`, `userInfo`, `isTransferring`). It's `[]` when there are none, and on platforms other than iOS. **Example** ```ts import RnWatchConnect from "rn-watch-connect"; // At launch: pick up Transfers from the previous run, with their new ids. for (const transfer of RnWatchConnect.outstandingUserInfoTransfers) { console.log("Still queued:", transfer.id, transfer.userInfo); } ``` ## `onUserInfoReceived` ```ts onUserInfoReceived: (userInfo: Record) => void ``` Fires when a User Info arrives from the Watch. The listener gets the dictionary itself. User Info arrives in the order the Watch sent it. **Example** ```tsx import { useEventListener } from "expo"; import RnWatchConnect from "rn-watch-connect"; export function WorkoutInbox() { useEventListener(RnWatchConnect, "onUserInfoReceived", (userInfo) => { console.log("Workout from the Watch:", userInfo.workout); }); return null; } ``` ## `onUserInfoTransferFinished` ```ts onUserInfoTransferFinished: (event: UserInfoTransferFinishedEvent) => void ``` Fires **exactly once per User Info Transfer**, however it ends. The event is the Transfer snapshot plus `error`: | Field | Type | Description | | --- | --- | --- | | `id` | `string` | The Transfer id. | | `userInfo` | `Record` | The User Info that was sent. | | `isTransferring` | `boolean` | Whether the Transfer was in progress. | | `error` | `{ message: string; code: WatchConnectErrorCode } \| null` | How it ended (see below). | | How it ended | `error` | | --- | --- | | Delivered to the Watch | `null` | | Failed | `{ message, code }` from WatchConnectivity, for example `code: "ERR_DELIVERY_FAILED"` | | Cancelled with `cancelUserInfoTransfer` | `{ message, code: "ERR_TRANSFER_CANCELLED" }`, sent immediately | If your Watch app is uninstalled during a Transfer, the Transfer stays outstanding and no finish event is sent, even after a reinstall. Cancel it to end it, for example when [`onWatchAppInstallChanged`](https://rn-watch-connect.vercel.app/docs/watch-status.md#onwatchappinstallchanged) reports `isWatchAppInstalled: false`. **Example** ```tsx import { useEventListener } from "expo"; import RnWatchConnect from "rn-watch-connect"; export function UserInfoTransferLog() { useEventListener(RnWatchConnect, "onUserInfoTransferFinished", (transfer) => { if (transfer.error?.code === "ERR_TRANSFER_CANCELLED") { console.log("Cancelled:", transfer.id); } else if (transfer.error) { console.log("Failed:", transfer.id, transfer.error.code); } else { console.log("Delivered:", transfer.id); } }); return null; } ``` # File Transfer (https://rn-watch-connect.vercel.app/docs/file-transfer) A **File Transfer** is a file, with optional metadata, queued for background delivery to the Watch. The Watch doesn't have to be reachable. Each file you send is a **Transfer** until it's delivered: - [`transferFile`](#transferfile) starts one and returns its **Transfer snapshot**. - [`outstandingFileTransfers`](#outstandingfiletransfers) lists the ones not delivered yet, with their progress. - [`cancelFileTransfer`](#cancelfiletransfer) cancels one by its id. - [`onFileTransferFinished`](#onfiletransferfinished) fires exactly once per Transfer: when it's delivered, fails or is cancelled. Files the Watch sends you are **Received files**. They arrive through [`onFileReceived`](#onfilereceived), or through [`onFileReceiveFailed`](#onfilereceivefailed) when one couldn't be kept. > **Transfer ids last for one app run** > > After your app restarts, a Transfer that's still outstanding gets a **new id**. Read > `outstandingFileTransfers` at launch to pick them up. Their finish events and > `cancelFileTransfer` use the new ids. ## `transferFile` ```ts transferFile(file: string, metadata?: Record): FileTransfer ``` Queues a file for delivery to the Watch and returns at once. The Watch app receives it in [`session(_:didReceive:)`](https://developer.apple.com/documentation/watchconnectivity/wcsessiondelegate/session(_:didreceive:)). WatchConnectivity keeps its own copy of the file, so you can delete yours right after calling `transferFile`. That doesn't stop or fail the Transfer. **Parameters** | Name | Type | Description | | --- | --- | --- | | `file` | `string` | A `file://` URL of an existing file, such as the `uri` of an `expo-file-system` `File`. | | `metadata` | `Record` (optional) | A dictionary sent with the file, for example its original name. | **Returns** the Transfer snapshot, `FileTransfer`, in the same shape as [`outstandingFileTransfers`](#outstandingfiletransfers): | Field | Type | Description | | --- | --- | --- | | `id` | `string` | The Transfer id, valid for the current app run. | | `isTransferring` | `boolean` | Whether the Transfer is in progress right now. | | `progress` | `FileProgress` | `fractionCompleted` (0 to 1), `completedUnitCount` and `totalUnitCount` (bytes). | | `file` | `File` | `fileURL` (the file being sent) and `metadata` (what you passed). | **Throws** (synchronously): | Code | When | | --- | --- | | `ERR_INVALID_FILE_URL` | `file` isn't a URL the library can read. | | `ERR_FILE_NOT_FOUND` | There's no file at `file`. | | `ERR_UNSUPPORTED_PLATFORM` | Called on a platform other than iOS. | Its error `message` starts with Expo's prefix, "Calling the 'transferFile' function has failed", so match on `code`. Delivery failures don't throw here: they arrive later, in [`onFileTransferFinished`](#onfiletransferfinished). **Example** ```ts import RnWatchConnect, { type WatchConnectErrorCode } from "rn-watch-connect"; const uri = "file:///path/to/Documents/route.gpx"; try { const transfer = RnWatchConnect.transferFile(uri, { name: "route.gpx" }); console.log("Queued Transfer", transfer.id); } catch (error) { const code = (error as { code?: WatchConnectErrorCode }).code; if (code === "ERR_FILE_NOT_FOUND") { // The file was moved or deleted before you could send it. } } ``` ## `cancelFileTransfer` ```ts cancelFileTransfer(transferId: string): { id: string } ``` Cancels an outstanding File Transfer. Its [`onFileTransferFinished`](#onfiletransferfinished) event is sent immediately, with `error.code` `"ERR_TRANSFER_CANCELLED"`. **Parameters** | Name | Type | Description | | --- | --- | --- | | `transferId` | `string` | The `id` of an outstanding Transfer, from the current app run. | **Returns** `{ id: string }`: the id of the cancelled Transfer. **Throws** (synchronously): | Code | When | | --- | --- | | `ERR_INVALID_TRANSFER_ID` | `transferId` isn't an outstanding Transfer: it was already delivered, already cancelled, or is from an earlier app run. | | `ERR_UNSUPPORTED_PLATFORM` | Called on a platform other than iOS. | Its error `message` starts with Expo's prefix, "Calling the 'cancelFileTransfer' function has failed", so match on `code`. If the Transfer was delivered just before you cancel, the cancel throws `ERR_INVALID_TRANSFER_ID` and the finish event reports the delivery, with `error: null`. **Example** ```ts import RnWatchConnect from "rn-watch-connect"; // Cancel every File Transfer still waiting. for (const transfer of RnWatchConnect.outstandingFileTransfers) { RnWatchConnect.cancelFileTransfer(transfer.id); } ``` ## `outstandingFileTransfers` ```ts readonly outstandingFileTransfers: FileTransfer[] ``` The File Transfers that haven't been delivered yet, as Transfer snapshots (`id`, `isTransferring`, `progress`, `file`). It's `[]` when there are none, and on platforms other than iOS. WatchConnectivity has no progress event. To show progress, read this property every so often, and use [`onFileTransferFinished`](#onfiletransferfinished) to know when a Transfer is done. **Example** ```ts import RnWatchConnect from "rn-watch-connect"; const timer = setInterval(() => { for (const { id, progress } of RnWatchConnect.outstandingFileTransfers) { console.log(id, `${Math.round(progress.fractionCompleted * 100)}%`); } }, 1000); // Later: clearInterval(timer); ``` ## `onFileReceived` ```ts onFileReceived: (file: File) => void ``` Fires when a Received file arrives from the Watch. **Event** | Field | Type | Description | | --- | --- | --- | | `fileURL` | `string` | Where the file is now: in your app's temporary directory. | | `metadata` | `Record` | What the Watch sent with the file. | **The file is yours from this event on.** The library moves each Received file into your app's temporary directory, then never moves, overwrites or deletes it again. - iOS can delete files in the temporary directory while your app isn't running. **Move or copy the file somewhere permanent if you need to keep it**, and delete it when you're done. - Received files never overwrite each other. If the name is taken, the new file is numbered the way Finder does it: `photo.jpg`, then `photo(1).jpg`, `photo(2).jpg`, and so on. `metadata` is passed through unchanged, so you can still read the original name if the Watch sent it there. **Example** ```tsx import { useEventListener } from "expo"; import RnWatchConnect from "rn-watch-connect"; export function FileInbox() { useEventListener(RnWatchConnect, "onFileReceived", ({ fileURL, metadata }) => { const name = metadata.name ?? fileURL.split("/").pop(); console.log(`Received ${name} at ${fileURL}`); // Move or copy it somewhere permanent here if you need to keep it. }); return null; } ``` ## `onFileReceiveFailed` ```ts onFileReceiveFailed: (event: FileReceiveFailedEvent) => void ``` Fires when a file from the Watch arrived but couldn't be kept, for example because the iPhone is out of storage. **The file is lost**: WatchConnectivity has already deleted it, and `onFileReceived` doesn't fire for it. Use `fileName` and `metadata` to tell which file it was, for example to ask the Watch to send it again. **Event** | Field | Type | Description | | --- | --- | --- | | `message` | `string` | Why the file couldn't be kept. | | `code` | `WatchConnectErrorCode` | `ERR_INSUFFICIENT_SPACE` when the iPhone is out of storage, otherwise `ERR_FILE_RECEIVE_FAILED`. | | `fileName` | `string` | The file name the Watch sent. | | `metadata` | `Record \| null` | What the Watch sent with the file, or `null` when it sent nothing. | **Example** ```tsx import { useEventListener } from "expo"; import RnWatchConnect from "rn-watch-connect"; export function LostFileWarning() { useEventListener(RnWatchConnect, "onFileReceiveFailed", ({ code, fileName }) => { if (code === "ERR_INSUFFICIENT_SPACE") { console.warn(`Not enough storage to keep ${fileName} from the Watch.`); } else { console.warn(`Couldn't keep ${fileName} from the Watch.`); } }); return null; } ``` ## `onFileTransferFinished` ```ts onFileTransferFinished: (event: FileTransferFinishedEvent) => void ``` Fires **exactly once per File Transfer**, however it ends. The event is the Transfer snapshot (`id`, `isTransferring`, `progress`, `file`) plus `error`: | How it ended | `error` | | --- | --- | | Delivered to the Watch | `null` | | Failed | `{ message, code }` from WatchConnectivity, for example `code: "ERR_DELIVERY_FAILED"` | | Cancelled with `cancelFileTransfer` | `{ message, code: "ERR_TRANSFER_CANCELLED" }`, sent immediately | If your Watch app is uninstalled during a Transfer, the Transfer stays outstanding and no finish event is sent, even after a reinstall. Cancel it to end it, for example when [`onWatchAppInstallChanged`](https://rn-watch-connect.vercel.app/docs/watch-status.md#onwatchappinstallchanged) reports `isWatchAppInstalled: false`. **Example** ```tsx import { useEventListener } from "expo"; import RnWatchConnect from "rn-watch-connect"; export function FileTransferLog() { useEventListener(RnWatchConnect, "onFileTransferFinished", (transfer) => { if (transfer.error?.code === "ERR_TRANSFER_CANCELLED") { console.log("Cancelled:", transfer.id); } else if (transfer.error) { console.log("Failed:", transfer.id, transfer.error.code); } else { console.log("Delivered:", transfer.file.fileURL); } }); return null; } ``` # Error codes (https://rn-watch-connect.vercel.app/docs/errors) Every failure has a stable **Error code** in `code`. You'll find it on errors that are thrown, on errors a Promise rejects with, and on the `error` in events such as [`onFileTransferFinished`](https://rn-watch-connect.vercel.app/docs/file-transfer.md#onfiletransferfinished) and [`onFileReceiveFailed`](https://rn-watch-connect.vercel.app/docs/file-transfer.md#onfilereceivefailed). > **Match on code, never on message** > > `message` is for people, and it can change between versions. Functions that throw directly also > get Expo's prefix in their message: "Calling the 'transferFile' function has failed → Caused by: …". > `code` stays the same. The same failure has the same code whichever function reports it. For example, [`sendMessage`](https://rn-watch-connect.vercel.app/docs/messages.md#sendmessage) and [`sendMessageWithoutReply`](https://rn-watch-connect.vercel.app/docs/messages.md#sendmessagewithoutreply) both use `ERR_WATCH_NOT_REACHABLE`. ## `WatchConnectErrorCode` ```ts import { type WatchConnectErrorCode } from "rn-watch-connect"; ``` The TypeScript union of every code on this page. Use it to type the codes you check, so a misspelled code fails to compile. **Example** ```ts import RnWatchConnect, { type WatchConnectErrorCode } from "rn-watch-connect"; function errorCode(error: unknown): WatchConnectErrorCode | undefined { return (error as { code?: WatchConnectErrorCode } | null)?.code; } try { await RnWatchConnect.sendMessageWithoutReply({ ping: 1 }); } catch (error) { switch (errorCode(error)) { case "ERR_WATCH_NOT_REACHABLE": // Try again later, or use transferUserInfo, which doesn't need the Watch to be reachable. break; case "ERR_UNSUPPORTED_PLATFORM": // Not on iOS: hide your Watch features. break; default: throw error; } } ``` ## The library's own codes | Code | When | Reported by | | --- | --- | --- | | `ERR_WATCH_NOT_REACHABLE` | A Message or Data Message was sent while the Watch isn't reachable. Nothing was sent. | [`sendMessage`](https://rn-watch-connect.vercel.app/docs/messages.md#sendmessage), [`sendMessageWithoutReply`](https://rn-watch-connect.vercel.app/docs/messages.md#sendmessagewithoutreply), [`sendDataMessage`](https://rn-watch-connect.vercel.app/docs/data-messages.md#senddatamessage), [`sendDataMessageWithoutReply`](https://rn-watch-connect.vercel.app/docs/data-messages.md#senddatamessagewithoutreply) | | `ERR_REPLY_NOT_PENDING` | A Reply was sent for a Message already replied to, or no longer waiting for a Reply. | The `reply` function of [`onMessageWithReply`](https://rn-watch-connect.vercel.app/docs/messages.md#onmessagewithreply) and [`onDataMessageWithReply`](https://rn-watch-connect.vercel.app/docs/data-messages.md#ondatamessagewithreply), and the deprecated `replyTo…` methods | | `ERR_INVALID_BASE64` | A Data Message or data Reply isn't valid base64. `message` says why. | [Data Messages](https://rn-watch-connect.vercel.app/docs/data-messages.md) | | `ERR_INVALID_TRANSFER_ID` | The id isn't an outstanding Transfer: already delivered, already cancelled, or from an earlier app run. | [`cancelUserInfoTransfer`](https://rn-watch-connect.vercel.app/docs/user-info.md#canceluserinfotransfer), [`cancelFileTransfer`](https://rn-watch-connect.vercel.app/docs/file-transfer.md#cancelfiletransfer) | | `ERR_INVALID_FILE_URL` | The file URL can't be read. | [`transferFile`](https://rn-watch-connect.vercel.app/docs/file-transfer.md#transferfile) | | `ERR_FILE_NOT_FOUND` | There's no file at the URL. | [`transferFile`](https://rn-watch-connect.vercel.app/docs/file-transfer.md#transferfile) | | `ERR_TRANSFER_CANCELLED` | The Transfer was cancelled. | The finish events: [`onUserInfoTransferFinished`](https://rn-watch-connect.vercel.app/docs/user-info.md#onuserinfotransferfinished), [`onFileTransferFinished`](https://rn-watch-connect.vercel.app/docs/file-transfer.md#onfiletransferfinished) | | `ERR_INSUFFICIENT_SPACE` | The iPhone is out of storage, so a file from the Watch couldn't be kept. (This is also WatchConnectivity's own code for running out of space.) | [`onFileReceiveFailed`](https://rn-watch-connect.vercel.app/docs/file-transfer.md#onfilereceivefailed) | | `ERR_FILE_RECEIVE_FAILED` | A file from the Watch couldn't be kept, for another reason. | [`onFileReceiveFailed`](https://rn-watch-connect.vercel.app/docs/file-transfer.md#onfilereceivefailed) | | `ERR_UNSUPPORTED_PLATFORM` | A method was called on a platform other than iOS. | Every method | | `ERR_UNEXPECTED` | Any other failure. | Any method | ## WatchConnectivity's codes WatchConnectivity's own errors, for example when sending, when updating the Application Context, or in finish events, use `ERR_` plus the name of Apple's [`WCError.Code`](https://developer.apple.com/documentation/watchconnectivity/wcerror/code). Its `notReachable` uses the library's `ERR_WATCH_NOT_REACHABLE`, so one failure has one code. | Code | Apple's `WCError.Code` | When | | --- | --- | --- | | `ERR_GENERIC_ERROR` | `genericError` | An unknown problem. | | `ERR_SESSION_NOT_SUPPORTED` | `sessionNotSupported` | This iPhone doesn't support WatchConnectivity. | | `ERR_SESSION_MISSING_DELEGATE` | `sessionMissingDelegate` | The Session has no delegate to handle it. | | `ERR_SESSION_NOT_ACTIVATED` | `sessionNotActivated` | The Session isn't activated. | | `ERR_SESSION_INACTIVE` | `sessionInactive` | The Session is inactive. | | `ERR_DEVICE_NOT_PAIRED` | `deviceNotPaired` | No Apple Watch is paired with this iPhone. | | `ERR_WATCH_APP_NOT_INSTALLED` | `watchAppNotInstalled` | Your Watch app isn't installed on the paired Watch. | | `ERR_COMPANION_APP_NOT_INSTALLED` | `companionAppNotInstalled` | The companion app isn't installed on the other side. | | `ERR_WATCH_ONLY_APP` | `watchOnlyApp` | The Watch app is a watch-only app, with no iPhone app to talk to. | | `ERR_INVALID_PARAMETER` | `invalidParameter` | A parameter is invalid. | | `ERR_PAYLOAD_TOO_LARGE` | `payloadTooLarge` | What you sent is over WatchConnectivity's size limit. | | `ERR_PAYLOAD_UNSUPPORTED_TYPES` | `payloadUnsupportedTypes` | A dictionary contains a value WatchConnectivity can't carry. | | `ERR_MESSAGE_REPLY_FAILED` | `messageReplyFailed` | The Reply couldn't be returned. | | `ERR_MESSAGE_REPLY_TIMED_OUT` | `messageReplyTimedOut` | The other side didn't reply in time. | | `ERR_FILE_ACCESS_DENIED` | `fileAccessDenied` | The file couldn't be read for a File Transfer. | | `ERR_DELIVERY_FAILED` | `deliveryFailed` | The Transfer couldn't be delivered. | | `ERR_INSUFFICIENT_SPACE` | `insufficientSpace` | The receiving side doesn't have enough space to store it. | | `ERR_TRANSFER_TIMED_OUT` | `transferTimedOut` | The Transfer hit its time limit before it finished. | # Migrating from 1.x to 2.0 (https://rn-watch-connect.vercel.app/docs/migration) Most apps need three things: the new requirements, one renamed field, and a check of the behaviour changes below. Everything else is optional. ## 1. Update the requirements 2.0 needs **Expo SDK 57 or later** (React Native 0.86+) and **iOS 16.4 or later**. If your app must stay on an older SDK or support iOS 15, stay on `rn-watch-connect@1.x`. ```bash npm install rn-watch-connect@2 ``` This is a native change, so **rebuild the native app**, for example with `npx expo run:ios` or a new development build. A JS reload isn't enough. ## 2. Fix the breaking change ### Rename the `transferFile` progress keys `transferFile` now returns the same progress keys as `outstandingFileTransfers` and the `FileTransfer` type. Rename `total` and `completed` wherever you read them. ```ts // Before (1.x) const { progress } = RnWatchConnect.transferFile(uri); console.log(progress.completed, "of", progress.total); ``` ```ts import RnWatchConnect from "rn-watch-connect"; const uri = "file:///path/to/file.txt"; // After (2.0) const { progress } = RnWatchConnect.transferFile(uri); console.log(progress.completedUnitCount, "of", progress.totalUnitCount); ``` ## 3. Check these behaviour changes ### Status events no longer fire when a listener is added In 1.x, a new listener for `onReachabilityChanged`, `onWatchPairedChanged` or `onWatchAppInstallChanged` was called right away with the current value. In 2.0 they fire **only on a real change**, so read the property for the starting value. With `useEvent`, pass it as the initial value: ```tsx // Before (1.x): relied on the event firing at once const event = useEvent(RnWatchConnect, "onReachabilityChanged"); const isWatchReachable = event?.isWatchReachable ?? false; ``` ```tsx import { useEvent } from "expo"; import RnWatchConnect from "rn-watch-connect"; export function Reachability() { // After (2.0): seed from the property const { isWatchReachable } = useEvent(RnWatchConnect, "onReachabilityChanged", { isWatchReachable: RnWatchConnect.isWatchReachable, }); return isWatchReachable; } ``` The same goes for the other events: they no longer replay the last value, or an empty `{}`, when you add a listener. If you filtered out those empty events, you can remove that code. See [Watch status](https://rn-watch-connect.vercel.app/docs/watch-status.md). ### Match errors on `code`, not on `message` In 1.x, almost every failure had `code: "ERR_UNEXPECTED"`. In 2.0 each failure has its own [Error code](https://rn-watch-connect.vercel.app/docs/errors.md), such as `ERR_WATCH_NOT_REACHABLE` or `ERR_INVALID_TRANSFER_ID`. On platforms other than iOS, the code is `ERR_UNSUPPORTED_PLATFORM`. Messages changed too. Functions that throw directly now get Expo's prefix, "Calling the '…' function has failed → Caused by: …". These are `sendMessageWithoutReply`, `sendDataMessageWithoutReply`, `replyToMessage`, `replyToDataMessage`, `updateApplicationContext`, `transferFile`, `cancelUserInfoTransfer` and `cancelFileTransfer`. If you matched on message text, match on `code` instead: ```ts // Before (1.x) if (String(error).includes("not reachable")) { /* … */ } ``` ```ts import { type WatchConnectErrorCode } from "rn-watch-connect"; declare const error: unknown; // After (2.0) if ((error as { code?: WatchConnectErrorCode }).code === "ERR_WATCH_NOT_REACHABLE") { // … } ``` ### The reply error has a new message Replying twice, or after a Message stopped waiting, now fails with "Already replied, or the message is no longer waiting for a Reply" instead of "Invalid reply ID". Match on its code, `ERR_REPLY_NOT_PENDING`. ### `transferUserInfo` returns the full Transfer snapshot It now includes `userInfo`, the same shape as `outstandingUserInfoTransfers`. Its type is `OutstandingUserInfoTransfer`. The old name, `UserInfoTransfer`, is a deprecated alias, so existing code still compiles. ### Stricter TypeScript types These are type changes only, with no runtime effect: - `watchActivationState` is now `WatchActivationState` (`"notActivated" | "inactive" | "activated" | "unknown"`) instead of `string`. A comparison against a misspelled value now fails to compile. - `applicationContext`, `receivedApplicationContext`, and what `onMessageReceived`, `onMessageWithReply`, `onApplicationContextChanged` and `onUserInfoReceived` send are now `Record` instead of `any`. ### Fixes that may touch your code - **Status event keys.** `onReachabilityChanged`, `onWatchPairedChanged` and `onWatchAppInstallChanged` now send the documented keys `{ isWatchReachable }`, `{ isWatchPaired }` and `{ isWatchAppInstalled }`. If you read the old keys (`isReachable`, `isPaired`, `isAppInstalled`) as a workaround, switch to the documented ones. - **`watchActivationState` off iOS.** It's now `"unknown"` on platforms other than iOS, instead of `"notActivated"`. - **Received files.** They no longer overwrite each other. A taken name is numbered, as in `photo(1).jpg`. See [`onFileReceived`](https://rn-watch-connect.vercel.app/docs/file-transfer.md#onfilereceived). - **Transfer ids.** They're now unique for the whole app run, but still valid only for the current run. ## 4. Move off the deprecated reply methods (optional) `replyToMessage`, `replyToDataMessage` and the events' `replyId` still work, and will be removed in a future major version. Call the event's **Reply function**, `reply`, instead: ```tsx // Before (deprecated) useEventListener(RnWatchConnect, "onMessageWithReply", ({ message, replyId }) => { RnWatchConnect.replyToMessage(replyId, { ok: true }); }); ``` ```tsx import { useEventListener } from "expo"; import RnWatchConnect from "rn-watch-connect"; export function Responder() { // After useEventListener(RnWatchConnect, "onMessageWithReply", ({ message, reply }) => { reply({ ok: true }); }); return null; } ``` Data Messages work the same way: `({ data, reply }) => reply(base64String)` replaces `replyToDataMessage(replyId, base64String)`. See [Messages](https://rn-watch-connect.vercel.app/docs/messages.md#onmessagewithreply) and [Data Messages](https://rn-watch-connect.vercel.app/docs/data-messages.md#ondatamessagewithreply). ## 5. Use what's new (optional) - **Finish events.** [`onUserInfoTransferFinished`](https://rn-watch-connect.vercel.app/docs/user-info.md#onuserinfotransferfinished) and [`onFileTransferFinished`](https://rn-watch-connect.vercel.app/docs/file-transfer.md#onfiletransferfinished) fire exactly once per Transfer, when it's delivered, fails or is cancelled. - **[`onFileReceiveFailed`](https://rn-watch-connect.vercel.app/docs/file-transfer.md#onfilereceivefailed).** Tells you when a file from the Watch couldn't be kept. In 1.x it was lost without any event. - **The `reply` function** on `onMessageWithReply` and `onDataMessageWithReply` (see step 4). - **[`WatchConnectErrorCode`](https://rn-watch-connect.vercel.app/docs/errors.md#watchconnecterrorcode).** The TypeScript union of every Error code.