@encorekit/web-sdk 0.1.1 → 0.1.7
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +94 -9
- package/dist/cjs/index.cjs +1 -1
- package/dist/cjs/index.cjs.map +1 -1
- package/dist/esm/index.js +1 -1
- package/dist/esm/index.js.map +1 -1
- package/dist/umd/encore.min.js +1 -1
- package/dist/umd/encore.min.js.map +1 -1
- package/embed/README.md +409 -0
- package/embed/index.html +57 -0
- package/embed/styles.css +154 -0
- package/examples/README.md +149 -0
- package/examples/angular/README.md +210 -0
- package/examples/angular/angular.json +73 -0
- package/examples/angular/package.json +32 -0
- package/examples/angular/src/app/app.component.html +56 -0
- package/examples/angular/src/app/app.component.ts +114 -0
- package/examples/angular/src/app/encore.service.ts +83 -0
- package/examples/angular/src/index.html +13 -0
- package/examples/angular/src/main.ts +7 -0
- package/examples/angular/src/styles.css +225 -0
- package/examples/angular/tsconfig.json +33 -0
- package/examples/ios-webview/EncoreURLBuilder.swift +87 -0
- package/examples/ios-webview/EncoreWebViewBridge.swift +426 -0
- package/examples/ios-webview/ExampleViewController.swift +233 -0
- package/examples/ios-webview/README.md +416 -0
- package/examples/ios-webview/SimpleEncoreView.swift +94 -0
- package/examples/ios-webview/SimpleExample.swift +131 -0
- package/examples/react/README.md +186 -0
- package/examples/react/index.html +13 -0
- package/examples/react/package.json +24 -0
- package/examples/react/src/App.tsx +173 -0
- package/examples/react/src/index.css +227 -0
- package/examples/react/src/main.tsx +11 -0
- package/examples/react/src/vite-env.d.ts +2 -0
- package/examples/react/tsconfig.json +25 -0
- package/examples/react/vite.config.ts +8 -0
- package/examples/svelte/README.md +233 -0
- package/examples/svelte/index.html +13 -0
- package/examples/svelte/package.json +25 -0
- package/examples/svelte/src/App.svelte +164 -0
- package/examples/svelte/src/app.css +224 -0
- package/examples/svelte/src/main.ts +9 -0
- package/examples/svelte/src/vite-env.d.ts +3 -0
- package/examples/svelte/svelte.config.js +8 -0
- package/examples/svelte/tsconfig.json +16 -0
- package/examples/svelte/tsconfig.node.json +11 -0
- package/examples/svelte/vite.config.ts +8 -0
- package/examples/vanilla-js/README.md +271 -0
- package/examples/vanilla-js/index.html +421 -0
- package/examples/vue/README.md +212 -0
- package/examples/vue/index.html +13 -0
- package/examples/vue/package.json +22 -0
- package/examples/vue/src/App.vue +170 -0
- package/examples/vue/src/main.ts +6 -0
- package/examples/vue/src/style.css +224 -0
- package/examples/vue/src/vite-env.d.ts +2 -0
- package/examples/vue/tsconfig.json +25 -0
- package/examples/vue/vite.config.ts +8 -0
- package/package.json +22 -3
- package/types/analytics/AnalyticsClient.d.ts +14 -0
- package/types/analytics/AnalyticsClient.d.ts.map +1 -0
- package/types/analytics/events.d.ts +63 -0
- package/types/analytics/events.d.ts.map +1 -0
- package/types/analytics/models.d.ts +17 -0
- package/types/analytics/models.d.ts.map +1 -0
- package/types/api/APIClient.d.ts +44 -8
- package/types/api/APIClient.d.ts.map +1 -1
- package/types/api/endpoints.d.ts +11 -7
- package/types/api/endpoints.d.ts.map +1 -1
- package/types/api/models.d.ts +134 -68
- package/types/api/models.d.ts.map +1 -1
- package/types/core/Configuration.d.ts +4 -0
- package/types/core/Configuration.d.ts.map +1 -1
- package/types/core/Encore.d.ts +16 -12
- package/types/core/Encore.d.ts.map +1 -1
- package/types/core/EntitlementManager.d.ts +9 -0
- package/types/core/EntitlementManager.d.ts.map +1 -1
- package/types/core/OfferManager.d.ts +27 -7
- package/types/core/OfferManager.d.ts.map +1 -1
- package/types/types.d.ts +1 -1
- package/types/types.d.ts.map +1 -1
- package/types/ui/OfferCard.d.ts.map +1 -1
- package/types/ui/OfferCarousel.d.ts.map +1 -1
- package/types/ui/Tooltip.d.ts +22 -0
- package/types/ui/Tooltip.d.ts.map +1 -0
- package/types/ui/styles.d.ts.map +1 -1
- package/dist/cjs/index.js +0 -2
- package/dist/cjs/index.js.map +0 -1
- package/types/src/api/APIClient.d.ts +0 -63
- package/types/src/api/APIClient.d.ts.map +0 -1
- package/types/src/api/endpoints.d.ts +0 -35
- package/types/src/api/endpoints.d.ts.map +0 -1
- package/types/src/api/models.d.ts +0 -156
- package/types/src/api/models.d.ts.map +0 -1
- package/types/src/core/Configuration.d.ts +0 -42
- package/types/src/core/Configuration.d.ts.map +0 -1
- package/types/src/core/Encore.d.ts +0 -81
- package/types/src/core/Encore.d.ts.map +0 -1
- package/types/src/core/EntitlementManager.d.ts +0 -65
- package/types/src/core/EntitlementManager.d.ts.map +0 -1
- package/types/src/core/OfferManager.d.ts +0 -35
- package/types/src/core/OfferManager.d.ts.map +0 -1
- package/types/src/core/PlacementBuilder.d.ts +0 -27
- package/types/src/core/PlacementBuilder.d.ts.map +0 -1
- package/types/src/core/SignalManager.d.ts +0 -51
- package/types/src/core/SignalManager.d.ts.map +0 -1
- package/types/src/core/StorageManager.d.ts +0 -34
- package/types/src/core/StorageManager.d.ts.map +0 -1
- package/types/src/core/VerificationPoller.d.ts +0 -27
- package/types/src/core/VerificationPoller.d.ts.map +0 -1
- package/types/src/index.d.ts +0 -7
- package/types/src/index.d.ts.map +0 -1
- package/types/src/types.d.ts +0 -156
- package/types/src/types.d.ts.map +0 -1
- package/types/src/ui/OfferCard.d.ts +0 -29
- package/types/src/ui/OfferCard.d.ts.map +0 -1
- package/types/src/ui/OfferCarousel.d.ts +0 -55
- package/types/src/ui/OfferCarousel.d.ts.map +0 -1
- package/types/src/ui/OfferModal.d.ts +0 -41
- package/types/src/ui/OfferModal.d.ts.map +0 -1
- package/types/src/ui/SuccessScreen.d.ts +0 -33
- package/types/src/ui/SuccessScreen.d.ts.map +0 -1
- package/types/src/ui/styles.d.ts +0 -44
- package/types/src/ui/styles.d.ts.map +0 -1
- package/types/src/utils/eventEmitter.d.ts +0 -50
- package/types/src/utils/eventEmitter.d.ts.map +0 -1
- package/types/src/utils/focusDetection.d.ts +0 -21
- package/types/src/utils/focusDetection.d.ts.map +0 -1
- package/types/src/utils/logger.d.ts +0 -21
- package/types/src/utils/logger.d.ts.map +0 -1
- package/types/src/utils/network.d.ts +0 -57
- package/types/src/utils/network.d.ts.map +0 -1
- package/types/src/utils/uuid.d.ts +0 -10
- package/types/src/utils/uuid.d.ts.map +0 -1
|
@@ -0,0 +1,426 @@
|
|
|
1
|
+
//
|
|
2
|
+
// EncoreWebViewBridge.swift
|
|
3
|
+
// Encore iOS WebView Bridge
|
|
4
|
+
//
|
|
5
|
+
// A Swift bridge for integrating Encore via WKWebView
|
|
6
|
+
// instead of the native iOS SDK.
|
|
7
|
+
//
|
|
8
|
+
|
|
9
|
+
import Foundation
|
|
10
|
+
import WebKit
|
|
11
|
+
|
|
12
|
+
/// Entitlement types that can be granted to users
|
|
13
|
+
public enum EncoreEntitlementType: String, Codable {
|
|
14
|
+
case freeTrial = "free_trial"
|
|
15
|
+
case discount = "discount"
|
|
16
|
+
case credit = "credit"
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/// Entitlement granted to a user
|
|
20
|
+
public struct EncoreEntitlement: Codable {
|
|
21
|
+
public let type: String
|
|
22
|
+
public let value: Int?
|
|
23
|
+
public let unit: String?
|
|
24
|
+
|
|
25
|
+
public init(type: String, value: Int? = nil, unit: String? = nil) {
|
|
26
|
+
self.type = type
|
|
27
|
+
self.value = value
|
|
28
|
+
self.unit = unit
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/// Reason why an entitlement was not granted
|
|
33
|
+
public enum EncoreDeclineReason: String {
|
|
34
|
+
case userClosedModal
|
|
35
|
+
case userClickedOutside
|
|
36
|
+
case userDeclinedLastOffer
|
|
37
|
+
case noOffersAvailable
|
|
38
|
+
case error
|
|
39
|
+
case unknown
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/// Error from Encore SDK
|
|
43
|
+
public struct EncoreError: Error, Codable {
|
|
44
|
+
public let code: String
|
|
45
|
+
public let message: String
|
|
46
|
+
public let context: String?
|
|
47
|
+
|
|
48
|
+
public init(code: String, message: String, context: String? = nil) {
|
|
49
|
+
self.code = code
|
|
50
|
+
self.message = message
|
|
51
|
+
self.context = context
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/// Delegate protocol for Encore events
|
|
56
|
+
public protocol EncoreWebViewBridgeDelegate: AnyObject {
|
|
57
|
+
/// Called when the embed page is ready
|
|
58
|
+
func encoreBridgeDidBecomeReady(_ bridge: EncoreWebViewBridge)
|
|
59
|
+
|
|
60
|
+
/// Called when SDK is initialized successfully
|
|
61
|
+
func encoreBridge(_ bridge: EncoreWebViewBridge, didInitializeWithUserId userId: String)
|
|
62
|
+
|
|
63
|
+
/// Called when an entitlement is granted
|
|
64
|
+
func encoreBridge(_ bridge: EncoreWebViewBridge, didGrantEntitlement entitlement: EncoreEntitlement)
|
|
65
|
+
|
|
66
|
+
/// Called when user declines an offer
|
|
67
|
+
func encoreBridge(_ bridge: EncoreWebViewBridge, userDeclinedWithReason reason: EncoreDeclineReason)
|
|
68
|
+
|
|
69
|
+
/// Called when an error occurs
|
|
70
|
+
func encoreBridge(_ bridge: EncoreWebViewBridge, didEncounterError error: EncoreError)
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/// Configuration for Encore SDK
|
|
74
|
+
public struct EncoreConfiguration {
|
|
75
|
+
public let apiKey: String
|
|
76
|
+
public let environment: String
|
|
77
|
+
public let userId: String?
|
|
78
|
+
public let attributes: [String: String]?
|
|
79
|
+
public let logLevel: String?
|
|
80
|
+
public let uiConfiguration: [String: String]?
|
|
81
|
+
|
|
82
|
+
public init(
|
|
83
|
+
apiKey: String,
|
|
84
|
+
environment: String = "production",
|
|
85
|
+
userId: String? = nil,
|
|
86
|
+
attributes: [String: String]? = nil,
|
|
87
|
+
logLevel: String? = nil,
|
|
88
|
+
uiConfiguration: [String: String]? = nil
|
|
89
|
+
) {
|
|
90
|
+
self.apiKey = apiKey
|
|
91
|
+
self.environment = environment
|
|
92
|
+
self.userId = userId
|
|
93
|
+
self.attributes = attributes
|
|
94
|
+
self.logLevel = logLevel
|
|
95
|
+
self.uiConfiguration = uiConfiguration
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/// Bridge class for communicating with Encore Web SDK in WKWebView
|
|
100
|
+
public class EncoreWebViewBridge: NSObject {
|
|
101
|
+
|
|
102
|
+
// MARK: - Properties
|
|
103
|
+
|
|
104
|
+
public weak var delegate: EncoreWebViewBridgeDelegate?
|
|
105
|
+
public private(set) var webView: WKWebView?
|
|
106
|
+
|
|
107
|
+
/// Indicates if the JavaScript embed is ready to receive messages
|
|
108
|
+
/// Reset to false when navigation starts (page reload/navigation)
|
|
109
|
+
public private(set) var isReady = false
|
|
110
|
+
|
|
111
|
+
/// Indicates if the Encore SDK has been initialized in JavaScript
|
|
112
|
+
/// Reset to false when navigation starts to handle page reloads properly
|
|
113
|
+
public private(set) var isInitialized = false
|
|
114
|
+
|
|
115
|
+
public private(set) var currentUserId: String?
|
|
116
|
+
|
|
117
|
+
private let embedURL: URL
|
|
118
|
+
private let configuration: EncoreConfiguration?
|
|
119
|
+
|
|
120
|
+
/// Message queue for storing messages sent before the embed is ready
|
|
121
|
+
/// Cleared on each navigation to prevent stale messages from being sent
|
|
122
|
+
private var messageQueue: [[String: Any]] = []
|
|
123
|
+
|
|
124
|
+
// MARK: - Initialization
|
|
125
|
+
|
|
126
|
+
/// Initialize with a custom embed URL
|
|
127
|
+
/// - Parameters:
|
|
128
|
+
/// - embedURL: The URL of the Encore embed page
|
|
129
|
+
/// - configuration: Optional configuration to auto-initialize SDK
|
|
130
|
+
public init(embedURL: URL, configuration: EncoreConfiguration? = nil) {
|
|
131
|
+
self.embedURL = embedURL
|
|
132
|
+
self.configuration = configuration
|
|
133
|
+
super.init()
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
/// Initialize with default Encore embed URL
|
|
137
|
+
/// - Parameter configuration: Optional configuration to auto-initialize SDK
|
|
138
|
+
public convenience init(configuration: EncoreConfiguration? = nil) {
|
|
139
|
+
// Default to production embed URL
|
|
140
|
+
let url = URL(string: "https://encorekit.com/embed/")!
|
|
141
|
+
self.init(embedURL: url, configuration: configuration)
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
// MARK: - Setup
|
|
145
|
+
|
|
146
|
+
/// Setup and configure the WKWebView
|
|
147
|
+
/// Call this method and add the returned webView to your view hierarchy
|
|
148
|
+
/// - Returns: Configured WKWebView instance
|
|
149
|
+
public func setupWebView() -> WKWebView {
|
|
150
|
+
let config = WKWebViewConfiguration()
|
|
151
|
+
|
|
152
|
+
// Enable JavaScript
|
|
153
|
+
config.preferences.javaScriptEnabled = true
|
|
154
|
+
|
|
155
|
+
// Add message handler for Encore messages
|
|
156
|
+
let contentController = WKUserContentController()
|
|
157
|
+
contentController.add(self, name: "encore")
|
|
158
|
+
config.userContentController = contentController
|
|
159
|
+
|
|
160
|
+
// Create webView with transparent background
|
|
161
|
+
let webView = WKWebView(frame: .zero, configuration: config)
|
|
162
|
+
webView.backgroundColor = .clear
|
|
163
|
+
webView.isOpaque = false
|
|
164
|
+
webView.scrollView.isScrollEnabled = false
|
|
165
|
+
webView.navigationDelegate = self
|
|
166
|
+
|
|
167
|
+
self.webView = webView
|
|
168
|
+
|
|
169
|
+
return webView
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
/// Load the Encore embed page
|
|
173
|
+
public func loadEmbed() {
|
|
174
|
+
guard let webView = webView else {
|
|
175
|
+
print("[Encore] Error: WebView not setup. Call setupWebView() first.")
|
|
176
|
+
return
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
var urlComponents = URLComponents(url: embedURL, resolvingAgainstBaseURL: true)
|
|
180
|
+
|
|
181
|
+
// If configuration provided, add as URL parameters for quick initialization
|
|
182
|
+
if let config = configuration {
|
|
183
|
+
var queryItems: [URLQueryItem] = [
|
|
184
|
+
URLQueryItem(name: "apiKey", value: config.apiKey),
|
|
185
|
+
URLQueryItem(name: "environment", value: config.environment)
|
|
186
|
+
]
|
|
187
|
+
|
|
188
|
+
if let userId = config.userId {
|
|
189
|
+
queryItems.append(URLQueryItem(name: "userId", value: userId))
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
urlComponents?.queryItems = queryItems
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
guard let finalURL = urlComponents?.url else {
|
|
196
|
+
print("[Encore] Error: Invalid embed URL")
|
|
197
|
+
return
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
let request = URLRequest(url: finalURL)
|
|
201
|
+
webView.load(request)
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
// MARK: - Public Methods
|
|
205
|
+
|
|
206
|
+
/// Configure the Encore SDK
|
|
207
|
+
/// - Parameter configuration: Configuration object
|
|
208
|
+
public func configure(_ configuration: EncoreConfiguration) {
|
|
209
|
+
var payload: [String: Any] = [
|
|
210
|
+
"apiKey": configuration.apiKey,
|
|
211
|
+
"environment": configuration.environment
|
|
212
|
+
]
|
|
213
|
+
|
|
214
|
+
if let userId = configuration.userId {
|
|
215
|
+
payload["userId"] = userId
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
if let attributes = configuration.attributes {
|
|
219
|
+
payload["attributes"] = attributes
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
if let logLevel = configuration.logLevel {
|
|
223
|
+
payload["logLevel"] = logLevel
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
if let uiConfiguration = configuration.uiConfiguration {
|
|
227
|
+
payload["uiConfiguration"] = uiConfiguration
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
sendMessage(type: "configure", payload: payload)
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
/// Present an offer to the user
|
|
234
|
+
public func presentOffer() {
|
|
235
|
+
sendMessage(type: "presentOffer", payload: [:])
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
/// Identify a user
|
|
239
|
+
/// - Parameters:
|
|
240
|
+
/// - userId: User identifier
|
|
241
|
+
/// - attributes: Optional user attributes
|
|
242
|
+
public func identify(userId: String, attributes: [String: String]? = nil) {
|
|
243
|
+
var payload: [String: Any] = ["userId": userId]
|
|
244
|
+
if let attributes = attributes {
|
|
245
|
+
payload["attributes"] = attributes
|
|
246
|
+
}
|
|
247
|
+
sendMessage(type: "identify", payload: payload)
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
/// Set user attributes
|
|
251
|
+
/// - Parameter attributes: User attributes dictionary
|
|
252
|
+
public func setUserAttributes(_ attributes: [String: String]) {
|
|
253
|
+
sendMessage(type: "setUserAttributes", payload: ["attributes": attributes])
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
/// Reset the SDK state
|
|
257
|
+
/// This sends a reset message to the JS SDK and clears the local state.
|
|
258
|
+
/// Note: Page reloads automatically reset state in didStartProvisionalNavigation
|
|
259
|
+
public func reset() {
|
|
260
|
+
sendMessage(type: "reset", payload: [:])
|
|
261
|
+
isInitialized = false
|
|
262
|
+
currentUserId = nil
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
/// Get current SDK state
|
|
266
|
+
public func getState() {
|
|
267
|
+
sendMessage(type: "getState", payload: [:])
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
// MARK: - Private Methods
|
|
271
|
+
|
|
272
|
+
private func sendMessage(type: String, payload: [String: Any]) {
|
|
273
|
+
let message: [String: Any] = [
|
|
274
|
+
"type": type,
|
|
275
|
+
"payload": payload
|
|
276
|
+
]
|
|
277
|
+
|
|
278
|
+
if isReady {
|
|
279
|
+
postMessageToWebView(message)
|
|
280
|
+
} else {
|
|
281
|
+
// Queue messages until webview is ready
|
|
282
|
+
messageQueue.append(message)
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
private func postMessageToWebView(_ message: [String: Any]) {
|
|
287
|
+
guard let webView = webView else { return }
|
|
288
|
+
|
|
289
|
+
do {
|
|
290
|
+
let jsonData = try JSONSerialization.data(withJSONObject: message)
|
|
291
|
+
let jsonString = String(data: jsonData, encoding: .utf8) ?? "{}"
|
|
292
|
+
|
|
293
|
+
let script = """
|
|
294
|
+
window.postMessage(\(jsonString), '*');
|
|
295
|
+
"""
|
|
296
|
+
|
|
297
|
+
webView.evaluateJavaScript(script) { _, error in
|
|
298
|
+
if let error = error {
|
|
299
|
+
print("[Encore] Error posting message: \(error)")
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
} catch {
|
|
303
|
+
print("[Encore] Error serializing message: \(error)")
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
private func flushMessageQueue() {
|
|
308
|
+
while !messageQueue.isEmpty {
|
|
309
|
+
let message = messageQueue.removeFirst()
|
|
310
|
+
postMessageToWebView(message)
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
private func handleMessage(_ body: Any) {
|
|
315
|
+
guard let dict = body as? [String: Any],
|
|
316
|
+
let type = dict["type"] as? String else {
|
|
317
|
+
return
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
let payload = dict["payload"] as? [String: Any] ?? [:]
|
|
321
|
+
|
|
322
|
+
print("[Encore] Received message: \(type)")
|
|
323
|
+
|
|
324
|
+
switch type {
|
|
325
|
+
case "encore:ready":
|
|
326
|
+
isReady = true
|
|
327
|
+
flushMessageQueue()
|
|
328
|
+
delegate?.encoreBridgeDidBecomeReady(self)
|
|
329
|
+
|
|
330
|
+
case "encore:initialized":
|
|
331
|
+
isInitialized = true
|
|
332
|
+
if let userId = payload["userId"] as? String {
|
|
333
|
+
currentUserId = userId
|
|
334
|
+
delegate?.encoreBridge(self, didInitializeWithUserId: userId)
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
case "encore:entitlementGranted":
|
|
338
|
+
if let entitlementData = payload["entitlement"] as? [String: Any],
|
|
339
|
+
let jsonData = try? JSONSerialization.data(withJSONObject: entitlementData),
|
|
340
|
+
let entitlement = try? JSONDecoder().decode(EncoreEntitlement.self, from: jsonData) {
|
|
341
|
+
delegate?.encoreBridge(self, didGrantEntitlement: entitlement)
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
case "encore:userDeclined":
|
|
345
|
+
if let reasonString = payload["reason"] as? String,
|
|
346
|
+
let reason = EncoreDeclineReason(rawValue: reasonString) {
|
|
347
|
+
delegate?.encoreBridge(self, userDeclinedWithReason: reason)
|
|
348
|
+
} else {
|
|
349
|
+
delegate?.encoreBridge(self, userDeclinedWithReason: .unknown)
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
case "encore:error":
|
|
353
|
+
if let jsonData = try? JSONSerialization.data(withJSONObject: payload),
|
|
354
|
+
let error = try? JSONDecoder().decode(EncoreError.self, from: jsonData) {
|
|
355
|
+
delegate?.encoreBridge(self, didEncounterError: error)
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
case "encore:state":
|
|
359
|
+
// Handle state response if needed
|
|
360
|
+
print("[Encore] Current state: \(payload)")
|
|
361
|
+
|
|
362
|
+
default:
|
|
363
|
+
print("[Encore] Unknown message type: \(type)")
|
|
364
|
+
}
|
|
365
|
+
}
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
// MARK: - WKScriptMessageHandler
|
|
369
|
+
|
|
370
|
+
extension EncoreWebViewBridge: WKScriptMessageHandler {
|
|
371
|
+
public func userContentController(
|
|
372
|
+
_ userContentController: WKUserContentController,
|
|
373
|
+
didReceive message: WKScriptMessage
|
|
374
|
+
) {
|
|
375
|
+
guard message.name == "encore" else { return }
|
|
376
|
+
handleMessage(message.body)
|
|
377
|
+
}
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
// MARK: - WKNavigationDelegate
|
|
381
|
+
|
|
382
|
+
extension EncoreWebViewBridge: WKNavigationDelegate {
|
|
383
|
+
public func webView(
|
|
384
|
+
_ webView: WKWebView,
|
|
385
|
+
didStartProvisionalNavigation navigation: WKNavigation!
|
|
386
|
+
) {
|
|
387
|
+
print("[Encore] WebView started loading")
|
|
388
|
+
|
|
389
|
+
// Reset state when navigation starts - the JS environment will be reloaded
|
|
390
|
+
// This ensures we re-initialize even if the user refreshes or navigates
|
|
391
|
+
isReady = false
|
|
392
|
+
isInitialized = false
|
|
393
|
+
messageQueue.removeAll()
|
|
394
|
+
|
|
395
|
+
print("[Encore] State reset for new navigation")
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
public func webView(
|
|
399
|
+
_ webView: WKWebView,
|
|
400
|
+
didFinish navigation: WKNavigation!
|
|
401
|
+
) {
|
|
402
|
+
print("[Encore] WebView finished loading")
|
|
403
|
+
|
|
404
|
+
// If we have initial configuration and haven't initialized yet, do it now
|
|
405
|
+
// Note: isInitialized will be false after didStartProvisionalNavigation reset,
|
|
406
|
+
// so this will properly re-initialize on page reloads
|
|
407
|
+
if !isInitialized, let config = configuration {
|
|
408
|
+
print("[Encore] Auto-configuring SDK with stored configuration")
|
|
409
|
+
configure(config)
|
|
410
|
+
}
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
public func webView(
|
|
414
|
+
_ webView: WKWebView,
|
|
415
|
+
didFail navigation: WKNavigation!,
|
|
416
|
+
withError error: Error
|
|
417
|
+
) {
|
|
418
|
+
print("[Encore] WebView navigation failed: \(error.localizedDescription)")
|
|
419
|
+
|
|
420
|
+
// Reset state on navigation failure as well
|
|
421
|
+
isReady = false
|
|
422
|
+
isInitialized = false
|
|
423
|
+
messageQueue.removeAll()
|
|
424
|
+
}
|
|
425
|
+
}
|
|
426
|
+
|
|
@@ -0,0 +1,233 @@
|
|
|
1
|
+
//
|
|
2
|
+
// ExampleViewController.swift
|
|
3
|
+
// Encore iOS WebView Example
|
|
4
|
+
//
|
|
5
|
+
// Example implementation showing how to integrate Encore via WKWebView
|
|
6
|
+
//
|
|
7
|
+
|
|
8
|
+
import UIKit
|
|
9
|
+
import WebKit
|
|
10
|
+
|
|
11
|
+
class ExampleViewController: UIViewController {
|
|
12
|
+
|
|
13
|
+
// MARK: - Properties
|
|
14
|
+
|
|
15
|
+
private var encoreBridge: EncoreWebViewBridge!
|
|
16
|
+
private var webView: WKWebView!
|
|
17
|
+
|
|
18
|
+
// UI Elements
|
|
19
|
+
private let statusLabel: UILabel = {
|
|
20
|
+
let label = UILabel()
|
|
21
|
+
label.textAlignment = .center
|
|
22
|
+
label.numberOfLines = 0
|
|
23
|
+
label.font = .systemFont(ofSize: 14)
|
|
24
|
+
label.textColor = .secondaryLabel
|
|
25
|
+
label.text = "Initializing Encore..."
|
|
26
|
+
return label
|
|
27
|
+
}()
|
|
28
|
+
|
|
29
|
+
private let presentOfferButton: UIButton = {
|
|
30
|
+
let button = UIButton(type: .system)
|
|
31
|
+
button.setTitle("Present Offer", for: .normal)
|
|
32
|
+
button.titleLabel?.font = .systemFont(ofSize: 18, weight: .semibold)
|
|
33
|
+
button.backgroundColor = .systemBlue
|
|
34
|
+
button.setTitleColor(.white, for: .normal)
|
|
35
|
+
button.layer.cornerRadius = 12
|
|
36
|
+
button.isEnabled = false
|
|
37
|
+
return button
|
|
38
|
+
}()
|
|
39
|
+
|
|
40
|
+
private let resultLabel: UILabel = {
|
|
41
|
+
let label = UILabel()
|
|
42
|
+
label.textAlignment = .center
|
|
43
|
+
label.numberOfLines = 0
|
|
44
|
+
label.font = .systemFont(ofSize: 16, weight: .medium)
|
|
45
|
+
label.textColor = .label
|
|
46
|
+
label.text = ""
|
|
47
|
+
return label
|
|
48
|
+
}()
|
|
49
|
+
|
|
50
|
+
// MARK: - Lifecycle
|
|
51
|
+
|
|
52
|
+
override func viewDidLoad() {
|
|
53
|
+
super.viewDidLoad()
|
|
54
|
+
|
|
55
|
+
title = "Encore WebView Example"
|
|
56
|
+
view.backgroundColor = .systemBackground
|
|
57
|
+
|
|
58
|
+
setupEncoreBridge()
|
|
59
|
+
setupUI()
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// MARK: - Setup
|
|
63
|
+
|
|
64
|
+
private func setupEncoreBridge() {
|
|
65
|
+
// Create configuration
|
|
66
|
+
let config = EncoreConfiguration(
|
|
67
|
+
apiKey: "your_api_key_here", // Replace with your actual API key
|
|
68
|
+
environment: "production", // or "localhost" for testing
|
|
69
|
+
userId: "user_123", // Optional: your user's ID
|
|
70
|
+
attributes: [ // Optional: user attributes for targeting
|
|
71
|
+
"email": "user@example.com",
|
|
72
|
+
"subscriptionTier": "free"
|
|
73
|
+
]
|
|
74
|
+
)
|
|
75
|
+
|
|
76
|
+
// Initialize bridge
|
|
77
|
+
encoreBridge = EncoreWebViewBridge(configuration: config)
|
|
78
|
+
encoreBridge.delegate = self
|
|
79
|
+
|
|
80
|
+
// Setup webView
|
|
81
|
+
webView = encoreBridge.setupWebView()
|
|
82
|
+
|
|
83
|
+
// Add webView to view hierarchy (invisible)
|
|
84
|
+
webView.translatesAutoresizingMaskIntoConstraints = false
|
|
85
|
+
view.addSubview(webView)
|
|
86
|
+
|
|
87
|
+
NSLayoutConstraint.activate([
|
|
88
|
+
// Make webView fill the entire screen
|
|
89
|
+
webView.topAnchor.constraint(equalTo: view.topAnchor),
|
|
90
|
+
webView.leadingAnchor.constraint(equalTo: view.leadingAnchor),
|
|
91
|
+
webView.trailingAnchor.constraint(equalTo: view.trailingAnchor),
|
|
92
|
+
webView.bottomAnchor.constraint(equalTo: view.bottomAnchor)
|
|
93
|
+
])
|
|
94
|
+
|
|
95
|
+
// Load the embed page
|
|
96
|
+
encoreBridge.loadEmbed()
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
private func setupUI() {
|
|
100
|
+
// Add UI elements on top of webView
|
|
101
|
+
let stackView = UIStackView(arrangedSubviews: [
|
|
102
|
+
statusLabel,
|
|
103
|
+
presentOfferButton,
|
|
104
|
+
resultLabel
|
|
105
|
+
])
|
|
106
|
+
stackView.axis = .vertical
|
|
107
|
+
stackView.spacing = 20
|
|
108
|
+
stackView.translatesAutoresizingMaskIntoConstraints = false
|
|
109
|
+
view.addSubview(stackView)
|
|
110
|
+
|
|
111
|
+
NSLayoutConstraint.activate([
|
|
112
|
+
stackView.centerYAnchor.constraint(equalTo: view.centerYAnchor),
|
|
113
|
+
stackView.leadingAnchor.constraint(equalTo: view.leadingAnchor, constant: 40),
|
|
114
|
+
stackView.trailingAnchor.constraint(equalTo: view.trailingAnchor, constant: -40),
|
|
115
|
+
|
|
116
|
+
presentOfferButton.heightAnchor.constraint(equalToConstant: 50)
|
|
117
|
+
])
|
|
118
|
+
|
|
119
|
+
presentOfferButton.addTarget(self, action: #selector(presentOfferTapped), for: .touchUpInside)
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
// MARK: - Actions
|
|
123
|
+
|
|
124
|
+
@objc private func presentOfferTapped() {
|
|
125
|
+
resultLabel.text = ""
|
|
126
|
+
encoreBridge.presentOffer()
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
// MARK: - EncoreWebViewBridgeDelegate
|
|
131
|
+
|
|
132
|
+
extension ExampleViewController: EncoreWebViewBridgeDelegate {
|
|
133
|
+
|
|
134
|
+
func encoreBridgeDidBecomeReady(_ bridge: EncoreWebViewBridge) {
|
|
135
|
+
print("✅ Encore bridge is ready")
|
|
136
|
+
statusLabel.text = "Encore is ready"
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
func encoreBridge(_ bridge: EncoreWebViewBridge, didInitializeWithUserId userId: String) {
|
|
140
|
+
print("✅ Encore initialized with user ID: \(userId)")
|
|
141
|
+
statusLabel.text = "Initialized (User: \(userId))"
|
|
142
|
+
presentOfferButton.isEnabled = true
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
func encoreBridge(_ bridge: EncoreWebViewBridge, didGrantEntitlement entitlement: EncoreEntitlement) {
|
|
146
|
+
print("🎉 Entitlement granted: \(entitlement)")
|
|
147
|
+
|
|
148
|
+
// Handle the entitlement in your app
|
|
149
|
+
// For example, unlock premium features, apply discount, etc.
|
|
150
|
+
|
|
151
|
+
let message = "Entitlement Granted!\nType: \(entitlement.type)"
|
|
152
|
+
resultLabel.text = message
|
|
153
|
+
resultLabel.textColor = .systemGreen
|
|
154
|
+
|
|
155
|
+
// Show success alert
|
|
156
|
+
let alert = UIAlertController(
|
|
157
|
+
title: "Success!",
|
|
158
|
+
message: "You've earned a \(entitlement.type) entitlement!",
|
|
159
|
+
preferredStyle: .alert
|
|
160
|
+
)
|
|
161
|
+
alert.addAction(UIAlertAction(title: "OK", style: .default))
|
|
162
|
+
present(alert, animated: true)
|
|
163
|
+
|
|
164
|
+
// TODO: Implement your app's logic to apply the entitlement
|
|
165
|
+
// Examples:
|
|
166
|
+
// - Update user's subscription status
|
|
167
|
+
// - Apply discount to checkout
|
|
168
|
+
// - Grant access to premium content
|
|
169
|
+
// - Update UI to reflect new entitlement
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
func encoreBridge(_ bridge: EncoreWebViewBridge, userDeclinedWithReason reason: EncoreDeclineReason) {
|
|
173
|
+
print("ℹ️ User declined offer: \(reason.rawValue)")
|
|
174
|
+
|
|
175
|
+
let message = "Offer declined: \(reason.rawValue)"
|
|
176
|
+
resultLabel.text = message
|
|
177
|
+
resultLabel.textColor = .systemOrange
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
func encoreBridge(_ bridge: EncoreWebViewBridge, didEncounterError error: EncoreError) {
|
|
181
|
+
print("❌ Encore error: \(error.code) - \(error.message)")
|
|
182
|
+
|
|
183
|
+
let message = "Error: \(error.message)"
|
|
184
|
+
resultLabel.text = message
|
|
185
|
+
resultLabel.textColor = .systemRed
|
|
186
|
+
|
|
187
|
+
// Show error alert
|
|
188
|
+
let alert = UIAlertController(
|
|
189
|
+
title: "Error",
|
|
190
|
+
message: error.message,
|
|
191
|
+
preferredStyle: .alert
|
|
192
|
+
)
|
|
193
|
+
alert.addAction(UIAlertAction(title: "OK", style: .default))
|
|
194
|
+
present(alert, animated: true)
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
// MARK: - Advanced Example: Cancellation Flow Integration
|
|
199
|
+
|
|
200
|
+
extension ExampleViewController {
|
|
201
|
+
|
|
202
|
+
/// Example: Present Encore offer during subscription cancellation
|
|
203
|
+
func handleSubscriptionCancellation() {
|
|
204
|
+
// User is trying to cancel their subscription
|
|
205
|
+
|
|
206
|
+
// First, present the Encore offer as a retention tool
|
|
207
|
+
encoreBridge.presentOffer()
|
|
208
|
+
|
|
209
|
+
// The delegate methods will be called:
|
|
210
|
+
// - If user completes offer: didGrantEntitlement() → Keep subscription active
|
|
211
|
+
// - If user declines: userDeclinedWithReason() → Proceed with cancellation
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
/// Example: Check if user has active entitlements on app launch
|
|
215
|
+
func checkEntitlementsOnLaunch() {
|
|
216
|
+
// Note: Entitlement validation should be done server-side
|
|
217
|
+
// This is just for demonstration of the bridge API
|
|
218
|
+
|
|
219
|
+
encoreBridge.getState()
|
|
220
|
+
// Response will be logged in console
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
/// Example: Update user attributes dynamically
|
|
224
|
+
func updateUserData() {
|
|
225
|
+
let newAttributes = [
|
|
226
|
+
"subscriptionTier": "premium",
|
|
227
|
+
"lastActiveDate": ISO8601DateFormatter().string(from: Date())
|
|
228
|
+
]
|
|
229
|
+
|
|
230
|
+
encoreBridge.setUserAttributes(newAttributes)
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
|