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

@thg-agentic/chatbot-sdk-react-native

v0.12.1

Published

React Native SDK for the THG AI Shopping Assistant — exports a single <Chatbot /> component that host apps can mount however they like.

Readme

@thg-agentic/chatbot-sdk-react-native

React Native SDK for the THG AI Shopping Assistant. Exports a single <Chatbot /> component that you mount wherever makes sense in your app — full screen, modal, drawer, bottom sheet.

Install

npm install @thg-agentic/chatbot-sdk-react-native

Required peer dependencies:

npm install react@>=18 react-native@>=0.72 react-native-svg

SSE streaming on React Native: the SDK uses fetch + ReadableStream for AG-UI streaming. Bare RN 0.72+ supports this on iOS, but Android can need a polyfill. Install react-native-polyfill-globals and add import 'react-native-polyfill-globals/auto'; to your app entry if you see "response.body is undefined" errors.

Optional peers (per feature)

| Feature | Peer dependency | What you get | |---|---|---| | Persistent session/conversation across launches | @react-native-async-storage/async-storage (async) or react-native-mmkv (sync) | Conversation history survives app restart | | Voice-to-text | @react-native-voice/voice | Mic button + live transcript | | Image upload | expo-image-picker (preferred) or react-native-image-picker | Attach button + base64 upload | | iOS Health activity connector | react-native-health | Lets the agent request a consent-gated Apple Health activity summary for fitness-informed Myprotein guidance |

If a peer isn't installed, the corresponding feature is silently hidden — no errors, no setup gymnastics.

iOS Health activity

The Health connector is opt-in at three layers:

  1. The backend /v1/config capability must return healthActivity: true for the channel.
  2. The native app must install and configure react-native-health with the required iOS HealthKit entitlement and NSHealthShareUsageDescription.
  3. The native app must pass a health.readActivity callback. The SDK declares get_health_activity only when both the backend capability and callback are present.
import { Chatbot } from '@thg-agentic/chatbot-sdk-react-native';
import {
  createAppleHealthKitActivityProvider,
  requestAppleHealthKitActivityAuthorization,
} from '@thg-agentic/chatbot-sdk-react-native/health/ios';

const health = createAppleHealthKitActivityProvider({
  defaultLookbackDays: 14,
  includeManuallyAdded: false,
});

// Recommended from a user action, e.g. a "Connect Apple Health" button.
await requestAppleHealthKitActivityAuthorization();

<Chatbot
  config={{
    channel: 'myprotein',
    locale: 'en-GB',
    apiUrl: 'https://api.thg.dev/agentic-commerce/shopping-assistant/ag-ui',
    apiKey: '<per-storefront-key>',
    health,
  }}
/>;

The adapter asks HealthKit for read access to step count, active energy, walking/running distance, workouts, and sleep analysis. It returns a bounded aggregate snapshot rather than raw samples, and partial permission/read failures are reported in the snapshot so the agent can explain what is missing. If the host does not preflight authorization, the adapter still calls HealthKit authorization when the agent first invokes get_health_activity; preflighting just makes the consent sheet explicit and easier to test.

iOS does not provide a supported deep link to an app's Health data-type permissions. If a user needs to review or change access after the first prompt, guide them to Health app > profile picture > Apps > your app, or Settings > Health > Data Access & Devices > your app.

Use

import { useEffect, useState } from 'react';
import { Chatbot } from '@thg-agentic/chatbot-sdk-react-native';
import type { StorageAdapter } from '@thg-agentic/chatbot-sdk-react-native';
import { createAsyncStorageAdapter } from '@thg-agentic/chatbot-sdk-react-native/storage/async-storage';

export function ChatScreen({ navigation }) {
  const [storage, setStorage] = useState<StorageAdapter | null>(null);

  useEffect(() => {
    const adapter = createAsyncStorageAdapter();
    let mounted = true;

    adapter.ready().then(() => {
      if (mounted) setStorage(adapter);
    });

    return () => { mounted = false; };
  }, []);

  if (!storage) return null;

  return (
    <Chatbot
      config={{
        channel: 'myprotein',
        locale: 'en-GB',
        apiUrl: 'https://api.thg.dev/agentic-commerce/shopping-assistant/ag-ui',
        apiKey: '<per-storefront-key>',
        theme: {
          primaryColor: '#003942',
          accentColor: '#003942',
        },
        content: {
          headerTitle: 'Fuel Coach',
          welcomeMessages: ['Hi! Ask me about anything from our range.'],
        },
        features: {
          showSuggestions: true,
          showFeedback: true,
          imageUpload: true,
          // In production this is overlaid from /v1/config; shown here only
          // for local mocks that bypass the config bootstrap.
          healthActivity: true,
        },
        health: createAppleHealthKitActivityProvider(),
      }}
      storage={storage}
      onSettingsPress={() => navigation.navigate('Personalisation')}
      onProductPress={(product) => navigation.navigate('PDP', { sku: product.sku })}
      onError={(code, message) => console.warn('[Chatbot]', code, message)}
    />
  );
}

Props

| Prop | Type | Notes | |---|---|---| | config | ChatbotConfig | Required. Same shape as the web SDK. | | storage | StorageAdapter | Optional. Defaults to in-memory (resets on reload). | | onProductPress | (p: CarouselProduct) => void | Override default product navigation. Without an override, the SDK opens http(s) product URLs with Linking.openURL; relative catalog paths are resolved against config.siteUrl. With one, the SDK records sponsored clicks itself — see Sponsored cards: links, clicks and SKUs. | | onError | (code, message) => void | Fires for every chat-stream error. | | onSettingsPress | () => void | Optional. Renders a header control that lets the host open its own memory/connectors/personalisation screen. | | onMemoryConsentChange | (detail: MemoryConsentChangedDetail) => void | Required (with onSettingsPress) to show the in-chat memory opt-in card — the SDK won't offer an opt-in it can't persist or that the user can't reverse. Fires on accept/decline so the host persists the decision; the SDK reflects it for the session (badge + per-request memory_settings). | | style | ViewStyle | Applied to the outer container. | | onActionPress | (actionId: string) => void | Called when the shopper taps a {{action:id\|Label}} placeholder in assistant copy — content asking the host to do something in-app (open support, show settings). Receives the bare id. Separate from onLinkPress, so an action isn't rendered with the external-link affordance. Without a handler the label still renders as inert text, never as raw {{action:…}}. | | voiceEnabled | boolean | Defaults to true. The host's veto on voice input, which is a different question from whether the platform supports it — pass false when your app ships without the speech permission, or simply doesn't want a mic in its assistant. | | keyboardBehavior | 'padding' \| 'height' \| 'position' \| null | Defaults to the platform idiom (padding on iOS, height on Android). Override when the default fights your navigation container — Android's height mode can leave the input bar under the keyboard. null disables keyboard avoidance entirely. |

The ChatbotConfig shape covers theme colors, content/i18n strings, feature flags, analytics callbacks, and session/customer identifiers. See @thg/chatbot-sdk-core's types.ts for the full interface.

Theming (multi-brand)

The UI is fully theme-driven — there are no brand colours baked into the components. Every brand (Myprotein, Lookfantastic, Cult Beauty, …) ships the same SDK and restyles it entirely through config.theme:

| Token | Drives | |---|---| | primaryColor | header title + icons, filled CTAs ("Turn on memory", send button when active), suggestion-chip outline, settings toggles | | primaryTextColor | text/icon colour on primaryColor fills | | accentColor | the sparkle mark, the active input-pill border, the voice waveform, review stars | | textColor | body + answer text | | textMutedColor | disclaimer, placeholder, review counts, struck-through RRP | | surfaceColor | raised neutral fills — user bubble, input pill, header icon circles, "memory on" badge (defaults to light grey; set it for dark brands) | | backgroundColor | chat background | | borderColor | dividers, card outlines | | errorColor | discount text + (with priceFormat: 'default') the "(N% off)" tag | | radius | base corner radius | | sparklesSvg | optional override for the header/suggestions sparkle glyph |

Anything left unset falls back to a neutral default, so a minimal { primaryColor, accentColor } already produces a coherent look. The example app ships three brand presets (config.ts) selectable with EXPO_PUBLIC_CHATBOT_BRAND=lookfantastic to demonstrate that the identical screens restyle per brand.

Internationalisation

i18n works out of the box. All user-facing copy is resolved through @thg/chatbot-sdk-core's label() against the locale pack chosen by config.locale (31 built-in languages, including the cross-session memory copy). Pass locale: 'fr-FR' (etc.) and the header, memory consent card, suggestions, feedback, errors and thinking states all render translated — no per-brand string tables required. Host overrides go in config.content (merged on top of the locale pack), so a brand can sharpen a few strings (e.g. the memory examples) without losing translations for everything else.

Storage adapters

The SDK uses a small, synchronous StorageAdapter contract:

interface StorageAdapter {
  getItem(key: string): string | null;
  setItem(key: string, value: string): void;
  removeItem(key: string): void;
}

Three built-in factories:

import { InMemoryStorageAdapter } from '@thg-agentic/chatbot-sdk-react-native';
import { createAsyncStorageAdapter } from '@thg-agentic/chatbot-sdk-react-native/storage/async-storage';
import { createMMKVStorageAdapter } from '@thg-agentic/chatbot-sdk-react-native/storage/mmkv';
  • InMemoryStorageAdapter — default. Conversation resets on reload.
  • createAsyncStorageAdapter() — async backend, sync façade via an in-memory cache pre-warmed at construction. Call await adapter.ready() before mounting <Chatbot /> so the first render reads the persisted session and conversation.
  • createMMKVStorageAdapter() — fully synchronous, recommended if you're already using MMKV in your app.

You can also implement StorageAdapter against your own store (SecureStore, Keychain, etc.).

Analytics

The SDK reports its own funnel to the backend's first-party /v1/events endpoint, which lands in the ecommerce_events BigQuery table the Looker reporting model reads. This is the same transport and the same row shape the web widget uses (both share AnalyticsClient in @thg/chatbot-sdk-core), so app and browser sessions are comparable rather than merely adjacent.

Nothing to wire — mounting <Chatbot> starts it. What it emits:

| Event | When | | --- | --- | | assistant_open / assistant_close | <Chatbot> mounts / unmounts (close carries duration_ms) | | assistant_product_click | a product card is tapped | | product_impression | cards are shown in a settled answer (carries card_position) | | add_to_cart → add_to_cart_success / add_to_cart_failure | the in-chat add-to-basket lifecycle | | suggestion_chip_click | a follow-up chip is tapped | | product_pills_impression / product_pill_click | <ProductPills> renders / is tapped | | assistant_link_click | an in-message link is tapped |

Rows are stamped event_source: native_ios | native_android (the web widget stamps web), which is the dimension reporting splits platforms on.

The two things only the host can report

import {
  ASSISTANT_EVENTS,
  trackAssistantEvent,
  trackSiteEvent,
} from '@thg-agentic/chatbot-sdk-react-native';

// 1. Reach — "the shopper could get to the assistant". The entry point is
//    yours (a tab, a header button), so the SDK cannot see it. Without this the
//    funnel has no top and "opened" has nothing to be a rate against.
trackAssistantEvent(ASSISTANT_EVENTS.IMPRESSION);

// 2. Outcomes in your OWN UI, outside the assistant. Without these the
//    assistant's measured contribution stops at the conversation and never
//    reaches the conversion it influenced.
trackSiteEvent('add_to_cart', { product_sku: '12345', value: 24.99, currency: 'GBP' });
trackSiteEvent('checkout_started', { value: 62.5, item_count: 3, currency: 'GBP' });

Both are safe to call before the assistant mounts: they buffer until something is mounted to send them. Mount <AnalyticsRoot> once near your app root so that is always true. Without it, a shopper who never opens the chat or a product page with pills never sends theirs.

import { AnalyticsRoot } from '@thg-agentic/chatbot-sdk-react-native';

// Same config and storage adapter as <Chatbot>.
<AnalyticsRoot config={config} storage={storage} />

Pass it the same storage adapter as <Chatbot>: it posts under the chat's session, which a <Chatbot> left on its default storage keeps to itself. It waits for an AsyncStorage adapter's ready() on its own, so unlike <Chatbot> and <ProductPills> it can mount before the preload finishes.

An add the assistant made is not double-counted when your basket code reports it back. Include product_sku or variant_sku: the echo is recognised by SKU.

Joining pills to the conversation

Pass <ProductPills storage={...}> the same storage adapter as <Chatbot>. That is what makes "tapped a pill → opened the assistant → asked something" one session. Without it pill engagement is still counted, but cannot be joined to the chat that followed.

Consent and opting out

config.analytics.eventConsentState (a value or a getter) is forwarded on every batch; the backend decides what to do with it, so consent policy lives in one place rather than drifting per platform. config.analytics.eventsEnabled: false switches the transport off entirely.

Retail media (sponsored products)

Sponsored products are blended into the carousels by the backend; the SDK renders them with a "Sponsored" label and reports them to the ad network. It works exactly like the web SDK, driven by the same per-channel settings in the Assistant Console (Retail media section):

| Console setting | What it names | |---|---| | Visitor id cookie | the first-party cookie that holds the ad-network visitor id | | Ad consent cookie + category (+ format) | the CMP cookie, and the category within it that governs advertising | | Ad consent default | what to do when nothing answers (deny unless set) |

On web the SDK reads those cookies from document.cookie. In an app it reads the native cookie jar instead — the same store, for the same site (config.siteUrl), that the app writes — through the optional peer @react-native-cookies/cookies:

npm install @react-native-cookies/cookies   # skip if the app already has it

So an app that already keeps its first-party cookies in the jar writes no retail-media code at all, exactly like a storefront. The jar is re-read on mount, on every return to the foreground, and before every chat turn, so a consent change takes effect on the next turn.

Through the jar alone, consent needs a real signal: the consent cookie must be present and say yes. The Console default never grants on its own here, and until the jar has been read once, nothing is granted. The default applies only when you state consent yourself with getAdConsent — getCookie is just a cookie source, the same as the jar.

If your cookies live somewhere else (MMKV, AsyncStorage, a session object), point the SDK at them with one bridge instead — it outranks the jar:

const config: ChatbotConfig = {
  // Return the CURRENT value of the named cookie, synchronously. The Console
  // decides which names are asked for.
  getCookie: (name) => myCookieStore[name],
};

That is the whole integration. Nothing here is specific to one retailer: an app is onboarded by naming its own cookies in the Console, on any CMP the parser supports (onetrust, didomi, cookiebot, token-list, json-path).

Consent is resolved in web's order — your explicit answer, then the Console-named cookie, then a well-known CMP cookie (OneTrust OptanonConsent / actualOptanonConsent, Cookiebot CookieConsent), then the Console default — and it is fail-closed: with no signal, no beacon fires and no visitor id is sent. Ads still render; they just aren't reported.

Two decisions, kept separate, as on web: firing a beacon is device access (IAB TCF purpose 1), forwarding the visitor id is profiling (purposes 3+4). A shopper can permit the first and refuse the second.

Optional overrides

Only if a cookie can't express it. Both outrank the Console settings.

// Your CMP's answer, stated directly. A bare boolean means both decisions.
getAdConsent: () => ({ storage: true, personalisation: true }),

// A visitor id no cookie holds. Must be the SAME id your other ad surfaces
// send — a per-surface id fragments attribution, which is worse than none.
getRetailerVisitorId: () => visitorId,
import { resetSponsoredImpressions } from '@thg-agentic/chatbot-sdk-react-native';

// Optional: start a fresh impression window, e.g. on a new product screen.
resetSponsoredImpressions();

setBeaconConsentGate is still exported for apps written against 0.10/0.11. Once you call it — with a gate, or with null to fail closed — the gate is yours for the session: the SDK never replaces it, and the cookie jar and getCookie are no longer read for consent. getAdConsent, if you also provide it, still governs the visitor id — exactly as in 0.11.

eventSource needs no configuration: the SDK stamps native_ios / native_android, which is what selects the app's ad placement.

Sponsored cards: links, clicks and SKUs

Clicks are recorded for you. Route a press however your app routes — by SKU to a native screen, as in the example above, or by URL. The SDK reports the click to the ad network on the press (consent-gated, like every beacon); do not fire anything yourself.

url is the product page in an app. The backend sends a native app the plain product page as a sponsored card's url. A card can still carry the ad network's click redirect (urlIsClickTracked: true) — on an older backend, or in a web view — so never put that into an in-app router: it is not a page on your site. productUrl, when present, is always the plain product page.

sku is the advertised variant. On a sponsored card sku is the exact shade or size the advertiser bid on, not necessarily the product's master SKU (on LOOKFANTASTIC, about one sponsored impression in six). Organic cards differ: there sku is the master and defaultVariantSku the variant. If your product screen takes only master SKUs, resolve the variant to its product, or route by productUrl.

What's not in this package

The web SDK has FAB triggers, popup/sidebar layouts, drag/resize, and a launcher hub for multiple assistants. None of that is here — the host app owns presentation. If you need a multi-assistant entry point, build your own screen that decides which assistantId to pass.

Example app

A working Expo example lives at ./example. After pnpm install at the repo root:

cd packages/chatbot-sdk/react-native/example
pnpm ios               # or: pnpm android, pnpm web, pnpm start

Metro is set up for the monorepo, so edits to this package (and to @thg/chatbot-sdk-core) hot-reload without any extra build step. See example/README.md for backend wiring and notes on which features run in Expo Go vs. need a dev build.

Local development

This package lives in the THG chatbot-adk monorepo. From the repo root:

pnpm install
pnpm --filter @thg-agentic/chatbot-sdk-react-native typecheck
pnpm --filter @thg-agentic/chatbot-sdk-react-native test
pnpm --filter @thg-agentic/chatbot-sdk-react-native lint

The package's main entry resolves to ./src/index.ts (via the example's Metro config) so linked monorepo consumers (e.g. the Expo example app under ./example) see live TypeScript edits without a build step.

The web SDK's mock API server at packages/web/scripts/dev-server.js exposes POST /v1/chat for local prototyping, but the AG-UI streaming endpoint isn't mocked there — use a real dev backend or write a lightweight SSE mock for now.