Skip to main content
Integrate Encore into your iOS app with just one Swift file. Perfect for teams that prefer web technologies or can’t add new package dependencies.

Overview

The WebView embed integration lets you use Encore’s full feature set through a simple SwiftUI view. No SPM packages, no CocoaPods, no build configuration changes required.

When to Use

Perfect For

  • Apps that can’t add SDK dependencies
  • Quick POCs and MVPs
  • Cross-platform teams
  • Web-first development workflows
  • Rapid integration (5 minutes)

Consider Native SDK

  • Need offline support
  • Want deepest iOS integration
  • Require custom UI components
  • App size is critical
  • Need advanced Swift features

Quick Start

Get up and running in 3 steps:
1

Copy the Integration File

Copy EncoreWebView.swift into your Xcode project. That’s it - one file, no dependencies!
EncoreWebView.swift
//
//  EncoreWebView.swift
//
//  Simple SwiftUI integration for Encore embed
//  Copy this entire file into your Xcode project - no other dependencies needed!
//

import SwiftUI
import WebKit
import SafariServices

// MARK: - Main View

/// SwiftUI view that embeds Encore offer presentation
/// Present as a sheet to show offers to your users
struct EncoreWebView: View {
    let apiKey: String
    let userId: String
    let environment: String
    let attributes: [String: String]
    
    @State private var safariURL: URL?
    @State private var showingSafari = false
    @Environment(\.dismiss) private var dismiss
    
    init(
        apiKey: String,
        userId: String,
        environment: String = "production",
        attributes: [String: String] = [:]
    ) {
        self.apiKey = apiKey
        self.userId = userId
        self.environment = environment
        self.attributes = attributes
    }
    
    var body: some View {
        WebViewRepresentable(
            url: buildURL(),
            onOpenURL: { url in
                safariURL = url
                showingSafari = true
            },
            onDismiss: {
                dismiss()
            }
        )
        .background(Color.white)
        .presentationDetents([.fraction(0.7), .large])
        .presentationDragIndicator(.visible)
        .presentationBackgroundInteraction(.enabled)
        .sheet(isPresented: $showingSafari) {
            if let url = safariURL {
                SafariView(url: url) {
                    showingSafari = false
                }
            }
        }
    }
    
    private func buildURL() -> URL {
        var components = URLComponents(string: "https://encorekit.com/embed")!
        var queryItems: [URLQueryItem] = [
            URLQueryItem(name: "apiKey", value: apiKey),
            URLQueryItem(name: "environment", value: environment),
            URLQueryItem(name: "autoPresentOffer", value: "true"),
            URLQueryItem(name: "t", value: String(Date().timeIntervalSince1970))
        ]
        
        queryItems.append(URLQueryItem(name: "userId", value: userId))
        
        if !attributes.isEmpty,
           let jsonData = try? JSONSerialization.data(withJSONObject: attributes),
           let jsonString = String(data: jsonData, encoding: .utf8) {
            queryItems.append(URLQueryItem(name: "attributes", value: jsonString))
        }
        
        components.queryItems = queryItems
        return components.url!
    }
}

// MARK: - Safari View

private struct SafariView: UIViewControllerRepresentable {
    let url: URL
    let onDismiss: () -> Void
    
    func makeUIViewController(context: Context) -> SFSafariViewController {
        let config = SFSafariViewController.Configuration()
        config.entersReaderIfAvailable = false
        config.barCollapsingEnabled = true
        
        let safariVC = SFSafariViewController(url: url, configuration: config)
        safariVC.delegate = context.coordinator
        safariVC.preferredBarTintColor = .systemBackground
        safariVC.preferredControlTintColor = .systemBlue
        
        return safariVC
    }
    
    func updateUIView controller(_ uiViewController: SFSafariViewController, context: Context) {}
    
    func makeCoordinator() -> Coordinator {
        Coordinator(onDismiss: onDismiss)
    }
    
    class Coordinator: NSObject, SFSafariViewControllerDelegate {
        let onDismiss: () -> Void
        
        init(onDismiss: @escaping () -> Void) {
            self.onDismiss = onDismiss
        }
        
        func safariViewControllerDidFinish(_ controller: SFSafariViewController) {
            onDismiss()
        }
    }
}

// MARK: - WebView Bridge

private struct WebViewRepresentable: UIViewRepresentable {
    let url: URL
    let onOpenURL: (URL) -> Void
    let onDismiss: () -> Void
    
    func makeCoordinator() -> Coordinator {
        Coordinator(onOpenURL: onOpenURL, onDismiss: onDismiss)
    }
    
    func makeUIView(context: Context) -> WKWebView {
        let config = WKWebViewConfiguration()
        config.websiteDataStore = .nonPersistent()
        
        let userContentController = WKUserContentController()
        userContentController.add(context.coordinator, name: "encore")
        config.userContentController = userContentController
        
        let webView = WKWebView(frame: .zero, configuration: config)
        webView.backgroundColor = .white
        webView.isOpaque = true
        webView.scrollView.backgroundColor = .white
        webView.scrollView.contentInsetAdjustmentBehavior = .never
        
        context.coordinator.webView = webView
        
        var request = URLRequest(url: url)
        request.cachePolicy = .reloadIgnoringLocalAndRemoteCacheData
        webView.load(request)
        
        return webView
    }
    
    func updateUIView(_ webView: WKWebView, context: Context) {}
    
    class Coordinator: NSObject, WKScriptMessageHandler, SFSafariViewControllerDelegate {
        weak var webView: WKWebView?
        let onOpenURL: (URL) -> Void
        let onDismiss: () -> Void
        
        init(onOpenURL: @escaping (URL) -> Void, onDismiss: @escaping () -> Void) {
            self.onOpenURL = onOpenURL
            self.onDismiss = onDismiss
        }
        
        func userContentController(_ userContentController: WKUserContentController, didReceive message: WKScriptMessage) {
            guard message.name == "encore",
                  let dict = message.body as? [String: Any],
                  let type = dict["type"] as? String else {
                return
            }
            
            switch type {
            case "encore:openURL":
                if let payload = dict["payload"] as? [String: Any],
                   let urlString = payload["url"] as? String,
                   let url = URL(string: urlString) {
                    DispatchQueue.main.async {
                        self.presentSafari(url: url)
                    }
                }
                
            case "encore:sheet:dismissed":
                DispatchQueue.main.async {
                    self.onDismiss()
                }
                
            default:
                break
            }
        }
        
        private func presentSafari(url: URL) {
            guard let windowScene = UIApplication.shared.connectedScenes.first as? UIWindowScene,
                  let rootViewController = windowScene.windows.first?.rootViewController else {
                return
            }
            
            var topController = rootViewController
            while let presented = topController.presentedViewController {
                topController = presented
            }
            
            let config = SFSafariViewController.Configuration()
            config.entersReaderIfAvailable = false
            config.barCollapsingEnabled = true
            
            let safariVC = SFSafariViewController(url: url, configuration: config)
            safariVC.delegate = self
            safariVC.preferredBarTintColor = .systemBackground
            safariVC.preferredControlTintColor = .systemBlue
            
            topController.present(safariVC, animated: true)
        }
        
        func safariViewControllerDidFinish(_ controller: SFSafariViewController) {
            let script = """
            window.postMessage({
                type: 'safariClosed',
                payload: { timestamp: Date.now() }
            }, '*');
            """
            webView?.evaluateJavaScript(script)
        }
    }
}

// MARK: - Preview

#Preview {
    EncoreWebView(
        apiKey: "your_api_key_here",
        userId: "test_user",
        attributes: [
            "email": "[email protected]",
            "subscriptionTier": "free"
        ]
    )
}
The file uses only built-in iOS frameworks - no external dependencies needed!
2

Add to Your View

Present EncoreWebView as a sheet when you want to show offers:
ContentView.swift
import SwiftUI

struct ContentView: View {
    @State private var showingEncore = false
    
    var body: some View {
        Button("Show Offers") {
            showingEncore = true
        }
        .sheet(isPresented: $showingEncore) {
            EncoreWebView(
                apiKey: "pk_live_your_key_here",
                userId: currentUser.id
            )
        }
    }
}
Replace pk_live_your_key_here with your actual API key from the Encore dashboard.
3

Test It

Run your app and tap the button - you should see the Encore offer sheet appear!
Users can browse offers, claim them, and the sheet automatically dismisses when done.

Usage Examples

Subscription Cancellation Flow

Present a retention offer before allowing cancellation:
CancellationView.swift
struct CancellationView: View {
    @State private var showingEncore = false
    @Environment(\.dismiss) private var dismiss
    
    var body: some View {
        VStack(spacing: 20) {
            Text("Cancel Subscription")
                .font(.title)
            
            Text("Are you sure you want to cancel?")
            
            Button("Show Me Offers First") {
                showingEncore = true
            }
            .buttonStyle(.borderedProminent)
            
            Button("Cancel Anyway", role: .destructive) {
                cancelSubscription()
            }
        }
        .sheet(isPresented: $showingEncore) {
            EncoreWebView(
                apiKey: "pk_live_...",
                userId: currentUser.id,
                attributes: [
                    "subscriptionTier": "premium",
                    "cancelReason": "price"
                ]
            )
        }
    }
    
    func cancelSubscription() {
        // Your cancellation logic
        dismiss()
    }
}

Feature Paywall

Gate premium features behind Encore offers:
PremiumFeatureView.swift
struct PremiumFeatureView: View {
    @State private var showingEncore = false
    let isPremium: Bool
    
    var body: some View {
        Group {
            if isPremium {
                PremiumContent()
            } else {
                VStack(spacing: 20) {
                    Image(systemName: "lock.fill")
                        .font(.largeTitle)
                    
                    Text("Premium Feature")
                        .font(.title)
                    
                    Text("Unlock this and more by completing an offer")
                        .multilineTextAlignment(.center)
                    
                    Button("Unlock Now") {
                        showingEncore = true
                    }
                    .buttonStyle(.borderedProminent)
                }
                .sheet(isPresented: $showingEncore) {
                    EncoreWebView(
                        apiKey: "pk_live_...",
                        userId: currentUser.id
                    )
                }
            }
        }
    }
}

Settings Integration

Add Encore to your app settings:
SettingsView.swift
struct SettingsView: View {
    @State private var showingEncore = false
    
    var body: some View {
        List {
            Section("Account") {
                // Your settings...
            }
            
            Section("Rewards") {
                Button {
                    showingEncore = true
                } label: {
                    Label("Unlock Premium Features", systemImage: "gift")
                }
            }
        }
        .sheet(isPresented: $showingEncore) {
            EncoreWebView(
                apiKey: "pk_live_...",
                userId: currentUser.id
            )
        }
    }
}

Advanced Configuration

Adding User Attributes

Pass user attributes for better offer targeting:
EncoreWebView(
    apiKey: "pk_live_...",
    userId: currentUser.id,
    attributes: [
        // Identity
        "email": "[email protected]",
        "firstName": "Jane",
        "lastName": "Doe",
        
        // Subscription context
        "subscriptionTier": "premium",
        "monthsSubscribed": "6",
        "billingCycle": "monthly",
        "planPrice": "9.99",
        
        // Demographics
        "city": "New York",
        "state": "NY",
        "countryCode": "US",
        "postalCode": "10001",
        
        // Behavior
        "lastActiveDate": "2024-01-15",
        "cancelReason": "price"
    ]
)
More attributes = better offer targeting. Pass any relevant user data to improve offer relevance.

Environment Configuration

Switch between production and development:
EncoreWebView(
    apiKey: isProduction ? "pk_live_..." : "pk_test_...",
    userId: currentUser.id,
    environment: isProduction ? "production" : "localhost"
)
Use localhost environment for testing with local development servers.

Customizing Sheet Presentation

Modify the sheet appearance by changing presentation modifiers in EncoreWebView.swift:
// In EncoreWebView.swift, modify the body:
.presentationDetents([.medium, .large])  // Different sizes
.presentationDragIndicator(.hidden)      // Hide drag indicator
.presentationBackgroundInteraction(.disabled)  // Disable background interaction

How It Works

Architecture Overview

The integration consists of three main components:
  1. EncoreWebView - SwiftUI view that manages the presentation
  2. WebViewRepresentable - Wraps WKWebView and handles JavaScript bridge
  3. SafariView - Opens offer URLs in Safari when user claims an offer

User Flow

  1. User triggers the sheet (e.g., clicks “Show Offers”)
  2. EncoreWebView loads the Encore embed in a WKWebView
  3. User browses and selects an offer
  4. When user clicks “Claim”:
    • Offer URL opens in Safari (within the app)
    • User completes the offer on the partner site
    • User closes Safari
  5. “Credit Claimed” confirmation appears
  6. User clicks “Done” and the sheet auto-dismisses

Safari URL Handling

When a user claims an offer, the URL opens in SFSafariViewController:
  • Users complete offers on partner websites
  • Safari appears as a modal over the Encore sheet
  • Full browser functionality (autofill, password manager, etc.)
  • Automatic tracking and attribution
  • Users return to your app after completion
Always validate offer completion server-side using the Encore entitlements API. Never trust client-side events alone. See Server-Side Validation for implementation details.

Handling Entitlements

When a user completes an offer, the embed automatically notifies your app in real-time. You can customize what happens when entitlements are granted.

How It Works

The EncoreWebView already includes built-in entitlement tracking. When a user completes an offer, you’ll receive an EncoreEntitlement object with:
struct EncoreEntitlement {
    let type: EntitlementType  // .freeTrial, .discount, or .credit
    let value: Int?            // e.g., 30 (days), 20 (percent), 10 (dollars)
    let unit: String?          // e.g., "days", "percent", "dollars"
}

Basic Usage

Simply pass a closure to handle entitlements when creating the view:
ContentView.swift
struct ContentView: View {
    @State private var showingEncore = false
    
    var body: some View {
        Button("Show Offers") {
            showingEncore = true
        }
        .sheet(isPresented: $showingEncore) {
            EncoreWebView(
                apiKey: "pk_live_...",
                userId: currentUser.id,
                onEntitlementGranted: { entitlement in
                    handleEntitlement(entitlement)
                }
            )
        }
    }
    
    func handleEntitlement(_ entitlement: EncoreEntitlement) {
        switch entitlement.type {
        case .freeTrial:
            let days = entitlement.value ?? 7
            print("✅ Granted \(days)-day free trial")
            enableFreeTrial(days: days)
            
        case .discount:
            let percent = entitlement.value ?? 20
            print("✅ Granted \(percent)% discount")
            applyDiscount(percent: percent)
            
        case .credit:
            let amount = entitlement.value ?? 10
            print("✅ Granted $\(amount) credit")
            addAccountCredit(amount: amount)
        }
    }
}

Complete Example

Here’s a real-world example with state management:
PremiumUnlockView.swift
struct PremiumUnlockView: View {
    @State private var showingEncore = false
    @State private var isPremium = false
    @State private var showSuccess = false
    @State private var successMessage = ""
    
    var body: some View {
        VStack(spacing: 20) {
            if isPremium {
                VStack {
                    Image(systemName: "checkmark.circle.fill")
                        .font(.system(size: 60))
                        .foregroundColor(.green)
                    Text("Premium Active!")
                        .font(.title)
                    PremiumContent()
                }
            } else {
                VStack {
                    Image(systemName: "lock.fill")
                        .font(.largeTitle)
                    Text("Unlock Premium Features")
                        .font(.title2)
                    Text("Complete an offer to get instant access")
                        .foregroundColor(.secondary)
                    
                    Button("View Offers") {
                        showingEncore = true
                    }
                    .buttonStyle(.borderedProminent)
                }
            }
        }
        .sheet(isPresented: $showingEncore) {
            EncoreWebView(
                apiKey: "pk_live_...",
                userId: currentUser.id,
                attributes: [
                    "subscriptionTier": "free",
                    "email": currentUser.email
                ],
                onEntitlementGranted: { entitlement in
                    handleEntitlementGranted(entitlement)
                }
            )
        }
        .alert("Success!", isPresented: $showSuccess) {
            Button("OK") {
                showSuccess = false
            }
        } message: {
            Text(successMessage)
        }
    }
    
    func handleEntitlementGranted(_ entitlement: EncoreEntitlement) {
        switch entitlement.type {
        case .freeTrial:
            let days = entitlement.value ?? 7
            enableFreeTrial(duration: days)
            successMessage = "You now have \(days) days of premium access!"
            
        case .discount:
            let percent = entitlement.value ?? 20
            applyDiscount(percent: percent)
            successMessage = "You've earned a \(percent)% discount on your next purchase!"
            
        case .credit:
            let amount = entitlement.value ?? 10
            addCredit(amount: amount)
            successMessage = "You've received $\(amount) in account credit!"
        }
        
        showSuccess = true
        showingEncore = false
    }
    
    func enableFreeTrial(duration: Int) {
        isPremium = true
        
        // Store locally
        UserDefaults.standard.set(true, forKey: "isPremium")
        UserDefaults.standard.set(Date().addingTimeInterval(Double(duration * 86400)), forKey: "premiumExpiry")
        
        // Sync with server (important!)
        Task {
            await syncPremiumStatusWithServer()
        }
    }
    
    func applyDiscount(percent: Int) {
        UserDefaults.standard.set(percent, forKey: "pendingDiscount")
    }
    
    func addCredit(amount: Int) {
        let currentCredit = UserDefaults.standard.integer(forKey: "accountCredit")
        UserDefaults.standard.set(currentCredit + amount, forKey: "accountCredit")
    }
    
    func syncPremiumStatusWithServer() async {
        // Validate with your backend
        // Your server calls Encore's entitlements API with HMAC authentication
    }
}
Security Important: The onEntitlementGranted callback is for immediate UX feedback only. Always validate entitlements server-side before granting actual access. Never trust client-side events for security decisions.

Server-Side Validation

Your app should validate entitlements by calling your backend, which then calls Encore’s server API with HMAC authentication:
EntitlementValidator.swift
class EntitlementValidator {
    
    /// Call your backend to validate entitlements
    func validateEntitlements(userId: String) async throws -> EntitlementResponse {
        let url = URL(string: "https://your-backend.com/api/validate-entitlements")!
        var request = URLRequest(url: url)
        request.httpMethod = "POST"
        request.setValue("application/json", forHTTPHeaderField: "Content-Type")
        request.httpBody = try JSONEncoder().encode(["userId": userId])
        
        let (data, _) = try await URLSession.shared.data(for: request)
        return try JSONDecoder().decode(EntitlementResponse.self, from: data)
    }
    
    /// Check if user has a specific entitlement
    func hasEntitlement(userId: String, type: String, scope: String = "all") async -> Bool {
        do {
            let response = try await validateEntitlements(userId: userId)
            let scopeData: [String: EntitlementData]?
            
            switch scope {
            case "verified": scopeData = response.verified
            case "provisional": scopeData = response.provisional
            default: scopeData = response.all
            }
            
            return scopeData?[type] != nil
        } catch {
            print("Validation failed: \(error)")
            return false
        }
    }
}

struct EntitlementResponse: Codable {
    let all: [String: EntitlementData]?
    let verified: [String: EntitlementData]?
    let provisional: [String: EntitlementData]?
}

struct EntitlementData: Codable {
    let type: String
    let value: Int?
    let unit: String?
    let scope: String
    let grantedAt: String
    let expiresAt: String
    let transactionId: String?
}
Your backend handles the secure HMAC-authenticated call to Encore:
your-backend/validate.js
import crypto from 'crypto';

function generateHmacSignature(privateKey, method, path, body) {
  const bodyJson = JSON.stringify(body);
  const canonical = `${method.toUpperCase()}|${path}|${bodyJson}`;
  const hmac = crypto.createHmac('sha256', privateKey);
  hmac.update(canonical);
  return hmac.digest('hex');
}

app.post('/api/validate-entitlements', async (req, res) => {
  const { userId } = req.body;
  
  const method = 'POST';
  const path = '/encore/v1/entitlements/server';
  const body = { user_id: userId };
  
  const signature = generateHmacSignature(
    process.env.ENCORE_PRIVATE_API_KEY,
    method,
    path,
    body
  );
  
  const timestamp = Math.floor(Date.now() / 1000);
  
  const response = await fetch(`${process.env.ENCORE_BASE_URL}${path}`, {
    method,
    headers: {
      'Content-Type': 'application/json',
      'X-API-Key': process.env.ENCORE_PUBLIC_API_KEY,
      'X-Signature': signature,
      'X-Timestamp': timestamp.toString(),
    },
    body: JSON.stringify(body),
  });
  
  const entitlements = await response.json();
  res.json(entitlements);
});
See Server-Side Validation for complete implementation guide.

Best Practices

The onEntitlementGranted callback is perfect for:
  • Showing success messages immediately
  • Optimistic UI updates
  • Navigation to relevant screens
  • Triggering animations
But always verify with the Encore server API before granting actual access:
func handleEntitlement(_ entitlement: EncoreEntitlement) {
    // Good: Immediate UI feedback
    showSuccessMessage()
    
    // Good: Optimistic local state
    isPremium = true
    
    // Critical: Server validation via Encore API
    Task {
        let isValid = await validateEntitlementsWithServer(userId: currentUser.id)
        if !isValid {
            isPremium = false
            showErrorMessage()
        }
    }
}
Support all three entitlement types for maximum flexibility:
func handleEntitlement(_ entitlement: EncoreEntitlement) {
    switch entitlement.type {
    case .freeTrial:
        // Grant temporary premium access
        enablePremium(duration: entitlement.value ?? 7)
        
    case .discount:
        // Apply to checkout
        applyDiscount(percent: entitlement.value ?? 20)
        
    case .credit:
        // Add to account balance
        addCredit(amount: entitlement.value ?? 10)
    }
}
Users should know immediately what they’ve earned:
func handleEntitlement(_ entitlement: EncoreEntitlement) {
    let message: String
    
    switch entitlement.type {
    case .freeTrial:
        let days = entitlement.value ?? 7
        message = "You've unlocked \(days) days of premium!"
        
    case .discount:
        let percent = entitlement.value ?? 20
        message = "You've earned \(percent)% off your next purchase!"
        
    case .credit:
        let amount = entitlement.value ?? 10
        message = "$\(amount) has been added to your account!"
    }
    
    showAlert(title: "Success!", message: message)
}

Understanding Entitlement Flow

  1. User Claims Offer → User clicks “Claim” button
  2. Safari Opens → Offer URL opens in Safari
  3. User Completes → User finishes offer on partner site
  4. Instant CallbackonEntitlementGranted fires immediately
  5. Server Validation → Your app calls backend to validate
  6. Final Confirmation → Backend verifies with Encore API
// Timeline example:
// t=0s:  User clicks "Claim"
// t=1s:  Safari opens with offer
// t=30s: User completes offer
// t=31s: onEntitlementGranted() fires ← Use for UX
// t=32s: Your app calls backend to validate ← Use for security
// t=33s: Backend calls Encore API, confirms entitlement ← Final confirmation
For the best user experience, show success immediately when onEntitlementGranted fires, but re-validate with your server in the background to ensure security.

API Reference

EncoreWebView Parameters

apiKey
String
required
Your Encore API key from the dashboard. Use pk_live_... for production or pk_test_... for testing.
userId
String
required
Your user’s unique identifier. Used for tracking and entitlement management.
environment
String
default:"production"
Environment to use:
  • production - Live offers (default)
  • localhost - Local development server
attributes
[String: String]
default:"[:]"
User attributes for offer targeting. All values must be strings.Common attributes:
  • email, firstName, lastName
  • subscriptionTier, billingCycle, planPrice
  • city, state, countryCode, postalCode
See Encore Attributes Guide for complete list.

Sheet Behavior

The view automatically:
  • Presents at 70% height, expandable to full screen
  • Shows a drag indicator for discoverability
  • Allows background interaction (sheet can be dismissed by tapping outside)
  • Auto-dismisses when user clicks “Done” after claiming
  • Handles Safari presentation for offer URLs

Customization Options

Modify these in EncoreWebView.swift:
// Sheet size and behavior
.presentationDetents([.fraction(0.7), .large])
.presentationDragIndicator(.visible)
.presentationBackgroundInteraction(.enabled)

// Background color
.background(Color.white)

// Safari presentation
.sheet(isPresented: $showingSafari) {
    // Safari configuration here
}

Troubleshooting

Possible Causes:
  • Invalid API key
  • No offers available for this user
  • Network connectivity issues
  • User doesn’t meet offer criteria
Solutions:
  1. Verify API key in Encore dashboard
  2. Check network connectivity
  3. Try with test API key to see test offers
  4. Check offer targeting rules in dashboard
  5. Verify user attributes match offer criteria
Debug Steps: Add logging to see what’s happening:
// In WebViewRepresentable.makeUIView, add:
webView.navigationDelegate = context.coordinator

// In Coordinator, add:
func webView(_ webView: WKWebView, didFail navigation: WKNavigation!, withError error: Error) {
    print("WebView error: \(error)")
}
Possible Causes:
  • Offer URL is invalid or broken
  • Network issues loading partner site
  • Partner site requires specific headers
Solutions:
  1. Test the offer URL in mobile Safari directly
  2. Check console logs for errors
  3. Verify offer is active in dashboard
  4. Contact Encore support if issue persists
Debug Steps:
// Add to SafariView.Coordinator:
func safariViewController(_ controller: SFSafariViewController, 
                        didCompleteInitialLoad didLoadSuccessfully: Bool) {
    print("Safari load success: \(didLoadSuccessfully)")
}
Possible Causes:
  • JavaScript bridge not working
  • Message handler not registered
  • Encore embed version mismatch
Solutions:
  1. Verify you’re using the latest EncoreWebView.swift
  2. Check that message handler is registered:
    userContentController.add(context.coordinator, name: "encore")
    
  3. Ensure coordinator implements WKScriptMessageHandler
  4. Clear app data and rebuild
Debug Steps:
// Add logging to message handler:
func userContentController(_ userContentController: WKUserContentController, 
                          didReceive message: WKScriptMessage) {
    print("📨 Received message: \(message.name)")
    print("📦 Body: \(message.body)")
    // Rest of handler...
}
Possible Causes:
  • WKWebView caching previous version
  • iOS data store not cleared
Solutions: The integration uses .nonPersistent() data store, but if you still see caching:
  1. Force clear on app launch:
// In AppDelegate or App struct:
let dataStore = WKWebsiteDataStore.default()
dataStore.removeData(
    ofTypes: WKWebsiteDataStore.allWebsiteDataTypes(),
    modifiedSince: Date(timeIntervalSince1970: 0)
) { }
  1. Or delete and reinstall app during development
Possible Causes:
  • Message not received from JavaScript
  • URL parsing failing
  • Safari presentation blocked
Solutions:
  1. Check message handler is receiving encore:openURL messages
  2. Verify URL is valid before presenting Safari
  3. Ensure app has proper URL scheme handling
Debug Steps:
case "encore:openURL":
    print("🔗 Attempting to open URL")
    if let payload = dict["payload"] as? [String: Any],
       let urlString = payload["url"] as? String {
        print("📍 URL string: \(urlString)")
        if let url = URL(string: urlString) {
            print("✅ Valid URL, presenting Safari")
            // present Safari...
        } else {
            print("❌ Invalid URL")
        }
    }

Best Practices

More attributes = better targeting = higher conversion rates.
// Good - rich targeting data
EncoreWebView(
    apiKey: "pk_live_...",
    userId: user.id,
    attributes: [
        "email": user.email,
        "subscriptionTier": user.tier,
        "monthsSubscribed": "\(user.monthsActive)",
        "city": user.location.city,
        "countryCode": user.location.country
    ]
)

// Not ideal - minimal data
EncoreWebView(
    apiKey: "pk_live_...",
    userId: user.id
)
Users may dismiss without completing offers. Handle this in your UX:
.sheet(isPresented: $showingEncore) {
    EncoreWebView(apiKey: "pk_live_...", userId: user.id)
}
.onChange(of: showingEncore) { isShowing in
    if !isShowing {
        // Sheet was dismissed
        // Continue with original user flow
        proceedWithOriginalAction()
    }
}
Never trust client-side events. Always validate with the Encore server API.
Use server-side validation to securely check entitlements:
  1. User completes offer
  2. Your app calls your backend to validate
  3. Your backend calls Encore’s server API with HMAC authentication
  4. Your backend returns the validated entitlement status
  5. Your app grants access based on server response
// After user dismisses Encore sheet
Task {
    let hasAccess = await validateEntitlementsWithServer(userId: currentUser.id)
    if hasAccess {
        enablePremiumFeatures()
    }
}
See Server-Side Validation for implementation details.
Test the integration with various user profiles:
// Free user
EncoreWebView(
    apiKey: "pk_test_...",
    userId: "test-free",
    attributes: ["subscriptionTier": "free"]
)

// Premium user considering cancel
EncoreWebView(
    apiKey: "pk_test_...",
    userId: "test-premium",
    attributes: [
        "subscriptionTier": "premium",
        "cancelReason": "price"
    ]
)

// Different geo
EncoreWebView(
    apiKey: "pk_test_...",
    userId: "test-intl",
    attributes: ["countryCode": "GB"]
)

Comparison: WebView vs Native SDK

FeatureWebView EmbedNative SDK
Setup Time5 minutes15-30 minutes
DependenciesNoneSPM/CocoaPods
Integration ComplexityVery SimpleModerate
File Size~1 fileMultiple files
Feature Completeness100%100%
Offline SupportLimitedFull
UI CustomizationLimitedFull
SwiftUI SupportNativeNative
UIKit SupportVia wrapperNative
UpdatesAutomaticApp release
Bundle Size Impact~50KB~200KB
PerformanceExcellentExcellent
Questions? Email [email protected]