Shopping
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)
- Signature: via
- See Cart
- Signature: via
JoinStories.addPlayerListener
- Event shape:
{ type: 'SeeToCart' }
- Typical use: dismiss the player, then navigate to your app’s cart screen
- Signature: via
- 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
- Signature: via
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
oraddPlayerAnalyticsListener
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 propsJoinStoriesCardView
(list/grid):alias
,format: 'list' | 'grid'
, optionalcolumn
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'
});
Updated 5 days ago