Skip to content

Commit cbdaff4

Browse files
authored
Add audience property logic (#119)
* Added support for real-time audiences segmentation in placements based on user properties * Fixed a bug that appeared only in 3.4.0 and could lead to incorrect audiences segmentation in placements based on store country.
1 parent 8b0a05f commit cbdaff4

File tree

7 files changed

+95
-74
lines changed

7 files changed

+95
-74
lines changed

ApphudSDK.podspec

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
Pod::Spec.new do |s|
22
s.name = 'ApphudSDK'
3-
s.version = '3.4.0'
3+
s.version = '3.5.0'
44
s.summary = 'Build and Measure In-App Subscriptions on iOS.'
55
s.description = 'Apphud covers every aspect when it comes to In-App Subscriptions from integration to analytics on iOS and Android.'
66
s.homepage = 'https://github.com/apphud/ApphudSDK'

Sources/Internal/ApphudInternal+Currency.swift

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,7 @@ extension ApphudInternal {
4141
private func fetchCurrencyWithMaxTimeout(_ completion: @escaping () -> Void) {
4242

4343
Task {
44-
let result: Storefront? = nil //await Storefront.current
44+
let result: Storefront? = await Storefront.current
4545
if let store = result, await currentUser?.currency?.countryCodeAlpha3 != store.countryCode {
4646

4747
storefrontCurrency = ApphudCurrency(countryCode: store.countryCode,
@@ -62,7 +62,7 @@ extension ApphudInternal {
6262

6363
// Task for the timeout
6464
Task {
65-
try? await Task.sleep(nanoseconds: 200_000_000) // 0.2 seconds
65+
try? await Task.sleep(nanoseconds: 300_000_000) // 0.3 seconds
6666
if !currencyTaskFinished {
6767
completion()
6868
}

Sources/Internal/ApphudInternal+Product.swift

Lines changed: 30 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -150,12 +150,16 @@ extension ApphudInternal {
150150
callback?([], error)
151151
}
152152
} else {
153-
ApphudStoreKitWrapper.shared.status = .none
154-
Task.detached { @MainActor in
155-
self.performWhenStoreKitProductFetched(maxAttempts: maxAttempts) { error in
156-
apphudPerformOnMainThread { callback?(ApphudStoreKitWrapper.shared.products, error) }
157-
}
158-
}
153+
refreshStoreKitProductsOnly(maxAttempts: maxAttempts, callback: callback)
154+
}
155+
}
156+
}
157+
158+
internal func refreshStoreKitProductsOnly(maxAttempts: Int, callback: (([SKProduct], Error?) -> Void)?) {
159+
ApphudStoreKitWrapper.shared.status = .none
160+
Task.detached { @MainActor in
161+
self.performWhenStoreKitProductFetched(maxAttempts: maxAttempts) { error in
162+
apphudPerformOnMainThread { callback?(ApphudStoreKitWrapper.shared.products, error) }
159163
}
160164
}
161165
}
@@ -253,18 +257,31 @@ extension ApphudInternal {
253257
internal func fetchOfferingsFull(maxAttempts: Int = APPHUD_DEFAULT_RETRIES, callback: @escaping (ApphudError?) -> Void) {
254258
let preparedAttempts = min(max(1, maxAttempts), 10)
255259
self.customRegistrationAttemptsCount = preparedAttempts
256-
performWhenUserRegistered(allowFailure: true) {
257-
if (self.currentUser == nil) {
258-
apphudLog("Failed to register user with error: \(self.userRegisterRetries.errorCode)", forceDisplay: true)
260+
261+
if deferPlacements {
262+
deferPlacements = false
263+
didPreparePaywalls = false
264+
refreshCurrentUser {
265+
self.refreshStoreKitProductsOnly(maxAttempts: maxAttempts) { _, error in
266+
callback(error != nil ? ApphudError(error: error!) : nil)
267+
}
259268
}
260-
self.performWhenStoreKitProductFetched(maxAttempts: preparedAttempts) { error in
261-
if self.paywallsLoadTime == 0 {
262-
self.paywallsLoadTime = Date().timeIntervalSince(self.initDate)
269+
} else {
270+
performWhenUserRegistered(allowFailure: true) {
271+
if (self.currentUser == nil) {
272+
apphudLog("Failed to register user with error: \(self.userRegisterRetries.errorCode)", forceDisplay: true)
273+
}
274+
self.performWhenStoreKitProductFetched(maxAttempts: preparedAttempts) { error in
275+
if self.paywallsLoadTime == 0 {
276+
self.paywallsLoadTime = Date().timeIntervalSince(self.initDate)
277+
}
278+
callback(error)
263279
}
264-
callback(error)
265280
}
266281
}
267282
}
283+
284+
268285

269286
// MARK: - Product Groups Helper Methods
270287

Sources/Internal/ApphudInternal+Purchase.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -318,7 +318,7 @@ extension ApphudInternal {
318318
if params["placement_id"] == nil && placement != nil {
319319
params["placement_id"] = placement?.id
320320
}
321-
paywall = placement?.paywalls.first(where: {$0.identifier == observerModePurchaseIdentifiers?.paywall})
321+
paywall = placement?.paywalls.first
322322
} else {
323323
paywall = await paywalls.first(where: {$0.identifier == observerModePurchaseIdentifiers?.paywall})
324324
}

Sources/Internal/ApphudInternal+UserUpdate.swift

Lines changed: 22 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -192,8 +192,8 @@ extension ApphudInternal {
192192
params["device_id"] = self.currentDeviceID
193193
params["is_debug"] = apphudIsSandbox()
194194
params["is_new"] = isFreshInstall && currentUser == nil
195-
params["need_paywalls"] = !didPreparePaywalls
196-
params["need_placements"] = !didPreparePaywalls
195+
params["need_paywalls"] = !didPreparePaywalls && !deferPlacements
196+
params["need_placements"] = !didPreparePaywalls && !deferPlacements
197197
params["opt_out"] = ApphudUtils.shared.optOutOfTracking
198198

199199
if params["user_id"] == nil, let userId = currentUser?.userId {
@@ -236,8 +236,13 @@ extension ApphudInternal {
236236
}
237237

238238
@objc internal func updateCurrentUser() {
239+
refreshCurrentUser {}
240+
}
241+
242+
@objc internal func refreshCurrentUser(completion: @escaping () -> Void) {
239243
Task.detached(priority: .userInitiated) {
240-
await self.createOrGetUser(initialCall: false)
244+
_ = await self.createOrGetUser(initialCall: false)
245+
completion()
241246
}
242247
}
243248

@@ -312,19 +317,26 @@ extension ApphudInternal {
312317
apphudLog("Invalid increment property type: (\(givenType)). Must be one of: [Int, Float, Double]", forceDisplay: true)
313318
return
314319
}
320+
321+
Task { @MainActor in
322+
let property = ApphudUserProperty(key: key.key, value: value, increment: increment, setOnce: setOnce, type: typeString)
323+
await ApphudDataActor.shared.addPendingUserProperty(property)
324+
}
315325

316326
performWhenUserRegistered {
317327
Task { @MainActor in
318-
let property = ApphudUserProperty(key: key.key, value: value, increment: increment, setOnce: setOnce, type: typeString)
319-
await ApphudDataActor.shared.addPendingUserProperty(property)
320328
self.setNeedsToUpdateUserProperties = true
321329
}
322330
}
323331
}
324332

325333
@objc internal func updateUserProperties() {
334+
flushUserProperties(force: false, completion: nil)
335+
}
336+
337+
internal func flushUserProperties(force: Bool, completion: ((Bool) -> Void)? = nil) {
326338
Task {
327-
let values = await self.preparePropertiesParams()
339+
let values = await self.preparePropertiesParams(isAudience: force)
328340
guard let params = values.0, let properties = values.1 else {
329341
return
330342
}
@@ -342,15 +354,18 @@ extension ApphudInternal {
342354
} else {
343355
apphudLog("User Properties update failed: \(error?.localizedDescription ?? "") with code: \(code)")
344356
}
357+
358+
completion?(result)
345359
}
346360
}
347361
}
348362

349-
private func preparePropertiesParams() async -> ([String: Any]?, [[String: Any?]]?, Bool) {
363+
private func preparePropertiesParams(isAudience:Bool = false) async -> ([String: Any]?, [[String: Any?]]?, Bool) {
350364
setNeedsToUpdateUserProperties = false
351365
guard await ApphudDataActor.shared.pendingUserProps.count > 0 else { return (nil, nil, false) }
352366
var params = [String: Any]()
353367
params["device_id"] = self.currentDeviceID
368+
params["force"] = isAudience
354369

355370
var canSaveToCache = true
356371
var properties = [[String: Any?]]()

Sources/Internal/ApphudInternal.swift

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -112,6 +112,7 @@ final class ApphudInternal: NSObject {
112112
internal var productsFetchRetries: ApphudRetryLog = (0, 0)
113113
internal let maxNumberOfProductsFetchRetries: Int = 25
114114
internal var didPreparePaywalls: Bool = false
115+
internal var deferPlacements: Bool = false
115116
internal var initDate = Date()
116117
internal var paywallsLoadTime: TimeInterval = 0
117118
internal var isRegisteringUser = false {

Sources/Public/Apphud.swift

Lines changed: 38 additions & 50 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ import Foundation
1414
import UserNotifications
1515
import SwiftUI
1616

17-
internal let apphud_sdk_version = "3.4.0"
17+
internal let apphud_sdk_version = "3.5.0"
1818

1919
// MARK: - Initialization
2020

@@ -189,61 +189,25 @@ final public class Apphud: NSObject {
189189
callback(ApphudInternal.shared.placements, error)
190190
}
191191
}
192-
192+
193193
/**
194-
Asynchronously retrieves the paywalls configured in Product Hub > Paywalls, potentially altered based on the user's involvement in A/B testing, if any. Awaits until the inner `SKProduct`s are loaded from the App Store.
195-
196-
- Important: In case of network issues this method may return empty array. To get the possible error use `paywallsDidLoadCallback` method instead.
194+
Disables automatic paywall and placement requests during the SDK's initial setup. Developers must explicitly call `fetchPlacements` or `await placements()` methods at a later point in the app's lifecycle to fetch placements with inner paywalls.
197195

198-
For immediate access without awaiting `SKProduct`s, use `rawPaywalls()` method.
199-
- parameter maxAttempts: Number of request attempts before throwing an error. Must be between 1 and 10. Default value is 3.
196+
Example:
200197

201-
- Important: This is deprecated method. Retrieve paywalls from within placements instead. See documentation for details: https://docs.apphud.com/docs/paywalls
202-
203-
- Returns: An array of `ApphudPaywall` objects, representing the configured paywalls.
204-
*/
205-
@available(*, deprecated, message: "Deprecated in favor of placements()")
206-
@MainActor
207-
@objc public static func paywalls(maxAttempts: Int = APPHUD_DEFAULT_RETRIES) async -> [ApphudPaywall] {
208-
await withCheckedContinuation { continuation in
209-
ApphudInternal.shared.fetchOfferingsFull(maxAttempts: maxAttempts) { error in
210-
continuation.resume(returning: ApphudInternal.shared.paywalls)
211-
}
198+
```swift
199+
Apphud.start(apiKey: "your_api_key")
200+
Apphud.deferPlacements()
201+
...
202+
Apphud.fetchPlacements { placements in
203+
// Handle fetched placements
212204
}
213-
}
214-
215-
/**
216-
A list of paywalls, potentially altered based on the user's involvement in A/B testing, if any.
217-
218-
- Important: This function doesn't await until inner `SKProduct`s are loaded from the App Store. That means paywalls may or may not have inner StoreKit products at the time you call this function.
219-
220-
- Important: This function will return empty array if user is not yet loaded, or placements are not set up in the Product Hub.
221-
222-
To get paywalls with awaiting for StoreKit products, use await Apphud.paywalls() or
223-
Apphud.paywallsDidLoadCallback(...) functions.
224-
225-
- Returns: An array of `ApphudPaywall` objects, representing the configured paywalls.
226-
*/
227-
@MainActor public static func rawPaywalls() -> [ApphudPaywall] {
228-
ApphudInternal.shared.paywalls
229-
}
230-
231-
/**
232-
Asynchronously retrieve a specific paywall by identifier configured in Product Hub > Paywalls, potentially altered based on the user's involvement in A/B testing, if any. Awaits until the inner `SKProduct`s are loaded from the App Store.
233-
234-
For immediate access without awaiting `SKProduct`s, use `ApphudDelegate`'s `userDidLoad` method or the callback in `Apphud.start(...)`.
205+
```
235206

236-
- Important: In case of network issues this method may return empty array. To get the possible error use `paywallsDidLoadCallback` method instead.
237-
238-
- Important: This is deprecated method. Retrieve paywalls from within placements instead. See documentation for details: https://docs.apphud.com/docs/placements
239-
240-
- parameter identifier: The unique identifier for the desired paywall.
241-
- Returns: An optional `ApphudPaywall` object if found, or `nil` if no matching paywall is found.
207+
Note: You can use this method alongside `forceFlushUserProperties` to achieve real-time user segmentation based on custom user properties.
242208
*/
243-
@available(*, deprecated, message: "Deprecated in favor of placement(_ identifier: String)")
244-
@MainActor
245-
@objc public static func paywall(_ identifier: String) async -> ApphudPaywall? {
246-
await paywalls().first(where: { $0.identifier == identifier })
209+
public static func deferPlacements() {
210+
ApphudInternal.shared.deferPlacements = true
247211
}
248212

249213
/**
@@ -677,6 +641,30 @@ final public class Apphud: NSObject {
677641
@objc public static func setUserProperty(key: ApphudUserPropertyKey, value: Any?, setOnce: Bool = false) {
678642
ApphudInternal.shared.setUserProperty(key: key, value: value, setOnce: setOnce, increment: false)
679643
}
644+
645+
/**
646+
This method sends all user properties immediately to Apphud. Should be used for audience segmentation in placements based on user properties.
647+
648+
Example:
649+
````swift
650+
Apphud.start(apiKey: "api_key")
651+
Apphud.deferPlacements()
652+
653+
Apphud.setUserProperty(key: .init("key_name"), value: "key_value")
654+
655+
Apphud.forceFlushUserProperties { done in
656+
// now placements will respect user properties that have been sent previously
657+
Apphud.fetchPlacements { placements, error in
658+
}
659+
}
660+
```
661+
*/
662+
663+
public static func forceFlushUserProperties(completion: @escaping (Bool) -> Void) {
664+
ApphudInternal.shared.performWhenUserRegistered {
665+
ApphudInternal.shared.flushUserProperties(force: true, completion: completion)
666+
}
667+
}
680668

681669
/**
682670

0 commit comments

Comments
 (0)