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:
Copy the Integration File
Copy EncoreWebView.swift into your Xcode project. That’s it - one file, no dependencies!
View complete EncoreWebView.swift file (click to expand)
//
// 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!
Add to Your View
Present EncoreWebView as a sheet when you want to show offers: 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.
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:
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:
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:
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:
EncoreWebView - SwiftUI view that manages the presentation
WebViewRepresentable - Wraps WKWebView and handles JavaScript bridge
SafariView - Opens offer URLs in Safari when user claims an offer
User Flow
User triggers the sheet (e.g., clicks “Show Offers”)
EncoreWebView loads the Encore embed in a WKWebView
User browses and selects an offer
When user clicks “Claim”:
Offer URL opens in Safari (within the app)
User completes the offer on the partner site
User closes Safari
“Credit Claimed” confirmation appears
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:
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:
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:
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
Use for UX, validate server-side
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 ()
}
}
}
Handle all entitlement types
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
User Claims Offer → User clicks “Claim” button
Safari Opens → Offer URL opens in Safari
User Completes → User finishes offer on partner site
Instant Callback → onEntitlementGranted fires immediately
Server Validation → Your app calls backend to validate
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
Your Encore API key from the dashboard. Use pk_live_... for production or pk_test_... for testing.
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
Sheet appears but no offers show
Possible Causes:
Invalid API key
No offers available for this user
Network connectivity issues
User doesn’t meet offer criteria
Solutions:
Verify API key in Encore dashboard
Check network connectivity
Try with test API key to see test offers
Check offer targeting rules in dashboard
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 ) " )
}
Safari opens blank white screen
Possible Causes:
Offer URL is invalid or broken
Network issues loading partner site
Partner site requires specific headers
Solutions:
Test the offer URL in mobile Safari directly
Check console logs for errors
Verify offer is active in dashboard
Contact Encore support if issue persists
Debug Steps: // Add to SafariView.Coordinator:
func safariViewController ( _ controller : SFSafariViewController,
didCompleteInitialLoad didLoadSuccessfully : Bool ) {
print ( "Safari load success: \( didLoadSuccessfully ) " )
}
Sheet doesn't dismiss after clicking Done
Possible Causes:
JavaScript bridge not working
Message handler not registered
Encore embed version mismatch
Solutions:
Verify you’re using the latest EncoreWebView.swift
Check that message handler is registered:
userContentController. add (context. coordinator , name : "encore" )
Ensure coordinator implements WKScriptMessageHandler
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...
}
Cache showing old content
Possible Causes:
WKWebView caching previous version
iOS data store not cleared
Solutions:
The integration uses .nonPersistent() data store, but if you still see caching:
Force clear on app launch:
// In AppDelegate or App struct:
let dataStore = WKWebsiteDataStore. default ()
dataStore. removeData (
ofTypes : WKWebsiteDataStore. allWebsiteDataTypes (),
modifiedSince : Date ( timeIntervalSince1970 : 0 )
) { }
Or delete and reinstall app during development
Offer URLs not opening in Safari
Possible Causes:
Message not received from JavaScript
URL parsing failing
Safari presentation blocked
Solutions:
Check message handler is receiving encore:openURL messages
Verify URL is valid before presenting Safari
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
Always pass user attributes
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
)
Handle sheet dismissal gracefully
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 ()
}
}
Validate completions server-side
Never trust client-side events. Always validate with the Encore server API.
Use server-side validation to securely check entitlements:
User completes offer
Your app calls your backend to validate
Your backend calls Encore’s server API with HMAC authentication
Your backend returns the validated entitlement status
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 with different user segments
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
Feature WebView Embed Native SDK Setup Time 5 minutes 15-30 minutes Dependencies None SPM/CocoaPods Integration Complexity Very Simple Moderate File Size ~1 file Multiple files Feature Completeness 100% 100% Offline Support Limited Full UI Customization Limited Full SwiftUI Support Native Native UIKit Support Via wrapper Native Updates Automatic App release Bundle Size Impact ~50KB ~200KB Performance Excellent Excellent