npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

customerio-expo-plugin

v3.11.0

Published

Expo config plugin for the Customer IO React Native SDK

Readme

Contributor Covenant

Customer.io Expo Plugin

This is the official Customer.io Expo plugin, supporting mobile apps.

The Expo plugin takes advantage of our React Native SDK, and requires very little setup. It extends the Expo config to let you customize the pre-build phase of managed workflow builds, which means you don't need to eject to a bare workflow.

After you add the plugin to your project, you'll need to install our React Native SDK and run pre-build. The plugin automatically generates and configures the necessary native code files required to make our React Native SDK to work on your project.

Getting started

You'll find our complete SDK documentation at https://customer.io/docs/sdk/expo.

Visual notification inbox accessibility labels

The SDK ships no text of its own in the visual notification inbox — the empty state is an icon and the loading state is a spinner — so accessibility labels are the one place a string is still needed. Apps supply their own through inApp.notificationInboxAccessibilityLabels when they call CustomerIO.initialize() from JavaScript:

CustomerIO.initialize({
  cdpApiKey: '...',
  inApp: {
    siteId: '...',
    notificationInboxAccessibilityLabels: {
      bell: t('inbox.bell'),
      bellWithUnreadCount: t('inbox.unread'), // e.g. "{count} unread notifications"
      loadingIndicator: t('inbox.loading'),
      emptyState: t('inbox.empty'),
    },
  },
});

bellWithUnreadCount is a template: {count} is replaced with the number of unread messages when the bell is announced. Spell it any other way and nothing is substituted, so the screen reader reads the text verbatim, braces included, and never says the count — the SDK warns in the JavaScript console when it spots that. That warning is a development-build diagnostic only — it is compiled out of release builds, so a malformed template ships silently.

These labels require JavaScript initialization. With native auto-initialization (a config block in the plugin options), the SDK is initialized before JavaScript loads, so a later CustomerIO.initialize() call is a no-op and the labels never reach the SDK. They are deliberately not exposed as plugin options: values in app.json are baked in at prebuild, which would ship a capability that only works for one locale.

No label falls back to English, but the elements do not all behave the same way when one is unset:

| Unset label | Result | | --- | --- | | bell | The bell is announced as an unnamed button. | | bellWithUnreadCount | Falls back to bell, so the button is still named — only the count goes unannounced. The badge itself is always hidden from screen readers, so the count is only ever spoken through this label. | | loadingIndicator | The spinner stops being an accessibility element: VoiceOver skips it, while TalkBack still reports Android's underlying progress role. | | emptyState | The empty-state icon is hidden from assistive technologies. |

So an auto-initializing app gets no configured labels, which is not the same as every element becoming unlabeled.

In-app message color scheme

In-app messages follow the device's light or dark appearance by default. An app with its own appearance setting — one that can disagree with the operating system — tells the SDK which variant to render with inApp.colorScheme:

import { CioColorScheme, CustomerIO } from 'customerio-reactnative';

CustomerIO.initialize({
  cdpApiKey: '...',
  inApp: {
    siteId: '...',
    colorScheme: CioColorScheme.Dark, // Auto (default) | Light | Dark
  },
});

Because an appearance setting can change while the app is running, the scheme can also be changed at any time:

CustomerIO.inAppMessaging.setColorScheme(CioColorScheme.Light);

That takes effect immediately — messages already on screen, inline views included, are re-themed in place, so it can be called straight from the app's own appearance toggle.

Unlike the inbox accessibility labels above, native auto-initialization only affects the config option, not the setter — provided the plugin config sets siteId. With a config block in the plugin options the SDK starts before JavaScript loads, so inApp.colorScheme never arrives, while setColorScheme() reaches the already-initialized SDK and works normally. Such an app can therefore still pin a variant by calling it once after startup.

That depends on config.siteId, because the plugin only adds the in-app messaging module when one is present. Without it there is no module for either path to reach: the config option is dropped as above, and the setter logs that in-app messaging is unavailable and leaves the scheme unchanged.

Whichever light and dark variants the message renders come from the Customer.io editor. A message authored with a single style looks the same under every scheme.

Scene deep links with native auto-initialization

When using Expo's scene lifecycle with Customer.io native auto-initialization, register your React Native Linking URL listener and then call CustomerIO.setDeepLinkRoutingReady(). This lets the plugin deliver URLs buffered during cold launch without requiring a second SDK initialization from JavaScript.

Customer.io notification deep links are delivered to that listener as url events after readiness; they are not returned by Linking.getInitialURL(). If readiness is not signaled within ten seconds, Customer.io falls back to opening the destination through the system.

Expo SDK 58 and later deliver cold-start URLs through SceneDelegate, so handleDeeplinkInKilledState is not injected for those versions. The scene router replaces that legacy AppDelegate launch-options workaround.

Live Activity links in Expo scene apps

Expo Router apps using the scene lifecycle must process Live Activity URLs once in a top-level app/+native-intent.tsx file:

import { CustomerIO } from 'customerio-reactnative';

export async function redirectSystemPath({ path }: { path: string }) {
  return CustomerIO.liveActivities.handleWidgetUrl(path);
}

The helper reports the opened event and returns the customer's destination before Expo Router navigates. Ordinary URLs pass through unchanged, and a Customer.io tracking URL without a destination returns null. Do not also call the helper from a Linking listener because processing the same tracking URL twice reports two opened events.

Expo apps without Expo Router should apply the same helper exactly once in their central initial-URL and URL-subscription pipeline. The plugin does not edit customer-owned routing files.

After disabling Live Notifications in a previously generated iOS project, run npx expo prebuild --clean --platform ios. An incremental prebuild stops with this instruction so it cannot leave the generated widget target or linked Live Activities code behind.

Local development

Here is a quick start guide to start with local development.

Contributing

Thanks for taking an interest in our project! We welcome your contributions.

We value an open, welcoming, diverse, inclusive, and healthy community for this project. We expect all contributors to follow our code of conduct.

License

MIT