@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.
Maintainers
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-contextEvery 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 unavailableA 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 closedA 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. WhenLinking.canOpenURLsays no Play Store app can take it (emulators, some builds), it opens thehttps://play.google.com/store/apps/detailslisting 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).
