@encorekit/web-sdk 2.0.4 → 2.0.6

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 (53) hide show
  1. package/README.md +112 -40
  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 +82 -367
  9. package/examples/ios-webview/EncoreURLBuilder.swift +179 -61
  10. package/examples/ios-webview/README.md +75 -354
  11. package/examples/ios-webview/SimpleEncoreView.swift +49 -36
  12. package/examples/ios-webview/SimpleExample.swift +30 -86
  13. package/package.json +2 -7
  14. package/types/api/APIClient.d.ts +3 -1
  15. package/types/api/APIClient.d.ts.map +1 -1
  16. package/types/api/errorReportLimits.d.ts +50 -0
  17. package/types/api/errorReportLimits.d.ts.map +1 -0
  18. package/types/api/generated/types.d.ts +54 -7
  19. package/types/api/generated/types.d.ts.map +1 -1
  20. package/types/core/Encore.d.ts +25 -2
  21. package/types/core/Encore.d.ts.map +1 -1
  22. package/types/core/PlacementBuilder.d.ts +14 -2
  23. package/types/core/PlacementBuilder.d.ts.map +1 -1
  24. package/types/core/TransactionStore.d.ts +57 -13
  25. package/types/core/TransactionStore.d.ts.map +1 -1
  26. package/types/core/VerificationPoller.d.ts.map +1 -1
  27. package/types/core/claimBehavior.d.ts +28 -0
  28. package/types/core/claimBehavior.d.ts.map +1 -0
  29. package/types/core/errorContext.d.ts +18 -0
  30. package/types/core/errorContext.d.ts.map +1 -0
  31. package/types/core/errorType.d.ts +59 -0
  32. package/types/core/errorType.d.ts.map +1 -0
  33. package/types/core/hostDisplay.d.ts +50 -0
  34. package/types/core/hostDisplay.d.ts.map +1 -0
  35. package/types/core/version.d.ts +1 -1
  36. package/types/types.d.ts +146 -1
  37. package/types/types.d.ts.map +1 -1
  38. package/types/ui/OfferModal.d.ts +20 -0
  39. package/types/ui/OfferModal.d.ts.map +1 -1
  40. package/types/ui/PrimerScreen.d.ts +18 -11
  41. package/types/ui/PrimerScreen.d.ts.map +1 -1
  42. package/types/ui/RedemptionScreen.d.ts +15 -19
  43. package/types/ui/RedemptionScreen.d.ts.map +1 -1
  44. package/types/ui/RewardActions.d.ts +26 -0
  45. package/types/ui/RewardActions.d.ts.map +1 -1
  46. package/types/ui/brandComponents.d.ts.map +1 -1
  47. package/types/ui/styles.d.ts.map +1 -1
  48. package/types/utils/logger.d.ts +11 -1
  49. package/types/utils/logger.d.ts.map +1 -1
  50. package/embed/index.html +0 -57
  51. package/embed/styles.css +0 -154
  52. package/examples/ios-webview/EncoreWebViewBridge.swift +0 -419
  53. package/examples/ios-webview/ExampleViewController.swift +0 -233
@@ -1,396 +1,117 @@
1
- # Encore iOS WebView Integration Example
1
+ # Encore on iOS without the native SDK
2
2
 
3
- This directory contains example Swift code for integrating Encore into your iOS app using WKWebView instead of the native Swift SDK.
3
+ Open one URL in a `WKWebView`. Encore hosts the offer page and the query string
4
+ configures it. There is nothing to install and nothing to implement.
4
5
 
5
- ## Overview
6
-
7
- This approach is ideal for:
8
- - Apps that can't or don't want to add new SDK dependencies
9
- - Quick integration without modifying build configuration
10
- - Cross-platform teams familiar with web technologies
11
- - Apps already using web views for other features
6
+ If you want native entitlement callbacks, StoreKit handling and offline
7
+ behaviour, use the [Swift SDK](https://github.com/EncoreKit/swift) instead. This
8
+ directory is for apps that want the smallest possible integration.
12
9
 
13
10
  ## Files
14
11
 
15
- - **`EncoreWebViewBridge.swift`** - Complete bridge implementation (copy this to your project)
16
- - **`ExampleViewController.swift`** - Example usage in a view controller
17
- - **`README.md`** - This file
18
-
19
- ## Quick Start
12
+ | File | Purpose |
13
+ |------|---------|
14
+ | `EncoreURLBuilder.swift` | Builds the URL. Copy this one. |
15
+ | `SimpleEncoreView.swift` | SwiftUI view wrapping the webview. |
16
+ | `SimpleExample.swift` | UIKit view controller and presentation patterns. |
20
17
 
21
- ### 1. Add the Bridge to Your Project
18
+ ## Setup
22
19
 
23
- Copy `EncoreWebViewBridge.swift` to your Xcode project. No additional dependencies required!
20
+ Copy `EncoreURLBuilder.swift` into your Xcode project, plus
21
+ `SimpleEncoreView.swift` if you want the SwiftUI wrapper. No dependencies.
24
22
 
25
- ### 2. Basic Integration
23
+ ## UIKit
26
24
 
27
25
  ```swift
28
26
  import UIKit
29
27
  import WebKit
30
28
 
31
- class YourViewController: UIViewController {
32
- private var encoreBridge: EncoreWebViewBridge!
33
- private var webView: WKWebView!
34
-
35
- override func viewDidLoad() {
36
- super.viewDidLoad()
37
-
38
- // 1. Create configuration
39
- let config = EncoreConfiguration(
40
- apiKey: "your_api_key_here",
41
- environment: "production",
42
- userId: "user_123"
43
- )
44
-
45
- // 2. Initialize bridge
46
- encoreBridge = EncoreWebViewBridge(configuration: config)
47
- encoreBridge.delegate = self
48
-
49
- // 3. Setup webView
50
- webView = encoreBridge.setupWebView()
51
- view.addSubview(webView)
52
-
53
- // 4. Configure constraints (webView should fill screen)
54
- webView.translatesAutoresizingMaskIntoConstraints = false
55
- NSLayoutConstraint.activate([
56
- webView.topAnchor.constraint(equalTo: view.topAnchor),
57
- webView.leadingAnchor.constraint(equalTo: view.leadingAnchor),
58
- webView.trailingAnchor.constraint(equalTo: view.trailingAnchor),
59
- webView.bottomAnchor.constraint(equalTo: view.bottomAnchor)
60
- ])
61
-
62
- // 5. Load embed
63
- encoreBridge.loadEmbed()
64
- }
65
- }
66
- ```
67
-
68
- ### 3. Implement Delegate Methods
69
-
70
- ```swift
71
- extension YourViewController: EncoreWebViewBridgeDelegate {
72
- func encoreBridgeDidBecomeReady(_ bridge: EncoreWebViewBridge) {
73
- print("Encore is ready")
74
- }
75
-
76
- func encoreBridge(_ bridge: EncoreWebViewBridge,
77
- didInitializeWithUserId userId: String) {
78
- print("Initialized with user: \(userId)")
79
- // Now you can present offers
80
- }
81
-
82
- func encoreBridge(_ bridge: EncoreWebViewBridge,
83
- didGrantEntitlement entitlement: EncoreEntitlement) {
84
- // User completed an offer!
85
- print("Granted: \(entitlement.type)")
86
-
87
- // TODO: Apply the entitlement in your app
88
- // - Unlock premium features
89
- // - Apply discount
90
- // - Update subscription status
91
- }
92
-
93
- func encoreBridge(_ bridge: EncoreWebViewBridge,
94
- userDeclinedWithReason reason: EncoreDeclineReason) {
95
- print("User declined: \(reason)")
96
- // Handle decline (optional)
97
- }
98
-
99
- func encoreBridge(_ bridge: EncoreWebViewBridge,
100
- didEncounterError error: EncoreError) {
101
- print("Error: \(error.message)")
102
- // Handle error (optional)
103
- }
104
- }
105
- ```
106
-
107
- ### 4. Present an Offer
108
-
109
- ```swift
110
- // When you want to show an offer (e.g., during cancellation flow)
111
- encoreBridge.presentOffer()
112
- ```
113
-
114
- ## Use Cases
115
-
116
- ### Subscription Cancellation Flow
117
-
118
- ```swift
119
- func handleCancelSubscription() {
120
- // Present retention offer before cancelling
121
- encoreBridge.presentOffer()
122
- }
123
-
124
- // In delegate:
125
- func encoreBridge(_ bridge: EncoreWebViewBridge,
126
- didGrantEntitlement entitlement: EncoreEntitlement) {
127
- // User completed offer - keep subscription active!
128
- keepSubscriptionActive()
129
- showSuccessMessage()
130
- }
29
+ let webView = WKWebView(frame: view.bounds)
30
+ view.addSubview(webView)
131
31
 
132
- func encoreBridge(_ bridge: EncoreWebViewBridge,
133
- userDeclinedWithReason reason: EncoreDeclineReason) {
134
- // User still wants to cancel - proceed
135
- proceedWithCancellation()
32
+ if let url = EncoreURL.build(
33
+ apiKey: "pk_live_...",
34
+ userId: currentUser.id,
35
+ attributes: ["subscriptionTier": currentUser.tier]
36
+ ) {
37
+ webView.load(URLRequest(url: url))
136
38
  }
137
39
  ```
138
40
 
139
- ### Feature Paywall
41
+ ## SwiftUI
140
42
 
141
43
  ```swift
142
- func showPremiumFeature() {
143
- guard userHasPremiumAccess() else {
144
- // Show Encore offer before paywall
145
- encoreBridge.presentOffer()
146
- return
44
+ struct MyView: View {
45
+ @State private var showOffer = false
46
+
47
+ var body: some View {
48
+ Button("Show offer") { showOffer = true }
49
+ .sheet(isPresented: $showOffer) {
50
+ EncoreWebView(
51
+ apiKey: "pk_live_...",
52
+ userId: currentUser.id,
53
+ attributes: ["subscriptionTier": currentUser.tier]
54
+ )
55
+ }
147
56
  }
148
-
149
- // Show premium feature
150
- }
151
- ```
152
-
153
- ### Dynamic User Attributes
154
-
155
- ```swift
156
- func updateUserContext() {
157
- let attributes = [
158
- "subscriptionTier": user.tier,
159
- "monthsSubscribed": "\(user.subscriptionMonths)",
160
- "lastActiveDate": ISO8601DateFormatter().string(from: Date())
161
- ]
162
-
163
- encoreBridge.setUserAttributes(attributes)
164
- }
165
- ```
166
-
167
- ## Configuration Options
168
-
169
- ### EncoreConfiguration
170
-
171
- ```swift
172
- public struct EncoreConfiguration {
173
- public let apiKey: String // Required: Your Encore API key
174
- public let environment: String // "production" or "localhost"
175
- public let userId: String? // Optional: User identifier
176
- public let attributes: [String: String]? // Optional: User attributes
177
- public let logLevel: String? // Optional: "debug" or "none"
178
57
  }
179
58
  ```
180
59
 
181
- ### User Attributes
182
-
183
- You can pass user attributes for better targeting:
60
+ ## Parameters
184
61
 
185
- ```swift
186
- let attributes = [
187
- // Identity
188
- "email": "user@example.com",
189
- "firstName": "Jane",
190
- "lastName": "Doe",
191
-
192
- // Subscription
193
- "subscriptionTier": "free",
194
- "monthsSubscribed": "6",
195
- "billingCycle": "monthly",
196
-
197
- // Demographics
198
- "postalCode": "10001",
199
- "city": "New York",
200
- "state": "NY",
201
- "countryCode": "US",
202
-
203
- // Engagement
204
- "lastActiveDate": "2024-01-15",
205
- "totalSessions": "150"
206
- ]
207
- ```
62
+ `apiKey` and `userId` are required. The page shows an error state without them.
208
63
 
209
- ## API Reference
64
+ Optional: `environment` (`production`, `development`, `staging`, `localhost`),
65
+ `useCase` (`rewardUsers` by default, or `reduceChurn`), `placement`,
66
+ `autoPresentOffer`, `headline`, `subheadline`.
210
67
 
211
- ### EncoreWebViewBridge
68
+ Targeting attributes are flat query parameters. Accepted keys are
69
+ `countryCode`, `postalCode`, `city`, `state`, `language`, `subscriptionTier`,
70
+ `monthsSubscribed`, `billingCycle`, `lastPaymentAmount`, `lastActiveDate`,
71
+ `totalSessions`, plus anything you pass through `custom:`.
212
72
 
213
- #### Methods
73
+ `email`, `firstName`, `lastName`, `phoneNumber`, `dateOfBirth`, `gender`,
74
+ `latitude` and `longitude` are refused. A URL is written to webview history and
75
+ to hosting access logs, so it is the wrong channel for a direct identifier. Ask
76
+ Encore for a short lived signed token if you need to target on one.
214
77
 
215
- ```swift
216
- // Setup and load
217
- func setupWebView() -> WKWebView
218
- func loadEmbed()
78
+ `EncoreURL.build` drops them before they reach the URL rather than relying on
79
+ the page to ignore them, because by then the value has already been written into
80
+ history and into a log. It also caps `custom.` keys at 20 and truncates values
81
+ at 256 characters, matching the page. Anything dropped is printed in `DEBUG`
82
+ builds so targeting does not quietly underperform.
219
83
 
220
- // Configuration
221
- func configure(_ configuration: EncoreConfiguration)
84
+ See the **[Hosted Offer Page guide](https://docs.encorekit.com/publishers/web/guides/hosted-offer-page)** for the full integration
85
+ walkthrough, or [the hosted page reference](../../embed/README.md) for the
86
+ query string contract on its own.
222
87
 
223
- // Offers
224
- func presentOffer()
88
+ ## Knowing when a user earned something
225
89
 
226
- // User management
227
- func identify(userId: String, attributes: [String: String]? = nil)
228
- func setUserAttributes(_ attributes: [String: String])
229
- func reset()
90
+ The page does not call your app back. Ask Encore from your backend:
230
91
 
231
- // State
232
- func getState()
233
92
  ```
234
-
235
- #### Properties
236
-
237
- ```swift
238
- weak var delegate: EncoreWebViewBridgeDelegate?
239
- var isReady: Bool { get }
240
- var isInitialized: Bool { get }
241
- var currentUserId: String? { get }
93
+ GET https://api.encorekit.com/encore/publisher/sdk/v1/entitlements/server?userId=<userId>
242
94
  ```
243
95
 
244
- ### Delegate Protocol
245
-
246
- ```swift
247
- protocol EncoreWebViewBridgeDelegate: AnyObject {
248
- func encoreBridgeDidBecomeReady(_ bridge: EncoreWebViewBridge)
249
- func encoreBridge(_ bridge: EncoreWebViewBridge, didInitializeWithUserId userId: String)
250
- func encoreBridge(_ bridge: EncoreWebViewBridge, didGrantEntitlement entitlement: EncoreEntitlement)
251
- func encoreBridge(_ bridge: EncoreWebViewBridge, userDeclinedWithReason reason: EncoreDeclineReason)
252
- func encoreBridge(_ bridge: EncoreWebViewBridge, didEncounterError error: EncoreError)
253
- }
254
- ```
96
+ authenticated with your API key plus an HMAC signature you compute with your
97
+ API secret. The secret itself is never sent. Poll it when the webview is
98
+ dismissed, or on your own schedule.
255
99
 
256
- ## Important Notes
100
+ ## What to expect when a user claims
257
101
 
258
- ### WebView Positioning
259
-
260
- The WebView should fill the entire screen (or your desired container) even though it's transparent. This ensures the offer modal can display properly when shown.
261
-
262
- ```swift
263
- // Correct: Full screen
264
- webView.frame = view.bounds
265
-
266
- // Or with constraints
267
- NSLayoutConstraint.activate([
268
- webView.topAnchor.constraint(equalTo: view.topAnchor),
269
- webView.leadingAnchor.constraint(equalTo: view.leadingAnchor),
270
- webView.trailingAnchor.constraint(equalTo: view.trailingAnchor),
271
- webView.bottomAnchor.constraint(equalTo: view.bottomAnchor)
272
- ])
273
- ```
274
-
275
- ### Entitlement Validation
276
-
277
- **Important:** Always validate entitlements on your server, not in the iOS app. The client-side entitlement data is for UX purposes only.
278
-
279
- ```swift
280
- // ❌ Don't do this
281
- func unlockPremium() {
282
- if entitlement.type == "freeTrial" {
283
- // Never trust client-side validation
284
- }
285
- }
286
-
287
- // ✅ Do this instead
288
- func unlockPremium() {
289
- // Call your server to validate
290
- api.validateEntitlement(userId) { result in
291
- if result.isValid {
292
- // Server confirmed - safe to unlock
293
- }
294
- }
295
- }
296
- ```
297
-
298
- ### Thread Safety
299
-
300
- All delegate methods are called on the main thread, so you can safely update UI directly.
301
-
302
- ### Memory Management
303
-
304
- The bridge holds a weak reference to its delegate to prevent retain cycles. Make sure your view controller maintains a strong reference to the bridge.
305
-
306
- ```swift
307
- // ✅ Correct
308
- class MyVC: UIViewController {
309
- private var encoreBridge: EncoreWebViewBridge! // Strong reference
310
- }
311
-
312
- // ❌ Wrong - bridge will be deallocated
313
- func setupBridge() {
314
- let bridge = EncoreWebViewBridge() // Local variable
315
- bridge.delegate = self
316
- // bridge is deallocated when function returns
317
- }
318
- ```
319
-
320
- ### State Management & Page Reloads
321
-
322
- The bridge automatically handles page reloads and navigation events:
323
-
324
- - **On navigation start** (`didStartProvisionalNavigation`): State is reset (`isReady = false`, `isInitialized = false`)
325
- - **On navigation finish** (`didFinish`): Configuration is automatically re-applied
326
- - **Message queue**: Cleared on navigation to prevent stale messages
327
-
328
- This means:
329
- - ✅ **Page refreshes work correctly** - SDK is automatically re-initialized
330
- - ✅ **No stale state** - Each page load starts fresh
331
- - ✅ **No race conditions** - Configuration waits for JS to be ready
332
-
333
- ```swift
334
- // You don't need to do anything special for page reloads
335
- // The bridge handles it automatically:
336
-
337
- // Initial load
338
- encoreBridge.loadEmbed() // → SDK initializes
339
-
340
- // User refreshes page (swipe down, etc.)
341
- // → Bridge detects navigation start
342
- // → State is reset
343
- // → Page finishes loading
344
- // → Configuration is automatically re-applied
345
- // → SDK initializes again
346
- ```
347
-
348
- **Important:** Don't rely on `isInitialized` staying `true` across navigations. If you need to track state across page loads, maintain it separately in your view controller.
102
+ Claiming navigates the whole webview to the advertiser rather than opening a
103
+ second window. `window.open` returns null inside a `WKWebView` unless the host
104
+ implements `WKUIDelegate.createWebViewWith`, so a top level navigation is the
105
+ only handoff that works without cooperation from your app. Dismiss your sheet
106
+ when the user is done and poll for the entitlement.
349
107
 
350
108
  ## Troubleshooting
351
109
 
352
- ### Offers not showing
353
-
354
- 1. Check that `didInitializeWithUserId` was called
355
- 2. Verify your API key is correct
356
- 3. Check network connectivity
357
- 4. Look for error messages in delegate methods
358
- 5. Try enabling debug mode: add `?debug=true` to embed URL
359
-
360
- ### WebView blank/not loading
361
-
362
- 1. Ensure Info.plist has network permissions
363
- 2. Check the embed URL is accessible
364
- 3. Verify WKWebView configuration allows JavaScript
365
- 4. Check console for JavaScript errors
366
-
367
- ### Messages not received
368
-
369
- 1. Verify message handler name is "encore"
370
- 2. Check that delegate is set
371
- 3. Ensure bridge has strong reference
372
- 4. Try adding print statements in delegate methods
373
-
374
- ### Build errors
375
-
376
- 1. Make sure you're targeting iOS 13.0 or later
377
- 2. Import WebKit framework
378
- 3. Check that EncoreWebViewBridge.swift is added to target
379
-
380
- ## Requirements
381
-
382
- - iOS 13.0+
383
- - Xcode 12.0+
384
- - Swift 5.0+
385
- - Network connectivity
386
-
387
- ## Support
388
-
389
- For issues or questions:
390
- - Email: admin@encorekit.com
391
- - Documentation: https://docs.encorekit.com
392
-
393
- ## License
110
+ **Blank page.** Check `apiKey` and `userId` are both present and that the key
111
+ matches the environment you passed.
394
112
 
395
- This example code is provided as-is for integration purposes.
113
+ **Targeting looks ignored.** Attributes go in as one parameter per key. A single
114
+ JSON `attributes=` value is ignored, and refused keys are dropped.
396
115
 
116
+ **No offers appear.** The campaign must be granted to your app and enabled.
117
+ Granting alone is not enough.
@@ -1,57 +1,73 @@
1
1
  //
2
2
  // SimpleEncoreView.swift
3
- // Encore iOS Simple Integration
3
+ // Encore hosted offer page, SwiftUI wrapper
4
4
  //
5
- // Dead simple SwiftUI wrapper for Encore HTML tag
5
+ // A SwiftUI view that loads the hosted offer page. Copy this file and
6
+ // `EncoreURLBuilder.swift` into your project.
6
7
  //
7
8
 
8
9
  import SwiftUI
9
10
  import WebKit
10
11
 
11
- /// Simple SwiftUI view that displays Encore embed
12
+ /// Displays the Encore hosted offer page.
12
13
  ///
13
14
  /// Usage:
14
15
  /// ```swift
15
16
  /// EncoreWebView(
16
- /// apiKey: "your_api_key",
17
- /// userId: "user_123",
18
- /// attributes: ["email": "user@example.com"]
17
+ /// apiKey: "pk_live_...",
18
+ /// userId: currentUser.id,
19
+ /// attributes: ["subscriptionTier": "premium"]
19
20
  /// )
20
21
  /// ```
22
+ ///
23
+ /// There is no completion callback, and that is deliberate rather than
24
+ /// missing. The page reports nothing back to the host app, so your backend
25
+ /// learns about an entitlement by asking Encore for it:
26
+ ///
27
+ /// GET https://api.encorekit.com/encore/publisher/sdk/v1/entitlements/server?userId=<userId>
28
+ ///
29
+ /// authenticated with your API key plus an HMAC signature you compute with
30
+ /// your API secret. The secret itself is never sent. Poll it when this view
31
+ /// goes away.
21
32
  public struct EncoreWebView: View {
22
33
  let apiKey: String
23
34
  let userId: String
24
35
  let environment: String
36
+ let useCase: EncoreURL.UseCase?
25
37
  let attributes: [String: String]
26
- let debug: Bool
27
-
38
+
28
39
  public init(
29
40
  apiKey: String,
30
41
  userId: String,
31
42
  environment: String = "production",
32
- attributes: [String: String] = [:],
33
- debug: Bool = false
43
+ useCase: EncoreURL.UseCase? = nil,
44
+ attributes: [String: String] = [:]
34
45
  ) {
35
46
  self.apiKey = apiKey
36
47
  self.userId = userId
37
48
  self.environment = environment
49
+ self.useCase = useCase
38
50
  self.attributes = attributes
39
- self.debug = debug
40
51
  }
41
-
52
+
42
53
  public var body: some View {
43
- WebViewRepresentable(url: buildURL())
44
- .ignoresSafeArea()
45
- }
46
-
47
- private func buildURL() -> URL {
48
- EncoreURL.build(
49
- apiKey: apiKey,
50
- userId: userId,
51
- environment: environment,
52
- attributes: attributes,
53
- debug: debug
54
- )!
54
+ Group {
55
+ if let url = EncoreURL.build(
56
+ apiKey: apiKey,
57
+ userId: userId,
58
+ environment: environment,
59
+ useCase: useCase,
60
+ attributes: attributes
61
+ ) {
62
+ WebViewRepresentable(url: url)
63
+ } else {
64
+ // Unreachable in practice: this view always builds against
65
+ // the hosted page's own base, which it does not expose. Showing
66
+ // nothing still beats trapping inside a publisher's app.
67
+ Color.clear
68
+ }
69
+ }
70
+ .ignoresSafeArea()
55
71
  }
56
72
  }
57
73
 
@@ -59,7 +75,7 @@ public struct EncoreWebView: View {
59
75
 
60
76
  private struct WebViewRepresentable: UIViewRepresentable {
61
77
  let url: URL
62
-
78
+
63
79
  func makeUIView(context: Context) -> WKWebView {
64
80
  let webView = WKWebView()
65
81
  webView.backgroundColor = .systemBackground
@@ -67,10 +83,10 @@ private struct WebViewRepresentable: UIViewRepresentable {
67
83
  webView.load(URLRequest(url: url))
68
84
  return webView
69
85
  }
70
-
71
- func updateUIView(_ webView: WKWebView, context: Context) {
72
- // No updates needed
73
- }
86
+
87
+ // Loading once in makeUIView is the whole lifecycle. Reloading here would
88
+ // fire on every SwiftUI update and restart the offer mid presentation.
89
+ func updateUIView(_ webView: WKWebView, context: Context) {}
74
90
  }
75
91
 
76
92
  // MARK: - Preview
@@ -79,16 +95,13 @@ private struct WebViewRepresentable: UIViewRepresentable {
79
95
  struct EncoreWebView_Previews: PreviewProvider {
80
96
  static var previews: some View {
81
97
  EncoreWebView(
82
- apiKey: "test_api_key",
98
+ apiKey: "pk_test_example",
83
99
  userId: "preview_user",
84
- environment: "localhost",
85
100
  attributes: [
86
- "email": "preview@example.com",
87
- "subscriptionTier": "free"
88
- ],
89
- debug: true
101
+ "subscriptionTier": "free",
102
+ "countryCode": "US",
103
+ ]
90
104
  )
91
105
  }
92
106
  }
93
107
  #endif
94
-