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

@yaps/support-react-native

v2.0.0

Published

Support Yaps for React Native and Expo: in-app support chat in pure TypeScript. Works in Expo Go.

Readme

@yaps/support-react-native

In-app support chat for React Native and Expo, in pure TypeScript. There's no native code, so it runs in Expo Go, in development builds and in bare React Native apps, on iOS and Android.

The chat follows your app's look: colours, font and corners come from your Appearance (or the one you publish in the inbox), not ours.

Install

npx expo install @yaps/support-react-native expo-secure-store @react-native-async-storage/async-storage
# Optional: photos, camera, files, downscaling and push
npx expo install expo-image-picker expo-document-picker expo-image-manipulator expo-notifications expo-clipboard
# Optional: exact safe-area insets
npx expo install react-native-safe-area-context

Every extra package is optional and detected at runtime:

| Package | Used for | Without it | |---|---|---| | expo-secure-store | Device token in the Keychain / Keystore | AsyncStorage, or memory | | @react-native-async-storage/async-storage | Offline thread cache and outbox | Memory (warned once): each launch starts fresh | | expo-image-picker | Photo library and camera | No photo options in the attach menu | | expo-document-picker | PDF, TXT, ZIP, LOG files | No file option | | expo-image-manipulator | JPEG ≤ 2,048 px and ≤ 1 MiB before upload | Photos upload as picked | | expo-notifications | Push helpers in @yaps/support-react-native/expo-notifications | Forward tokens yourself | | expo-clipboard | Copy on code cards (included in Expo Go) | React Native's built-in Clipboard; if that's missing too, the code is selectable and the card says "Select the code to copy it" | | react-native-safe-area-context | Exact insets | Platform defaults |

Metro treats require calls inside try/catch as optional, so a missing package never breaks the bundle.

Quick start

import { YapsSupport, YapsSupportProvider, useUnreadCount } from "@yaps/support-react-native";

// Once, at startup. The App ID is public; copy it from the inbox.
YapsSupport.configure({ appId: "app_YOUR_APP_ID" });

export default function App() {
  return (
    <YapsSupportProvider>
      <Navigation />
    </YapsSupportProvider>
  );
}

function SupportRow() {
  const unread = useUnreadCount();
  return <SettingsRow title="Support" badge={unread || undefined} onPress={() => YapsSupport.openChat()} />;
}

<YapsSupportProvider> hosts the chat: openChat() presents it as a sheet (iOS) or full-screen modal (Android) and closeChat() dismisses it. Put it inside your SafeAreaProvider if you have one.

To embed the chat in your own screen instead, render it in a container with a height:

import { SupportChat } from "@yaps/support-react-native";

<SupportChat style={{ flex: 1 }} showHeader={false} />;

Before configure, identify, setMetadata and setPushToken are queued, and settings (appearance, usesRemoteAppearance, languageOverride, onEvent, paywalls, onPaywall, paywallAvailability, screens, onScreen) and listeners are kept; all apply when it runs. openChat logs an error, track and handlePushNotification are dropped (resolving or returning false), and logout, deleteData and refreshPaywalls do nothing.

API

| | | |---|---| | configure({ appId, baseUrl?, logLevel?, appVersion?, appBuild?, bundleId? }) | Once at startup. baseUrl defaults to https://support.yaps.ai. | | identify({ userId, name, email }) / identify({ token }) | Labels, or a verified identity (below). | | setMetadata(record) | Replaces the whole dictionary (≤ 30 entries, string values). | | logout() | On sign-out. Server history is kept; the next session starts fresh. | | deleteData() | Deletes this device's support data. Throws SupportError on failure. | | openChat() / closeChat() | Present or dismiss the sheet. | | useUnreadCount() / useChatOpen() | Hooks. Outside React: unreadCount, isChatOpen, addUnreadCountListener, addChatOpenListener. | | appearance | Your theme (below). usesRemoteAppearance = false ignores the inbox's published look. | | languageOverride | Only if your app has its own language picker. null follows the device. | | onEvent | chatOpened, chatClosed, messageSent, imageSent, fileSent, messageReceived, messageFailed, reviewOpened, paywallOpened, appPromoOpened, codeCopied, codeOpened, linkOpened, identityRejected, loggedOut, dataDeleted. | | paywalls, onPaywall, paywallAvailability, availablePaywalls, refreshPaywalls() | Offer cards (below). | | screens, onScreen | Link cards that open a screen in your app (below). | | track(name, { opensChat }) | Reports a moment for automations. Never throws. | | setPushToken(token, "apns" \| "fcm", environment?) | The native token, not an Expo push token. | | handlePushNotification(data, { openChat }) | true for Support pushes, false for anything else. |

Appearance

Pass your app's existing theme values; leave out anything your theme doesn't have.

YapsSupport.appearance = {
  accentColor: { light: theme.light.accent, dark: theme.dark.accent },
  onAccentColor: { light: "#FFFFFF", dark: "#000000" },
  chatBackground: { light: theme.light.background, dark: theme.dark.background },
  incomingBackground: { light: "#F2F2F4", dark: "#1C1C1F" },
  incomingForeground: { light: "#111113", dark: "#EDEDEF" },
  fontFamily: "Inter",          // a font your app has loaded
  cornerStyle: "round",         // round | soft | square
  title: "Help",                // or { en: "Help", es: "Ayuda" }
};

Precedence for every field: the appearance published in the inbox, then yours, then the SDK default. A newly published appearance applies the next time the chat opens, never mid-chat. Dark mode follows the system unless the appearance sets colorScheme. Built-in strings ship in 26 languages, including Arabic and Hebrew with right-to-left layout.

Identity verification

Labels (userId, name, email) are unverified. To verify a user, your server signs a JWT (HS256) with the app's identity secret (ids_…, in the inbox) containing user_id and exp, and the app passes it on:

const token = await api.getSupportToken(); // from your backend
if (auth.currentUserId === userIdWhenRequested) YapsSupport.identify({ token });

Never put the identity secret in the app. A verified customer is one person across their devices in this app (App ID): every device that presents a token for the same user_id shares one conversation. Your other apps keep their own; the inbox shows them under "Also uses". Refresh the token when you see identityRejected with error.code === "identity_token_expired"; the chat keeps working unverified meanwhile. Call logout() before identifying a different account.

Push

Support sends pushes with your own APNs key and Firebase project, straight to the device's native token. Push needs a development or store build (Expo Go can't receive your app's pushes); the chat itself works in Expo Go.

import * as Notifications from "expo-notifications";
import { startSupportPush, supportNotificationBehavior } from "@yaps/support-react-native/expo-notifications";

Notifications.setNotificationHandler({
  handleNotification: async (n) =>
    supportNotificationBehavior(n) ?? { shouldShowBanner: true, shouldShowList: true, shouldPlaySound: true, shouldSetBadge: true },
});

// In your root component, after YapsSupport.configure:
useEffect(() => startSupportPush(), []);

startSupportPush() never asks for permission. Once your own prompt has been granted it forwards getDevicePushTokenAsync() (apns on iOS, fcm on Android), keeps it up to date, refreshes the unread count when a Support push arrives, and opens the chat when one is tapped, including the tap that launched the app. Call refreshSupportPush() right after your permission prompt. The APNs environment is sandbox in __DEV__ and production otherwise; override it with startSupportPush({ environment }). startSupportPush({ updatesAppBadge: true }) also keeps the app icon badge equal to the Support unread count (off by default), updatesAppBadge: false also stops Support pushes setting the iOS badge, and openChatOnTap: false leaves notification taps to you.

Using another push library? Forward the native token with YapsSupport.setPushToken(token, "fcm") and pass incoming data to YapsSupport.handlePushNotification(data, { openChat: wasTapped }).

Paywalls

Your team can send an offer card. Tapping it calls your code, so purchases stay in your app:

YapsSupport.paywalls = [{ id: "yearly_offer", title: "Yearly 40% off" }]; // ≤ 20; the title shows on the offer card
YapsSupport.onPaywall = (id) => showPaywall(id);                           // present it over the chat
YapsSupport.paywallAvailability = async (id) => canShowPaywall(id);        // optional; errors and 3 s timeouts count as unavailable

A card is tappable only when its paywall is available and onPaywall is set; otherwise it reads "No longer available". After a purchase, call YapsSupport.refreshPaywalls({ invalidate: true }). Review cards open the store's write-review page; they never call the in-app review API.

Link cards and screens

Your team can send a link card: the message with one button, drawn like an offer card. It opens a web page, an email, phone or text message link (https:, mailto:, tel:, sms: only) with Linking.openURL, your app's page in the system settings with Linking.openSettings(), or a screen in your app. Register the screens your team can send:

YapsSupport.screens = [{ id: "edit_profile", title: "Edit profile" }]; // ≤ 50; titles are for your team
YapsSupport.onScreen = (id) => navigation.navigate(id);                // the chat has already closed

A screen card is tappable only when its screen is in screens and onScreen is set; otherwise it reads "No longer available", like an offer card. Tapping it closes the chat if <YapsSupportProvider> presented it, then calls onScreen. Screens are reported to the inbox like paywalls, after configure and whenever they change.

| Event | Fields | |---|---| | linkOpened | messageId, target (url, app_settings or screen), screenId?, linkId? |

linkOpened and the open receipt follow a successful open (Linking resolved, or onScreen was called); a link that fails to open sends neither. Links in message text (http/https) are underlined in the bubble's colour and open with Linking.openURL; email addresses stay plain text.

App promo and code cards

Your team can promote another of your apps or send a discount code. Both arrive resolved for the device, so there's nothing to set up.

  • App promo: the app's icon, name and pitch with Get it, which opens the App Store page on iOS and market://details?id=… on Android. When Linking.canOpenURL says no Play Store app can take it (emulators, some builds), it opens the https://play.google.com/store/apps/details listing instead. If the customer already uses the app the card says "You already have it" and has no button; with no store link for this platform only the message text shows.
  • Code: the code in a monospaced box with Copy and Redeem. Copy uses expo-clipboard (see the table above). Redeem opens Apple's offer-code sheet with the code filled in, or Google Play's redeem page. A code for another platform shows Copy and "Redeem on iPhone" / "Redeem on Android" / "Redeem on the web" without Redeem. Past its expiry date the code is struck through with "Expired" and no buttons, and a code on screen turns expired when the time comes.

| Event | Fields | |---|---| | appPromoOpened | messageId, url, promotedAppId | | codeCopied | messageId, campaignId | | codeOpened | messageId, campaignId |

Each tap also sends the action receipt (open for Get it and Redeem, copy for Copy) so your team sees it in the inbox. Screen readers hear the code spelled out ("Code: S T U D E N T 2 0"). The cards follow the chat's light or dark appearance and right-to-left layout; the code itself always reads left to right. Button and status strings come in all 26 built-in languages.

Example

example/App.tsx is a minimal Expo app screen. To try it, create an app with npx create-expo-app support-demo, install this package and the optional packages above, and copy the file over App.tsx. Point baseUrl at your machine's LAN IP to use a local server (http://192.168.1.20:8787).