@encorekit/web-sdk 2.0.3 → 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 (79) 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 +3 -7
  14. package/types/analytics/DiagnosticsOutbox.d.ts +22 -0
  15. package/types/analytics/DiagnosticsOutbox.d.ts.map +1 -0
  16. package/types/analytics/ReliableOutbox.d.ts +40 -0
  17. package/types/analytics/ReliableOutbox.d.ts.map +1 -0
  18. package/types/analytics/events.d.ts +100 -0
  19. package/types/analytics/events.d.ts.map +1 -1
  20. package/types/analytics/placementResolved.d.ts +150 -0
  21. package/types/analytics/placementResolved.d.ts.map +1 -0
  22. package/types/api/APIClient.d.ts +10 -2
  23. package/types/api/APIClient.d.ts.map +1 -1
  24. package/types/api/errorReportLimits.d.ts +50 -0
  25. package/types/api/errorReportLimits.d.ts.map +1 -0
  26. package/types/api/generated/types.d.ts +54 -7
  27. package/types/api/generated/types.d.ts.map +1 -1
  28. package/types/core/Configuration.d.ts +5 -0
  29. package/types/core/Configuration.d.ts.map +1 -1
  30. package/types/core/Encore.d.ts +61 -2
  31. package/types/core/Encore.d.ts.map +1 -1
  32. package/types/core/OfferManager.d.ts +29 -3
  33. package/types/core/OfferManager.d.ts.map +1 -1
  34. package/types/core/PlacementBuilder.d.ts +26 -2
  35. package/types/core/PlacementBuilder.d.ts.map +1 -1
  36. package/types/core/StrictUnlock.d.ts +20 -0
  37. package/types/core/StrictUnlock.d.ts.map +1 -0
  38. package/types/core/StrictUnlockReconciler.d.ts +94 -0
  39. package/types/core/StrictUnlockReconciler.d.ts.map +1 -0
  40. package/types/core/TransactionStore.d.ts +57 -13
  41. package/types/core/TransactionStore.d.ts.map +1 -1
  42. package/types/core/VerificationPoller.d.ts.map +1 -1
  43. package/types/core/claimBehavior.d.ts +28 -0
  44. package/types/core/claimBehavior.d.ts.map +1 -0
  45. package/types/core/errorContext.d.ts +18 -0
  46. package/types/core/errorContext.d.ts.map +1 -0
  47. package/types/core/errorType.d.ts +59 -0
  48. package/types/core/errorType.d.ts.map +1 -0
  49. package/types/core/hostDisplay.d.ts +50 -0
  50. package/types/core/hostDisplay.d.ts.map +1 -0
  51. package/types/core/useCase.d.ts +21 -0
  52. package/types/core/useCase.d.ts.map +1 -1
  53. package/types/core/version.d.ts +1 -1
  54. package/types/experiments/ExposureOutbox.d.ts +4 -28
  55. package/types/experiments/ExposureOutbox.d.ts.map +1 -1
  56. package/types/types.d.ts +188 -3
  57. package/types/types.d.ts.map +1 -1
  58. package/types/ui/OfferCard.d.ts +3 -1
  59. package/types/ui/OfferCard.d.ts.map +1 -1
  60. package/types/ui/OfferCarousel.d.ts +5 -0
  61. package/types/ui/OfferCarousel.d.ts.map +1 -1
  62. package/types/ui/OfferModal.d.ts +20 -0
  63. package/types/ui/OfferModal.d.ts.map +1 -1
  64. package/types/ui/PrimerScreen.d.ts +18 -11
  65. package/types/ui/PrimerScreen.d.ts.map +1 -1
  66. package/types/ui/RedemptionScreen.d.ts +15 -19
  67. package/types/ui/RedemptionScreen.d.ts.map +1 -1
  68. package/types/ui/RewardActions.d.ts +35 -0
  69. package/types/ui/RewardActions.d.ts.map +1 -1
  70. package/types/ui/appearance.d.ts +14 -0
  71. package/types/ui/appearance.d.ts.map +1 -1
  72. package/types/ui/brandComponents.d.ts.map +1 -1
  73. package/types/ui/styles.d.ts.map +1 -1
  74. package/types/utils/logger.d.ts +11 -1
  75. package/types/utils/logger.d.ts.map +1 -1
  76. package/embed/index.html +0 -57
  77. package/embed/styles.css +0 -154
  78. package/examples/ios-webview/EncoreWebViewBridge.swift +0 -419
  79. package/examples/ios-webview/ExampleViewController.swift +0 -233
package/embed/README.md CHANGED
@@ -1,409 +1,124 @@
1
- # Encore iOS WebView Embed
1
+ # Encore Hosted Offer Page
2
2
 
3
- This directory contains the embeddable HTML page and JavaScript bridge that allows iOS apps to use Encore via WKWebView without installing the native Swift SDK.
3
+ A publisher who cannot install an SDK opens one URL in a plain webview. Encore
4
+ hosts the page, and the entire configuration surface is the query string.
4
5
 
5
- ## 🎨 Mobile-Native UI
6
+ There is no native bridge on any platform. Nothing is posted back to the host
7
+ app, and no message handler needs registering. If you are looking for
8
+ `webkit.messageHandlers`, a `WKScriptMessageHandler`, or an `encore:*` message
9
+ protocol, they were removed: the bridge is what made the old embed iOS only,
10
+ and dropping it is what made this page work everywhere.
6
11
 
7
- The embed now features an **iOS-native bottom sheet UI** that provides a mobile-optimized experience matching the iOS SDK's visual design:
12
+ ## The URL
8
13
 
9
- - ✅ **Bottom Sheet Design** - Slides up from bottom (not a centered popup)
10
- - ✅ **Native Animations** - Smooth 300ms cubic-bezier animations
11
- - ✅ **Swipe-to-Dismiss** - Pull down gesture to close
12
- - ✅ **Auto-Present** - Automatically shows offers on load
13
- - ✅ **iOS Styling** - SF Pro font, system blue, native shadows
14
- - ✅ **Safe Area Support** - Respects notches and home indicators
15
- - ✅ **Dark Mode** - Automatic dark mode support
16
-
17
- 📖 **[Full Mobile UI Guide](./MOBILE-UI-GUIDE.md)** | 🧪 **[Testing Guide](../../encore-swift-sdk/EncoreSwiftUIDemo/MOBILE-UI-TESTING.md)**
18
-
19
- ## Overview
20
-
21
- The embed solution consists of:
22
-
23
- - **`index.html`** - The main HTML page to be loaded in WKWebView
24
- - **`bridge.js`** - JavaScript-iOS communication bridge via `webkit.messageHandlers`
25
- - **`app.js`** - Application logic connecting Encore Web SDK to the bridge
26
- - **`styles.css`** - Base styling (transparent until offer shown)
27
- - **`mobile-styles.css`** - iOS-native bottom sheet components
28
- - **`mobile-overrides.css`** - Transforms Web SDK modal to mobile sheet
29
-
30
- ## Architecture
31
-
32
- ```
33
- iOS App (WKWebView)
34
- ↕ (webkit.messageHandlers)
35
- Bridge.js
36
- ↕
37
- App.js
38
- ↕
39
- Encore Web SDK
40
- ```
41
-
42
- ### URL Flow (How Offers Are Opened)
43
-
44
- When a user claims an offer, the URL flows through the system as follows:
45
-
46
- 1. **SDK builds final URL** - `Encore.ts` constructs the advertiser URL with tracking parameters (transaction ID, user ID, etc.)
47
- 2. **SDK calls window.open()** - The SDK opens the URL using the standard `window.open(finalUrl)` API
48
- 3. **Embed intercepts** - `app.js` overrides `window.open()` to capture the URL before it opens
49
- 4. **Embed sends to iOS** - The captured URL is sent via `bridge.sendToIOS('encore:openURL', { url })`
50
- 5. **iOS opens Safari** - The native app receives the message and opens the URL in `SFSafariViewController`
51
-
52
- This design ensures:
53
- - ✅ **No DOM coupling** - The SDK doesn't need to write URLs to DOM attributes
54
- - ✅ **Clean separation** - The SDK is unaware of the iOS integration
55
- - ✅ **Tracking preserved** - All tracking parameters flow through naturally
56
- - ✅ **Framework agnostic** - Works regardless of how the SDK UI is implemented
57
-
58
- ## Message Protocol
59
-
60
- ### Web-to-iOS Messages
61
-
62
- Messages sent from JavaScript to iOS via `webkit.messageHandlers.encore.postMessage()`:
63
-
64
- | Message Type | Payload | Description |
65
- |-------------|---------|-------------|
66
- | `encore:ready` | `{ timestamp, hasURLConfig, isMobile }` | Embed app loaded and ready |
67
- | `encore:initialized` | `{ success, userId, version }` | SDK initialized successfully |
68
- | `encore:sheet:presented` | `{ timestamp }` | **[Mobile]** Bottom sheet shown |
69
- | `encore:sheet:dismissed` | `{ reason, timestamp }` | **[Mobile]** Bottom sheet closed |
70
- | `encore:openURL` | `{ url, timestamp }` | **[Mobile]** Request to open URL in Safari |
71
- | `encore:entitlementGranted` | `{ entitlement, timestamp }` | User completed offer |
72
- | `encore:userDeclined` | `{ reason, details, timestamp }` | User declined offer |
73
- | `encore:error` | `{ code, message, context }` | Error occurred |
74
- | `encore:userIdentified` | `{ success, userId }` | User identified |
75
- | `encore:attributesSet` | `{ success }` | Attributes set |
76
- | `encore:reset` | `{ success }` | SDK reset |
77
- | `encore:state` | `{ isInitialized, userId, version, config }` | Current state |
78
-
79
- ### iOS-to-Web Messages
80
-
81
- Messages sent from iOS to JavaScript via `webView.evaluateJavaScript()` or `postMessage()`:
82
-
83
- | Message Type | Payload | Description |
84
- |-------------|---------|-------------|
85
- | `configure` | `{ apiKey, environment?, userId?, attributes?, logLevel? }` | Initialize SDK |
86
- | `presentOffer` | `{}` | Show offer modal |
87
- | `identify` | `{ userId, attributes? }` | Identify user |
88
- | `setUserAttributes` | `{ attributes }` | Update user attributes |
89
- | `reset` | `{}` | Reset SDK state |
90
- | `getState` | `{}` | Request current state |
91
-
92
- ## Integration Methods
93
-
94
- ### Method 1: URL Builder (Recommended for Mobile)
95
-
96
- Use the `EncoreURL` builder to create a properly formatted URL with auto-present:
97
-
98
- ```swift
99
- import SwiftUI
100
-
101
- let url = EncoreURL.build(
102
- apiKey: "your_api_key",
103
- userId: "user123",
104
- environment: "production",
105
- autoPresentOffer: true, // Auto-show offers
106
- debug: false
107
- )
108
-
109
- // In SwiftUI
110
- .sheet(isPresented: $showEncore) {
111
- EncoreWebView(
112
- apiKey: "your_api_key",
113
- userId: "user123",
114
- autoPresentOffer: true
115
- )
116
- }
117
14
  ```
118
-
119
- **Pros**: Dead simple, iOS-native UI, auto-presents, swipe gestures
120
- **Cons**: API key visible in URL (use server-side API key rotation)
121
-
122
- ### Method 2: URL Parameters (Manual)
123
-
124
- Load the embed page with configuration in URL:
125
-
126
- ```swift
127
- let url = URL(string: "https://encorekit.com/embed/?apiKey=YOUR_KEY&userId=USER_123&environment=production&autoPresentOffer=true")!
128
- webView.load(URLRequest(url: url))
15
+ https://encorekit.com/offers?apiKey=pk_live_...&userId=user_123
129
16
  ```
130
17
 
131
- ### Method 3: postMessage (Advanced)
18
+ `/embed` and `/payrange` serve the same page, so URLs already shipped against
19
+ them keep working. Prefer `/offers` for anything new.
132
20
 
133
- Load empty embed page, then configure via postMessage:
21
+ Open it in whatever webview the platform gives you and load the request. That
22
+ is the whole integration.
134
23
 
135
24
  ```swift
136
- // 1. Load embed page
137
- webView.load(URL(string: "https://encorekit.com/embed/")!)
138
-
139
- // 2. Configure after load (in WKNavigationDelegate)
140
- func webView(_ webView: WKWebView, didFinish navigation: WKNavigation!) {
141
- let config = """
142
- window.postMessage({
143
- type: 'configure',
144
- payload: {
145
- apiKey: '\(apiKey)',
146
- userId: '\(userId)',
147
- environment: 'production'
148
- }
149
- }, '*');
150
- """
151
- webView.evaluateJavaScript(config)
25
+ guard var components = URLComponents(string: "https://encorekit.com/offers") else { return }
26
+ components.queryItems = [
27
+ URLQueryItem(name: "apiKey", value: apiKey),
28
+ URLQueryItem(name: "userId", value: userId),
29
+ ]
30
+ if let url = components.url {
31
+ webView.load(URLRequest(url: url))
152
32
  }
153
33
  ```
154
34
 
155
- **Pros**: More secure, flexible, dynamic configuration
156
- **Cons**: Slightly more code
157
-
158
- ### Method 4: JavaScript with Callbacks (Most Control)
159
-
160
- For scenarios where you need to control the flow after initialization:
161
-
162
- ```javascript
163
- // In your custom JavaScript or via evaluateJavaScript
164
- if (window.EncoreBridge && window.Encore) {
165
- window.EncoreBridge.on('encore:initialized', (payload) => {
166
- console.log('SDK initialized:', payload);
167
- // Now safe to present offer
168
- window.postMessage({ type: 'presentOffer', payload: {} }, '*');
169
- });
170
-
171
- window.EncoreBridge.on('encore:error', (payload) => {
172
- console.error('Initialization error:', payload);
173
- });
174
-
175
- // Trigger initialization
176
- window.postMessage({
177
- type: 'configure',
178
- payload: {
179
- apiKey: 'pk_live_...',
180
- userId: 'user_123',
181
- environment: 'production'
182
- }
183
- }, '*');
184
- }
185
- ```
186
-
187
- **Pros**: Full control, proper error handling, no race conditions
188
- **Cons**: More code, requires understanding of message flow
189
-
190
- ## Usage Examples
191
-
192
- ### Present an Offer
193
-
194
- ```swift
195
- // From iOS
196
- let script = """
197
- window.postMessage({
198
- type: 'presentOffer',
199
- payload: {}
200
- }, '*');
201
- """
202
- webView.evaluateJavaScript(script)
203
- ```
35
+ Build the query string with `URLComponents` rather than interpolating into a
36
+ string, so values are percent encoded for you.
204
37
 
205
- ### Handle Entitlement Granted
206
-
207
- ```swift
208
- // In iOS message handler
209
- func userContentController(_ userContentController: WKUserContentController,
210
- didReceive message: WKScriptMessage) {
211
- guard message.name == "encore" else { return }
212
-
213
- guard let dict = message.body as? [String: Any],
214
- let type = dict["type"] as? String else {
215
- return
216
- }
217
-
218
- switch type {
219
- case "encore:entitlementGranted":
220
- if let payload = dict["payload"] as? [String: Any],
221
- let entitlement = payload["entitlement"] {
222
- handleEntitlementGranted(entitlement)
223
- }
224
- case "encore:userDeclined":
225
- if let payload = dict["payload"] as? [String: Any],
226
- let reason = payload["reason"] as? String {
227
- handleUserDeclined(reason)
228
- }
229
- case "encore:error":
230
- if let payload = dict["payload"] as? [String: Any] {
231
- handleError(payload)
232
- }
233
- case "encore:openURL":
234
- if let payload = dict["payload"] as? [String: Any],
235
- let urlString = payload["url"] as? String,
236
- let url = URL(string: urlString) {
237
- openInSafari(url: url)
238
- }
239
- default:
240
- break
241
- }
242
- }
243
-
244
- func openInSafari(url: URL) {
245
- let safari = SFSafariViewController(url: url)
246
- safari.delegate = self
247
- present(safari, animated: true)
248
- }
249
-
250
- // SFSafariViewControllerDelegate
251
- func safariViewControllerDidFinish(_ controller: SFSafariViewController) {
252
- // User closed Safari, notify webview
253
- let script = """
254
- window.postMessage({
255
- type: 'safariClosed',
256
- payload: { timestamp: \(Date().timeIntervalSince1970 * 1000) }
257
- }, '*');
258
- """
259
- webView.evaluateJavaScript(script)
260
- }
261
- ```
38
+ ## Parameters
262
39
 
263
- ## Bridge API
40
+ `apiKey` and `userId` are both required. Without either, the page shows an
41
+ error state naming what is missing rather than a blank screen.
264
42
 
265
- The `EncoreBridge` object provides methods for communication between JavaScript and iOS:
43
+ | Parameter | Notes |
44
+ |-----------|-------|
45
+ | `apiKey` | Your publishable key |
46
+ | `userId` | Your identifier for the end user |
266
47
 
267
- ### Listening for Messages
48
+ Optional.
268
49
 
269
- ```javascript
270
- // Listen for messages (stays subscribed)
271
- const unsubscribe = bridge.on('encore:initialized', (payload) => {
272
- console.log('SDK initialized:', payload);
273
- });
50
+ | Parameter | Default | Notes |
51
+ |-----------|---------|-------|
52
+ | `environment` | `production` | One of `production`, `development`, `staging`, `localhost` |
53
+ | `useCase` | `rewardUsers` | Or `reduceChurn` for the churn carousel |
54
+ | `placement` | `hosted_reward` or `hosted_offer` | Follows `useCase` so the two stay separable in reporting |
55
+ | `autoPresentOffer` | `true` | Set `false` to suppress the automatic presentation |
56
+ | `headline` | | Overrides the default headline |
57
+ | `subheadline` | | Overrides the default subheadline |
274
58
 
275
- // Stop listening
276
- unsubscribe();
59
+ The default `useCase` here is `rewardUsers`, which deliberately differs from the
60
+ Web SDK's own `reduceChurn`. The SDK default suits a save attempt fired as
61
+ someone leaves. This page is opened deliberately by your app, and that moment is
62
+ a reward far more often than it is a cancellation.
277
63
 
278
- // Listen once (automatically unsubscribes after first call)
279
- bridge.once('encore:initialized', (payload) => {
280
- console.log('SDK initialized (one-time):', payload);
281
- // Handler is automatically removed after this
282
- });
283
- ```
64
+ ## Targeting attributes
284
65
 
285
- ### Sending Messages
66
+ Attributes are flat query parameters, one per key. An earlier revision of these
67
+ examples packed them into a single JSON `attributes=` parameter; that form is
68
+ ignored.
286
69
 
287
- ```javascript
288
- // Send message to iOS
289
- bridge.sendToIOS('encore:customEvent', {
290
- data: 'value',
291
- timestamp: Date.now()
292
- });
293
70
  ```
294
-
295
- ### Initialization Callbacks
296
-
297
- The `initializeSDK` function supports optional success/error callbacks to avoid race conditions:
298
-
299
- ```javascript
300
- // Proper way to auto-present after initialization
301
- initializeSDK(
302
- { apiKey: 'pk_...', userId: 'user_123' },
303
- () => {
304
- // Success - SDK is ready
305
- presentOffer();
306
- },
307
- (error) => {
308
- // Error - handle failure
309
- console.error('Init failed:', error);
310
- }
311
- );
71
+ &countryCode=US&subscriptionTier=premium&custom.plan=gold
312
72
  ```
313
73
 
314
- **⚠️ Important**: Never use `setTimeout()` to wait for initialization. Always use callbacks or listen for the `encore:initialized` message to ensure the SDK is truly ready.
74
+ Accepted: `countryCode`, `postalCode`, `city`, `state`, `language`,
75
+ `subscriptionTier`, `monthsSubscribed`, `billingCycle`, `lastPaymentAmount`,
76
+ `lastActiveDate`, `totalSessions`, and any `custom.` prefixed key.
315
77
 
316
- ## Debug Mode
78
+ Custom keys are capped at 20 per URL and every value is truncated at 256
79
+ characters. An unknown parameter is ignored rather than forwarded.
317
80
 
318
- Enable debug mode to see message logs:
81
+ ### Refused on purpose
319
82
 
320
- 1. **Via URL**: Add `?debug=true` to the embed URL
321
- 2. **Via Console**: Run `enableEncoreDebug()` in the web inspector
322
- 3. **Persistent**: Once enabled, it's saved to localStorage
83
+ `email`, `firstName`, `lastName`, `phoneNumber`, `dateOfBirth`, `gender`,
84
+ `latitude` and `longitude` are dropped, and the page reports that it dropped
85
+ them. A query string lands in webview history and in hosting access logs, so
86
+ direct identifiers and precise location do not belong in one. If you genuinely
87
+ need one, ask for a short lived signed token instead of sending the raw value.
323
88
 
324
- Debug panel shows:
325
- - All messages sent/received
326
- - SDK initialization status
327
- - Errors and warnings
328
- - Timestamps
89
+ ## Learning about the entitlement
329
90
 
330
- ## File Structure
91
+ The page does not call your app back. Your backend asks Encore instead:
331
92
 
332
93
  ```
333
- embed/
334
- ├── index.html # Main HTML page
335
- ├── bridge.js # iOS-JavaScript bridge (SOURCE FILE - vanilla JS)
336
- ├── app.js # Application logic (SOURCE FILE - vanilla JS)
337
- ├── styles.css # Base styles (transparent background)
338
- ├── mobile-styles.css # iOS-native bottom sheet UI
339
- ├── mobile-overrides.css # Web SDK → mobile sheet transforms
340
- ├── dist/
341
- │ ├── bridge.min.js # Minified bridge (generated)
342
- │ ├── bridge.min.js.map # Source map (generated)
343
- │ ├── app.min.js # Minified app (generated)
344
- │ └── app.min.js.map # Source map (generated)
345
- ├── README.md # This file
346
- ├── MOBILE-UI-GUIDE.md # Comprehensive mobile UI docs
347
- └── NEXTJS-DEPLOYMENT.md # Deployment guide
94
+ GET https://api.encorekit.com/encore/publisher/sdk/v1/entitlements/server?userId=<userId>
348
95
  ```
349
96
 
350
- **Note**: `bridge.js` and `app.js` are vanilla JavaScript source files (not TypeScript). They are intentionally written as pure ES6+ to avoid transpilation overhead and are only minified for production.
351
-
352
- ## Building for Production
353
-
354
- ```bash
355
- # Build the embed bundle (minifies bridge.js and app.js)
356
- npm run build:embed
357
-
358
- # Or build everything (SDK + embed)
359
- npm run build
360
-
361
- # Output will be in embed/dist/
362
- ```
97
+ authenticated with your API key and HMAC signature. Poll it when the webview
98
+ closes, or on your own schedule.
363
99
 
364
- ## Security Considerations
100
+ ## One thing to know about webviews
365
101
 
366
- 1. **HTTPS Only**: Always use HTTPS for the embed URL
367
- 2. **API Key Security**: API keys are public but should have usage limits
368
- 3. **Message Validation**: Bridge validates all message structures
369
- 4. **CSP Headers**: Content Security Policy should be configured on server
370
- 5. **Origin Validation**: Messages should validate expected origins
102
+ When the user claims an offer, the page navigates the whole webview to the
103
+ advertiser rather than opening a second window. `window.open` returns null in a
104
+ bare `WebView` or `WKWebView` unless the host app implements the multiple window
105
+ callbacks, so a top level navigation is the only handoff that works without any
106
+ cooperation from you. Expect the webview to leave the offer page at that point.
371
107
 
372
- ## Browser Compatibility
108
+ ## What used to be here
373
109
 
374
- - iOS 13+ (WKWebView)
375
- - Modern JavaScript (ES6+)
376
- - No external dependencies
110
+ This directory once held a static embed page (`index.html`, `bridge.js`,
111
+ `app.js` and its CSS) that spoke a `webkit.messageHandlers` bridge to a native
112
+ iOS host. Those files are deleted. Nothing served them: their build output was
113
+ gitignored and never published, the page loaded a bundle no build produced, and
114
+ the copy that briefly hosted them was removed from the website separately.
377
115
 
378
- ## Troubleshooting
379
-
380
- ### Messages not being received by iOS
381
-
382
- Check:
383
- 1. Message handler is registered: `userContentController.add(self, name: "encore")`
384
- 2. WKWebView configuration is correct
385
- 3. Debug mode is enabled to see logs
386
-
387
- ### SDK not initializing
388
-
389
- Check:
390
- 1. API key is valid
391
- 2. Network connectivity
392
- 3. CORS settings (if self-hosting)
393
- 4. Check debug panel for error messages
394
-
395
- ### Offer modal not showing
396
-
397
- Check:
398
- 1. SDK is initialized (`encore:initialized` received)
399
- 2. Offers are available for this user
400
- 3. CSS/styles are loaded properly
401
- 4. Check console for errors
116
+ This README is all that remains, because the URL contract above is the
117
+ integration that replaced them.
402
118
 
403
119
  ## Support
404
120
 
405
- For more information:
406
- - [Full iOS WebView Integration Guide](../docs/ios-webview-integration.md)
407
- - [Swift Example Code](../examples/ios-webview/)
408
- - [Encore Documentation](https://docs.encorekit.com)
409
-
121
+ - **[Hosted Offer Page guide](https://docs.encorekit.com/publishers/web/guides/hosted-offer-page)** for the full integration walkthrough,
122
+ including the backend entitlement check
123
+ - [Swift examples](../examples/ios-webview/) for a URL builder you can copy
124
+ - [Encore documentation](https://docs.encorekit.com)