@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.
Files changed (134) hide show
  1. package/README.md +94 -9
  2. package/dist/cjs/index.cjs +1 -1
  3. package/dist/cjs/index.cjs.map +1 -1
  4. package/dist/esm/index.js +1 -1
  5. package/dist/esm/index.js.map +1 -1
  6. package/dist/umd/encore.min.js +1 -1
  7. package/dist/umd/encore.min.js.map +1 -1
  8. package/embed/README.md +409 -0
  9. package/embed/index.html +57 -0
  10. package/embed/styles.css +154 -0
  11. package/examples/README.md +149 -0
  12. package/examples/angular/README.md +210 -0
  13. package/examples/angular/angular.json +73 -0
  14. package/examples/angular/package.json +32 -0
  15. package/examples/angular/src/app/app.component.html +56 -0
  16. package/examples/angular/src/app/app.component.ts +114 -0
  17. package/examples/angular/src/app/encore.service.ts +83 -0
  18. package/examples/angular/src/index.html +13 -0
  19. package/examples/angular/src/main.ts +7 -0
  20. package/examples/angular/src/styles.css +225 -0
  21. package/examples/angular/tsconfig.json +33 -0
  22. package/examples/ios-webview/EncoreURLBuilder.swift +87 -0
  23. package/examples/ios-webview/EncoreWebViewBridge.swift +426 -0
  24. package/examples/ios-webview/ExampleViewController.swift +233 -0
  25. package/examples/ios-webview/README.md +416 -0
  26. package/examples/ios-webview/SimpleEncoreView.swift +94 -0
  27. package/examples/ios-webview/SimpleExample.swift +131 -0
  28. package/examples/react/README.md +186 -0
  29. package/examples/react/index.html +13 -0
  30. package/examples/react/package.json +24 -0
  31. package/examples/react/src/App.tsx +173 -0
  32. package/examples/react/src/index.css +227 -0
  33. package/examples/react/src/main.tsx +11 -0
  34. package/examples/react/src/vite-env.d.ts +2 -0
  35. package/examples/react/tsconfig.json +25 -0
  36. package/examples/react/vite.config.ts +8 -0
  37. package/examples/svelte/README.md +233 -0
  38. package/examples/svelte/index.html +13 -0
  39. package/examples/svelte/package.json +25 -0
  40. package/examples/svelte/src/App.svelte +164 -0
  41. package/examples/svelte/src/app.css +224 -0
  42. package/examples/svelte/src/main.ts +9 -0
  43. package/examples/svelte/src/vite-env.d.ts +3 -0
  44. package/examples/svelte/svelte.config.js +8 -0
  45. package/examples/svelte/tsconfig.json +16 -0
  46. package/examples/svelte/tsconfig.node.json +11 -0
  47. package/examples/svelte/vite.config.ts +8 -0
  48. package/examples/vanilla-js/README.md +271 -0
  49. package/examples/vanilla-js/index.html +421 -0
  50. package/examples/vue/README.md +212 -0
  51. package/examples/vue/index.html +13 -0
  52. package/examples/vue/package.json +22 -0
  53. package/examples/vue/src/App.vue +170 -0
  54. package/examples/vue/src/main.ts +6 -0
  55. package/examples/vue/src/style.css +224 -0
  56. package/examples/vue/src/vite-env.d.ts +2 -0
  57. package/examples/vue/tsconfig.json +25 -0
  58. package/examples/vue/vite.config.ts +8 -0
  59. package/package.json +22 -3
  60. package/types/analytics/AnalyticsClient.d.ts +14 -0
  61. package/types/analytics/AnalyticsClient.d.ts.map +1 -0
  62. package/types/analytics/events.d.ts +63 -0
  63. package/types/analytics/events.d.ts.map +1 -0
  64. package/types/analytics/models.d.ts +17 -0
  65. package/types/analytics/models.d.ts.map +1 -0
  66. package/types/api/APIClient.d.ts +44 -8
  67. package/types/api/APIClient.d.ts.map +1 -1
  68. package/types/api/endpoints.d.ts +11 -7
  69. package/types/api/endpoints.d.ts.map +1 -1
  70. package/types/api/models.d.ts +134 -68
  71. package/types/api/models.d.ts.map +1 -1
  72. package/types/core/Configuration.d.ts +4 -0
  73. package/types/core/Configuration.d.ts.map +1 -1
  74. package/types/core/Encore.d.ts +16 -12
  75. package/types/core/Encore.d.ts.map +1 -1
  76. package/types/core/EntitlementManager.d.ts +9 -0
  77. package/types/core/EntitlementManager.d.ts.map +1 -1
  78. package/types/core/OfferManager.d.ts +27 -7
  79. package/types/core/OfferManager.d.ts.map +1 -1
  80. package/types/types.d.ts +1 -1
  81. package/types/types.d.ts.map +1 -1
  82. package/types/ui/OfferCard.d.ts.map +1 -1
  83. package/types/ui/OfferCarousel.d.ts.map +1 -1
  84. package/types/ui/Tooltip.d.ts +22 -0
  85. package/types/ui/Tooltip.d.ts.map +1 -0
  86. package/types/ui/styles.d.ts.map +1 -1
  87. package/dist/cjs/index.js +0 -2
  88. package/dist/cjs/index.js.map +0 -1
  89. package/types/src/api/APIClient.d.ts +0 -63
  90. package/types/src/api/APIClient.d.ts.map +0 -1
  91. package/types/src/api/endpoints.d.ts +0 -35
  92. package/types/src/api/endpoints.d.ts.map +0 -1
  93. package/types/src/api/models.d.ts +0 -156
  94. package/types/src/api/models.d.ts.map +0 -1
  95. package/types/src/core/Configuration.d.ts +0 -42
  96. package/types/src/core/Configuration.d.ts.map +0 -1
  97. package/types/src/core/Encore.d.ts +0 -81
  98. package/types/src/core/Encore.d.ts.map +0 -1
  99. package/types/src/core/EntitlementManager.d.ts +0 -65
  100. package/types/src/core/EntitlementManager.d.ts.map +0 -1
  101. package/types/src/core/OfferManager.d.ts +0 -35
  102. package/types/src/core/OfferManager.d.ts.map +0 -1
  103. package/types/src/core/PlacementBuilder.d.ts +0 -27
  104. package/types/src/core/PlacementBuilder.d.ts.map +0 -1
  105. package/types/src/core/SignalManager.d.ts +0 -51
  106. package/types/src/core/SignalManager.d.ts.map +0 -1
  107. package/types/src/core/StorageManager.d.ts +0 -34
  108. package/types/src/core/StorageManager.d.ts.map +0 -1
  109. package/types/src/core/VerificationPoller.d.ts +0 -27
  110. package/types/src/core/VerificationPoller.d.ts.map +0 -1
  111. package/types/src/index.d.ts +0 -7
  112. package/types/src/index.d.ts.map +0 -1
  113. package/types/src/types.d.ts +0 -156
  114. package/types/src/types.d.ts.map +0 -1
  115. package/types/src/ui/OfferCard.d.ts +0 -29
  116. package/types/src/ui/OfferCard.d.ts.map +0 -1
  117. package/types/src/ui/OfferCarousel.d.ts +0 -55
  118. package/types/src/ui/OfferCarousel.d.ts.map +0 -1
  119. package/types/src/ui/OfferModal.d.ts +0 -41
  120. package/types/src/ui/OfferModal.d.ts.map +0 -1
  121. package/types/src/ui/SuccessScreen.d.ts +0 -33
  122. package/types/src/ui/SuccessScreen.d.ts.map +0 -1
  123. package/types/src/ui/styles.d.ts +0 -44
  124. package/types/src/ui/styles.d.ts.map +0 -1
  125. package/types/src/utils/eventEmitter.d.ts +0 -50
  126. package/types/src/utils/eventEmitter.d.ts.map +0 -1
  127. package/types/src/utils/focusDetection.d.ts +0 -21
  128. package/types/src/utils/focusDetection.d.ts.map +0 -1
  129. package/types/src/utils/logger.d.ts +0 -21
  130. package/types/src/utils/logger.d.ts.map +0 -1
  131. package/types/src/utils/network.d.ts +0 -57
  132. package/types/src/utils/network.d.ts.map +0 -1
  133. package/types/src/utils/uuid.d.ts +0 -10
  134. 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
+