Skip to content

Commit 4eeda93

Browse files
authored
Merge pull request #398 from superwall/develop
4.10.7
2 parents 886c35c + ac89877 commit 4eeda93

24 files changed

Lines changed: 528 additions & 39 deletions

File tree

CHANGELOG.md

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,19 @@
22

33
The changelog for `SuperwallKit`. Also see the [releases](https://github.com/superwall/Superwall-iOS/releases) on GitHub.
44

5+
## 4.10.7
6+
7+
### Enhancements
8+
9+
- Adds support for `Set user attributes` action.
10+
- Adds new `SuperwallDelegate` method called `userAttributesDidChange` that notifies you when user attributes change from an external source.
11+
12+
### Fixes
13+
14+
- Fixes a crash caused by a race condition when accessing JSON dictionaries concurrently.
15+
- Fixes issue returning the `PurchaseResult` from `Superwall.shared.purchase(_:)` when using StoreKit 1 inside a `PurchaseController`.
16+
- Fixes `handleDeepLink` returning true for non-Superwall URLs when called before configuration completes.
17+
518
## 4.10.6
619

720
### Fixes

Examples/Advanced/Advanced/SuperwallAdvancedApp.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ struct SuperwallAdvancedApp: App {
2222

2323
// MARK: - Option 1: Let Superwall handle everything
2424
Superwall.configure(apiKey: apiKey)
25-
25+
2626
// MARK: - Option 2: Use a Purchase Controller with StoreKit
2727
/*
2828
// Step 1 - Create your Purchase Controller

Sources/SuperwallKit/Config/Models/FeatureFlags.swift

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ struct FeatureFlags: Codable, Equatable {
2222
var enableMultiplePaywallUrls: Bool
2323
var enableConfigRefresh: Bool
2424
var enableTextInteraction: Bool
25+
var enableIframeNavigation: Bool
2526

2627
enum CodingKeys: String, CodingKey {
2728
case toggles
@@ -43,6 +44,7 @@ struct FeatureFlags: Codable, Equatable {
4344
enableMultiplePaywallUrls = rawFeatureFlags.value(forKey: "enable_multiple_paywall_urls", default: false)
4445
enableConfigRefresh = rawFeatureFlags.value(forKey: "enable_config_refresh_v2", default: false)
4546
enableTextInteraction = rawFeatureFlags.value(forKey: "enable_text_interaction", default: false)
47+
enableIframeNavigation = rawFeatureFlags.value(forKey: "enable_iframe_navigation", default: false)
4648
}
4749

4850
func encode(to encoder: Encoder) throws {
@@ -57,7 +59,8 @@ struct FeatureFlags: Codable, Equatable {
5759
RawFeatureFlag(key: "enable_none_scheduling_policy", enabled: enableNoneSchedulingPolicy),
5860
RawFeatureFlag(key: "enable_multiple_paywall_urls", enabled: enableMultiplePaywallUrls),
5961
RawFeatureFlag(key: "enable_config_refresh_v2", enabled: enableConfigRefresh),
60-
RawFeatureFlag(key: "enable_text_interaction", enabled: enableTextInteraction)
62+
RawFeatureFlag(key: "enable_text_interaction", enabled: enableTextInteraction),
63+
RawFeatureFlag(key: "enable_iframe_navigation", enabled: enableIframeNavigation)
6164
]
6265

6366
try container.encode(rawFeatureFlags, forKey: .toggles)
@@ -73,7 +76,8 @@ struct FeatureFlags: Codable, Equatable {
7376
enableMultiplePaywallUrls: Bool,
7477
enableConfigRefresh: Bool,
7578
enableTextInteraction: Bool,
76-
enableCELLogging: Bool
79+
enableCELLogging: Bool,
80+
enableIframeNavigation: Bool
7781
) {
7882
self.enableExpressionParameters = enableExpressionParameters
7983
self.enableUserIdSeed = enableUserIdSeed
@@ -84,6 +88,7 @@ struct FeatureFlags: Codable, Equatable {
8488
self.enableMultiplePaywallUrls = enableMultiplePaywallUrls
8589
self.enableConfigRefresh = enableConfigRefresh
8690
self.enableTextInteraction = enableTextInteraction
91+
self.enableIframeNavigation = enableIframeNavigation
8792
}
8893
}
8994

@@ -111,7 +116,8 @@ extension FeatureFlags: Stubbable {
111116
enableMultiplePaywallUrls: true,
112117
enableConfigRefresh: true,
113118
enableTextInteraction: true,
114-
enableCELLogging: true
119+
enableCELLogging: true,
120+
enableIframeNavigation: true
115121
)
116122
}
117123
}

Sources/SuperwallKit/DeepLinkRouter.swift

Lines changed: 66 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,9 @@ final class DeepLinkRouter {
3838
}
3939

4040
let deepLinkUrl: URL
41-
if url.isSuperwallDeepLink {
41+
let isSuperwallDeepLink = url.isSuperwallDeepLink
42+
43+
if isSuperwallDeepLink {
4244
deepLinkUrl = url.superwallDeepLinkMappedURL
4345

4446
Task { @MainActor in
@@ -52,11 +54,26 @@ final class DeepLinkRouter {
5254
deepLinkUrl = url
5355
}
5456

55-
5657
Task {
5758
await Superwall.shared.track(InternalSuperwallEvent.DeepLink(url: deepLinkUrl))
5859
}
59-
return debugManager.handle(deepLinkUrl: deepLinkUrl)
60+
61+
// Check if this is a debug URL
62+
if debugManager.handle(deepLinkUrl: deepLinkUrl) {
63+
return true
64+
}
65+
66+
// Return true for Superwall deep links (we handled it above)
67+
if isSuperwallDeepLink {
68+
return true
69+
}
70+
71+
// Return true if there's a deepLink_open trigger configured
72+
if configManager.triggersByPlacementName[SuperwallEventObjc.deepLink.description] != nil {
73+
return true
74+
}
75+
76+
return false
6077
}
6178

6279
private func listenToConfig() {
@@ -80,9 +97,54 @@ final class DeepLinkRouter {
8097
}
8198

8299
/// Stores the deep link until it can be handled.
100+
///
101+
/// Called when `handleDeepLink` is invoked before Superwall configuration completes.
102+
/// The URL is always stored so it can be processed once config loads, but the return
103+
/// value indicates whether Superwall will definitely handle this URL.
104+
///
105+
/// - Note: The URL is always stored regardless of return value because the fresh config
106+
/// might have a `deepLink_open` trigger even if cached config doesn't. This ensures
107+
/// deep links aren't lost during app launch. If the URL isn't a Superwall URL,
108+
/// returning `false` allows other handlers in a handler chain to process it.
109+
///
110+
/// - Parameter url: The deep link URL to store.
111+
/// - Returns: `true` if the URL is a Superwall URL that will be handled, `false` otherwise.
83112
static func storeDeepLink(_ url: URL) -> Bool {
113+
// Always store the URL - the fresh config might have deepLink_open trigger
114+
// even if cached config doesn't
84115
pendingDeepLink = url
85-
return true
116+
117+
// Only return true if we're confident Superwall will handle this URL
118+
return isSuperwallURL(url)
119+
}
120+
121+
/// Checks if the URL is one that Superwall will handle.
122+
private static func isSuperwallURL(_ url: URL) -> Bool {
123+
// Superwall universal links (*.superwall.app/app-link/*)
124+
if url.isSuperwallDeepLink {
125+
return true
126+
}
127+
128+
// Redemption codes
129+
if url.redeemableCode != nil {
130+
return true
131+
}
132+
133+
// Debug/preview URLs
134+
if DebugManager.outcomeForDeepLink(url: url) != nil {
135+
return true
136+
}
137+
138+
// Check cached config for deepLink_open trigger
139+
let cache = Cache()
140+
if let config = cache.read(LatestConfig.self) {
141+
let triggers = ConfigLogic.getTriggersByPlacementName(from: config.triggers)
142+
if triggers[SuperwallEventObjc.deepLink.description] != nil {
143+
return true
144+
}
145+
}
146+
147+
return false
86148
}
87149
}
88150

Sources/SuperwallKit/Delegate/SuperwallDelegate.swift

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -153,6 +153,15 @@ public protocol SuperwallDelegate: AnyObject {
153153
from oldValue: CustomerInfo,
154154
to newValue: CustomerInfo
155155
)
156+
157+
/// Called when user attributes change due to a paywall action.
158+
///
159+
/// This is triggered by user interactions within the paywall that update
160+
/// user attributes.
161+
///
162+
/// - Parameter newAttributes: The new merged user attributes after the update.
163+
@MainActor
164+
func userAttributesDidChange(newAttributes: [String: Any])
156165
}
157166

158167
extension SuperwallDelegate {
@@ -202,4 +211,6 @@ extension SuperwallDelegate {
202211
from oldValue: CustomerInfo,
203212
to newValue: CustomerInfo
204213
) {}
214+
215+
public func userAttributesDidChange(newAttributes: [String: Any]) {}
205216
}

Sources/SuperwallKit/Delegate/SuperwallDelegateAdapter.swift

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -189,4 +189,13 @@ final class SuperwallDelegateAdapter {
189189
)
190190
}
191191
}
192+
193+
@MainActor
194+
func userAttributesDidChange(newAttributes: [String: Any]) {
195+
if let swiftDelegate = swiftDelegate {
196+
swiftDelegate.userAttributesDidChange(newAttributes: newAttributes)
197+
} else if let objcDelegate = objcDelegate {
198+
objcDelegate.userAttributesDidChange?(newAttributes: newAttributes)
199+
}
200+
}
192201
}

Sources/SuperwallKit/Delegate/SuperwallDelegateObjc.swift

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -154,4 +154,13 @@ public protocol SuperwallDelegateObjc: AnyObject {
154154
from oldValue: CustomerInfo,
155155
to newValue: CustomerInfo
156156
)
157+
158+
/// Called when user attributes change due to a paywall action.
159+
///
160+
/// This is triggered by user interactions within the paywall that update
161+
/// user attributes.
162+
///
163+
/// - Parameter newAttributes: The new merged user attributes after the update.
164+
@MainActor
165+
@objc optional func userAttributesDidChange(newAttributes: [String: Any])
157166
}

Sources/SuperwallKit/Dependencies/DependencyContainer.swift

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -141,7 +141,13 @@ final class DependencyContainer {
141141
deviceHelper: deviceHelper,
142142
storage: storage,
143143
configManager: configManager,
144-
webEntitlementRedeemer: webEntitlementRedeemer
144+
webEntitlementRedeemer: webEntitlementRedeemer,
145+
// swiftlint:disable:next trailing_closure
146+
notifyUserChange: { [weak self] newAttributes in
147+
Task { @MainActor in
148+
self?.delegateAdapter.userAttributesDidChange(newAttributes: newAttributes)
149+
}
150+
}
145151
)
146152

147153
appSessionManager = AppSessionManager(

Sources/SuperwallKit/Identity/IdentityManager.swift

Lines changed: 30 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -118,17 +118,20 @@ class IdentityManager {
118118
private unowned let storage: Storage
119119
private unowned let configManager: ConfigManager
120120
private unowned let webEntitlementRedeemer: WebEntitlementRedeemer
121+
private let notifyUserChange: (([String: Any]) -> Void)?
121122

122123
init(
123124
deviceHelper: DeviceHelper,
124125
storage: Storage,
125126
configManager: ConfigManager,
126-
webEntitlementRedeemer: WebEntitlementRedeemer
127+
webEntitlementRedeemer: WebEntitlementRedeemer,
128+
notifyUserChange: (([String: Any]) -> Void)? = nil
127129
) {
128130
self.deviceHelper = deviceHelper
129131
self.storage = storage
130132
self.configManager = configManager
131133
self.webEntitlementRedeemer = webEntitlementRedeemer
134+
self.notifyUserChange = notifyUserChange
132135
self._appUserId = storage.get(AppUserId.self)
133136

134137
var extraAttributes: [String: Any] = [:]
@@ -373,17 +376,36 @@ extension IdentityManager {
373376
queue.async { [weak self] in
374377
self?._mergeUserAttributes(
375378
newUserAttributes,
376-
shouldTrackMerge: shouldTrackMerge
379+
shouldTrackMerge: shouldTrackMerge,
380+
shouldNotify: false
381+
)
382+
}
383+
}
384+
385+
/// Merges the attributes and notifies the delegate.
386+
/// This is used when attributes are updated from the paywall.
387+
func mergeUserAttributesAndNotify(
388+
_ newUserAttributes: [String: Any?],
389+
shouldTrackMerge: Bool = true
390+
) {
391+
queue.async { [weak self] in
392+
self?._mergeUserAttributes(
393+
newUserAttributes,
394+
shouldTrackMerge: shouldTrackMerge,
395+
shouldNotify: true
377396
)
378397
}
379398
}
380399

381400
/// Merges the provided user attributes with existing attributes then saves them.
382401
///
383-
/// - Parameter shouldTrackMerge: A boolean indicated whether the merge should be tracked in analytics.
402+
/// - Parameters:
403+
/// - shouldTrackMerge: A boolean indicated whether the merge should be tracked in analytics.
404+
/// - shouldNotify: A boolean indicating whether to notify the delegate of the change.
384405
private func _mergeUserAttributes(
385406
_ newUserAttributes: [String: Any?],
386-
shouldTrackMerge: Bool = true
407+
shouldTrackMerge: Bool = true,
408+
shouldNotify: Bool = false
387409
) {
388410
let mergedAttributes = IdentityLogic.mergeAttributes(
389411
newUserAttributes,
@@ -403,5 +425,9 @@ extension IdentityManager {
403425

404426
storage.save(mergedAttributes, forType: UserAttributes.self)
405427
_userAttributes = mergedAttributes
428+
429+
if shouldNotify {
430+
notifyUserChange?(mergedAttributes)
431+
}
406432
}
407433
}

Sources/SuperwallKit/Misc/Constants.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,5 +18,5 @@ let sdkVersion = """
1818
*/
1919

2020
let sdkVersion = """
21-
4.10.6
21+
4.10.7
2222
"""

0 commit comments

Comments
 (0)