@salesforce-personalization/react-native-personalization
v1.0.0
Published
A React Native bridge library that enables seamless integration between the Personalization native iOS/Android SDK and React Native applications.This library exposes the native SDK’s features and APIs to JavaScript, allowing React Native client apps to ac
Downloads
24
Readme
Salesforce Personalization SDK for React Native
A React Native plugin that wraps the native iOS / Android Salesforce Personalization SDKs. Renders personalized UI (Banner, Recommendations) in React Native apps and lets you ship your own custom components against the same backend.
Looking for a runnable demo? See
example/.
Table of Contents
- Requirements
- Install
- Platform Setup
- Quick Start
- Core APIs
- Custom Components
- Architecture Compatibility
- More
Requirements
| Requirement | Version | | ------------ | ------------- | | React Native | 0.76+ | | React | 18.3+ or 19.x | | Node.js | 18+ | | iOS | 15.0+ | | Android | API 26+ |
Install
npm install @salesforce-personalization/react-native-personalization
# or
yarn add @salesforce-personalization/react-native-personalizationPlatform Setup
The host app owns native SDK initialization — there is no JS-side
configure(...). The SDK needs three values: CDP_APP_ID,
CDP_ENDPOINT, and CDN_URL. Contact your Salesforce Marketing Cloud
administrator to obtain them, and don't commit real credentials.
After installing, follow the platform setup guide:
- iOS — see docs/iOS.md for
Info.plist, CocoaPods, andAppDelegate.swiftsetup. - Android — see docs/Android.md for
AndroidManifest.xmlandMainApplication.ktsetup.
Quick Start
Once the SDK is initialized natively, render personalized content with
ContentZone. It fetches content for the named zone, matches the backend's
component name against your allowedComponents, and renders the result.
import { ContentZone, Banner, Recommendations } from '@salesforce-personalization/react-native-personalization'
function HomeScreen() {
return (
<ContentZone
name="HomeScreen"
allowedComponents={[
Banner({ onTap: (event) => console.log('Banner tapped:', event.header) }),
Recommendations({ onTap: (event) => console.log('Item tapped:', event.item.name) }),
]}
loading={<Text>Loading personalized content...</Text>}
fallback={(error) => <Text>Could not load content</Text>}
/>
)
}Only name and allowedComponents are required. If the backend returns a
component name not in allowedComponents — or anything else goes wrong — the
optional fallback is rendered. When fallback is omitted the zone renders
nothing.
See docs/GETTING_STARTED.md for the full guide.
Core APIs
ContentZone
The main component for displaying personalized content.
| Prop | Type | Required | Description |
| ------------------------- | -------------------------------- | -------- | --------------------------------------------- |
| name | string | Yes | Unique zone identifier matching server config |
| allowedComponents | Component[] | Yes | Component renderers the zone may use |
| controller | ContentZoneController | No | Controller for programmatic refresh |
| timeoutSeconds | number | No | Fetch timeout in seconds (default: 10) |
| decisionsRequestContext | DecisionsRequestContext | No | Contextual data to bias personalization |
| loading | ReactNode | No | Loading-state UI |
| fallback | (error: Error) => ReactNode | No | Error-state UI |
Pull-to-refresh. Pass a controller from useContentZoneController and
route a RefreshControl to controller.refresh():
const controller = useContentZoneController()
<ScrollView
refreshControl={<RefreshControl refreshing={refreshing} onRefresh={() => controller.refresh()} />}
>
<ContentZone controller={controller} name="HomeScreen" allowedComponents={[Banner()]} />
</ScrollView>controller.refresh() keeps existing content visible during the fetch; call
controller.refresh(true) to show the loading state instead.
Preview Mode
Forward preview deep links to the SDK so active zones refresh with preview content:
import { Linking } from 'react-native'
useEffect(() => {
const handleUrl = ({ url }: { url: string }) => {
if (url.includes('sfp-preview')) PersonalizationModule.handlePreviewUrl(url)
}
Linking.getInitialURL().then((url) => url && handleUrl({ url }))
const sub = Linking.addEventListener('url', handleUrl)
return () => sub.remove()
}, [])Deep-link setup is covered in docs/iOS.md and docs/Android.md.
Out-of-the-Box Components
Two ready-to-use components render production content with automatic
View/Click engagement tracking. Both accept an optional onTap callback
and a style config.
Banner
A single banner with image, title, subtitle, and CTA. Backed by the
Salesforce_Banner transformer.
Banner({
onTap: (event) => console.log('Banner tapped:', event.header),
style: {
backgroundColor: '#f0f0f0',
headerTextColor: '#1a1a1a',
ctaTextColor: '#0066cc',
},
})The tap event is the full BannerModel: { id?, header, subheader?, imageUrl, ctaText?, ctaUrl? }.
Recommendations
A scrollable list of recommendation cards. Backed by the Salesforce_Recommendations
transformer.
Recommendations({
onTap: (event) => console.log('Item tapped:', event.item.name, 'at index:', event.index),
style: {
cardSpacing: 16,
card: { nameTextColor: '#1a1a1a', descriptionTextColor: '#666' },
},
})The model is shared ctaText plus a list of items:
type RecommendationItem = {
id: string
name: string
description?: string
imageUrl: string
url?: string
}
interface RecommendationsModel {
sectionHeader?: string
ctaText?: string // shared by all items
items: RecommendationItem[]
}The tap event is { item: RecommendationItem, index: number }. See
docs/GETTING_STARTED.md
for the full style reference.
Engagement Tracking
The OOTB Banner and Recommendations components track View and Click
automatically — no work required. Tracking runs for both PRODUCTION and
PREVIEW content; MOCK content (via MockContentZone) is never tracked
because it carries no engagement payloads.
Custom components opt in via the engagementPayloads the SDK delivers on
ComponentContext. The shape depends on the transformer:
Salesforce_Banner(single element) →PerAction. Look payloads up withgetPayloadForAction(payloads, action).Salesforce_Recommendations(list) →PerItemAndAction. Look payloads up per card withgetPayloadForItemAndAction(payloads, index, action).
Both helpers and trackEngagement are imported from the package root:
import { getPayloadForAction, trackEngagement } from '@salesforce-personalization/react-native-personalization'
// Inside a custom component's compose():
useEffect(() => {
if (!context.engagementPayloads) return
const payload = getPayloadForAction(context.engagementPayloads, 'View')
if (payload) trackEngagement(payload)
}, [context.personalizationId]) // re-fires on refresh — new content, new ViewAction names ('View', 'Click', …) are server-defined and matched
case-insensitively. Key View tracking on personalizationId so a refresh
fires a fresh View. See the
Custom Components Guide for a full
worked example.
Identity
import { PersonalizationModule } from '@salesforce-personalization/react-native-personalization'
// Profile ID (contact key)
await PersonalizationModule.setProfileId('user-123')
// Attributes — all values must be strings
await PersonalizationModule.setAttribute('tier', 'gold')
await PersonalizationModule.setAttributes({ firstName: 'John', lastName: 'Doe' })
// Read
const profileId = await PersonalizationModule.getProfileId()
const attributes = await PersonalizationModule.getAttributes()
// Clear (typical logout path)
await PersonalizationModule.clearAllAttributes()
// Party identification — per-field setters / getters
await PersonalizationModule.setPartyIdentificationName('John Doe')
await PersonalizationModule.setPartyIdentificationNumber('12345')
await PersonalizationModule.setPartyIdentificationType('individual')Events
All tracking flows through PersonalizationModule.track(event). Pick the
objType that matches the customer journey:
// Custom — free-form name + attributes
await PersonalizationModule.track({
objType: 'CustomEvent',
name: 'product_viewed',
attributes: { productId: 'ABC123', category: 'shoes' },
})
// Cart — subtype: 'add' | 'remove' use a singular `lineItem`;
// subtype: 'replace' uses a `lineItems` array
await PersonalizationModule.track({
objType: 'CartEvent',
subtype: 'add',
lineItem: { catalogObjectType: 'product', catalogObjectId: 'ABC123', quantity: 2, price: 49.99, currency: 'USD' },
})
// Order — subtype: 'purchase' | 'preorder' | 'cancel' | 'ship' | 'deliver' | 'return' | 'exchange'
await PersonalizationModule.track({
objType: 'OrderEvent',
subtype: 'purchase',
order: { id: 'ORDER-123', lineItems: [/* ... */], totalValue: 99.99, currency: 'USD' },
})
// Catalog — subtype: 'view' | 'viewDetail' | 'quickView' | 'favorite' | 'share' | 'review' | 'comment'
await PersonalizationModule.track({
objType: 'CatalogEvent',
subtype: 'view',
catalogObject: { type: 'product', id: 'ABC123', attributes: { name: 'Blue Sneakers' } },
})See docs/GETTING_STARTED.md for the full event reference.
Consent
Personalization requires consent. Until the user is opted in, the SDK does not fetch personalized content.
await PersonalizationModule.setConsent(true) // OPT_IN
await PersonalizationModule.setConsent(false) // OPT_OUT
const isOptedIn = await PersonalizationModule.isConsentOptIn()Custom Components
Render personalized content however you want by implementing Component<T>.
Two patterns:
- Reuse an OOTB transformer (e.g.
Salesforce_Banner,Salesforce_Recommendations) with your own UI — you keep automatic engagement tracking and need no backend configuration. - Define a fully custom transformer with your own fields — you implement engagement tracking manually.
import {
Component, ComponentModel, ComponentContext,
getPayloadForAction, trackEngagement,
} from '@salesforce-personalization/react-native-personalization'
interface ProductCardModel extends ComponentModel {
header: string
imageUrl: string
ctaText?: string
}
export function ProductCard(config?: { onTap?: (m: ProductCardModel) => void }): Component<ProductCardModel> {
return {
name: 'Salesforce_Banner', // reuse the Banner transformer's data + engagement payloads
validateAndCreateComponentModel: (json, context) => {
const data = JSON.parse(json)
if (!data.header || !data.imageUrl) throw new Error('Missing required fields')
return data as ProductCardModel
},
compose: (model, context) => {
useEffect(() => {
if (context.engagementPayloads) {
const payload = getPayloadForAction(context.engagementPayloads, 'View')
if (payload) trackEngagement(payload)
}
}, [context.personalizationId])
return (
<TouchableOpacity onPress={() => config?.onTap?.(model)}>
<Image source={{ uri: model.imageUrl }} style={{ width: '100%', height: 200 }} />
<Text>{model.header}</Text>
</TouchableOpacity>
)
},
}
}ComponentContext carries:
contentSource:'PRODUCTION'|'PREVIEW'|'MOCK'personalizationId: unique per fetch (changes on refresh — key yourViewtracking on it)engagementPayloads: automatic tracking data (OOTB transformers only)
📖 See the Custom Components Guide
for complete examples with engagement tracking, including Recommendations-shaped
payloads (getPayloadForItemAndAction).
Architecture Compatibility
This plugin uses TurboModules but works in both Legacy (Bridge) and New
Architecture apps on React Native 0.76+. Your JS/TS code needs no changes
either way — the plugin ships a codegenConfig in package.json, and React
Native's autolinking generates the native TurboModule specs for you on both
architectures. You don't run Codegen manually or add any Gradle/CocoaPods
wiring yourself.
To run your app on the New Architecture:
Android — set in
android/gradle.properties:newArchEnabled=truethen do a clean rebuild:
cd android && ./gradlew clean.iOS — set the flag before installing pods:
cd ios && RCT_NEW_ARCH_ENABLED=1 pod install
To stay on the Legacy Architecture, leave newArchEnabled=false
(Android's default in the RN template) and run a plain pod install (no
RCT_NEW_ARCH_ENABLED) — the plugin runs through React Native's interop
layer with no code changes required.
Either way, PersonalizationModule, ContentZone, and every other exported
API behave identically. See
TROUBLESHOOTING.md
if you hit a Codegen or CMake linking error after switching architectures.
More
- docs/GETTING_STARTED.md — full API reference and usage guide.
- docs/TROUBLESHOOTING.md — common issues and fixes.
- docs/iOS.md / docs/Android.md — host-app setup guides.
- docs/RUNNING_THE_EXAMPLE.md — first-time guide to build & run the iOS/Android example app.
- example/ — runnable demo app with OOTB and custom components.
- CHANGELOG.md — release notes.
- SECURITY.md — security and vulnerability reporting.
- CONTRIBUTING.md — how to contribute.
License
BSD-3-Clause License — see LICENSE for details.
Support
- 🐛 Issues: GitHub Issues
- 📖 Documentation: docs/GETTING_STARTED.md
