React Native Low-Code Integration

Use the Salesforce Personalization React Native plugin to display personalized content in your React Native app with minimal setup. The plugin provides live and mock content zones, out-of-the-box banner and recommendation renderers, identity and event APIs, preview handling, and engagement helpers for custom components.

  1. Install the SDK
  2. Initialize the SDK
  3. Display a Content Zone
  4. Out-of-the-Box Components
  5. Custom Components
  6. Engagement Tracking
  7. Preview
  8. Design-Time Rendering
  9. Identity and Events

Install the SDK 

1npm install @salesforce-personalization/react-native-personalization
2# or
3yarn add @salesforce-personalization/react-native-personalization

Add the package. It resolves to the latest published version.

The plugin supports both React Native architectures. Autolinking and package Codegen configuration register the native bridge. Do not manually register the package or invoke Codegen only for this plugin.

Requirements 

  • React Native 0.76 or later, but earlier than 1.0
  • React 18.3 or React 19
  • Node.js 18 or later
  • iOS 15 or later
  • Android API 26 or later
  • Java 17 for Android builds

The compatible release must define the final Android compile SDK, target SDK, Kotlin Gradle Plugin, Gradle, and Android Gradle Plugin matrix. Do not infer those versions from the JavaScript package version.

Initialize the SDK 

The host app initializes the native SDK. There is no JavaScript configure(...) API. Complete both the iOS and Android platform setup below. Leave consent unset during native initialization.

iOS setup 

Add tenant configuration to the app’s Info.plist. Add the optional CDN value only when your administrator provides an explicit override.

1<key>com.salesforce.personalization.CDP_APP_ID</key>
2<string>YOUR_CDP_APP_ID</string>
3<key>com.salesforce.personalization.CDP_ENDPOINT</key>
4<string>YOUR_CDP_ENDPOINT_HOST</string>
5<key>com.salesforce.personalization.DATASPACE</key>
6<string>default</string>

Install the native dependencies after the compatible package is available.

1cd ios
2pod install

Merge the following helper into the existing AppDelegate.swift. Keep the app’s generated React Native setup, module name, bundle URL, initial properties, and other delegate methods.

1import SFMCSDK
2import Cdp
3import Personalization
4
5private func initializePersonalizationSDK() {
6  let info = Bundle.main.infoDictionary ?? [:]
7  let appId = info["com.salesforce.personalization.CDP_APP_ID"] as? String ?? ""
8  let endpoint = info["com.salesforce.personalization.CDP_ENDPOINT"] as? String ?? ""
9  let dataspace = info["com.salesforce.personalization.DATASPACE"] as? String ?? "default"
10  if appId.isEmpty || appId.hasPrefix("YOUR_") ||
11     endpoint.isEmpty || endpoint.hasPrefix("YOUR_") {
12    NSLog("Personalization is not configured; SDK init skipped")
13    return
14  }
15
16  let cdpConfig = CdpConfigBuilder(appId: appId, endpoint: endpoint)
17    .trackScreens(false)
18    .trackLifecycle(false)
19    .build()
20  let personalizationConfig = PersonalizationConfigBuilder()
21    .dataspace(dataspace)
22    .build()
23  let sdkConfig = ConfigBuilder()
24    .setCdp(config: cdpConfig)
25    .setPersonalization(config: personalizationConfig)
26    .build()
27  SFMCSdk.initializeSdk(sdkConfig) { statuses in
28    statuses.forEach { print("\($0.moduleName.rawValue): \($0.initStatus.rawValue)") }
29  }
30}

Call initializePersonalizationSDK() once from the existing application(_:didFinishLaunchingWithOptions:) before returning. This example disables automatic screen and lifecycle tracking. Choose those settings according to your app’s tracking policy.

Android setup 

The compatible release must publish validated Android build versions. The reviewed source expects at least API 26 and Java 17 and currently resolves a Personalization 3.x dependency dynamically. Its source and example Kotlin versions do not establish a safe customer compatibility guarantee. Treat any Kotlin metadata error as a release compatibility issue, not as a reason to blindly upgrade one build plugin.

Declare the Salesforce Maven repositories in the host app. Library repository declarations are not inherited by consuming apps.

1repositories {
2  google()
3  mavenCentral()
4  maven {
5    url "https://salesforce-marketingcloud.github.io/MarketingCloudSDK-Android/repository"
6  }
7  maven {
8    url "https://salesforce-marketingcloud.github.io/mobile-sdk-cdp-android/repository"
9  }
10  maven {
11    url "https://salesforce-marketingcloud.github.io/Personalization-Android/repository"
12  }
13}

Use the location that matches the app’s repository policy. If repositories are centralized in settings.gradle, use RepositoriesMode.PREFER_SETTINGS. Do not use FAIL_ON_PROJECT_REPOS with a plugin release that still declares project-level repositories. Do not add the native Personalization dependency directly; the plugin exposes it transitively.

Ensure release builds have network access and place configuration metadata inside the application element.

1<uses-permission android:name="android.permission.INTERNET" />
2
3<application>
4  <meta-data
5    android:name="com.salesforce.personalization.CDP_APP_ID"
6    android:value="YOUR_CDP_APP_ID" />
7  <meta-data
8    android:name="com.salesforce.personalization.CDP_ENDPOINT"
9    android:value="YOUR_CDP_ENDPOINT_HOST" />
10  <meta-data
11    android:name="com.salesforce.personalization.DATASPACE"
12    android:value="default" />
13</application>

Add optional CDN metadata only when your administrator supplies an override. Merge this helper into the existing MainApplication class and call it once from onCreate() after super.onCreate().

1import android.content.pm.PackageManager
2import com.salesforce.marketingcloud.cdp.CdpConfig
3import com.salesforce.marketingcloud.sfmcsdk.SFMCSdk
4import com.salesforce.marketingcloud.sfmcsdk.SFMCSdkModuleConfig
5import com.salesforce.personalization.PersonalizationConfig
6
7private fun initializePersonalizationSDK() {
8  val metaData = packageManager
9    .getApplicationInfo(packageName, PackageManager.GET_META_DATA)
10    .metaData
11  val appId = metaData.getString("com.salesforce.personalization.CDP_APP_ID").orEmpty()
12  val endpoint = metaData.getString("com.salesforce.personalization.CDP_ENDPOINT").orEmpty()
13  val dataspace = metaData.getString("com.salesforce.personalization.DATASPACE") ?: "default"
14  if (appId.isBlank() || appId.startsWith("YOUR_") ||
15      endpoint.isBlank() || endpoint.startsWith("YOUR_")) {
16    android.util.Log.w("PersonalizedApp", "Personalization is not configured; SDK init skipped")
17    return
18  }
19  val cdpConfig = CdpConfig.Builder(this, appId, endpoint).build()
20  val personalizationConfig = PersonalizationConfig.Builder(this)
21    .dataspace(dataspace)
22    .build()
23  SFMCSdk.configure(this, SFMCSdkModuleConfig.build {
24    cdpModuleConfig = cdpConfig
25    personalizationModuleConfig = personalizationConfig
26  }) {}
27}

Apps using the New Architecture must follow their React Native version’s normal clean-build flow. The reviewed guide has not established Android’s default automatic screen and lifecycle collection behavior for the pinned native SDK. Verify that behavior and document the supported pre-consent opt-out calls before publication.

Display a Content Zone 

With the SDK initialized, place a ContentZone in your UI. The JavaScript factory names are SalesforceBanner and SalesforceRecommendations. Their backend experience template names remain Salesforce_Banner and Salesforce_Recommendations.

After adding a ContentZone to your app code, create the matching content zone record in Salesforce Personalization. See Set Up Mobile Content Zones for instructions on defining personalization points, assigning components, and configuring engagement definitions.

1import {
2  ContentZone,
3  SalesforceBanner,
4  SalesforceRecommendations,
5} from "@salesforce-personalization/react-native-personalization";
6
7const homeComponents = [SalesforceBanner(), SalesforceRecommendations()];
8
9export function HomePersonalization() {
10  return <ContentZone name="HomeScreen" allowedComponents={homeComponents} fallback={() => null} />;
11}

The principal props are:

  • name: Required nonblank server-configured zone name.
  • allowedComponents: Required component instances with unique normalized names.
  • loading: Optional UI displayed during initial loading.
  • fallback: Optional error renderer. Do not expose error details to customers.
  • timeoutSeconds: Positive request timeout. The default is 10 seconds.
  • controller: Programmatic refresh controller. Use one per mounted zone.
  • decisionsRequestContext: Context used by the next decision request.

Component names are trimmed and lowercased for matching. Do not place two instances with the same normalized component name in one zone - duplicates are not rejected, the last one silently wins. The live zone captures allowedComponents when it mounts, so remount the zone to replace the component contract. Changing timeout or decision context alone does not fetch; the next refresh uses the latest values.

Apply consent 

Do not mount a live content zone until native opt-in succeeds. On opt-out, unmount live zones immediately, then await the native opt-out call. If opt-out fails, keep zones unmounted and present a retry path. Persist a choice only after the native call succeeds.

1import { useState } from "react";
2import { Button, Text, View } from "react-native";
3import {
4  ContentZone,
5  PersonalizationModule,
6  SalesforceBanner,
7} from "@salesforce-personalization/react-native-personalization";
8
9type ConsentState = "unknown" | "optedIn" | "optedOut";
10const homeComponents = [SalesforceBanner()];
11export function ConsentGate({
12  initialChoice,
13  saveChoice,
14}: {
15  initialChoice: boolean | null;
16  saveChoice: (optIn: boolean) => Promise<void>;
17}) {
18  const [consent, setConsent] = useState<ConsentState>(
19    initialChoice === null ? "unknown" : initialChoice ? "optedIn" : "optedOut",
20  );
21  const [updating, setUpdating] = useState(false);
22  const [error, setError] = useState<string | null>(null);
23  const updateConsent = async (optIn: boolean) => {
24    if (updating) return;
25    const previous = consent;
26    setUpdating(true);
27    setError(null);
28    if (!optIn) setConsent("optedOut");
29    try {
30      await PersonalizationModule.setConsent(optIn);
31      setConsent(optIn ? "optedIn" : "optedOut");
32      try {
33        await saveChoice(optIn);
34      } catch {
35        if (optIn) {
36          await PersonalizationModule.setConsent(false);
37          setConsent("optedOut");
38        }
39        setError("Consent was applied, but the preference could not be saved.");
40      }
41    } catch {
42      setConsent(previous);
43      setError("The consent choice could not be applied. Try again.");
44    } finally {
45      setUpdating(false);
46    }
47  };
48  return (
49    <View>
50      {consent === "optedIn" ? (
51        <ContentZone name="HomeScreen" allowedComponents={homeComponents} />
52      ) : (
53        <Text>Personalized content is not displayed.</Text>
54      )}
55      {error ? <Text accessibilityRole="alert">{error}</Text> : null}
56      {updating ? <Text accessibilityLiveRegion="polite">Updating choice</Text> : null}
57      <Button
58        disabled={updating}
59        title="Allow personalization"
60        onPress={() => void updateConsent(true)}
61      />
62      <Button
63        disabled={updating}
64        title="Turn off personalization"
65        onPress={() => void updateConsent(false)}
66      />
67    </View>
68  );
69}

isConsentOptIn() returns false for both unset consent and explicit opt-out. Persist the explicit user choice separately when the app must distinguish those states. Hydrate that choice before rendering this gate and reconcile a saved opt-in with isConsentOptIn() at startup. A consent change does not automatically retry a prior content request.

Out-of-the-Box Components 

The built-in SalesforceBanner and SalesforceRecommendations renderers handle rendering and engagement tracking automatically. Their backend experience template names are Salesforce_Banner and Salesforce_Recommendations.

Banner responses require nonblank header and imageUrl strings. A banner is actionable when it has an app onTap callback or a nonblank ctaUrl. The app callback takes precedence over default URL opening.

Recommendation responses require a nonempty items array. Every usable item requires nonblank id, name, and imageUrl strings. Production responses must also provide unique IDs, but this source version does not enforce or remove duplicates. Duplicate IDs become duplicate React Native list keys and produce undefined rendering behavior.

The plugin removes individually invalid recommendation items. This is only a display-resilience feature. It does not remap engagement payload indices, so a removed item can cause later View and Click events to be attributed to the wrong item. Serve only valid, unique items in their original order.

Handle URLs safely 

Personalized image, CTA, and preview URLs cross a trust boundary. The built-in renderers warn about malformed URLs but do not enforce your app’s trusted hosts. Use trusted campaign configuration and override CTA handling with onTap when the app must enforce navigation policy.

Parse URLs and allow only an explicit scheme, host, port, and closed-form path. Decide explicitly whether query strings and fragments are permitted. Do not use substring matching or raw prefix matching for security decisions.

1import { Linking } from "react-native";
2import { SalesforceBanner } from "@salesforce-personalization/react-native-personalization";
3
4function isTrustedCta(rawUrl: string): boolean {
5  try {
6    const url = new URL(rawUrl);
7    const path = decodeURIComponent(url.pathname);
8    return (
9      url.protocol === "https:" &&
10      url.hostname.toLowerCase() === "shop.example.com" &&
11      url.port === "" &&
12      url.username === "" &&
13      url.password === "" &&
14      url.search === "" &&
15      url.hash === "" &&
16      /^\/products\/[A-Za-z0-9_-]+$/.test(path)
17    );
18  } catch {
19    return false;
20  }
21}
22
23const secureBanner = SalesforceBanner({
24  onTap: async ({ ctaUrl }) => {
25    if (!ctaUrl || !isTrustedCta(ctaUrl)) return;
26    await Linking.openURL(ctaUrl);
27  },
28});

Apply equivalent HTTPS host and path allowlists to image URLs in a custom renderer. Do not permit javascript, data, file, arbitrary custom schemes, or attacker-controlled redirect hosts. Review trusted paths so a valid host cannot route users to an unintended endpoint.

Custom Components 

Custom components can reuse an existing backend experience template. Put React Hooks in the React component returned by compose, not directly in compose.

1import type {
2  Component,
3  ComponentContext,
4  ComponentModel,
5} from "@salesforce-personalization/react-native-personalization";
6import {
7  EngagementAction,
8  trackEngagement,
9  useTrackEngagementViewOnce,
10} from "@salesforce-personalization/react-native-personalization";
11import { Image, Pressable, Text } from "react-native";
12
13interface ProductCardModel extends ComponentModel {
14  header: string;
15  imageUrl: string;
16}
17function ProductCardView({
18  model,
19  context,
20}: {
21  model: ProductCardModel;
22  context: ComponentContext;
23}) {
24  useTrackEngagementViewOnce(context);
25
26  const handlePress = () => {
27    trackEngagement(context, EngagementAction.click);
28  };
29  return (
30    <Pressable
31      accessibilityRole="button"
32      accessibilityLabel={model.header}
33      onPress={handlePress}
34      style={{ minHeight: 48 }}
35    >
36      <Image
37        accessible={false}
38        source={{ uri: model.imageUrl }}
39        style={{ width: "100%", height: 200 }}
40      />
41      <Text>{model.header}</Text>
42    </Pressable>
43  );
44}
45export function ProductCard(): Component<ProductCardModel> {
46  return {
47    name: "Salesforce_Banner",
48    validateAndCreateComponentModel: (json) => {
49      const value: unknown = JSON.parse(json);
50      if (typeof value !== "object" || value === null) {
51        throw new Error("ProductCard requires an object");
52      }
53      const record = value as Record<string, unknown>;
54      if (
55        typeof record.header !== "string" ||
56        record.header.trim() === "" ||
57        typeof record.imageUrl !== "string" ||
58        record.imageUrl.trim() === ""
59      ) {
60        throw new Error("ProductCard requires nonblank header and imageUrl");
61      }
62      let imageUrl: URL;
63      try {
64        imageUrl = new URL(record.imageUrl);
65      } catch {
66        throw new Error("ProductCard image URL is invalid");
67      }
68      if (
69        imageUrl.protocol !== "https:" ||
70        imageUrl.hostname !== "images.example.com" ||
71        imageUrl.port !== ""
72      ) {
73        throw new Error("ProductCard requires nonblank header and imageUrl");
74      }
75      return {
76        header: record.header,
77        imageUrl: record.imageUrl,
78      };
79    },
80    compose: (model, context) => <ProductCardView model={model} context={context} />,
81  };
82}

Treat ComponentContext and all nested engagement data as opaque. TypeScript readonly fields are compile-time protection only. Do not mutate the context, copy it, spread it, reconstruct it, or copy nested engagement payloads. Pass the exact object received from compose to every engagement helper. View dedup uses that object’s identity, not personalizationId; a copy creates a new dedup scope.

Engagement Tracking 

Before engagement events are recorded, ensure your engagement definitions (View, Click) are configured in the Data 360 mobile connector. See Set Up Mobile Engagement Tracking for instructions on defining engagement actions alongside your components and content zones.

Out-of-the-box components track View and Click automatically. For custom components:

For a single element, use useTrackEngagementViewOnce(context) for View and trackEngagement(context, action) for repeatable actions such as Click. For a FlatList or VirtualizedList, attach the handler returned by useTrackEngagementViewOncePerItem(context) and use trackEngagementPerItem(context, index, action) for repeatable item actions. Merely calling the hook does not report item Views.

1import { FlatList, type ListRenderItem } from 'react-native'
2import type { ComponentContext } from '@salesforce-personalization/react-native-personalization'
3import { useTrackEngagementViewOncePerItem } from '@salesforce-personalization/react-native-personalization'
4
5export function RecommendationList<T>({ context, items, renderItem }: {
6  context: ComponentContext
7  items: readonly T[]
8  renderItem: ListRenderItem<T>
9}) {
10  const onViewableItemsChanged = useTrackEngagementViewOncePerItem(context)
11  return <FlatList
12    data={items}
13    renderItem={renderItem}
14    onViewableItemsChanged={onViewableItemsChanged}
15    viewabilityConfig={{ itemVisiblePercentThreshold: 50 }}
16  />
17}

Per-item engagement uses one unchanged, zero-based, flat serving index. Do not filter, sort, insert, group, or use section-relative indices before tracking. If UI transformation is unavoidable, retain and pass each item’s original flat serving index. Do not pass a SectionList row index directly because it restarts in every section.

Preview 

Preview URLs contain a token. Treat the complete URL and token as sensitive. Do not log, persist, add to analytics, include in crash breadcrumbs, or transmit them to any service other than the SDK preview handler.

The reviewed Android bridge logs the complete preview URL and identity values at DEBUG. This must be redacted before release. Until it is fixed, do not enable DEBUG logging while processing real preview tokens or customer identity data; use PersonalizationModule.setLogging('NONE') in production. Preview is not a non-tracking mode: preview content can carry engagement payloads, so keep it behind the same successful opt-in gate. Use MockContentZone for local content that must avoid network decisions and engagement.

Custom URL scheme 

Custom schemes do not authenticate the receiving app and can be claimed by another installed app. Use this example only on controlled development or internal preview devices; use verified links for production preview tokens. The sample uses yourappscheme and the URL shape below. Replace both with the exact scheme, host, path, and query contract generated by the authoritative mobile backend procedure; do not assume this illustrative shape is universal.

1yourappscheme://preview?sfp-preview=TOKEN

Register the scheme in iOS Info.plist.

1<key>CFBundleURLTypes</key>
2<array>
3  <dict>
4    <key>CFBundleURLSchemes</key>
5    <array>
6      <string>yourappscheme</string>
7    </array>
8  </dict>
9</array>

Forward custom-scheme URLs from the existing iOS app delegate.

1override func application(
2  _ app: UIApplication,
3  open url: URL,
4  options: [UIApplication.OpenURLOptionsKey: Any] = [:]
5) -> Bool {
6  return RCTLinkingManager.application(app, open: url, options: options)
7}

On Android, preserve the existing launcher filter and activity attributes, set the main activity launch mode to singleTask, and add the custom-scheme filter.

1<activity
2  android:name=".MainActivity"
3  android:exported="true"
4  android:launchMode="singleTask">
5
6  <intent-filter>
7    <action android:name="android.intent.action.VIEW" />
8    <category android:name="android.intent.category.DEFAULT" />
9    <category android:name="android.intent.category.BROWSABLE" />
10    <data
11      android:scheme="yourappscheme"
12      android:host="preview" />
13  </intent-filter>
14</activity>

Validate scheme, host, path, credentials, and the exact query parameter before passing a URL to native code. Handle both cold-start and foreground URLs.

1import { useEffect } from "react";
2import { Linking } from "react-native";
3import { PersonalizationModule } from "@salesforce-personalization/react-native-personalization";
4
5function getTrustedPreviewUrl(rawUrl: string): string | null {
6  try {
7    const url = new URL(rawUrl);
8    const token = url.searchParams.get("sfp-preview");
9    const queryKeys = Array.from(url.searchParams.keys());
10    const trusted =
11      url.protocol === "yourappscheme:" &&
12      url.hostname === "preview" &&
13      (url.pathname === "" || url.pathname === "/") &&
14      url.username === "" &&
15      url.password === "" &&
16      url.port === "" &&
17      url.hash === "" &&
18      queryKeys.length === 1 &&
19      queryKeys[0] === "sfp-preview" &&
20      url.searchParams.getAll("sfp-preview").length === 1 &&
21      typeof token === "string" &&
22      token.trim() !== "";
23
24    return trusted ? rawUrl : null;
25  } catch {
26    return null;
27  }
28}
29
30export function PreviewUrlHandler() {
31  useEffect(() => {
32    const handleUrl = async (rawUrl: string) => {
33      const trustedUrl = getTrustedPreviewUrl(rawUrl);
34      if (!trustedUrl) return;
35      try {
36        await PersonalizationModule.handlePreviewUrl(trustedUrl);
37      } catch {
38        console.error("Preview URL could not be applied");
39      }
40    };
41    void Linking.getInitialURL()
42      .then((url) => {
43        if (url) void handleUrl(url);
44      })
45      .catch(() => console.error("Initial URL could not be read"));
46    const subscription = Linking.addEventListener("url", ({ url }) => {
47      void handleUrl(url);
48    });
49    return () => subscription.remove();
50  }, []);
51  return null;
52}

Preview receipt updates refetch only the affected zone. A mounted zone can also refetch through its controller or when its name changes. PersonalizationModule.isPreview(name) reads the current state for that zone.

Universal Links and Android App Links 

HTTPS links are a separate integration, not a replacement scheme string in the custom-scheme examples. Before accepting HTTPS preview URLs, complete all of the following platform verification work and change the parser allowlist to the exact HTTPS host and path issued by your organization.

For iOS Universal Links:

  • Host an apple-app-site-association file on the trusted HTTPS domain.
  • Add the Associated Domains capability with the matching applinks domain.
  • Restrict the association file to the exact preview path.
  • Forward NSUserActivityTypeBrowsingWeb from the app delegate to RCTLinkingManager.application(_:continue:restorationHandler:).
  • Test installation and link association on a physical device.

For Android App Links:

  • Host assetlinks.json with the production application ID and signing certificate.
  • Add an HTTPS intent filter with exact scheme, host, and path constraints.
  • Set android:autoVerify="true" on that filter.
  • Verify association status for the installed release-signed app.
  • Test cold-start and foreground delivery on a physical device.

Do not claim Universal Link or App Link support until these domain-controlled files, native forwarding methods, release signing identities, and path rules have been validated together.

Design-Time Rendering 

MockContentZone renders local data without a network decision and never sends engagement because mock context has no engagement payloads.

Keep both allowedComponents and mockContent identities stable. In the reviewed source, changing either identity retriggers the effect. Recreating either inline on every render re-runs the mock fetch each render - an unnecessary-refetch footgun from missing memoization. It is not a self-sustaining loop: the effect only updates the zone’s own state and never feeds those prop identities back. Use module-level constants or useMemo so the fetch runs only when the fixture actually changes.

1import {
2  MockContentZone,
3  SalesforceBanner,
4} from "@salesforce-personalization/react-native-personalization";
5
6const mockBannerComponents = [SalesforceBanner()];
7
8const provideMockBanner = () => ({
9  componentName: "Salesforce_Banner",
10  header: "Welcome!",
11  imageUrl: "https://images.example.com/banner.jpg",
12});
13
14export function BannerPreview() {
15  return (
16    <MockContentZone
17      name="HomeScreen"
18      allowedComponents={mockBannerComponents}
19      mockContent={provideMockBanner}
20    />
21  );
22}

If mock data or handlers depend on component props, create stable identities once for the mounted preview and remount intentionally when the fixture changes. Do not silence the loop by setting the simulated delay to zero.

Identity and Events 

Set identity after consent and before mounting the zone that needs it. Identity changes affect later requests but do not refresh an already mounted zone. Attribute values must be strings, and setAttributes requires a nonempty object.

1import { PersonalizationModule } from "@salesforce-personalization/react-native-personalization";
2
3export async function applySignedInIdentity(): Promise<void> {
4  await PersonalizationModule.setProfileId("user-123");
5  await PersonalizationModule.setAttributes({ tier: "gold", age: String(30) });
6}

Handle rejection at the app’s identity boundary. After all identity mutations succeed, refresh that zone’s dedicated controller once; for an account switch, unmount live zones, complete the identity transition, then remount them. clearAllAttributes() does not clear the profile ID or party-identification fields, and this plugin version exposes no public API to clear them. Do not set those identifiers in an app that must remove identity on logout until the compatible release documents a supported native identity-reset operation.

Track application events through the same public module from a named async function. Handle rejection at the app’s event boundary.

1import { PersonalizationModule } from "@salesforce-personalization/react-native-personalization";
2
3export async function trackProductView(productId: string): Promise<void> {
4  try {
5    await PersonalizationModule.track({
6      objType: "CustomEvent",
7      name: "product_viewed",
8      attributes: { productId },
9    });
10  } catch {
11    console.error("Product view could not be tracked");
12  }
13}

The event union also supports engagement, system, cart, order, and catalog events. Use the event shape exported by the installed compatible release.

Prepare Salesforce (backend prerequisite): A Salesforce administrator must configure the mobile personalization resources before the app can receive content: create a mobile content zone for each placement, match the app’s content-zone name exactly (spelling and case), assign every experience template the zone is allowed to serve (Salesforce_Banner, Salesforce_Recommendations, or your cloned/custom names), supply required fields, configure View and Click engagement payloads, activate the configuration, and verify a test decision for each zone. See Set Up Mobile Content Zones. Do not substitute the web sitemap content-zone procedure for the mobile procedure.

Note