# developer Salesforce Personalization (einstein-personalization) - Full Documentation # Android Integration Configure the Android app to identify users and manage consent settings, enabling you to request and display content tailored to specific user interactions. 1. [Install the SDK](#install-the-sdk) 2. [Initialize the Data 360 and Personalization Modules](#initialize-the-data-360-and-personalization-modules) 3. [Request Personalization](#request-personalization) 4. [Access Responses](#access-responses) 5. [Error Handling](#error-handling) ## Install the SDK Integrate the Personalization module into your Android project by updating your Gradle configuration files. 1. Update the Personalization repository in your module-level `build.gradle` file. ```kotlin repositories { maven { url "https://salesforce-marketingcloud.github.io/MarketingCloudSDK-Android/repository" } maven { url "https://salesforce-marketingcloud.github.io/mobile-sdk-cdp-android/repository" } maven { url "https://salesforce-marketingcloud.github.io/Personalization-Android/repository" } } ``` 2. Add the SDK dependencies to your app's `build.gradle` file. ```kotlin dependencies { // Personalization SDK (includes SFMC SDK and CDP SDK dependencies) implementation 'com.salesforce.personalization:sdk:3.+' } ``` ## Initialize the Data 360 and Personalization Modules The host app owns SDK initialization. Initialize both the [Data 360 module](https://developer.salesforce.com/docs/data/data-cloud-engagement-mobile-sdk/guide/c360a-api-engagement-mobile-sdk-initialization.html) and the Personalization module in your `Application.onCreate()`, before making any `fetchDecisions` calls. ```kotlin import com.salesforce.marketingcloud.cdp.CdpConfig import com.salesforce.marketingcloud.sfmcsdk.SFMCSdk import com.salesforce.marketingcloud.sfmcsdk.SFMCSdkModuleConfig import com.salesforce.personalization.PersonalizationConfig /* Initialization of the CDP module for data collection */ val flushRate = EventFlushRate.Quantity(quantity = 1) val cdpConfig = CdpConfig.Builder( context = this, appId = "", // Your CDP Mobile Connector app ID endpoint = "" // Your CDP Mobile Connector endpoint ).eventFlushRate(flushRate) .build() /* Initialization of the personalization module with needed configuration */ val personalizationConfig = PersonalizationConfig.Builder(this) // Optionally define your dataspace. The "default" data space is used if you don't explicitly provide one. .dataspace("myDataspaceApiName") .build() /* The CDP and Personalization configurations initialized above are provided to SFMCSdk.configure */ SFMCSdk.configure(this, SFMCSdkModuleConfig.build { cdpModuleConfig = cdpConfig personalizationModuleConfig = personalizationConfig }) { initStatus -> Log.d(TAG, "SDK initialized: ${initStatus.status}") } ``` **note:** In your production application, you must explicitly manage user consent using the solution provided in the Engagement Mobile SDK. See [Data 360 Consent Management](https://developer.salesforce.com/docs/data/data-cloud-engagement-mobile-sdk/guide/c360a-api-engagement-mobile-sdk-consent-management.html). ### Considerations - Call this initialization once, from `Application.onCreate()` - not from an `Activity` or `Fragment` - so the SDK is ready before any screen requests decisions. - Use the same context instance (`this`, referring to the `Application`) for both `CdpConfig.Builder` and `PersonalizationConfig.Builder`. - The system uses the `default` data space if you don't explicitly provide one. - To enable debug logging during development, add this snippet before `SFMCSdk.configure(...)`, so logging is active for the whole init sequence. Make sure this setting is disabled or removed before building your application for production. ```kotlin SFMCSdk.setLogging(DEBUG, AndroidLogger()) ``` For more information on logging, see [Logging and Debugging](https://developer.salesforce.com/docs/data/data-cloud-engagement-mobile-sdk/guide/c360a-api-engagement-mobile-sdk-logging-and-debugging.html). ## Request Personalization The `fetchDecisions` method enables you to interact with the Decisioning API and request one or more personalizations for specific areas of your mobile application. The SDK provides two methods for fetching personalization decisions: suspend functions (recommended) and callbacks. ### Use a Suspend Function to Fetch Decisions (Recommended, Kotlin Only) Call `fetchDecisions` within a coroutine scope to handle personalization requests asynchronously. ```kotlin import com.salesforce.personalization.PersonalizationSdk import com.salesforce.personalization.errors.PersonalizationException import com.salesforce.personalization.http.DecisionsRequestContext import com.salesforce.personalization.http.DecisionsResponse import com.salesforce.personalization.utils.Result // In a coroutine scope (ViewModel, LaunchedEffect, and so on.) val result = PersonalizationSdk.fetchDecisions( personalizationPointNames = listOf("point1", "point2"), // Optionally use additional context to influence the decisions. // See "Request Personalization By Using Context" for details. decisionsRequestContext = decisionsRequestContext, // Optional timeoutMs = 10000L // Optional: 10 seconds (default: 10s) ) when (result) { is Result.Success -> { // Handle successful response val response: DecisionsResponse = result.output // Process personalizations... } is Result.Failure -> { // Handle error val exception: PersonalizationException = result.throwable Log.e(TAG, "Personalization failed: ${exception.message}") } } ``` | Parameter | Description | | :------------------------ | :----------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | personalizationPointNames | A list of at least one personalization point API Name (not the display label) for which you want to return decisions. | | decisionsRequestContext | (Optional) Additional context information. See [Request Personalization By Using Context](#request-personalization-by-using-context). If you don't have any context, pass nil. | | timeoutMs | (Optional) The duration, in seconds, before the fetch task times out. The default is 10 seconds. | **note:** Grouping personalization point names by datagraph is the responsibility of the caller. This method does not automatically group or separate requests based on datagraph configuration. See [Request Personalization](https://developer.salesforce.com/docs/marketing/einstein-personalization/guide/decisioning-api-request-personalization.html#body). ### Use a Callback to Fetch Decisions If your architecture does not support coroutines, use the callback-based implementation of `fetchDecisions` to handle responses. ```kotlin import com.salesforce.personalization.PersonalizationSdk import com.salesforce.personalization.errors.PersonalizationException import com.salesforce.personalization.http.DecisionsRequestContext import com.salesforce.personalization.http.DecisionsResponse import com.salesforce.personalization.utils.Result PersonalizationSdk.fetchDecisions( scope = lifecycleScope, // or viewModelScope, rememberCoroutineScope(), and so on. personalizationPointNames = listOf("point1", "point2"), // Optionally use additional context to influence the decisions. // See "Request Personalization By Using Context" for details. decisionsRequestContext = decisionsRequestContext, timeoutMs = 10000L, // Optional: 10 seconds (default: 10s) callbackDispatcher = Dispatchers.Main, // Optional dispatcher for callback. (Defaults to Dispatchers.Main.) callback = { result -> when (result) { is Result.Success -> { // Handle successful response val response: DecisionsResponse = result.output // Process personalizations... } is Result.Failure -> { // Handle error val exception: PersonalizationException = result.throwable Log.e(TAG, "Personalization failed: ${exception.message}") } } } ) ``` | Parameter | Description | | :------------------------ | :------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | | personalizationPointNames | A list of at least one personalization point API Name (not the display label) for which you want to return decisions. | | context | (Optional) Additional context information. See [Request Personalization By Using Context](#request-personalization-by-using-context). If you don't have any context, pass nil. | | timeoutMs | (Optional) The duration, in seconds, before the fetch task times out. The default is 10 seconds. | | callbackDispatcher | (Optional) The dispatcher for callback. The default is `Dispatchers.Main`. | | callback | Function that executes when the fetch operation completes. The callback receives a `Result` object containing either a `DecisionsResponse` on success or a `PersonalizationException` on failure. | **note:** Grouping personalization point names by datagraph is the responsibility of the caller. This method does not automatically group or separate requests based on datagraph configuration. See [Request Personalization](https://developer.salesforce.com/docs/marketing/einstein-personalization/guide/decisioning-api-request-personalization.html#body). ### Request Personalization By Using Context You can use additional context to influence personalization decisions. Use this method only if you have specific context available to affect the decisions. To know about context variables and how they can be used, see [Filter Recommendations Using Dynamic Context Variables](https://help.salesforce.com/s/articleView?id=mktg.persnl_recommender_filter_using_dynamic_variable.htm). Construct a `DecisionsRequestContext` object and use the required context. ```kotlin import com.salesforce.personalization.http.DecisionsRequestContext /* Optionally use a request context */ val decisionsRequestContext = DecisionsRequestContext.Builder() // Optionally define the anchor item for your recommendations .anchorId("PRODUCT_123") // Optionally define type of an anchor item .anchorDmoName("ssot__GoodsProduct__dlm") // Optionally define the contextual attributes and their values .contextualAttribute(name: "category", value: "shoes") .contextualAttribute(name: "color", value: "red") .build() ``` For the `DecisionsRequestContext` builder, optionally add the following fields. | Optional Field | Description | | :-------------------- | :---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `anchorID` | The unique identifier of the specific item the user is currently viewing. This is helpful in context-aware strategies, such as "People who bought this item also bought…" | | `anchorDmoName` | The API Name of the DMO that corresponds to the anchorId. This tells the system what kind of item the anchor is. The DMO name should correspond to the actual one set up in Data 360. Example: `ssot__GoodsProduct__dlm`. If omitted, the system infers the anchor type from `anchorId` automatically. | | `contextualAttribute` | Run-time attributes that help refine recommendation responses and enable advanced filtering logic in your recommenders. To pass multiple attributes, you need to chain a separate method call for each specific key-value pair. For information on contextual attributes, see [Filter Recommendations Using Dynamic Context Variables](https://help.salesforce.com/s/articleView?id=mktg.persnl_recommender_filter_using_dynamic_variable.htm). | The `fetchDecisions` method interacts with the Decisioning API and requests one or more personalizations for specific areas of your mobile application. ```kotlin import com.salesforce.personalization.PersonalizationSdk import com.salesforce.personalization.http.DecisionsRequestContext // In a coroutine scope (suspend function) val result = PersonalizationSdk.fetchDecisions( personalizationPointNames = listOf("Hero_Banner", "Footer_Promo"), decisionsRequestContext = decisionsRequestContext // Applies to ALL personalization points above ) ``` ## Access Responses The `fetchDecisions` method returns a `DecisionsResponse` object. This object contains both the raw list of personalizations and an optimized dictionary lookup. ### `DecisionsResponse` Structure | Field | Type | Description | | :--------------------- | :---------------------------------------------- | :---------------------------------------------------------------------------------------------------------------- | | requestId | String | The unique identifier associated with the underlying network request, useful for diagnostics. | | personalizations | `List` | A list of all personalization objects returned. | | personalizationsByName | `Map` | A dictionary of personalization points indexed by name. We recommend this instead of a list for efficient access. | ### `DecisionsResponsePersonalization` Structure Each item in the `personalizations` list (accessible via `personalizationsByName`) represents the content returned for a specific personalization point: | Field | Type | Description | | :----------------------- | :------------------------------------- | :--------------------------------------------------------------------------------------------------------------------------------- | | `personalizationId` | String | The unique ID of the personalization. | | personalizationPointName | String | The name of the personalization point requested. | | `personalizationPointId` | String | The ID of the personalization point. | | attributes | Map\ | Mapped JSON object of attributes (metadata) returned for this personalization. The schema is defined by the personalization setup. | | data | List\ | The JSON object with content items (the actual personalized content). | | decisionId | String? | (Optional) The unique identifier for the specific decision made by the server. | ### Example to Access Response Content ```kotlin import com.salesforce.personalization.http.DecisionsResponse import com.salesforce.personalization.http.DecisionsResponsePersonalization import com.salesforce.personalization.http.DecisionsResponseContentObject val response: DecisionsResponse = result.output // Using lookup (preferred) val homeHero = response.personalizationsByName["homeHero"] if (homeHero != null) { // Access attributes // Safe string extraction val intro = homeHero.attributes["IntroductionText"] ?.takeIf { it is JsonPrimitive && it.isString } ?.let { (it as JsonPrimitive).content } // Safe number extraction val priority = homeHero.attributes["priority"] ?.takeIf { it is JsonPrimitive && it.isString } ?.let { (it as JsonPrimitive).content?.toIntOrNull() } // Access content objects homeHero.data.forEach { contentObj -> val contentId = contentObj.personalizationContentId val title = contentObj["title"] ?.takeIf { it is JsonPrimitive && it.isString } ?.let { (it as JsonPrimitive).content } ?: "Default Title" val imageUrl = contentObj["imageUrl"] ?.takeIf { it is JsonPrimitive && it.isString } ?.let { (it as JsonPrimitive).content } // Check if key exists if (contentObj.containsKey("description")) { val description = contentObj["description"] ?.takeIf { it is JsonPrimitive && it.isString } ?.let { (it as JsonPrimitive).content } } // Get all available keys val allKeys = contentObj.keys() } } // Iterate all personalizations response.personalizations.forEach { personalization -> println("Point: ${personalization.personalizationPointName}") println("Items: ${personalization.data.size}") } ``` In the example, `fetchDecisions` sends the user context to the server to request content for both `homeHero` and `product_recs` simultaneously. After the response arrives, `personalizationsByName` allows you to access the results for each point independently. ## Error Handling Implement error handling for decision fetching. We recommend logging the exception. ```kotlin import com.salesforce.personalization.PersonalizationSdk import com.salesforce.personalization.errors.PersonalizationException import com.salesforce.personalization.http.DecisionsResponse import com.salesforce.personalization.utils.Result // Simple Error Handling (Recommended) val result = PersonalizationSdk.fetchDecisions(listOf(pointName)) when (result) { is Result.Success -> { // Handle success val response = result.output // Process personalizations... } is Result.Failure -> { // Log the error - no need to differentiate exception types for most use cases Log.e(TAG, "Personalization failed: ${result.throwable.message}", result.throwable) } } // Detailed Error Handling (Optional) val result = PersonalizationSdk.fetchDecisions(listOf(pointName)) when (result) { is Result.Success -> { // Handle success } is Result.Failure -> { val exception = result.throwable // Optional: Differentiate by exception type if needed when (exception.type) { PersonalizationException.Type.TIMEOUT -> { Log.e(TAG, "Request timeout: ${exception.reason}") // Show retry option to user } PersonalizationException.Type.NETWORK -> { Log.e(TAG, "Network error: ${exception.reason}") // Show offline message } else -> { Log.e(TAG, "Personalization error: ${exception.message}") } } } } ``` # Android Low-Code Integration Use the LowCodeMobile Android SDK to display personalized content in your Android app with minimal setup. The SDK renders out-of-the-box Banner and Recommendations components and supports custom components for fully custom UI. 1. [Install the SDK](#install-the-sdk) 2. [Initialize the SDK](#initialize-the-sdk) 3. [Display a Content Zone](#display-a-content-zone) 4. [Out-of-the-Box Components](#out-of-the-box-components) 5. [Custom Components](#custom-components) 6. [Engagement Tracking](#engagement-tracking) 7. [Preview](#preview) 8. [Design-Time Rendering](#design-time-rendering) 9. [Error Handling](#error-handling) ## Install the SDK Add the required Maven repositories to your project-level `settings.gradle.kts` (or `build.gradle`): ```kotlin repositories { maven { url "https://salesforce-marketingcloud.github.io/MarketingCloudSDK-Android/repository" } maven { url "https://salesforce-marketingcloud.github.io/mobile-sdk-cdp-android/repository" } maven { url "https://salesforce-marketingcloud.github.io/Personalization-Android/repository" } } ``` Add the LowCodeMobile dependencies to your app-level `build.gradle.kts`: If you are using the out-of-the-box components (the SDK and lowcodemobile dependencies are pulled in automatically): ```kotlin dependencies { implementation("com.salesforce.personalization:ootbcomponents:1.+") } ``` If you are not using the out-of-the-box components (the SDK dependency is pulled in automatically): ```kotlin dependencies { implementation("com.salesforce.personalization:lowcodemobile:1.+") } ``` ### Required SDK Levels Your app's SDK levels must meet or exceed the following minimums. | Setting | Required | | :----------- | :--------------- | | `minSdk` | 26 (Android 8.0) | | `compileSdk` | 36 (Android 16) | | Kotlin | 2.4+ | ## Initialize the SDK The host app owns SDK initialization. Initialize both the [Data 360 module](https://developer.salesforce.com/docs/data/data-cloud-engagement-mobile-sdk/guide/c360a-api-engagement-mobile-sdk-initialization.html) and Personalization modules in your `Application.onCreate()`. The SDK queues any content zone requests made before initialization completes, so you do not need to gate your UI on init status. ```kotlin import com.salesforce.marketingcloud.cdp.CdpConfig import com.salesforce.marketingcloud.sfmcsdk.SFMCSdk import com.salesforce.marketingcloud.sfmcsdk.SFMCSdkModuleConfig import com.salesforce.personalization.PersonalizationConfig /* Initialization of the CDP module for data collection */ val flushRate = EventFlushRate.Quantity(quantity = 1) val cdpConfig = CdpConfig.Builder( context = this, appId = "", // Your CDP Mobile Connector app ID endpoint = "" // Your CDP Mobile Connector endpoint ).eventFlushRate(flushRate) .build() val personalizationConfig = PersonalizationConfig.Builder(this) .dataspace("default") // Optional — defaults to "default" if omitted .cdnUrl("") // Your CDN URL from the Mobile Connector .build() /* The CDP and Personalization configurations initialized above are provided to SFMCSdk.configure */ SFMCSdk.configure(this, SFMCSdkModuleConfig.build { cdpModuleConfig = cdpConfig personalizationModuleConfig = personalizationConfig }) { initStatus -> Log.d(TAG, "SDK initialized: ${initStatus.status}") } ``` **note:** In your production application, you must explicitly manage user consent using the solution provided in the Engagement Mobile SDK. See [Data 360 Consent Management](https://developer.salesforce.com/docs/data/data-cloud-engagement-mobile-sdk/guide/c360a-api-engagement-mobile-sdk-consent-management.html). ### Considerations - Replace `""`, `""`, and `""` with the values provided by your Salesforce Marketing Cloud administrator. - To avoid committing credentials, read these values from `AndroidManifest.xml` `` entries or a build configuration. - To enable debug logging during development, add `SFMCSdk.setLogging(LogLevel.DEBUG, AndroidLogger())` before `SFMCSdk.configure(...)`, so logging is active for the whole init sequence. Remove this before building for production. ## Display a Content Zone Once the SDK is initialized, use the `ContentZone` composable in Jetpack Compose. Pass the content zone identifier and a list of components the zone is allowed to render. The SDK fetches a decision from the backend, matches the component name returned, and renders the matching component. After adding a ContentZone to your app code, create the matching content zone record in Salesforce Personalization. See [Set Up Mobile Content Zones](https://help.salesforce.com/s/articleView?id=mktg.persnl_experience_mobile_content_zones_register.htm) for instructions on defining personalization points, assigning components, and configuring engagement definitions. ### Jetpack Compose ```kotlin import com.salesforce.personalization.lowcodemobile.ContentZone import com.salesforce.personalization.ootbcomponents.SalesforceBanner import com.salesforce.personalization.ootbcomponents.SalesforceRecommendations @Composable fun HomeScreen() { ContentZone( name = "HomeScreen", allowedComponents = listOf( SalesforceBanner(), SalesforceRecommendations() ) ) } ``` `ContentZone` is a standard composable - place it anywhere in your layout alongside other content. The [Pull-to-refresh](#pull-to-refresh) example below shows it embedded within a scrollable screen. The out-of-the-box component names registered internally are `"Salesforce_Banner"` and `"Salesforce_Recommendations"`. ### ContentZone Parameters | Parameter | Required | Description | | :------------------------ | :------- | :----------------------------------------------------------------------------------------------------------- | | `name` | Yes | The content zone identifier, matching your backend configuration (case-sensitive). | | `allowedComponents` | Yes | The `Components` the zone may render. If the backend returns a name not in this list, the fallback is shown. | | `decisionsRequestContext` | No | Optional context to bias personalization decisions. | | `timeoutMs` | No | Fetch timeout in milliseconds. Default: 10,000 (10 seconds). | | `controller` | No | `ContentZoneController` for programmatic refresh. | | `loading` | No | Composable shown while content is loading. | | `fallback` | No | Composable shown when content cannot be loaded or no component matches. | ### Pull-to-refresh Use a `ContentZoneController` to refresh the zone programmatically: ```kotlin import androidx.compose.material3.pulltorefresh.PullToRefreshBox import androidx.compose.runtime.remember import com.salesforce.personalization.lowcodemobile.ContentZone import com.salesforce.personalization.lowcodemobile.ContentZoneController import com.salesforce.personalization.ootbcomponents.banner.SalesforceBanner import com.salesforce.personalization.ootbcomponents.recs.SalesforceRecommendations import kotlinx.coroutines.launch @Composable fun HomeScreen() { val zoneController = remember { ContentZoneController() } var isRefreshing by remember { mutableStateOf(false) } val scope = rememberCoroutineScope() PullToRefreshBox( isRefreshing = isRefreshing, onRefresh = { isRefreshing = true scope.launch { try { zoneController.refresh() } finally { isRefreshing = false } } } ) { Column(modifier = Modifier.verticalScroll(rememberScrollState())) { Text("Welcome back!") // Other content in your screen ContentZone( name = "HomeScreen", controller = zoneController, allowedComponents = listOf( SalesforceBanner(), SalesforceRecommendations() ) ) // Additional content below the zone } } } ``` ## Out-of-the-Box Components The SDK ships with two pre-built components - `SalesforceBanner` and `SalesforceRecommendations` - that handle layout, styling, and configuration-driven engagement tracking out of the box. Use these when you want to display personalized content without writing a custom UI. Both support an `onTap` callback for custom tap handling; by default, tapping opens the `ctaUrl` if one is provided by the backend. These components are available out-of-the-box in the UI and are registered under the names `"Salesforce_Banner"` and `"Salesforce_Recommendations"`. ### SalesforceBanner ```kotlin ContentZone( name = "HomeScreen", allowedComponents = listOf( SalesforceBanner( onTap = { model -> // Custom tap handling - default behavior opens ctaUrl if provided Log.d("App", "Banner tapped: ${model.header}") } ) ), loading = { CircularProgressIndicator() }, fallback = { Text("Content unavailable") } ) ``` ### SalesforceRecommendations Pass a `DecisionsRequestContext` to bias recommendations based on what the user is currently viewing. All fields are optional - if you only supply `anchorId`, the system infers the type automatically. ```kotlin val context = DecisionsRequestContext.Builder() .anchorId("PRODUCT_123") // Optional: explicitly specify the anchor type. If omitted, the system infers it from anchorId. .anchorDmoName("ssot__GoodsProduct__dlm") .contextualAttribute(name = "category", value = "shoes") .build() ContentZone( name = "ProductRecommendations", allowedComponents = listOf( Recommendations( onTap = { event -> // Custom tap handling - default behavior opens ctaUrl if provided Log.d("App", "Tapped ${event.item.header} at index ${event.index}") } ) ), decisionsRequestContext = context ) ``` ### Custom Styling Override the default appearance of out-of-the-box components by passing a style object: ```kotlin import androidx.compose.ui.graphics.Color import androidx.compose.ui.unit.dp import com.salesforce.personalization.lowcodemobile.ContentZone import com.salesforce.personalization.ootbcomponents.banner.BannerStyle import com.salesforce.personalization.ootbcomponents.banner.SalesforceBanner ContentZone( name = "MyScreen", allowedComponents = listOf( SalesforceBanner( style = BannerStyle( backgroundColor = Color(0xFFF5F5F5), headerTextColor = Color(0xFF1A1A1A), contentPadding = 16.dp ) ) ) ) ``` ## Custom Components Implement the `Component` interface (package com.salesforce.personalization.lowcodemobile) to render a content zone with your own UI. Each component defines: - `val name: String` - The component name that matches the backend experience template's component name. - `val modelClass: KClass` - The Kotlin class reference for the component model. - `validateAndCreateComponentModel(unvalidatedJson, componentContext)` - Validates the JSON payload and returns a Result.Success(model) or Result.Failure(exception). - `@Composable Compose(model, componentContext)` - Renders the composable UI for the component. ### Define a custom component Define a custom component with its own component name, model, and associated experience template in the backend. Give the component a unique name (for example, `CustomHero`), then create a matching experience template in the Core UI and a matching custom component in your app. Your model's fields and types must match the experience template's schema, though you don't need to use all of them. ```kotlin import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier import androidx.compose.ui.layout.ContentScale import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.unit.dp import coil.compose.AsyncImage import com.salesforce.personalization.errors.PersonalizationException import com.salesforce.personalization.lowcodemobile.Component import com.salesforce.personalization.lowcodemobile.ComponentContext import com.salesforce.personalization.lowcodemobile.ComponentModel import com.salesforce.personalization.lowcodemobile.EngagementAction import com.salesforce.personalization.lowcodemobile.TrackEngagementViewOnce import com.salesforce.personalization.utils.Result import kotlinx.serialization.Serializable import kotlinx.serialization.json.Json @Serializable data class HeroModel( val header: String, val imageUrl: String, val ctaText: String? ) : ComponentModel class CustomHero : Component { override val name = "CustomHero" override val modelClass = HeroModel::class internal companion object { private val json = Json { ignoreUnknownKeys = true } } override fun validateAndCreateComponentModel( unvalidatedJson: String, componentContext: ComponentContext ): Result { val model = json.decodeFromString(unvalidatedJson) // Optionally, validate model (blank strings, no fallbacks, missing critical pieces) and if invalid: // return Result.Failure(PersonalizationException.ComponentDataModelInvalid(...)) return Result.Success(model) } @Composable override fun Compose(model: HeroModel, componentContext: ComponentContext) { Column( modifier = Modifier .fillMaxWidth() .clickable { // Track clicks when the user taps componentContext.trackEngagement(EngagementAction.CLICK) // You could add a tapUrl to the model and open/route as well } ) { // This example uses AsyncImage from the Coil dependency AsyncImage( model = model.imageUrl, contentDescription = model.header, modifier = Modifier.fillMaxWidth().height(200.dp), contentScale = ContentScale.Crop ) Text( text = model.header, style = MaterialTheme.typography.titleMedium, modifier = Modifier.fillMaxWidth(), textAlign = TextAlign.Center ) model.ctaText?.let { Text( text = it, modifier = Modifier.fillMaxWidth(), color = MaterialTheme.colorScheme.primary, textAlign = TextAlign.Center ) } } // Track a view once per personalization TrackEngagementViewOnce(componentContext) } } // Usage ContentZone( name = "HomeScreen", allowedComponents = listOf(CustomHero()) ) ``` ### Define a multi-item custom component For a component that renders a list (like `SalesforceRecommendations`), use the per-item variants of the engagement functions to report engagement for each item in the list. ```kotlin import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.size import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.layout.ContentScale import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.unit.dp import coil.compose.AsyncImage import com.salesforce.personalization.errors.PersonalizationException import com.salesforce.personalization.lowcodemobile.Component import com.salesforce.personalization.lowcodemobile.ComponentContext import com.salesforce.personalization.lowcodemobile.ComponentModel import com.salesforce.personalization.lowcodemobile.EngagementAction import com.salesforce.personalization.lowcodemobile.TrackEngagementViewOncePerItem import com.salesforce.personalization.utils.Result import kotlinx.serialization.Serializable import kotlinx.serialization.json.Json @Serializable data class ItemListModel( val sectionHeader: String?, val items: List ) : ComponentModel @Serializable data class ItemModel( val id: String, val title: String, val imageUrl: String, val description: String ) class CustomItemList : Component { override val name = "CustomItemList" override val modelClass = ItemListModel::class internal companion object { private val json = Json { ignoreUnknownKeys = true } } override fun validateAndCreateComponentModel( unvalidatedJson: String, componentContext: ComponentContext ): Result { val model = json.decodeFromString(unvalidatedJson) // Optionally, validate model (blank strings, no fallbacks, missing critical pieces) and if invalid: // return Result.Failure(PersonalizationException.ComponentDataModelInvalid(...)) return Result.Success(model) } @Composable override fun Compose(model: ItemListModel, componentContext: ComponentContext) { Column( modifier = Modifier.fillMaxWidth(), verticalArrangement = Arrangement.spacedBy(16.dp) ) { model.sectionHeader?.takeIf { it.isNotBlank() }?.let { header -> Text( text = header, style = MaterialTheme.typography.titleMedium, modifier = Modifier.fillMaxWidth(), textAlign = TextAlign.Center ) } model.items.forEachIndexed { index, item -> ComposeItem(item, componentContext, index) } } } } @Composable private fun ComposeItem(model: ItemModel, componentContext: ComponentContext, index: Int) { Row( verticalAlignment = Alignment.CenterVertically, modifier = Modifier.clickable { // Track clicks when the user taps componentContext.trackEngagementPerItem(index, EngagementAction.CLICK) // You could add a tapUrl to the model and open/route as well } ) { // Image on the left. This example uses AsyncImage from the Coil dependency. AsyncImage( model = model.imageUrl, contentDescription = model.title, modifier = Modifier.size(100.dp), contentScale = ContentScale.Crop ) // Text on the right Column( modifier = Modifier.weight(1f), verticalArrangement = Arrangement.Center ) { Text( text = model.title, fontWeight = FontWeight.SemiBold, maxLines = 1, overflow = TextOverflow.Ellipsis ) Spacer(modifier = Modifier.height(8.dp)) Text( text = model.description, fontWeight = FontWeight.Normal, maxLines = 1, overflow = TextOverflow.Ellipsis ) } } // Track a view once per personalization, per item TrackEngagementViewOncePerItem(componentContext, index) } // Usage ContentZone( name = "HomeScreen", allowedComponents = listOf(CustomItemList()) ) ``` ## See Also - [Create a Mobile Experience Template](https://help.salesforce.com/s/articleView?id=mktg.persnl_experience_mobile_template_create.htm) ## 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](https://help.salesforce.com/s/articleView?id=mktg.persnl_experience_mobile_engagement_tracking.htm) for instructions on defining engagement actions alongside your components and content zones. Out-of-the-box components (`SalesforceBanner`, `SalesforceRecommendations`) track `View` and `Click` engagement automatically. These are the only two actions currently supported end-to-end. The SDK accepts custom action strings for future expansion. For custom components, engagement is not automatic - you must call these methods explicitly from your component code: | Function | Use for | | :-------------------------------------------------------- | :---------------------------------------------------------------------------------------------------- | | `componentContext.trackEngagement(action)` | Clicks on a single-item component (for example, `CustomHero`) | | `componentContext.trackEngagementPerItem(index, action)` | Clicks on an item within a multi-item component (for example, `CustomItemList`) | | `TrackEngagementViewOnce(componentContext)` | Views of a single-item component - a Composable, call once per personalization | | `TrackEngagementViewOncePerItem(componentContext, index)` | Views of an item within a multi-item component - a Composable, call once per item per personalization | See [Define a custom component](#define-a-custom-component) and [Define a multi-item custom component](#define-a-multi-item-custom-component) above for both in use. ## Preview The SDK supports previewing personalized content via QR code or preview URL. When a URL containing the `sfp-preview` parameter is opened, the SDK renders preview content in all active content zones. Before using preview, make sure: 1. Your app is configured to handle URL schemes or app links (via `AndroidManifest.xml` intent filters). 2. The base URL is set in your Data 360 mobile connector configuration. ```kotlin class MainActivity : ComponentActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) PersonalizationSdk.handlePreviewIntent(intent) } override fun onNewIntent(intent: Intent) { super.onNewIntent(intent) PersonalizationSdk.handlePreviewIntent(intent) } } ``` ## Design-Time Rendering Use `MockDataContentZone` to render components with mock data for styling and layout without any backend setup or networking. This is not related to QR-code based previewing - it simply lets you see how components look during development. ```kotlin @Preview // Compose design-time rendering - not related to QR-code based previewing @Composable fun HomeScreenPreview() { MockDataContentZone( name = "HomeScreen", allowedComponents = listOf(SalesforceBanner(), SalesforceRecommendations()), mockContent = Result.Success(myMockComponentModel) // replace myMockComponentModel with the model you need, for example: // BannerModel( // header = "Hello", // imageUrl = "https://example.com/banner.png", // ctaText = "Find out more" // ) ) } ``` To try out your `fallback` composable, pass a `Result.Failure`: ```kotlin @Preview @Composable fun ErrorStatePreview() { MockDataContentZone( name = "HomeScreen", allowedComponents = listOf(SalesforceBanner()), mockContent = Result.Failure( PersonalizationException(PersonalizationException.ContentZoneUnableToRender("Preview Error")) ) ) } ``` ## Error Handling The SDK uses `PersonalizationException` with a `Type` enum for error reporting: | Type | Description | | :----------------- | :-------------------------------------------------------- | | `UNKNOWN` | An unknown error occurred. | | `INITIALIZATION` | The SDK has not been initialized or failed to initialize. | | `CONSENT` | User consent is not set to opt-in. | | `REQUEST_INVALID` | The request parameters are invalid. | | `NETWORK` | A network error occurred during the fetch. | | `RESPONSE_INVALID` | The server response could not be parsed. | | `TIMEOUT` | The fetch request exceeded the timeout duration. | ContentZone handles errors internally. If no `fallback` is provided, the zone logs the error and shows nothing - this is the typical production behavior. If you want to display fallback content, pass a `fallback` composable. The following example is for illustration only - consider what behavior is appropriate for your production app: ```kotlin ContentZone( name = "HomeScreen", allowedComponents = listOf(SalesforceBanner()), fallback = { error -> when ((error as? PersonalizationException)?.type) { PersonalizationException.Type.TIMEOUT -> Text("Request timed out. Pull to refresh.") PersonalizationException.Type.NETWORK -> Text("No network connection.") else -> Text("Content unavailable.") } } ) ``` # Batch Personalization Output DMO The Batch Personalization Output Data Model Object (DMO) captures information about the decisions created for a batch personalization. ## Object API Name The API name is automatically generated from the batch personalization name. ## Primary Key Personalization Batch Decision Output Id (`Id__c`). ## All Fields | Field Name | Field API Name | Description | Data Type | | :----------------------------------------------------- | :---------------------------------- | :------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | :-------- | | Profile Id | `ProfileID__c` | The unified individual ID of the record that's personalized (if available) or the individual ID. | Text | | Individual Id | `IndividualId__c` | The individual identifier used during personalization. | Text | | Key Qualifier Personalization Batch Decision Output Id | `KQ_Id__c` | The internal ID used by Data Cloud for table indexing. | Text | | Segment Id | `SegmentId__c` | The ID of the segment the batch Personalization is run on. | Text | | Personalization Point Id | `PersonalizationPointId__c` | The ID of the personalization point that is used to generate the activation data. | Text | | Personalization Point Api Name | `PersonalizationPointApiName__c` | The API name or developer name of the personalization point that is used to generate the activation data. | Text | | Personalization Decision Id | `PersonalizationDecisionId__c` | The ID of the personalization decision selected in the personalization point that is used to generate the activation data. | Text | | Personalization Batch Decision Id | `PersonalizationBatchDecisionId__c` | The ID of the batch personalization that created the output. | Text | | Personalization Id | `PersonalizationId__c` | The unique identifier for the personalization request that is generated by Personalization. | Text | | Last Updated Date Time | `LastUpdatedDateTime__c` | The date and time when the personalization is performed. | DateTime | | Activation Data | `ActivationData__c` | The JSON payload containing the personalization response returned by the personalization platform for the given individual, unified individual, personalization point, and personalization decision. The JSON payload structure is similar to a standard personalization decision response. | Text | | Unified Individual Id | `UnifiedIndividualId__c` | The unified individual ID of the record that’s personalized. | Text | # Configure Content Security Policies for Personalization Content Security Policies (CSP) help secure web applications by limiting loadable and executable JavaScript resources. While implementing Personalization on your website using Interactions SDK and Web Personalization Manager (WPM), you must configure certain CSPs to ensure secure and seamless functionality. At this time, Interactions SDK and WPM use inline scripts, necessitating their enablement within your CSP. Due to potential security implications, we strongly recommend implementing a CSP nonce. A nonce is a cryptographically-generated random value that changes with each page load. By including the correct nonce in your CSP and ` ``` **note:** Replace `` with the actual nonce you generated on your server. If opting for this approach to configure CSPs, make sure that you use a server-side framework that supports nonce generation and injection. Also make sure that you apply the same nonce value to the CSP header and ` ``` ### Example: Personalization Content Handler React Component Here's an example that demonstrates how to implement a custom content handler component in React. This component registers itself as a content zone handler and manages the rendering of personalized content: ```jsx import React, { useEffect, useState } from "react"; export const PersonalizationContentHandler = ({ children, contentZoneName, contentZoneLabel }) => { /** Define state variable for Personalized Content */ const [personalizedContent, setPersonalizedContent] = useState(undefined); /** Define state variable for your component's elementId the unique CSS selector */ const [elementId, setElementId] = useState(undefined); /** Initialize the component */ useEffect(() => { /** Generate elementId for the unique CSS selector */ setElementId(`__sf_personalization_contentzonehandler_${contentZoneName}`); /** Register Content Zone Handler */ window.SalesforceInteractions.Personalization.Config.ContentZoneHandler.set( /** Use content zone name from an attribute of the component */ contentZoneName, { /** Use content zone label from an attribute of the component */ label: contentZoneLabel, /** Generate a unique CSS selector for the component based on the generated elementId */ path: `#${elementId}`, /** Set onReady function. 'handleReady' function is defined below */ onReady: handleReady, /** Set onRevert function. */ onRevert: () => { /** Reset the value of the 'personalizedContent' state variable */ setPersonalizedContent(undefined); }, }, ); return () => { /** Deregister content zone handler at cleanup */ window.SalesforceInteractions.Personalization.Config.ContentZoneHandler.set(contentZoneName, { onReady: () => {}, }); }; }); const handleReady = (content) => { /** Assign personalized content returned by Personalization to the 'personalizedContent' state variable */ setPersonalizedContent(content); }; /** Render personalized content, if available */ /** Content is wrapped with a DIV that has a unique ID attribute */ if (personalizedContent) { return
; } else { /** Render original content if personalized content is not available */ /** Content is wrapped with a DIV that has a unique ID attribute */ return
{children}
; } }; ``` Here's how you can use the custom content handler component in your application. ```jsx

Non-personalized content

This content will be replaced with Personalized Content

``` ## Best Practices - Each content zone handler must have a unique, machine-friendly name. - Implement `onRevert` for proper WPM preview/cancel support. - Use `path` for simple highlight scenarios; use `onHighlight` for advanced or Shadow DOM cases. - The `onReady` callback must render the personalized content. If not set, show the original children. - Unregister or reset handlers on component unmount if needed. - The `label` property makes the handler user-friendly in WPM. ## Troubleshooting - If personalized content disappears, ensure you are not manipulating the DOM outside React. - If preview cancel in WPM does not work, implement `onRevert`. - If WPM cannot highlight your zone, check your `path` or implement `onHighlight`. ## See Also - [Personalize Web Experiences](https://developer.salesforce.com/docs/marketing/einstein-personalization/guide/personalize-web-experiences.md) - [Integrate the Salesforce Interactions SDK](https://developer.salesforce.com/docs/marketing/einstein-personalization/guide/integrate-salesforce-interactions-sdk.md) - [Example Sitemap](https://developer.salesforce.com/docs/marketing/einstein-personalization/guide/example-sitemap.md) # Integrate the Salesforce Interactions SDK To integrate the Salesforce Interactions SDK with your website and enable personalization capabilities: 1. [Create a data space](#create-a-data-space) 2. [Create a website connector](#create-a-website-connector) 3. [Upload your event schema](#upload-your-event-schema) 4. [Install the Interactions SDK on your website](#install-the-interactions-sdk-on-your-website) 5. [Create and upload a sitemap](#create-and-upload-a-sitemap) 6. [Create a website data stream](#create-a-website-data-stream) 7. [Map website connector object fields](#map-website-connector-object-fields) ## Create a Data Space You can connect Personalization to any data space, including the default data space that’s provided when you install Data Cloud. Until you create additional data spaces, all Data Cloud objects are mapped to the default data space. To segregate your brand, region, or department data and services, create additional data spaces. To create more data spaces, you need the Data Spaces add-on license. To create a data space, perform these steps. 1. Go to Data Cloud Setup. 2. Under Data Management, click **Data Spaces**. 3. Click **New**, and give the data space a unique name. 4. Enter a unique data space prefix, starting with a letter and including up to three alphanumeric characters. **note:** After you save the data space, you can’t change its prefix because it becomes part of the API name used to differentiate objects that exist in multiple data spaces. 5. Add an optional description about the purpose of the data space. 6. Click **Save**. **See Also** - _Salesforce Help_: [Manage Data Spaces](https://help.salesforce.com/s/articleView?id=sf.c360_a_data_spaces.htm) - _Salesforce Help_: [Create a Data Space](https://help.salesforce.com/s/articleView?id=sf.c360_a_data_spaces_create.htm) ## Create a Website Connector To capture website data and control the scope of data ingestion, connect your website to Data Cloud by creating a website connector. 1. Go to Data Cloud Setup. 2. Under CONFIGURATION, click **Websites & Mobile Apps**. 3. Click **New**. 4. Provide a **Connector Name**. 5. From the Connector Type dropdown, select **Website** and click **Save**. ## Upload Your Event Schema With Interactions SDK, you can track various types of interactions on your website, with each interaction capturing a specific set of data. When setting up a website connector to send this data to Data Cloud, you must upload a schema file in JSON format that outlines the structure of the event data you’re tracking. The SDK provides a recommended schema file that covers common events such as adding items to a cart or viewing an item in a product catalog. This recommended schema includes mappings for: - Engagement Events - Cart Interaction Mapping - Catalog Interaction Mapping - Order Interaction Mapping - Consent Event Mapping - Profile Events - Contact Point Email Mapping - Contact Point Phone Mapping - Identity Mapping - Party Identification Mapping To use the power of the unified event model, we suggest using the recommended schema. Alternatively, you can upload your own custom schema. Once uploaded, each interaction type appears as an object with corresponding attributes. 1. [Download the recommended schema file](https://cdn.c360a.salesforce.com/cdp/schemas/250/web-connector-schema.json) or use your own custom schema file. 2. To upload the schema file, click **Upload Schema** under the Schema block. 3. Confirm that all events and their associated fields are populated correctly. 4. Click **Save**. After you upload the schema, examine it for accuracy. 1. To change your schema at any point, click **Update Schema**. **important:** You can only add new events and fields. The schema must retain all previous events and fields. 2. To update your schema, click **Yes, Update**. 3. To view the full schema you uploaded, click **View Full Schema**. **See Also** - _Download_: [Recommended Web Connector Schema (JSON)](https://cdn.c360a.salesforce.com/cdp/schemas/250/web-connector-schema.json) ## Install the Interactions SDK On Your Website After you’ve created a connector for your website and configured your event schema, you can install the Interactions SDK on your website. 1. Scroll down to the Integration Guide section of the website connector setup page. 2. Copy the Content Delivery Network (CDN) URL. 3. Add the script to the `` section of your website’s code using a `