Skip to content

Commit 8a734cc

Browse files
authored
Add new delegate methods, add screenName and ruleName, fix bug with user_id, add error to restore purchases method (#13)
what's new in 0.8.5: * Added new ApphudUIDelegate method `func apphudShouldPerformRule(rule: ApphudRule) -> Bool`. See Apphud.swift for method explanation. * (Attention!) Renamed ApphudUIDelegate method to `func apphudShouldShowScreen(screenName: String) -> Bool`. Don't forget to update this method parameter, if you were using it. * Added Adjust support. * Renamed `func restoreSubscriptions(callback: @escaping ([ApphudSubscription]?, Error?) -> Void)`. Added `Error?` parameter. * Improved logic for `func migrateSubscriptionsIfNeeded` method. * Pretty printed logs. * Fixed bug with user id being changed sometimes. * User ID is now saved to Keychain, so after re-install it is restored. * Fixed bug with push opened event not being sent in some cases. * Add rule name and screen name to delegate methods. * Add more logs.
1 parent 68e7e35 commit 8a734cc

File tree

17 files changed

+155
-104
lines changed

17 files changed

+155
-104
lines changed

ApphudSDK.podspec

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,7 @@
11
Pod::Spec.new do |s|
22
s.name = 'ApphudSDK'
3-
s.version = '0.8'
4-
s.summary = 'Track and control iOS auto-renewable subscriptions.'
5-
3+
s.version = '0.8.5'
4+
s.summary = 'Track and control iOS auto-renewable subscriptions.'
65
s.description = 'Track, control and analyze iOS auto-renewable subscriptions with Apphud.'
76
s.homepage = 'https://github.com/apphud/ApphudSDK'
87
s.license = { :type => 'MIT', :file => 'LICENSE' }

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -144,7 +144,7 @@ Apphud.subscription()
144144
If your app doesn't have a login system, which identifies a premium user by his credentials, then you need a restore mechanism. If you already have a restore purchases mechanism by calling `SKPaymentQueue.default().restoreCompletedTransactions()`, then you have nothing to worry about – Apphud SDK will automatically intercept and send latest App Store Receipt to Apphud servers when your restoration is completed. However, better to call our restore method from SDK:
145145

146146
```swift
147-
Apphud.restoreSubscriptions{ subscriptions in
147+
Apphud.restoreSubscriptions{ subscriptions, error in
148148
// handle here
149149
}
150150
```

Sources/ApphudSDK/Apphud.swift

Lines changed: 14 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -54,11 +54,16 @@ public typealias ApphudBoolCallback = ((Bool) -> Void)
5454
@objc public protocol ApphudUIDelegate {
5555

5656
/**
57-
You can return `false` to this delegate method if you don't want Apphud Screen to be displayed at this time.
57+
You can return `false` to ignore this rule. You should only do this if you want to handle your rules by yourself. Default implementation is `true`.
58+
*/
59+
@objc optional func apphudShouldPerformRule(rule: ApphudRule) -> Bool
60+
61+
/**
62+
You can return `false` to this delegate method if you don't want to delay Apphud Screen presentation.
5863

59-
If you returned `false`, this controller will be accessible in `Apphud.pendingScreen()` method. You will be able to present it manually later.
64+
Controller will be kept in memory until you present it via `Apphud.showPendingScreen()` method. If you don't want to show screen at all, you should check `apphudShouldPerformRule` delegate method.
6065
*/
61-
@objc optional func apphudShouldShowScreen(controller: UIViewController) -> Bool
66+
@objc optional func apphudShouldShowScreen(screenName: String) -> Bool
6267

6368
/**
6469
Return `UIViewController` instance from which you want to present given Apphud controller. If you don't implement this method, then top visible viewcontroller from key window will be used.
@@ -100,7 +105,7 @@ public typealias ApphudBoolCallback = ((Bool) -> Void)
100105
/// List of available attribution providers
101106
@objc public enum ApphudAttributionProvider : Int {
102107
case appsFlyer
103-
108+
case adjust
104109
/**
105110
Branch is implemented and doesn't require any additional code from Apphud SDK
106111
More details: https://docs.apphud.com/integrations/attribution/branch
@@ -266,7 +271,7 @@ final public class Apphud: NSObject {
266271
* To migrate existing subsribers to Apphud. If you want your current subscribers to be tracked in Apphud, call this method once at the first launch.
267272
- parameter callback: Required. Returns array of subscription (or subscriptions in case you more than one subscription group). Returns nil if user never purchased a subscription.
268273
*/
269-
@objc public static func restoreSubscriptions(callback: @escaping ([ApphudSubscription]?) -> Void) {
274+
@objc public static func restoreSubscriptions(callback: @escaping ([ApphudSubscription]?, Error?) -> Void) {
270275
ApphudInternal.shared.restoreSubscriptions(callback: callback)
271276
}
272277

@@ -286,8 +291,10 @@ final public class Apphud: NSObject {
286291
*/
287292
@objc public static func migrateSubscriptionsIfNeeded(callback: @escaping ([ApphudSubscription]?) -> Void) {
288293
if apphudShouldMigrate() {
289-
ApphudInternal.shared.restoreSubscriptions { subscriptions in
290-
apphudDidMigrate()
294+
ApphudInternal.shared.restoreSubscriptions { (subscriptions, error) in
295+
if error == nil {
296+
apphudDidMigrate()
297+
}
291298
callback(subscriptions)
292299
}
293300
}

Sources/ApphudSDK/ApphudHttpClient.swift

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -165,15 +165,17 @@ public class ApphudHttpClient {
165165

166166
let code = httpResponse.statusCode
167167
if code >= 200 && code < 300 {
168-
169-
if data != nil {
170-
let stringResponse = String(data: data!, encoding: .utf8)
171-
apphudLog("Request \(method) \(request.url?.absoluteString ?? "") success with response: \n\(stringResponse ?? "")")
168+
169+
if let dictionary = dictionary,
170+
let json = try? JSONSerialization.data(withJSONObject: dictionary, options: .prettyPrinted),
171+
let string = String(data: json, encoding: .utf8){
172+
apphudLog("Request \(method) \(request.url?.absoluteString ?? "") success with response: \n\(string)")
172173
}
174+
173175
callback?(true, dictionary, nil)
174176
return
175177
}
176-
apphudLog("Request \(method) \(request.url?.absoluteString ?? "") failed with code \(code), error: \(error?.localizedDescription ?? "") response: \(dictionary ?? [:])")
178+
apphudLog("Request \(method) \(request.url?.absoluteString ?? "") failed with code \(code), error: \(error?.localizedDescription ?? "") response: \(dictionary ?? [:])", forceDisplay: true)
177179
}
178180

179181
callback?(false, nil, error)

Sources/ApphudSDK/ApphudInternal.swift

Lines changed: 20 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ import Foundation
1010
import AdSupport
1111
import StoreKit
1212

13-
let sdk_version = "0.8"
13+
let sdk_version = "0.8.5"
1414

1515
@available(iOS 11.2, *)
1616
final class ApphudInternal {
@@ -41,7 +41,7 @@ final class ApphudInternal {
4141

4242
private var submitReceiptCallback : ((Error?) -> Void)?
4343

44-
private var restoreSubscriptionCallback : (([ApphudSubscription]?) -> Void)?
44+
private var restoreSubscriptionCallback : (([ApphudSubscription]?, Error?) -> Void)?
4545

4646
private var allowInitialize = true
4747

@@ -71,15 +71,22 @@ final class ApphudInternal {
7171
self.httpClient.apiKey = apiKey
7272

7373
self.currentUser = ApphudUser.fromCache()
74+
let userIDFromKeychain = ApphudKeychain.loadUserID()
7475

7576
if userID != nil {
7677
self.currentUserID = userID!
7778
} else if let existingUserID = self.currentUser?.user_id {
7879
self.currentUserID = existingUserID
80+
} else if userIDFromKeychain != nil{
81+
self.currentUserID = userIDFromKeychain!
7982
} else {
8083
self.currentUserID = ApphudKeychain.generateUUID()
8184
}
8285

86+
if self.currentUserID != userIDFromKeychain {
87+
ApphudKeychain.saveUserID(userID: self.currentUserID)
88+
}
89+
8390
self.productsGroupsMap = fromUserDefaultsCache(key: "productsGroupsMap")
8491

8592
continueToRegisteringUser()
@@ -147,6 +154,8 @@ final class ApphudInternal {
147154

148155
let minCheckInterval :Double = 30
149156

157+
ApphudRulesManager.shared.handlePendingAPSInfo()
158+
150159
if Date().timeIntervalSince(lastCheckDate) > minCheckInterval{
151160
self.checkForUnreadNotifications()
152161
self.refreshCurrentUser()
@@ -186,6 +195,7 @@ final class ApphudInternal {
186195
guard let userID = self.currentUser?.user_id else {return}
187196
if self.currentUserID != userID {
188197
self.currentUserID = userID
198+
ApphudKeychain.saveUserID(userID: self.currentUserID)
189199
if tellDelegate {
190200
self.delegate?.apphudDidChangeUserID?(userID)
191201
}
@@ -349,7 +359,7 @@ final class ApphudInternal {
349359

350360
//MARK:- Main Purchase and Submit Receipt methods
351361

352-
internal func restoreSubscriptions(callback: @escaping ([ApphudSubscription]?) -> Void) {
362+
internal func restoreSubscriptions(callback: @escaping ([ApphudSubscription]?, Error?) -> Void) {
353363
self.restoreSubscriptionCallback = callback
354364
self.submitReceiptRestore(allowsReceiptRefresh: true)
355365
}
@@ -371,15 +381,15 @@ final class ApphudInternal {
371381
ApphudStoreKitWrapper.shared.refreshReceipt()
372382
} else {
373383
apphudLog("App Store receipt is missing on device and couldn't be refreshed.", forceDisplay: true)
374-
self.restoreSubscriptionCallback?(nil)
384+
self.restoreSubscriptionCallback?(nil, nil)
375385
self.restoreSubscriptionCallback = nil
376386
}
377387
return
378388
}
379389

380390
let exist = performWhenUserRegistered {
381391
self.submitReceipt(receiptString: receiptString, notifyDelegate: true) { error in
382-
self.restoreSubscriptionCallback?(self.currentUser?.subscriptions)
392+
self.restoreSubscriptionCallback?(self.currentUser?.subscriptions, error)
383393
self.restoreSubscriptionCallback = nil
384394
}
385395
}
@@ -748,7 +758,9 @@ final class ApphudInternal {
748758
let params = ["device_id": self.currentDeviceID] as [String : String]
749759
self.httpClient.startRequest(path: "notifications", apiVersion: .v2, params: params, method: .get, callback: { (result, response, error) in
750760

751-
if result, let dataDict = response?["data"] as? [String : Any], let notifArray = dataDict["results"] as? [[String : Any]], let notifDict = notifArray.first, let ruleDict = notifDict["rule"] as? [String : Any] {
761+
if result, let dataDict = response?["data"] as? [String : Any], let notifArray = dataDict["results"] as? [[String : Any]], let notifDict = notifArray.first, var ruleDict = notifDict["rule"] as? [String : Any] {
762+
let properties = notifDict["properties"] as? [String : Any]
763+
ruleDict = ruleDict.merging(properties ?? [:], uniquingKeysWith: {old, new in new})
752764
let rule = ApphudRule(dictionary: ruleDict)
753765
ApphudRulesManager.shared.handleRule(rule: rule)
754766
}
@@ -777,6 +789,8 @@ final class ApphudInternal {
777789
}
778790
params["appsflyer_id"] = identifer
779791
params["appsflyer_data"] = data
792+
} else if provider == .adjust {
793+
params["adjust_data"] = data
780794
}
781795

782796
self.httpClient.startRequest(path: "customers/attribution", params: params, method: .post) { (result, response, error) in

Sources/ApphudSDK/ApphudKeychain.swift

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ import Security
1313
let userAccount = "ApphudUser"
1414
let accessGroup = "SecuritySerivice"
1515
let deviceIdKey : NSString = "ApphudDeviceID"
16+
let userIdKey : NSString = "ApphudUserID"
1617

1718
// Arguments for the keychain queries
1819
let kSecClassValue = NSString(format: kSecClass)
@@ -39,6 +40,14 @@ internal class ApphudKeychain: NSObject {
3940
self.save(deviceIdKey, data: deviceID)
4041
}
4142

43+
internal class func loadUserID() -> String? {
44+
return self.load(userIdKey)
45+
}
46+
47+
internal class func saveUserID(userID : String) {
48+
self.save(userIdKey, data: userID)
49+
}
50+
4251
private class func save(_ service: NSString, data: String) {
4352
if let dataFromString = data.data(using: .utf8, allowLossyConversion: false) {
4453

Sources/ApphudSDK/ApphudRule.swift

Lines changed: 20 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -8,11 +8,27 @@
88

99
import UIKit
1010

11-
struct ApphudRule {
12-
var id: String
13-
var screen_id: String
14-
init(dictionary: [String : Any]) {
11+
public class ApphudRule: NSObject {
12+
13+
/**
14+
Rule name that is visible in Apphud Rules dashboard
15+
*/
16+
@objc public let rule_name: String
17+
/**
18+
Screen name that is visible in Apphud Screens dashboard
19+
*/
20+
@objc public let screen_name: String
21+
22+
internal let id: String
23+
internal let screen_id: String
24+
25+
// MARK:- Private methods
26+
27+
/// Subscription private initializer
28+
init(dictionary : [String : Any]) {
1529
id = dictionary["id"] as? String ?? ""
1630
screen_id = dictionary["screen_id"] as? String ?? ""
31+
rule_name = dictionary["rule_name"] as? String ?? ""
32+
screen_name = dictionary["screen_name"] as? String ?? ""
1733
}
1834
}

Sources/ApphudSDK/ApphudUI/ApphudRulesManager.swift

Lines changed: 34 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,8 @@ internal class ApphudRulesManager {
1818

1919
private var handledRules = [String]()
2020

21+
private var apsInfo : [AnyHashable : Any]?
22+
2123
@discardableResult internal func handleNotification(_ apsInfo: [AnyHashable : Any]) -> Bool{
2224

2325
guard let rule_id = apsInfo["rule_id"] as? String else {
@@ -28,25 +30,42 @@ internal class ApphudRulesManager {
2830
return true
2931
}
3032

31-
apphudLog("handle APS: \(apsInfo as AnyObject)")
33+
self.apsInfo = apsInfo
34+
self.handledRules.append(rule_id)
35+
36+
if UIApplication.shared.applicationState == .active {
37+
self.handlePendingAPSInfo()
38+
} else {
39+
// do nothing, because ApphudInternal will call once app is active
40+
apphudLog("Got APS info, but app is not yet active, waiting for app to be active, then will handle push notification.")
41+
}
42+
43+
return true
44+
}
45+
46+
@objc internal func handlePendingAPSInfo(){
47+
48+
guard let rule_id = apsInfo?["rule_id"] as? String else {
49+
return
50+
}
51+
52+
apphudLog("handle push notification: \(apsInfo as AnyObject)")
3253

3354
ApphudInternal.shared.trackEvent(params: ["rule_id" : rule_id, "name" : "$push_opened"]) {}
3455

35-
if let screen_id = apsInfo["screen_id"] as? String {
36-
handleRule(ruleID: rule_id, screenID: screen_id)
56+
if apsInfo?["screen_id"] != nil {
57+
handleRule(ruleID: rule_id, data: apsInfo as? [String : Any])
3758
}
3859

39-
handledRules.append(rule_id)
60+
self.apsInfo = nil
4061
// allow handling the same push notification rule after 5 seconds. This is needed for testing rules from Apphud dashboard
4162
DispatchQueue.main.asyncAfter(deadline: .now() + 5) {
4263
self.handledRules.removeAll()
4364
}
44-
45-
return true
4665
}
4766

48-
internal func handleRule(ruleID: String, screenID: String){
49-
let dict = ["id" : ruleID, "screen_id" : screenID]
67+
internal func handleRule(ruleID: String, data: [String : Any]?){
68+
let dict = ["id" : ruleID].merging(data ?? [:], uniquingKeysWith: {old, new in new})
5069
let rule = ApphudRule(dictionary: dict)
5170
self.handleRule(rule: rule)
5271
}
@@ -55,6 +74,10 @@ internal class ApphudRulesManager {
5574

5675
guard self.pendingController == nil else { return }
5776
guard rule.screen_id.count > 0 else { return }
77+
guard ApphudInternal.shared.uiDelegate?.apphudShouldPerformRule?(rule: rule) ?? true else {
78+
apphudLog("apphudShouldPerformRule returned false for rule \(rule.rule_name), exiting")
79+
return
80+
}
5881

5982
let controller = ApphudScreenController(rule: rule, screenID: rule.screen_id) {_ in}
6083
controller.loadScreenPage()
@@ -63,8 +86,10 @@ internal class ApphudRulesManager {
6386
nc.setNavigationBarHidden(true, animated: false)
6487
self.pendingController = nc
6588

66-
if ApphudInternal.shared.uiDelegate?.apphudShouldShowScreen?(controller: nc) ?? true {
89+
if ApphudInternal.shared.uiDelegate?.apphudShouldShowScreen?(screenName: rule.screen_name) ?? true {
6790
showPendingScreen()
91+
} else {
92+
apphudLog("apphudShouldShowScreen returned false for screen \(rule.screen_name), exiting")
6893
}
6994
}
7095

Sources/ApphudSDK/ApphudUI/ApphudScreenController.swift

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -374,7 +374,7 @@ class ApphudScreenController: UIViewController{
374374
isPurchasing = true
375375
self.startLoading()
376376

377-
ApphudInternal.shared.uiDelegate?.apphudWillPurchase?(product: product, offerID: offerID!, screenName: self.screen?.name ?? "unknown")
377+
ApphudInternal.shared.uiDelegate?.apphudWillPurchase?(product: product, offerID: offerID!, screenName: self.rule.screen_name)
378378

379379
ApphudInternal.shared.purchasePromo(product: product, discountID: offerID!) { (subscription, transaction, error) in
380380
self.handlePurchaseResult(product: product, offerID: offerID!, subscription: subscription, transaction: transaction, error: error)
@@ -390,7 +390,7 @@ class ApphudScreenController: UIViewController{
390390
isPurchasing = true
391391
self.startLoading()
392392

393-
ApphudInternal.shared.uiDelegate?.apphudWillPurchase?(product: product, offerID: nil, screenName: self.screen?.name ?? "unknown")
393+
ApphudInternal.shared.uiDelegate?.apphudWillPurchase?(product: product, offerID: nil, screenName: self.rule.screen_name)
394394

395395
ApphudInternal.shared.purchase(product: product) { (subscription, transaction, error) in
396396
self.handlePurchaseResult(product: product, subscription: subscription, transaction: transaction, error: error)
@@ -425,7 +425,7 @@ class ApphudScreenController: UIViewController{
425425

426426
private func restoreTapped(){
427427
self.startLoading()
428-
Apphud.restoreSubscriptions { subscriptions in
428+
Apphud.restoreSubscriptions { subscriptions, error in
429429
self.stopLoading()
430430
if subscriptions?.first?.isActive() ?? false {
431431
self.dismiss()
@@ -642,7 +642,7 @@ extension ApphudScreenController {
642642

643643
ApphudInternal.shared.trackEvent(params: params) {}
644644

645-
ApphudInternal.shared.uiDelegate?.apphudDidPurchase?(product: product, offerID: offerID, screenName: self.screen?.name ?? "unknown")
645+
ApphudInternal.shared.uiDelegate?.apphudDidPurchase?(product: product, offerID: offerID, screenName: self.rule.screen_name)
646646

647647
dismiss() // dismiss only when purchase is successful
648648

@@ -653,10 +653,10 @@ extension ApphudScreenController {
653653
// if error occurred, restore subscriptions
654654
if !(errorCode == .paymentCancelled) {
655655
// maybe remove?
656-
Apphud.restoreSubscriptions { subscriptions in }
656+
Apphud.restoreSubscriptions { subscriptions, error in }
657657
}
658658

659-
ApphudInternal.shared.uiDelegate?.apphudDidFailPurchase?(product: product, offerID: offerID, errorCode: errorCode, screenName: self.screen?.name ?? "unknown")
659+
ApphudInternal.shared.uiDelegate?.apphudDidFailPurchase?(product: product, offerID: offerID, errorCode: errorCode, screenName: self.rule.screen_name)
660660
}
661661
}
662662

0 commit comments

Comments
 (0)