@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,87 +1,205 @@
|
|
|
1
1
|
//
|
|
2
2
|
// EncoreURLBuilder.swift
|
|
3
|
-
// Encore iOS
|
|
3
|
+
// Encore hosted offer page, iOS integration
|
|
4
4
|
//
|
|
5
|
-
//
|
|
5
|
+
// Build a URL, open it in a WKWebView. That is the whole integration:
|
|
6
|
+
// there is no bridge, no message handler, and nothing to call back into.
|
|
7
|
+
//
|
|
8
|
+
// Copy this one file into your project.
|
|
6
9
|
//
|
|
7
10
|
|
|
8
11
|
import Foundation
|
|
9
12
|
|
|
10
|
-
///
|
|
13
|
+
/// Builds URLs for the Encore hosted offer page.
|
|
14
|
+
///
|
|
15
|
+
/// The page's entire configuration surface is its query string, so this type is
|
|
16
|
+
/// a thin, checkable wrapper around it rather than a client of any kind.
|
|
11
17
|
public struct EncoreURL {
|
|
12
|
-
|
|
13
|
-
///
|
|
18
|
+
|
|
19
|
+
/// Which experience the page presents.
|
|
20
|
+
public enum UseCase: String {
|
|
21
|
+
/// The reward sheet. This is the hosted page's default.
|
|
22
|
+
case rewardUsers
|
|
23
|
+
/// The churn carousel, for when you open this from a cancellation flow.
|
|
24
|
+
case reduceChurn
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/// The canonical hosted page.
|
|
28
|
+
///
|
|
29
|
+
/// `/embed` and `/payrange` serve the same page and stay supported, so
|
|
30
|
+
/// builds already shipped against them keep working.
|
|
31
|
+
public static let defaultBaseURL = "https://encorekit.com/offers"
|
|
32
|
+
|
|
33
|
+
/// Targeting keys the page accepts over the query string.
|
|
34
|
+
///
|
|
35
|
+
/// A key outside this set is dropped.
|
|
36
|
+
///
|
|
37
|
+
/// Publisher defined values do NOT go here. Pass them through the separate
|
|
38
|
+
/// `custom:` parameter, which sends them `custom.` prefixed and caps them
|
|
39
|
+
/// at `maxCustomKeys`. Putting an already-prefixed `custom.foo` into
|
|
40
|
+
/// `attributes` does not work: it is not in this set, so it is dropped like
|
|
41
|
+
/// any other unrecognised key.
|
|
42
|
+
///
|
|
43
|
+
/// `email`, `firstName`, `lastName`, `phoneNumber`, `dateOfBirth`,
|
|
44
|
+
/// `gender`, `latitude` and `longitude` are refused on purpose. A query
|
|
45
|
+
/// string is written to webview history and to hosting access logs, which
|
|
46
|
+
/// is no place for a direct identifier. If you need to target on one, ask
|
|
47
|
+
/// Encore for a short lived signed token rather than sending the raw value.
|
|
48
|
+
public static let acceptedAttributes: Set<String> = [
|
|
49
|
+
"countryCode", "postalCode", "city", "state", "language",
|
|
50
|
+
"subscriptionTier", "monthsSubscribed", "billingCycle",
|
|
51
|
+
"lastPaymentAmount", "lastActiveDate", "totalSessions",
|
|
52
|
+
]
|
|
53
|
+
|
|
54
|
+
/// Length cap the builder applies to attribute and `custom.` values.
|
|
55
|
+
///
|
|
56
|
+
/// The page caps every parameter it reads at this length; the builder only
|
|
57
|
+
/// pre-applies it to the targeting values it normalises, so `headline`,
|
|
58
|
+
/// `subheadline` and `userId` go out whole and are cut server side.
|
|
59
|
+
public static let maxValueLength = 256
|
|
60
|
+
|
|
61
|
+
/// The page accepts at most this many `custom.` keys per URL.
|
|
62
|
+
public static let maxCustomKeys = 20
|
|
63
|
+
|
|
64
|
+
/// Wire prefix for publisher defined keys.
|
|
65
|
+
private static let customPrefix = "custom."
|
|
66
|
+
|
|
67
|
+
/// Trim, drop blanks, and truncate, matching what the page does on receipt.
|
|
68
|
+
private static func normalized(_ value: String?) -> String? {
|
|
69
|
+
guard let value else { return nil }
|
|
70
|
+
let trimmed = value.trimmingCharacters(in: .whitespacesAndNewlines)
|
|
71
|
+
guard !trimmed.isEmpty else { return nil }
|
|
72
|
+
return String(trimmed.prefix(maxValueLength))
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/// Build a hosted offer page URL.
|
|
76
|
+
///
|
|
14
77
|
/// - Parameters:
|
|
15
|
-
/// - apiKey: Your Encore
|
|
16
|
-
/// - userId:
|
|
17
|
-
///
|
|
18
|
-
/// -
|
|
19
|
-
/// -
|
|
20
|
-
///
|
|
78
|
+
/// - apiKey: Your publishable Encore key. Required.
|
|
79
|
+
/// - userId: Your identifier for the end user. Required: the page shows
|
|
80
|
+
/// an error state without it rather than a blank screen.
|
|
81
|
+
/// - environment: `production`, `development`, `staging` or `localhost`.
|
|
82
|
+
/// - useCase: Omit to take the page's own default, which is `rewardUsers`.
|
|
83
|
+
/// - placement: Omit to let the page derive it from `useCase`, which
|
|
84
|
+
/// keeps the reward and churn surfaces separable in reporting.
|
|
85
|
+
/// - autoPresentOffer: Pass `false` to load the page without presenting.
|
|
86
|
+
/// - headline: Overrides the default headline.
|
|
87
|
+
/// - subheadline: Overrides the default subheadline.
|
|
88
|
+
/// - attributes: Flat targeting values. See `acceptedAttributes`.
|
|
89
|
+
/// - custom: Publisher defined values, sent as `custom.<key>`. Pass the
|
|
90
|
+
/// bare name or the prefixed one: `plan` and `custom.plan` both go out
|
|
91
|
+
/// as `custom.plan`. Passing both keeps one and names the other in the
|
|
92
|
+
/// DEBUG drop log; release builds drop it without a diagnostic.
|
|
93
|
+
/// - baseURL: Point this at a locally running site to test against it.
|
|
94
|
+
/// - Returns: The URL to load, or `nil` if `baseURL` is not a valid URL.
|
|
21
95
|
public static func build(
|
|
22
96
|
apiKey: String,
|
|
23
|
-
userId: String
|
|
97
|
+
userId: String,
|
|
24
98
|
environment: String = "production",
|
|
25
|
-
|
|
26
|
-
|
|
99
|
+
useCase: UseCase? = nil,
|
|
100
|
+
placement: String? = nil,
|
|
101
|
+
autoPresentOffer: Bool = true,
|
|
102
|
+
headline: String? = nil,
|
|
103
|
+
subheadline: String? = nil,
|
|
104
|
+
attributes: [String: String] = [:],
|
|
105
|
+
custom: [String: String] = [:],
|
|
106
|
+
baseURL: String = defaultBaseURL
|
|
27
107
|
) -> URL? {
|
|
28
|
-
guard var components = URLComponents(string:
|
|
29
|
-
|
|
30
|
-
}
|
|
31
|
-
|
|
108
|
+
guard var components = URLComponents(string: baseURL) else { return nil }
|
|
109
|
+
|
|
32
110
|
var queryItems: [URLQueryItem] = [
|
|
33
111
|
URLQueryItem(name: "apiKey", value: apiKey),
|
|
34
|
-
URLQueryItem(name: "
|
|
112
|
+
URLQueryItem(name: "userId", value: userId),
|
|
113
|
+
URLQueryItem(name: "environment", value: environment),
|
|
35
114
|
]
|
|
36
|
-
|
|
37
|
-
if let
|
|
38
|
-
queryItems.append(URLQueryItem(name: "
|
|
115
|
+
|
|
116
|
+
if let useCase {
|
|
117
|
+
queryItems.append(URLQueryItem(name: "useCase", value: useCase.rawValue))
|
|
39
118
|
}
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
// Encode attributes as JSON string
|
|
43
|
-
if let jsonData = try? JSONSerialization.data(withJSONObject: attributes),
|
|
44
|
-
let jsonString = String(data: jsonData, encoding: .utf8) {
|
|
45
|
-
queryItems.append(URLQueryItem(name: "attributes", value: jsonString))
|
|
46
|
-
}
|
|
119
|
+
if let placement {
|
|
120
|
+
queryItems.append(URLQueryItem(name: "placement", value: placement))
|
|
47
121
|
}
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
122
|
+
// The page presents unless told not to, so only the opt out is worth
|
|
123
|
+
// sending. Spelling out the default would just make the URL longer.
|
|
124
|
+
if !autoPresentOffer {
|
|
125
|
+
queryItems.append(URLQueryItem(name: "autoPresentOffer", value: "false"))
|
|
51
126
|
}
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
/// Build URL for localhost testing
|
|
58
|
-
public static func buildLocal(
|
|
59
|
-
apiKey: String,
|
|
60
|
-
userId: String? = nil,
|
|
61
|
-
port: Int = 3001,
|
|
62
|
-
attributes: [String: String]? = nil
|
|
63
|
-
) -> URL? {
|
|
64
|
-
var components = URLComponents(string: "http://localhost:\(port)/embed/")!
|
|
65
|
-
|
|
66
|
-
var queryItems: [URLQueryItem] = [
|
|
67
|
-
URLQueryItem(name: "apiKey", value: apiKey),
|
|
68
|
-
URLQueryItem(name: "environment", value: "localhost"),
|
|
69
|
-
URLQueryItem(name: "debug", value: "true")
|
|
70
|
-
]
|
|
71
|
-
|
|
72
|
-
if let userId = userId {
|
|
73
|
-
queryItems.append(URLQueryItem(name: "userId", value: userId))
|
|
127
|
+
if let headline {
|
|
128
|
+
queryItems.append(URLQueryItem(name: "headline", value: headline))
|
|
129
|
+
}
|
|
130
|
+
if let subheadline {
|
|
131
|
+
queryItems.append(URLQueryItem(name: "subheadline", value: subheadline))
|
|
74
132
|
}
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
133
|
+
|
|
134
|
+
// One parameter per attribute. An earlier version of this file packed
|
|
135
|
+
// them into a single JSON `attributes=` value, which the page ignores.
|
|
136
|
+
//
|
|
137
|
+
// The allowlist is enforced HERE rather than left to the page, and that
|
|
138
|
+
// is the whole point of it: a refused key is refused because the URL is
|
|
139
|
+
// itself the exposure. Once `email` has been written into a webview's
|
|
140
|
+
// history and a hosting access log, the page dropping it server side
|
|
141
|
+
// does not unwrite it.
|
|
142
|
+
var dropped: [String] = []
|
|
143
|
+
|
|
144
|
+
for key in attributes.keys.sorted() {
|
|
145
|
+
guard acceptedAttributes.contains(key) else {
|
|
146
|
+
dropped.append(key)
|
|
147
|
+
continue
|
|
148
|
+
}
|
|
149
|
+
// A blank value removes targeting just as surely as a refused
|
|
150
|
+
// key does, so it joins `dropped` rather than vanishing silently.
|
|
151
|
+
guard let value = normalized(attributes[key]) else {
|
|
152
|
+
dropped.append(key)
|
|
153
|
+
continue
|
|
80
154
|
}
|
|
155
|
+
queryItems.append(URLQueryItem(name: key, value: value))
|
|
81
156
|
}
|
|
82
|
-
|
|
157
|
+
|
|
158
|
+
var customCount = 0
|
|
159
|
+
var claimedCustomNames = Set<String>()
|
|
160
|
+
for key in custom.keys.sorted() {
|
|
161
|
+
// Tolerate a key the caller already prefixed. `plan` and
|
|
162
|
+
// `custom.plan` must both go out as `custom.plan`, never
|
|
163
|
+
// `custom.custom.plan`, which the page would ignore.
|
|
164
|
+
let bare = key.hasPrefix(customPrefix)
|
|
165
|
+
? String(key.dropFirst(customPrefix.count))
|
|
166
|
+
: key
|
|
167
|
+
let wireName = customPrefix + bare
|
|
168
|
+
|
|
169
|
+
guard !bare.isEmpty else {
|
|
170
|
+
dropped.append(key)
|
|
171
|
+
continue
|
|
172
|
+
}
|
|
173
|
+
// Because both spellings collapse to one wire name, a caller who
|
|
174
|
+
// passes `plan` AND `custom.plan` would otherwise emit the
|
|
175
|
+
// parameter twice and spend two of the 20 slots on it. First in
|
|
176
|
+
// sorted order wins; the loser is named so the clash is visible.
|
|
177
|
+
guard claimedCustomNames.insert(wireName).inserted else {
|
|
178
|
+
dropped.append(key)
|
|
179
|
+
continue
|
|
180
|
+
}
|
|
181
|
+
guard customCount < maxCustomKeys else {
|
|
182
|
+
dropped.append(wireName)
|
|
183
|
+
continue
|
|
184
|
+
}
|
|
185
|
+
guard let value = normalized(custom[key]) else {
|
|
186
|
+
dropped.append(wireName)
|
|
187
|
+
continue
|
|
188
|
+
}
|
|
189
|
+
queryItems.append(URLQueryItem(name: wireName, value: value))
|
|
190
|
+
customCount += 1
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
// Say what was dropped instead of letting targeting quietly
|
|
194
|
+
// underperform with nothing anywhere explaining why. DEBUG only: a
|
|
195
|
+
// release build drops the same keys without any diagnostic.
|
|
196
|
+
#if DEBUG
|
|
197
|
+
if !dropped.isEmpty {
|
|
198
|
+
print("[Encore] not sent in the offer URL: \(dropped.joined(separator: ", "))")
|
|
199
|
+
}
|
|
200
|
+
#endif
|
|
201
|
+
|
|
83
202
|
components.queryItems = queryItems
|
|
84
203
|
return components.url
|
|
85
204
|
}
|
|
86
205
|
}
|
|
87
|
-
|