Shopping Interaction

Stories created in JOIN Studio can include shopping interactions (CTAs) such as Add to Cart, See Cart, and Shopping Redirect. The React Native SDK wires these callbacks through the player event stream; you just subscribe to the events, implement your app logic, and acknowledge add-to-cart when needed.

Events and callback signatures

All shopping callbacks are delivered via the player event listener:

  • Add to Cart
    • Signature: via JoinStories.addPlayerListener((event) => { ... })
    • Event shape: { type: 'AddToCart', offerId: string, requestId: string }
    • Typical use: add the item to your app cart, update UI, then acknowledge using JoinStories.resolveAddToCart(requestId, accepted)
  • See Cart
    • Signature: via JoinStories.addPlayerListener
    • Event shape: { type: 'SeeToCart' }
    • Typical use: dismiss the player, then navigate to your app’s cart screen
  • Shopping Redirect
    • Signature: via JoinStories.addPlayerListener
    • Event shape: { type: 'ShoppingRedirect', offerId: string }
    • Typical use: open a PDP or deep link inside your app; if external, use your web handler

Notes

  • Delivery: events are delivered on the JS side through React Native’s event emitter. Use JoinStories.addPlayerListener.
  • Acknowledgment: Add to Cart requires an explicit acknowledgment: call JoinStories.resolveAddToCart(requestId, accepted) once your cart operation succeeds or fails.
  • Robustness: treat missing or empty offerId gracefully.
  • Analytics: you may also observe addAnalyticsListener or addPlayerAnalyticsListener for tracking.

Usage with widgets (Bubble / Card)

Attach a single listener for shopping events and render the appropriate widget:

import React, { useEffect, useRef } from 'react';
import { Alert, Platform, ToastAndroid } from 'react-native';
import { JoinStories, JoinStoriesView, JoinStoriesCardView } from '@join-stories/react-native-widgets';

function showToast(message: string) {
  if (Platform.OS === 'android') {
    ToastAndroid.show(message, ToastAndroid.SHORT);
  } else {
    Alert.alert('', message);
  }
}

export default function ShoppingWidgets() {
  const addToCartTimerRef = useRef<ReturnType<typeof setTimeout> | null>(null);

  useEffect(() => {
    const sub = JoinStories.addPlayerListener(async (event) => {
      switch (event.type) {
        case 'AddToCart': {
          // 1) Update your cart using event.offerId (may be empty — handle gracefully)
          try {
            // await cart.add(event.offerId);
            // 2) Acknowledge to the SDK (true if accepted, false if declined/failed)
            await JoinStories.resolveAddToCart(event.requestId, true);
            showToast('Added to cart');
          } catch {
            await JoinStories.resolveAddToCart(event.requestId, false);
            showToast('Add to cart failed');
          }
          break;
        }
        case 'SeeToCart': {
          await JoinStories.dismissPlayer();
          // navigate('/cart');
          showToast('See Cart');
          break;
        }
        case 'ShoppingRedirect': {
          await JoinStories.dismissPlayer();
          // navigate(`/product/${event.offerId}`);
          showToast(`Redirect: ${event.offerId}`);
          break;
        }
      }
    });

    return () => {
      sub.remove();
      if (addToCartTimerRef.current) {
        clearTimeout(addToCartTimerRef.current);
        addToCartTimerRef.current = null;
      }
    };
  }, []);

  return (
    <>
      {/* Bubble widget */}
      <JoinStoriesView alias="homepage-stories" />

      {/* Card widget (list) */}
      <JoinStoriesCardView alias="recommendations" format="list" />

      {/* Card widget (grid) */}
      <JoinStoriesCardView alias="best-sellers" format="grid" column={2} />
    </>
  );
}

Key widget props:

  • JoinStoriesView (bubble): alias, plus display and player configuration props
  • JoinStoriesCardView (list/grid): alias, format: 'list' | 'grid', optional column for grid, plus display and player configuration props

Usage with the Standalone Player

Pass your widget alias to start the standalone player and keep using the same event subscription for shopping callbacks:

import { JoinStories } from '@join-stories/react-native-widgets';

await JoinStories.startStandAlonePlayer({
  alias: 'homepage-stories',
  // optional: customParameters, and player configuration (colors, radii, etc.)
});

Your previously registered addPlayerListener will receive the same shopping events while the standalone player is displayed. You can also manually dismiss:

await JoinStories.dismissPlayer();

Acknowledge add-to-cart when done:

await JoinStories.resolveAddToCart(requestId, /* accepted */ true);

Best practices

  • Keep UI responsive: perform cart/network operations asynchronously; use lightweight toasts/snackbars.
  • Dismiss before navigation: if a story player is visible, call JoinStories.dismissPlayer() before navigating to avoid overlapping UIs.
  • Handle offerId carefully: it may be missing or empty; route safely.
  • Error handling: if cart updates fail, communicate clearly and allow retry.
  • Testing: verify each CTA set in JOIN Studio triggers the expected behavior and that you acknowledge add-to-cart correctly.

Optional: Analytics

You can observe analytics in parallel:

import { JoinStories } from '@join-stories/react-native-widgets';

const sub = JoinStories.addAnalyticsListener((payload) => {
  // analytics.log('join_event', payload);
});

// Or player-level analytics:
const sub2 = JoinStories.addPlayerAnalyticsListener((event) => {
  // event.type: 'StoryClickOnCallToAction' | 'StoryError' | 'StoryLastPageVisible' | 'StoryPageVisible'
});