@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
@@ -1,87 +1,205 @@
1
1
  //
2
2
  // EncoreURLBuilder.swift
3
- // Encore iOS Simple Integration
3
+ // Encore hosted offer page, iOS integration
4
4
  //
5
- // Dead simple: Build a URL, open it in WKWebView. That's it.
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
- /// Build Encore embed URLs with your configuration
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
- /// Build an Encore embed URL
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 API key
16
- /// - userId: User identifier (optional)
17
- /// - environment: "production" or "localhost" (default: "production")
18
- /// - attributes: User attributes for targeting (optional)
19
- /// - debug: Enable debug mode (default: false)
20
- /// - Returns: URL to load in WKWebView
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? = nil,
97
+ userId: String,
24
98
  environment: String = "production",
25
- attributes: [String: String]? = nil,
26
- debug: Bool = false
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: "https://encorekit.com/embed/") else {
29
- return nil
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: "environment", value: environment)
112
+ URLQueryItem(name: "userId", value: userId),
113
+ URLQueryItem(name: "environment", value: environment),
35
114
  ]
36
-
37
- if let userId = userId {
38
- queryItems.append(URLQueryItem(name: "userId", value: userId))
115
+
116
+ if let useCase {
117
+ queryItems.append(URLQueryItem(name: "useCase", value: useCase.rawValue))
39
118
  }
40
-
41
- if let attributes = attributes, !attributes.isEmpty {
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
- if debug {
50
- queryItems.append(URLQueryItem(name: "debug", value: "true"))
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
- components.queryItems = queryItems
54
- return components.url
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
- if let attributes = attributes, !attributes.isEmpty {
77
- if let jsonData = try? JSONSerialization.data(withJSONObject: attributes),
78
- let jsonString = String(data: jsonData, encoding: .utf8) {
79
- queryItems.append(URLQueryItem(name: "attributes", value: jsonString))
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
-