Skip to content

arupsarkar-sfdc/Pronto-Food-Service-Mobile

Repository files navigation

Pronto Food Delivery App

Modern iOS food delivery application with Salesforce Data Cloud and AI-powered personalization


🎯 Business Overview

Pronto is a next-generation food delivery platform that leverages Salesforce Data Cloud and real-time personalization to deliver individualized experiences across all channels. Whether a user browses on the web or mobile app, their preferences and behaviors are unified in real-time, enabling intelligent recommendations that drive engagement and conversion.

The Problem We Solve

Traditional food delivery apps treat each channel (web, mobile, tablet) as isolated silos. A user who clicks "Sushi" 6 times on the website still sees generic "Pizza" recommendations when they open the mobile app. This disconnect leads to:

  • ❌ Poor user experience (irrelevant recommendations)
  • ❌ Lost revenue (lower conversion rates)
  • ❌ Wasted engineering effort (complex custom logic per channel)

Our Solution

Pronto uses Salesforce Data Cloud for omni-channel identity resolution and Pro Code Personalization to deliver context-aware recommendations:

  • βœ… Real-time data sync (<300ms from web click to mobile update)
  • βœ… Cross-channel identity resolution (automatic device linking)
  • βœ… AI-driven personalization (no custom ranking logic needed)
  • βœ… Developer-friendly SDK (single API call replaces 500+ lines of code)

Result: Users see "Sushi" recommendations on mobile because the system knows they prefer it from their web behaviorβ€”automatically, in real-time, with zero custom logic.


πŸš€ Key Features

1. Omni-Channel Personalization

  • Pro Code SDK Integration: One SDK call fetches personalized content based on unified user profiles
  • Real-Time Decision Engine: Data Cloud evaluates rules server-side and returns optimal content
  • Cross-Platform Context: Web clicks instantly influence mobile recommendations (<300ms)
  • Dynamic Content Rendering: "Sushi" vs "Pizza" decided by actual user engagement, not static rules

2. Intelligent User Engagement Tracking

  • Automatic Identity Resolution: Links browser sessions to mobile devices via Unified Individual profiles
  • Comprehensive Event Tracking: Cart, catalog, favorites, orders, searchesβ€”all tracked automatically
  • Privacy-First Consent Management: GDPR-compliant opt-in/opt-out with full transparency
  • Location-Aware Personalization: GPS tracking for hyper-local restaurant recommendations

3. Modern iOS Architecture

  • SwiftUI + MVVM: Declarative UI with clean separation of concerns
  • Async/Await: Modern concurrency for smooth, responsive UX
  • Type-Safe Networking: Strongly-typed API layer with Combine support
  • Modular Codebase: Feature-based organization for scalability

4. Salesforce Data Cloud Integration

  • Customer Data Platform (CDP): Unified customer profiles across all touchpoints
  • Real-Time Event Streaming: Engagement data ingested in <300ms
  • Data Graph API Access: Query user behavior and preferences on-demand
  • Mobile Connector: Native iOS SDK for seamless Data Cloud communication

πŸ’» Engineering Highlights

Why Pro Code Personalization is a Game-Changer

Traditional personalization requires developers to:

  1. Fetch all content options from an API
  2. Fetch user engagement data from another API
  3. Write custom scoring/ranking algorithms
  4. Handle identity resolution across devices
  5. Maintain complex rule engines

With Salesforce Personalization SDK, this entire flow collapses into a single SDK call:

Traditional Approach (Multiple APIs + Custom Logic)

// ❌ Old Way: 500+ lines of code
let allOptions = try await fetchPersonalizationOptions()
let engagements = try await fetchUserEngagements()
let deviceLinks = try await linkDeviceIdentities()
let winner = customScoringAlgorithm(allOptions, engagements, deviceLinks)
renderContent(winner)

Pro Code SDK Approach (Single Call)

// βœ… New Way: 3 lines of code
let response = try await PersonalizationModule.fetchDecisions(
    personalizationPointNames: ["Pronto"]
)
renderContent(response.personalizations["Pronto"])  // Done!

Two Killer SDK Capabilities

1. Automatic Device Identity Resolution

Problem: Users interact with brands across multiple devices. Linking these identities manually requires complex correlation logic.

Solution: The SDK handles cross-channel identity resolution seamlessly:

  • Web browser session (device ID: 4be44eaae0770cdd)
  • iOS app session (device ID: D46475B0-B111-4EA0-A6BF-5E1AD8FC4675)
  • Automatically linked via UnifiedIndividual (9ea2aa85ce5b5a1e15498c204306aa76)

Developer Impact: Zero lines of identity stitching code. The SDK correlates devices automatically using email, cookies, device IDs, and more.


2. Server-Side Decision Evaluation

Problem: REST APIs return raw data, forcing apps to implement custom scoring, ranking, and rule evaluation.

Solution: The SDK evaluates personalization rules in Data Cloud and returns the optimal decision:

  • User clicks "Sushi" 6 times on web + "Pizza" 4 times β†’ SDK returns "Sushi"
  • No client-side logic needed
  • Rules managed centrally in Data Cloud (no app updates required)

Developer Impact: No conditional logic, no scoring algorithms, no rule engines. Just render the result.


Real-World Demo Flow

1. Web Interaction
   User clicks "Sushi" (6x) and "Pizza" (4x) on website
   ↓
2. Data Cloud Ingestion (<300ms)
   Events streamed β†’ Identity resolved β†’ Profile updated
   ↓
3. Mobile App Launch
   iOS app calls: fetchDecisions("Pronto")
   ↓
4. Smart Response
   SDK returns "Sushi" (higher engagement) automatically
   ↓
5. Instant Render
   App displays personalized "Sushi" contentβ€”no custom logic

Key Insight: What would typically require 500+ lines of identity resolution, engagement tracking, scoring logic, and rule evaluation is reduced to a single SDK call.


πŸ—οΈ Technical Architecture

Architecture Pattern: MVVM (Model-View-ViewModel)

β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚                      SwiftUI Views                   β”‚
β”‚  (Declarative UI, @StateObject, @ObservedObject)    β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
                       β”‚
                       β”‚ Observes @Published properties
                       β”‚
β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β–Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚                    ViewModels                        β”‚
β”‚  (Business Logic, State Management, ObservableObject)β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
                       β”‚
                       β”‚ Uses Services
                       β”‚
β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β–Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚                  Service Layer                       β”‚
β”‚ β€’ PersonalizationService (Pro Code SDK)             β”‚
β”‚ β€’ DataCloudService (Event Tracking)                 β”‚
β”‚ β€’ DataGraphQueryService (Real-Time Data)            β”‚
β”‚ β€’ ProfileDataService (Identity Management)          β”‚
β”‚ β€’ ConsentService (Privacy Controls)                 β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
                       β”‚
                       β”‚ Communicates with
                       β”‚
β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β–Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚              Salesforce Data Cloud                   β”‚
β”‚ β€’ Customer Data Platform (CDP)                       β”‚
β”‚ β€’ Personalization Engine                             β”‚
β”‚ β€’ Data Graph API                                     β”‚
β”‚ β€’ Identity Resolution                                β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜

Core Services

Service Purpose Key Features
PersonalizationService Fetch AI-driven content decisions β€’ Single SDK call
β€’ Server-side rule evaluation
β€’ Cross-channel context
DataCloudService Track user engagement events β€’ Cart, catalog, favorites
β€’ Automatic batching
β€’ Real-time streaming
DataGraphQueryService Query user behavior on-demand β€’ Direct API access
β€’ JWT token management
β€’ Unified profile data
ProfileDataService Manage identity lifecycle β€’ Anonymous β†’ Known transition
β€’ Device info capture
β€’ Contact attributes
ConsentService GDPR-compliant privacy β€’ Opt-in/opt-out
β€’ Persistent storage
β€’ Auto event blocking
LocationTrackingService GPS-based personalization β€’ CoreLocation integration
β€’ Automatic permission mgmt
β€’ 60s expiration

πŸ“Š Salesforce Integration Deep Dive

1. Data Cloud Event Tracking

Every user action is tracked and streamed to Data Cloud in real-time:

User Action Event Type Data Captured
Browse menu catalog Product ID, category, timestamp
Add to favorites addToFavorite Product ID, name, price
Add to cart cart + cartItem Quantity, price, currency, session
Place order order + orderItem Order ID, items, total, address
Update profile identity Name, email, phone
Screen navigation appEvents Screen name, duration
Consent changes consentLog Status, purpose, timestamp

Example: Add to Cart

engagementService.trackAddToCart(
    productId: "pizza-123",
    productName: "Margherita Pizza",
    quantity: 1,
    price: 11.88,
    currency: "USD"
)

This automatically includes:

  • βœ… Device ID
  • βœ… Session ID
  • βœ… Timestamp (ISO 8601)
  • βœ… User identity (if known)
  • βœ… Location (if enabled)

2. Pro Code Personalization Flow

Step 1: User Browses (Web)

User clicks "Sushi" 6x on website
   ↓
Event: { productId: "sushi", channel: "web" }
   ↓
Data Cloud: Streamed + Ingested (<300ms)
   ↓
Individual Profile: Updated with engagement

Step 2: User Opens Mobile App

// iOS App calls Personalization SDK
let response = try await PersonalizationModule.fetchDecisions(
    personalizationPointNames: ["Pronto"]
)

Step 3: Data Cloud Evaluates Rules

Data Cloud:
  1. Identifies user via UnifiedIndividual
  2. Queries ProductBrowseEngagement events
  3. Aggregates clicks: { Sushi: 6, Pizza: 4 }
  4. Evaluates personalization rules
  5. Returns winning decision: "Sushi"

Step 4: App Renders Content

// SDK response contains winning decision
let winner = response.personalizations["Pronto"]
// Display: Sushi background image, CTA, etc.

Key Insight: The app never writes custom logic to determine what to show. Data Cloud does the heavy lifting.


3. Identity Resolution

Anonymous β†’ Known User Journey

// 1. App Launch (Anonymous)
ProfileDataService.shared.setAnonymousProfile()
// β†’ SDK generates anonymous ID
// β†’ All events tagged with anonymous ID

// 2. User Signs Up / Logs In
ProfileDataService.shared.setKnownProfile(
    firstName: "Leander",
    lastName: "Paes",
    email: "leander.paes@example.com"
)
// β†’ Profile transitions to "known"
// β†’ ALL past anonymous events linked to known user
// β†’ Identity attributes propagated to future events

// 3. Update Contact Info
ProfileDataService.shared.updateContactInformation(
    phone: "+1-555-123-4567",
    address: Address(...)
)
// β†’ Contact attributes sent to Data Cloud
// β†’ Future events include phone + address

Why This Matters:

  • No manual event migration needed
  • Historical behavior preserved
  • Single unified profile across all channels

4. Data Graph API

Direct access to unified customer data:

let result = try await DataGraphQueryService.shared.queryDataGraph(
    dataGraphName: "C360_Contact_RT",
    dmoName: "UnifiedLinkssotIndividualI1__dlm",
    fieldName: "UnifiedRecordId__c",
    value: "9ea2aa85ce5b5a1e15498c204306aa76"
)

Returns:

{
  "data": [{
    "ssot__FirstName__c": "Leander",
    "ssot__LastName__c": "Paes",
    "ssot__ProductBrowseEngagement__dlm": [
      { "ssot__ProductId__c": "Sushi", "ssot__CreatedDate__c": "..." },
      { "ssot__ProductId__c": "Pizza", "ssot__CreatedDate__c": "..." }
    ]
  }]
}

Use Cases:

  • Display user's order history
  • Show favorite products
  • Calculate engagement metrics
  • Personalize content in real-time

πŸ› οΈ Getting Started

Prerequisites

  • iOS 15.0+
  • Xcode 14.0+
  • Swift 5.7+
  • Salesforce Data Cloud Tenant (with Mobile Connector configured)
  • SPM (Swift Package Manager) for dependencies

Installation

  1. Clone the Repository

    git clone https://github.com/yourusername/ProntoFoodDeliveryApp.git
    cd ProntoFoodDeliveryApp
  2. Open in Xcode

    open ProntoFoodDeliveryApp.xcodeproj
  3. Configure Salesforce Credentials

    Update Sources/Core/Services/Salesforce/DataCloud/DataCloudConfiguration.swift:

    static var development: DataCloudConfiguration {
        DataCloudConfiguration(
            appId: "YOUR_DEV_APP_ID",        // From Mobile Connector
            endpoint: "YOUR_DEV_ENDPOINT",    // From Mobile Connector
            enableLogging: true
        )
    }
  4. Install Dependencies

    Xcode will automatically resolve SPM dependencies on first build:

    • Salesforce Marketing Cloud SDK
    • CDP Module
    • Personalization Module
  5. Build and Run

    ⌘ + R  (or click the Play button)
    

Salesforce Data Cloud Setup

  1. Create Mobile Connector

    • Navigate to Data Cloud β†’ Mobile & Web
    • Create a new Mobile Connector
    • Copy appId and endpoint
  2. Configure Personalization Point

    • Navigate to Personalization β†’ Points
    • Create "Pronto" personalization point
    • Add decision records: "Pizza", "Sushi"
    • Define attributes: BackgroundImageUrl, CallToActionText, CallToActionUrl
  3. Set Up Identity Resolution

    • Configure reconciliation rules (email, device ID)
    • Map data sources (Web, Mobile)
    • Enable real-time identity resolution
  4. Test Integration

    • Run the app
    • Check Xcode console for SDK initialization logs
    • Verify events in Data Cloud β†’ Engagement Streams

πŸ“ Project Structure

ProntoFoodDeliveryApp/
β”œβ”€β”€ README.md                                 # This file
β”œβ”€β”€ Package.swift                             # SPM dependencies
β”œβ”€β”€ Sources/
β”‚   β”œβ”€β”€ App/
β”‚   β”‚   └── ProntoFoodDeliveryAppApp.swift   # App entry point & SDK init
β”‚   β”‚
β”‚   β”œβ”€β”€ Core/
β”‚   β”‚   β”œβ”€β”€ Models/
β”‚   β”‚   β”‚   β”œβ”€β”€ Product.swift                # Business entities
β”‚   β”‚   β”‚   └── PersonalizationDecisionRecord.swift  # Pro Code models
β”‚   β”‚   β”‚
β”‚   β”‚   β”œβ”€β”€ Services/
β”‚   β”‚   β”‚   └── Salesforce/
β”‚   β”‚   β”‚       β”œβ”€β”€ DataCloud/               # Event tracking services
β”‚   β”‚   β”‚       β”‚   β”œβ”€β”€ DataCloudService.swift
β”‚   β”‚   β”‚       β”‚   β”œβ”€β”€ EngagementTrackingService.swift
β”‚   β”‚   β”‚       β”‚   β”œβ”€β”€ ProfileDataService.swift
β”‚   β”‚   β”‚       β”‚   β”œβ”€β”€ ConsentService.swift
β”‚   β”‚   β”‚       β”‚   β”œβ”€β”€ LocationTrackingService.swift
β”‚   β”‚   β”‚       β”‚   └── DataGraphQueryService.swift
β”‚   β”‚   β”‚       β”‚
β”‚   β”‚   β”‚       └── Personalization/         # Pro Code SDK
β”‚   β”‚   β”‚           └── PersonalizationService.swift
β”‚   β”‚   β”‚
β”‚   β”‚   β”œβ”€β”€ Networking/                      # API layer
β”‚   β”‚   β”œβ”€β”€ Persistence/                     # Local storage
β”‚   β”‚   └── Utilities/                       # Helpers
β”‚   β”‚
β”‚   β”œβ”€β”€ Features/                            # MVVM feature modules
β”‚   β”‚   β”œβ”€β”€ Home/
β”‚   β”‚   β”‚   β”œβ”€β”€ Views/
β”‚   β”‚   β”‚   β”‚   └── HomeView.swift
β”‚   β”‚   β”‚   └── ViewModels/
β”‚   β”‚   β”‚       └── HomeViewModel.swift
β”‚   β”‚   β”‚
β”‚   β”‚   β”œβ”€β”€ Profile/
β”‚   β”‚   β”‚   β”œβ”€β”€ Views/
β”‚   β”‚   β”‚   β”‚   └── FavoritesView.swift      # Pro Code Personalization UI
β”‚   β”‚   β”‚   └── ViewModels/
β”‚   β”‚   β”‚       └── PersonalizationViewModel.swift  # Decision logic
β”‚   β”‚   β”‚
β”‚   β”‚   β”œβ”€β”€ Cart/                            # Shopping cart
β”‚   β”‚   β”œβ”€β”€ Order/                           # Order management
β”‚   β”‚   └── Search/                          # Restaurant search
β”‚   β”‚
β”‚   └── Shared/
β”‚       β”œβ”€β”€ UI/
β”‚       β”‚   β”œβ”€β”€ Components/                  # Reusable SwiftUI views
β”‚       β”‚   └── Modifiers/                   # Custom view modifiers
β”‚       └── Protocols/                       # Shared interfaces
β”‚
β”œβ”€β”€ Tests/
β”‚   β”œβ”€β”€ UnitTests/                           # Logic tests
β”‚   β”œβ”€β”€ IntegrationTests/                    # Service tests
β”‚   └── UITests/                             # End-to-end tests
β”‚
β”œβ”€β”€ Resources/
β”‚   β”œβ”€β”€ Images/                              # App assets
β”‚   β”œβ”€β”€ Configuration/                       # Environment configs
β”‚   └── Localizations/                       # i18n strings
β”‚
└── Documentation/
    β”œβ”€β”€ API/                                 # Service docs
    └── Architecture/                        # Design docs

🎨 Code Examples

1. ViewModel with Personalization

import Foundation
import Combine

@MainActor
final class PersonalizationViewModel: ObservableObject {
    
    // MARK: - Published Properties
    
    @Published var winningDecision: PersonalizationDecisionRecord?
    @Published var allDecisionRecords: [PersonalizationDecisionRecord] = []
    @Published var isLoading = false
    @Published var error: String?
    
    // MARK: - Services
    
    private let personalizationService: PersonalizationServiceProtocol
    private let dataGraphService: DataGraphQueryServiceProtocol
    
    // MARK: - Initialization
    
    init(
        personalizationService: PersonalizationServiceProtocol = PersonalizationService.shared,
        dataGraphService: DataGraphQueryServiceProtocol = DataGraphQueryService.shared
    ) {
        self.personalizationService = personalizationService
        self.dataGraphService = dataGraphService
    }
    
    // MARK: - Fetch Personalization with Winner Selection
    
    func fetchPersonalizationWithWinner() async {
        isLoading = true
        defer { isLoading = false }
        
        do {
            // Step 1: Fetch all decision records from SDK
            let result = try await personalizationService.fetchDecisions(
                personalizationPointNames: ["Pronto"]
            )
            
            // Step 2: Parse decision records
            let decisions = parseAllDecisionRecords(from: result)
            self.allDecisionRecords = decisions
            
            // Step 3: Fetch click counts from Data Graph
            let clickCounts = try await fetchClickCountsFromDataGraph()
            
            // Step 4: Select winner based on clicks
            let winner = selectWinner(decisions: decisions, clickCounts: clickCounts)
            self.winningDecision = winner
            
        } catch {
            self.error = error.localizedDescription
        }
    }
    
    // MARK: - Parse Decision Records
    
    private func parseAllDecisionRecords(
        from result: PersonalizationDecisionsResult
    ) -> [PersonalizationDecisionRecord] {
        guard let prontoDecision = result.personalizations["Pronto"] else {
            return []
        }
        
        var records: [PersonalizationDecisionRecord] = []
        
        // Parse from attributes (single decision)
        if let header = prontoDecision.attributes["Header"] as? String {
            let record = PersonalizationDecisionRecord(
                id: prontoDecision.personalizationId,
                name: header,
                backgroundImageUrl: prontoDecision.attributes["BackgroundImageUrl"] as? String,
                callToActionText: prontoDecision.attributes["CallToActionText"] as? String,
                callToActionUrl: prontoDecision.attributes["CallToActionUrl"] as? String,
                header: prontoDecision.attributes["Header"] as? String,
                subheader: prontoDecision.attributes["Subheader"] as? String
            )
            records.append(record)
        }
        
        // Parse from contentObjects (multiple decisions)
        for contentObject in prontoDecision.contentObjects {
            if let name = contentObject.data["name"] as? String ?? contentObject.data["Header"] as? String {
                let record = PersonalizationDecisionRecord(
                    id: contentObject.personalizationContentId,
                    name: name,
                    backgroundImageUrl: contentObject.data["BackgroundImageUrl"] as? String,
                    callToActionText: contentObject.data["CallToActionText"] as? String,
                    callToActionUrl: contentObject.data["CallToActionUrl"] as? String,
                    header: contentObject.data["Header"] as? String,
                    subheader: contentObject.data["Subheader"] as? String
                )
                records.append(record)
            }
        }
        
        return records
    }
    
    // MARK: - Fetch Click Counts from Data Graph
    
    private func fetchClickCountsFromDataGraph() async throws -> [String: Int] {
        let result = try await dataGraphService.queryDataGraph(
            dataGraphName: "C360_Contact_RT",
            dmoName: "UnifiedLinkssotIndividualI1__dlm",
            fieldName: "UnifiedRecordId__c",
            value: "9ea2aa85ce5b5a1e15498c204306aa76"
        )
        
        var counts: [String: Int] = [:]
        
        // Parse ProductBrowseEngagement events
        if let dataArray = result["data"] as? [[String: Any]],
           let firstData = dataArray.first,
           let jsonBlobString = firstData["json_blob__c"] as? String,
           let jsonBlobData = jsonBlobString.data(using: .utf8),
           let jsonBlob = try? JSONSerialization.jsonObject(with: jsonBlobData) as? [String: Any],
           let unifiedLinks = jsonBlob["UnifiedLinkssotIndividualI1__dlm"] as? [[String: Any]] {
            
            for link in unifiedLinks {
                if let individuals = link["ssot__Individual__dlm"] as? [[String: Any]],
                   let individual = individuals.first,
                   let engagements = individual["ssot__ProductBrowseEngagement__dlm"] as? [[String: Any]] {
                    
                    for engagement in engagements {
                        if let productId = engagement["ssot__ProductId__c"] as? String {
                            // Normalize product name
                            let normalized = productId.prefix(1).uppercased() + productId.dropFirst().lowercased()
                            counts[normalized, default: 0] += 1
                        }
                    }
                }
            }
        }
        
        return counts
    }
    
    // MARK: - Select Winner (Case-Insensitive)
    
    private func selectWinner(
        decisions: [PersonalizationDecisionRecord],
        clickCounts: [String: Int]
    ) -> PersonalizationDecisionRecord? {
        guard !decisions.isEmpty else { return nil }
        
        // Merge click counts (case-insensitive)
        var decisionsWithCounts = decisions.map { decision in
            var d = decision
            for (key, count) in clickCounts {
                if key.lowercased() == decision.name.lowercased() {
                    d.clickCount = count
                    break
                }
            }
            return d
        }
        
        // Sort by click count (descending)
        decisionsWithCounts.sort { $0.clickCount > $1.clickCount }
        
        return decisionsWithCounts.first
    }
}

2. SwiftUI View with Pro Code Personalization

struct FavoritesView: View {
    @StateObject private var viewModel = PersonalizationViewModel()
    
    var body: some View {
        ScrollView {
            VStack(spacing: 20) {
                // Pro Code Personalization Section
                proCodePersonalizationCard
                
                // Other content...
            }
        }
        .task {
            await viewModel.fetchPersonalizationWithWinner()
        }
    }
    
    private var proCodePersonalizationCard: some View {
        VStack(alignment: .leading, spacing: 0) {
            // Header with Dynamic Background
            ZStack(alignment: .topTrailing) {
                if let winner = viewModel.winningDecision,
                   let imageUrl = winner.backgroundImageUrl {
                    AsyncImage(url: URL(string: imageUrl)) { image in
                        image
                            .resizable()
                            .aspectRatio(contentMode: .fill)
                            .frame(maxWidth: .infinity, maxHeight: 200)
                            .clipped()
                    } placeholder: {
                        gradientBackground
                    }
                } else {
                    gradientBackground
                }
                
                // "Pro Code" Badge
                HStack(spacing: 8) {
                    Image(systemName: "wand.and.stars")
                    Text("Pro Code")
                        .fontWeight(.semibold)
                }
                .font(.caption)
                .foregroundColor(.white)
                .padding(.horizontal, 12)
                .padding(.vertical, 6)
                .background(Capsule().fill(Color.black.opacity(0.6)))
                .padding(12)
            }
            .frame(height: 200)
            
            // Winning Decision Content
            if let winner = viewModel.winningDecision {
                VStack(alignment: .leading, spacing: 8) {
                    HStack {
                        Text("πŸ† Top Choice for You")
                            .font(.headline)
                        Spacer()
                        if winner.clickCount > 0 {
                            Text("\(winner.clickCount) clicks")
                                .font(.subheadline)
                                .foregroundColor(.secondary)
                        }
                    }
                    
                    Text(winner.name)
                        .font(.title2)
                        .fontWeight(.bold)
                    
                    if let cta = winner.callToActionText,
                       let url = winner.callToActionUrl {
                        Link(destination: URL(string: url)!) {
                            HStack {
                                Text(cta)
                                Image(systemName: "arrow.right.circle.fill")
                            }
                            .font(.subheadline)
                            .fontWeight(.semibold)
                            .foregroundColor(.white)
                            .padding()
                            .background(Capsule().fill(Color.blue))
                        }
                    }
                }
                .padding()
            }
        }
        .background(Color.white)
        .cornerRadius(16)
        .shadow(radius: 4)
        .padding()
    }
    
    private var gradientBackground: some View {
        LinearGradient(
            colors: [Color.purple.opacity(0.6), Color.pink.opacity(0.6)],
            startPoint: .topLeading,
            endPoint: .bottomTrailing
        )
    }
}

πŸŽ“ Best Practices

1. Always Set Identity Early

// βœ… GOOD: Set known profile immediately after login
func handleSuccessfulLogin(user: User) {
    ProfileDataService.shared.setKnownProfile(
        firstName: user.firstName,
        lastName: user.lastName,
        email: user.email
    )
}

2. Check Consent Before Tracking

// βœ… GOOD: Services automatically check consent
engagementService.trackAddToCart(...)  // Only tracks if user opted in

// ❌ BAD: Direct SDK calls bypass consent
SFMCSdk.track(event: myEvent)  // Don't do this

3. Use Dependency Injection

// βœ… GOOD: Inject services for testability
class MyViewModel: ObservableObject {
    private let personalizationService: PersonalizationServiceProtocol
    
    init(personalizationService: PersonalizationServiceProtocol = PersonalizationService.shared) {
        self.personalizationService = personalizationService
    }
}

4. Leverage View Modifiers

// βœ… GOOD: Automatic screen tracking
struct HomeView: View {
    var body: some View {
        VStack { ... }
            .trackScreen("Home")
            .locationAware()
    }
}

5. Keep ViewModels Testable

// βœ… GOOD: Protocol-based services enable mocking
final class MockPersonalizationService: PersonalizationServiceProtocol {
    var mockResponse: PersonalizationDecisionsResult?
    
    func fetchDecisions(...) async throws -> PersonalizationDecisionsResult {
        return mockResponse ?? .empty
    }
}

πŸ› Debugging & Troubleshooting

Enable Debug Logging

// In DEBUG builds, logging is automatic
#if DEBUG
let config = DataCloudConfiguration.development  // enableLogging: true
#endif

Check SDK Status

// Print comprehensive SDK status
DataCloudLoggingService.shared.printSdkStatus()

// Output:
// ============================================================
// πŸ“Š SFMC SDK Status Report
// ============================================================
// SDK State: operational
// CDP Module Status: operational
// Consent Status: optIn
// ============================================================

Common Issues

Issue Solution
Events not appearing in Data Cloud β€’ Check appId and endpoint in config
β€’ Verify consent is set to optIn
β€’ Check network connectivity
Personalization returning empty β€’ Verify personalization point "Pronto" exists in Data Cloud
β€’ Ensure decision records are published
β€’ Check identity resolution is working
Identity not linking devices β€’ Verify email reconciliation is configured
β€’ Check that setKnownProfile() was called
β€’ Ensure web and mobile use same email
Location not working β€’ Request location permission
β€’ Call startTracking()
β€’ Check Info.plist has location usage strings

πŸ“š Additional Resources

Documentation

Salesforce Resources


πŸ‘₯ Contributing

We welcome contributions! Please follow these guidelines:

  1. Fork the repository
  2. Create a feature branch (git checkout -b feature/amazing-feature)
  3. Follow MVVM architecture and existing code patterns
  4. Write unit tests for new features
  5. Update documentation as needed
  6. Commit with meaningful messages
  7. Push to your branch (git push origin feature/amazing-feature)
  8. Open a Pull Request

πŸ“„ License

This project is proprietary and confidential. All rights reserved.


πŸ™ Acknowledgments

  • Salesforce Data Cloud Team for the powerful CDP platform
  • Salesforce Mobile SDK Team for the robust iOS SDK
  • SwiftUI Community for best practices and inspiration

πŸ“ž Support

For questions or issues:

  • Email: support@pronto.com
  • Slack: #pronto-dev-support
  • Internal Wiki: confluence.pronto.com/ios-app

Built with ❀️ by the Pronto Engineering Team

Last Updated: November 2025
Version: 1.0.0
iOS SDK Version: Salesforce Marketing Cloud 8.0+
Minimum iOS: 15.0+

About

This is a generic mobile app which showcases Salesforce Data Cloud and Sub Second Real Time with Personalization

Resources

Stars

Watchers

Forks

Releases

No releases published

Packages

 
 
 

Contributors