@yaps/support-core
v2.0.0
Published
Platform-agnostic client core for Support Yaps: API client, realtime socket, outbox, message store, appearance and push helpers.
Maintainers
Readme
@yaps/support-core
The platform-agnostic client behind the Support Yaps React Native SDK and web widget. It has no
DOM or React Native imports; platforms plug in storage, fetch, a WebSocket constructor, file
reading, URL and settings opening through a PlatformAdapter. App developers use
@yaps/support-react-native or @yaps/support-web, not this package directly.
What's inside, following protocol/PROTOCOL.md:
| Module | |
|---|---|
| ApiClient | Every /v1/sdk/* call, the error model (SupportError with code, status, retryAfter), retries with 1 s, 2 s, 4 s… backoff capped at 60 s, Retry-After on 429, appearance with If-None-Match. |
| SocketClient | hello with the last seq on every open (resume), 25 s pings with a pong timeout, reconnect at 1, 2, 4, 8, 16, 30 s ±20 %, refused after three failed opens (polling fallback). Unknown frames are ignored. |
| Outbox | ULID client IDs, persist-then-send, one message at a time in order, retry with backoff, failed after 5 attempts or a permanent 4xx, dedupe by client ID. |
| MessageStore | Merges by id, orders by seq, keeps client-ID placeholders until the server confirms them (rows keep a stable key), delivery and read state. |
| SupportClient | The session: registration, identity and metadata sync (only changed fields are sent), paywalls and screens, the chat lifecycle, typing throttling, read receipts, unread count, uploads, cards, track, push routing, logout and deletion. |
| Helpers | displayText (translation rule), linkSegments (http(s) links in message text), parseSupportPush, resolveAppearance (remote > local > default), PaywallRegistry (availability with timeouts), validateScreens, t() strings in 26 languages, attachment checks and the downscale plan. |
Using it on a new platform
import { SupportClient, MemoryStorage } from "@yaps/support-core";
const client = new SupportClient({
appId: "app_123",
platform: {
platform: "web",
sdk: "my-platform/1.0.0",
device: () => ({ language: "en" }),
systemLanguage: () => "en-GB",
storage: new MemoryStorage(),
WebSocket, // optional: omit to poll
readAttachment: async (a) => blobFor(a), // bytes to upload
openUrl: (url) => open(url),
openSettings: () => openAppSettings(), // optional: omit where there is none (the web)
copyText: async (text) => copy(text), // optional: resolves false when it couldn't
},
});
client.start();
const detach = client.attach(); // a chat view is on screen
client.subscribe((state) => render(state)); // items, connection, typing…
client.send("Hello");Cards
Cards arrive resolved for the device (PROTOCOL.md §5). cardState(card, now) is a pure helper
both UIs share; client.cardState(message) calls it with the paywall registry for offer cards,
the registered screens and onScreen for screen link cards, and openSettings for settings
link cards.
| kind | Card | Render |
|---|---|---|
| available | any | The button works. Code cards: Copy always, Redeem only when url is set. |
| installed | app | "You already have it", no button. |
| unavailable | review, offer, link | "No longer available", disabled button. |
| unavailable + redeemOn | code | Another platform: the code with Copy and redeemHint(lang, redeemOn) ("Redeem on iPhone"), no Redeem. |
| expired | code | Past expiresAt: code struck through, "Expired", no buttons. |
| text | unknown type, app with no store link here, code without a code | The message text only. |
client.openCard(message): the main button. Review opensurl, offer callsonPaywall, app promo and code openurlthroughopenUrl. Link cards openurlthroughopenUrl, the app's settings page throughopenSettings, or close the SDK-presented chat (closeChat) and callonScreen(screenId). EmitsreviewOpened,paywallOpened,appPromoOpened,codeOpenedorlinkOpenedand sendsPOST /v1/sdk/actions/:messageId {"action":"open"}.client.copyCode(message): Copy on a code card. Puts the code on the clipboard withcopyText, emitscodeCopied, sends{"action":"copy"}and resolves whether the clipboard took it (onfalse, let the user copy by hand). Expired codes can't be copied.cardView(card, state, lang): what a card shows (main button label, Copy, the status line, the spelled-out code label), so both UIs render the same thing.spellCode("STUDENT20")→"S T U D E N T 2 0"for accessibility labels.
| Event | Fields |
|---|---|
| appPromoOpened | messageId, url, promotedAppId? |
| codeCopied | messageId, code, campaignId? |
| codeOpened | messageId, code, url, campaignId? |
| linkOpened | messageId, target (url, app_settings or screen), url?, screenId?, linkId? |
Link cards
{ "type": "link", "buttonLabel", "target", "url"?, "screenId"?, "linkId"? } draws like an offer
card: the message text, then one button. The button is available when the server says
available and:
url: the URL ishttps:,mailto:,tel:orsms:(isAllowedLinkUrl); anything else is refused whatever the server sent.app_settings: the platform hasopenSettings(never on the web).screen:screenIdis inclient.screensandclient.onScreenis set.
client.screens = [{ id: "edit_profile", title: "Edit profile" }] registers the host app's
screens (≤ 50, id 1–64 characters of [A-Za-z0-9_.-], title ≤ 60; invalid and duplicate entries
are dropped with a warning). They're reported in PATCH /v1/sdk/device screens after
registration and whenever they change, like paywalls. Unlike paywalls there's no availability
callback.
Links in message text
linkSegments(text) splits message text into plain runs and http(s) links (no trailing
punctuation or closing bracket, no Markdown or HTML; email addresses stay text). Both UIs use it,
with the same pattern as the Android SDK.
Built-in strings for the cards ("Get it", "Redeem", "Copy", "Copied", "You already have it", "Expired", "Redeem on iPhone / Android / the web", "Select the code to copy it") ship in all 26 languages.
Tests
npm test -w @yaps/support-coreThe suite decodes every file in protocol/fixtures/ and drives the socket with a fake WebSocket
(reconnect, resume, ping timeouts, refusal) and the client with a fake server.
The package builds to dist/ with tsc; scripts/inline-protocol.mjs copies the protocol type
declarations into dist/_protocol so the published package doesn't depend on the private
@support-yaps/protocol workspace.
