@encorekit/web-sdk 2.0.4 → 2.0.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 +112 -40
- 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 +82 -367
- package/examples/ios-webview/EncoreURLBuilder.swift +179 -61
- package/examples/ios-webview/README.md +75 -354
- package/examples/ios-webview/SimpleEncoreView.swift +49 -36
- package/examples/ios-webview/SimpleExample.swift +30 -86
- package/package.json +2 -7
- package/types/api/APIClient.d.ts +3 -1
- package/types/api/APIClient.d.ts.map +1 -1
- package/types/api/errorReportLimits.d.ts +50 -0
- package/types/api/errorReportLimits.d.ts.map +1 -0
- package/types/api/generated/types.d.ts +54 -7
- package/types/api/generated/types.d.ts.map +1 -1
- package/types/core/Encore.d.ts +25 -2
- package/types/core/Encore.d.ts.map +1 -1
- package/types/core/PlacementBuilder.d.ts +14 -2
- package/types/core/PlacementBuilder.d.ts.map +1 -1
- package/types/core/TransactionStore.d.ts +57 -13
- package/types/core/TransactionStore.d.ts.map +1 -1
- package/types/core/VerificationPoller.d.ts.map +1 -1
- package/types/core/claimBehavior.d.ts +28 -0
- package/types/core/claimBehavior.d.ts.map +1 -0
- package/types/core/errorContext.d.ts +18 -0
- package/types/core/errorContext.d.ts.map +1 -0
- package/types/core/errorType.d.ts +59 -0
- package/types/core/errorType.d.ts.map +1 -0
- package/types/core/hostDisplay.d.ts +50 -0
- package/types/core/hostDisplay.d.ts.map +1 -0
- package/types/core/version.d.ts +1 -1
- package/types/types.d.ts +146 -1
- package/types/types.d.ts.map +1 -1
- package/types/ui/OfferModal.d.ts +20 -0
- package/types/ui/OfferModal.d.ts.map +1 -1
- package/types/ui/PrimerScreen.d.ts +18 -11
- package/types/ui/PrimerScreen.d.ts.map +1 -1
- package/types/ui/RedemptionScreen.d.ts +15 -19
- package/types/ui/RedemptionScreen.d.ts.map +1 -1
- package/types/ui/RewardActions.d.ts +26 -0
- package/types/ui/RewardActions.d.ts.map +1 -1
- package/types/ui/brandComponents.d.ts.map +1 -1
- package/types/ui/styles.d.ts.map +1 -1
- package/types/utils/logger.d.ts +11 -1
- package/types/utils/logger.d.ts.map +1 -1
- package/embed/index.html +0 -57
- package/embed/styles.css +0 -154
- package/examples/ios-webview/EncoreWebViewBridge.swift +0 -419
- package/examples/ios-webview/ExampleViewController.swift +0 -233
|
@@ -1,396 +1,117 @@
|
|
|
1
|
-
# Encore iOS
|
|
1
|
+
# Encore on iOS without the native SDK
|
|
2
2
|
|
|
3
|
-
|
|
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
|
-
|
|
6
|
-
|
|
7
|
-
|
|
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
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
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
|
-
|
|
18
|
+
## Setup
|
|
22
19
|
|
|
23
|
-
Copy `
|
|
20
|
+
Copy `EncoreURLBuilder.swift` into your Xcode project, plus
|
|
21
|
+
`SimpleEncoreView.swift` if you want the SwiftUI wrapper. No dependencies.
|
|
24
22
|
|
|
25
|
-
|
|
23
|
+
## UIKit
|
|
26
24
|
|
|
27
25
|
```swift
|
|
28
26
|
import UIKit
|
|
29
27
|
import WebKit
|
|
30
28
|
|
|
31
|
-
|
|
32
|
-
|
|
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
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
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
|
-
|
|
41
|
+
## SwiftUI
|
|
140
42
|
|
|
141
43
|
```swift
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
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
|
-
|
|
182
|
-
|
|
183
|
-
You can pass user attributes for better targeting:
|
|
60
|
+
## Parameters
|
|
184
61
|
|
|
185
|
-
|
|
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
|
-
|
|
64
|
+
Optional: `environment` (`production`, `development`, `staging`, `localhost`),
|
|
65
|
+
`useCase` (`rewardUsers` by default, or `reduceChurn`), `placement`,
|
|
66
|
+
`autoPresentOffer`, `headline`, `subheadline`.
|
|
210
67
|
|
|
211
|
-
|
|
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
|
-
|
|
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
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
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
|
-
|
|
221
|
-
|
|
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
|
-
|
|
224
|
-
func presentOffer()
|
|
88
|
+
## Knowing when a user earned something
|
|
225
89
|
|
|
226
|
-
|
|
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
|
-
|
|
245
|
-
|
|
246
|
-
|
|
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
|
-
##
|
|
100
|
+
## What to expect when a user claims
|
|
257
101
|
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
3
|
+
// Encore hosted offer page, SwiftUI wrapper
|
|
4
4
|
//
|
|
5
|
-
//
|
|
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
|
-
///
|
|
12
|
+
/// Displays the Encore hosted offer page.
|
|
12
13
|
///
|
|
13
14
|
/// Usage:
|
|
14
15
|
/// ```swift
|
|
15
16
|
/// EncoreWebView(
|
|
16
|
-
/// apiKey: "
|
|
17
|
-
/// userId:
|
|
18
|
-
/// attributes: ["
|
|
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
|
-
|
|
27
|
-
|
|
38
|
+
|
|
28
39
|
public init(
|
|
29
40
|
apiKey: String,
|
|
30
41
|
userId: String,
|
|
31
42
|
environment: String = "production",
|
|
32
|
-
|
|
33
|
-
|
|
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
|
-
|
|
44
|
-
.
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
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
|
-
|
|
72
|
-
|
|
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: "
|
|
98
|
+
apiKey: "pk_test_example",
|
|
83
99
|
userId: "preview_user",
|
|
84
|
-
environment: "localhost",
|
|
85
100
|
attributes: [
|
|
86
|
-
"
|
|
87
|
-
"
|
|
88
|
-
]
|
|
89
|
-
debug: true
|
|
101
|
+
"subscriptionTier": "free",
|
|
102
|
+
"countryCode": "US",
|
|
103
|
+
]
|
|
90
104
|
)
|
|
91
105
|
}
|
|
92
106
|
}
|
|
93
107
|
#endif
|
|
94
|
-
|