Skip to content

Commit f5f4e37

Browse files
authored
Non renewing purchases support (#14)
* Support for consumable, non-consumable and non-renewing subscription in-app purchases. * Apple Search Ads attribution support * Added many new methods to SDK. * Added new class `ApphudNonRenewingPurchase` which contains product identifier, purchase date and and optional cancellation date. * Apphud SDK now tries to re-register user and re-fetch products when application becomes active if there were errors on app launch; for example, if there was no Internet connection.
1 parent e184497 commit f5f4e37

15 files changed

+479
-216
lines changed

.swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata

Lines changed: 7 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

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 = '0.8.5'
3+
s.version = '0.9.0'
44
s.summary = 'Track and control iOS auto-renewable subscriptions.'
55
s.description = 'Track, control and analyze iOS auto-renewable subscriptions with Apphud.'
66
s.homepage = 'https://github.com/apphud/ApphudSDK'

Sources/ApphudSDK/Apphud.swift

Lines changed: 86 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -20,12 +20,15 @@ public typealias ApphudBoolCallback = ((Bool) -> Void)
2020
/**
2121
Returns array of subscriptions that user ever purchased. Empty array means user never purchased a subscription. If you have just one subscription group in your app, you will always receive just one subscription in an array.
2222

23-
This method is called when any subscription in an array has been changed (for example, status changed from `trial` to `expired`).
24-
25-
In most cases you don't need this method because you already have completion blocks in `purchase`, `purchasePromo` and `submitReceipt` methods. However this method may be useful to detect whether subscription was purchased in Apphud's puchase screen.
23+
This method is called when subscription is purchased or updated (for example, status changed from `trial` to `expired` or `isAutorenewEnabled` changed to `false`). SDK also checks for subscription updates when app becomes active.
2624
*/
2725
@objc optional func apphudSubscriptionsUpdated(_ subscriptions: [ApphudSubscription])
2826

27+
/**
28+
Called when any of non renewing purchases changes. Called when purchase is made or has been refunded.
29+
*/
30+
@objc optional func apphudNonRenewingPurchasesUpdated(_ purchases: [ApphudNonRenewingPurchase])
31+
2932
/**
3033
Called when user ID has been changed. Use this if you implement integrations with Analytics services.
3134

@@ -46,7 +49,7 @@ public typealias ApphudBoolCallback = ((Bool) -> Void)
4649
/**
4750
Returns array of `SKProduct` objects after they are fetched from StoreKit. Note that you have to add all product identifiers in Apphud.
4851

49-
You can use this delegate method or observe for `Apphud.didFetchProductsNotification()` notification.
52+
You can use `productsDidFetchCallback` callback or observe for `didFetchProductsNotification()` or implement `apphudDidFetchStoreKitProducts` delegate method. Use whatever you like most.
5053
*/
5154
@objc optional func apphudDidFetchStoreKitProducts(_ products: [SKProduct])
5255
}
@@ -106,6 +109,7 @@ public typealias ApphudBoolCallback = ((Bool) -> Void)
106109
@objc public enum ApphudAttributionProvider : Int {
107110
case appsFlyer
108111
case adjust
112+
case appleSearchAds
109113
/**
110114
Branch is implemented and doesn't require any additional code from Apphud SDK
111115
More details: https://docs.apphud.com/integrations/attribution/branch
@@ -167,16 +171,34 @@ final public class Apphud: NSObject {
167171
/**
168172
This notification is sent when SKProducts are fetched from StoreKit. Note that you have to add all product identifiers in Apphud.
169173

170-
You can observe for this notification or implement `apphudDidFetchStoreKitProducts` delegate method.
174+
You can use `productsDidFetchCallback` callback or observe for `didFetchProductsNotification()` or implement `apphudDidFetchStoreKitProducts` delegate method. Use whatever you like most.
171175
*/
172176
@objc public static func didFetchProductsNotification() -> Notification.Name {
173177
return Notification.Name("ApphudDidFetchProductsNotification")
174178
}
175179

180+
/**
181+
This callback is called when SKProducts are fetched from StoreKit. Note that you have to add all product identifiers in Apphud.
182+
183+
You can use `productsDidFetchCallback` callback or observe for `didFetchProductsNotification()` or implement `apphudDidFetchStoreKitProducts` delegate method. Use whatever you like most.
184+
*/
185+
@objc public static func productsDidFetchCallback(_ callback: @escaping ([SKProduct]) -> Void) {
186+
ApphudStoreKitWrapper.shared.customProductsFetchedBlock = callback
187+
}
188+
189+
/**
190+
Refreshes SKProducts from the App Store. You have to add all product identifiers in Apphud.
191+
192+
__Note__: You shouldn't call this method at app launch, because Apphud SDK automatically fetches products during initialization. Only use this method as a fallback.
193+
*/
194+
@objc public static func refreshStoreKitProducts(_ callback: (([SKProduct]) -> Void)?) {
195+
ApphudInternal.shared.refreshStoreKitProductsWithCallback(callback: callback)
196+
}
197+
176198
/**
177199
Returns array of `SKProduct` objects that you added in Apphud.
178200

179-
Note that this method will return `nil` if products are not yet fetched. You should observe for `Apphud.didFetchProductsNotification()` notification or implement `apphudDidFetchStoreKitProducts` delegate method.
201+
Note that this method will return `nil` if products are not yet fetched. You should observe for `Apphud.didFetchProductsNotification()` notification or implement `apphudDidFetchStoreKitProducts` delegate method or set `productsDidFetchCallback` block.
180202
*/
181203
@objc public static func products() -> [SKProduct]? {
182204
guard ApphudStoreKitWrapper.shared.products.count > 0 else {
@@ -188,7 +210,7 @@ final public class Apphud: NSObject {
188210
/**
189211
Returns `SKProduct` object by product identifier. Note that you have to add this product identifier in Apphud.
190212

191-
Will retun `nil` if product is not yet fetched from StoreKit.
213+
Will return `nil` if product is not yet fetched from StoreKit.
192214
*/
193215
@objc public static func product(productIdentifier : String) -> SKProduct? {
194216
return ApphudStoreKitWrapper.shared.products.first(where: {$0.productIdentifier == productIdentifier})
@@ -197,38 +219,42 @@ final public class Apphud: NSObject {
197219
/**
198220
Purchases product and automatically submits App Store Receipt to Apphud.
199221

200-
__Note__: This method automatically sends in-app purchase receipt to Apphud, so you don't need to call `submitReceipt` method.
222+
__Note__: You are not required to purchase product using Apphud SDK methods. You can purchase subscription or any in-app purchase using your own code. App Store receipt will be sent to Apphud anyway.
201223

202-
- parameter product: Required. This is an `SKProduct` object that user wants to purchase.
203-
- parameter callback: Optional. Returns `ApphudSubscription` object if succeeded and an optional error otherwise.
224+
- parameter product: Required. This is an `SKProduct` object that user wants to purchase.
225+
- parameter callback: Optional. Returns `ApphudPurchaseResult` object.
204226
*/
205-
@objc public static func purchase(_ product: SKProduct, callback: ((ApphudSubscription?, Error?) -> Void)?){
227+
@objc public static func purchase(_ product: SKProduct, callback: ((ApphudPurchaseResult) -> Void)?){
206228
ApphudInternal.shared.purchase(product: product, callback: callback)
207229
}
208230

231+
/**
232+
Purchases product and automatically submits App Store Receipt to Apphud. This method doesn't wait until Apphud validates receipt from Apple and immediately returns transaction object. This method may be useful if you don't care about receipt validation in callback.
233+
234+
__Note__: You are not required to purchase product using Apphud SDK methods. You can purchase subscription or any in-app purchase using your own code. App Store receipt will be sent to Apphud anyway.
235+
236+
- parameter product: Required. This is an `SKProduct` object that user wants to purchase.
237+
- parameter callback: Optional. Returns optional `SKPaymentTransaction` object and an optional error.
238+
*/
239+
@objc public static func purchaseWithoutValidation(_ product: SKProduct, callback: ((SKPaymentTransaction, Error?) -> Void)?){
240+
ApphudInternal.shared.purchaseWithoutValidation(product: product, callback: callback)
241+
}
242+
209243
/**
210244
Purchases subscription (promotional) offer and automatically submits App Store Receipt to Apphud.
211245

212246
__Note__: This method automatically sends in-app purchase receipt to Apphud, so you don't need to call `submitReceipt` method.
213247

214248
- parameter product: Required. This is an `SKProduct` object that user wants to purchase.
215249
- parameter discountID: Required. This is a `SKProductDiscount` Identifier String object that you would like to apply.
216-
- parameter callback: Optional. Returns `ApphudSubscription` object if succeeded and an optional error otherwise.
250+
- parameter callback: Optional. Returns `ApphudPurchaseResult` object.
217251
*/
218252
@available(iOS 12.2, *)
219-
@objc public static func purchasePromo(_ product: SKProduct, discountID: String, _ callback: ((ApphudSubscription?, Error?) -> Void)?){
253+
@objc public static func purchasePromo(_ product: SKProduct, discountID: String, _ callback: ((ApphudPurchaseResult) -> Void)?){
220254
ApphudInternal.shared.purchasePromo(product: product, discountID: discountID, callback: callback)
221255
}
222256

223-
/**
224-
__Deprecated__. Starting now Apphud SDK automatically tracks all your in-app purchases and submits App Store receipt to Apphud. You can safely remove this method from your code. If you were using callback, you can use `apphudSubscriptionsUpdated` delegate method instead.
225-
*/
226-
@available(*, deprecated, message: "Starting now Apphud SDK automatically tracks all your in-app purchases and submits App Store receipt to Apphud. You can safely remove this method from your code. If you were using callback, you can use `apphudSubscriptionsUpdated` delegate method instead.")
227-
@objc public static func submitReceipt(_ productIdentifier : String, _ callback : ((ApphudSubscription?, Error?) -> Void)?) {
228-
ApphudInternal.shared.submitReceipt(productId: productIdentifier, callback: callback)
229-
}
230-
231-
//MARK:- Handle Subscriptions
257+
//MARK:- Handle Purchases
232258

233259
/**
234260
Returns true if user has active subscription.
@@ -261,6 +287,22 @@ final public class Apphud: NSObject {
261287
return ApphudInternal.shared.currentUser?.subscriptions
262288
}
263289

290+
/**
291+
Returns an array of all standard in-app purchases (consumables, nonconsumables or nonrenewing subscriptions) that this user has ever purchased. Purchases are cached on device. This array is sorted by purchase date. Apphud only tracks consumables if they were purchased after integrating Apphud SDK.
292+
*/
293+
@objc public static func nonRenewingPurchases() -> [ApphudNonRenewingPurchase]? {
294+
return ApphudInternal.shared.currentUser?.purchases
295+
}
296+
297+
/**
298+
Returns `true` if current user has purchased standard in-app purchase with given product identifier. Returns `false` if this product is refunded or never purchased. Includes consumables, nonconsumables or non-renewing subscriptions. Apphud only tracks consumables if they were purchased after integrating Apphud SDK.
299+
300+
__Note__: Purchases are sorted by purchase date, so it returns Bool value for the most recent purchase by given product identifier.
301+
*/
302+
@objc public static func isNonRenewingPurchaseActive(productIdentifier : String) -> Bool {
303+
return ApphudInternal.shared.currentUser?.purchases.first(where: {$0.productId == productIdentifier})?.isActive() ?? false
304+
}
305+
264306
/**
265307
Implements `Restore Purchases` mechanism. Basically it just sends current App Store Receipt to Apphud and returns subscriptions info.
266308

@@ -269,33 +311,33 @@ final public class Apphud: NSObject {
269311
You should use this method in 2 cases:
270312
* Upon tap on `Restore Purchases` button in your UI.
271313
* 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.
272-
- 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.
314+
- parameter callback: Required. Returns array of subscription (or subscriptions in case you have more than one subscription group), array of standard in-app purchases and an error. All of three parameters are optional.
273315
*/
274-
@objc public static func restoreSubscriptions(callback: @escaping ([ApphudSubscription]?, Error?) -> Void) {
275-
ApphudInternal.shared.restoreSubscriptions(callback: callback)
316+
@objc public static func restorePurchases(callback: @escaping ([ApphudSubscription]?, [ApphudNonRenewingPurchase]?, Error?) -> Void) {
317+
ApphudInternal.shared.restorePurchases(callback: callback)
276318
}
277319

278320
/**
279-
If you already have a live app with paying users and you want Apphud to track their subscriptions, you should import their App Store receipts into Apphud. Call this method at launch of your app for your paying users. This method should be used only to migrate existing paying users that are not yet tracked by Apphud.
321+
If you already have a live app with paying users and you want Apphud to track their purchases, you should import their App Store receipts into Apphud. Call this method at launch of your app for your paying users. This method should be used only to migrate existing paying users that are not yet tracked by Apphud.
280322

281323
Example:
282324

283325
````
284326
// hasPurchases - is your own boolean value indicating that current user is paying user.
285327
if hasPurchases {
286-
Apphud.migrateSubscriptionsIfNeeded {_ in}
328+
Apphud.migratePurchasesIfNeeded { _, _, _ in}
287329
}
288330
````
289331

290332
__Note__: You can remove this method after a some period of time, i.e. when you are sure that all paying users are already synced with Apphud.
291333
*/
292-
@objc public static func migrateSubscriptionsIfNeeded(callback: @escaping ([ApphudSubscription]?) -> Void) {
334+
@objc public static func migratePurchasesIfNeeded(callback: @escaping ([ApphudSubscription]?, [ApphudNonRenewingPurchase]?, Error?) -> Void) {
293335
if apphudShouldMigrate() {
294-
ApphudInternal.shared.restoreSubscriptions { (subscriptions, error) in
336+
ApphudInternal.shared.restorePurchases { (subscriptions, purchases, error) in
295337
if error == nil {
296338
apphudDidMigrate()
297339
}
298-
callback(subscriptions)
340+
callback(subscriptions, purchases, error)
299341
}
300342
}
301343
}
@@ -409,6 +451,20 @@ final public class Apphud: NSObject {
409451
ApphudUtils.enableDebugLogs()
410452
}
411453

454+
/**
455+
Automatically finishes all pending transactions. By default, Apphud SDK only finishes transactions, that were started by Apphud SDK, i.e. by calling any of `Apphud.purchase..()` methods.
456+
457+
However, when you debug in-app purchases and/or change Apple ID too often, some transactions may stay in the queue (for example, if you broke execution until transaction is finished). And these transactions will try to finish at every next app launch. In this case you may see a system alert prompting to enter your Apple ID password. To fix this annoying issue, you can add this method.
458+
459+
You may also use this method in production if you don't care about handling pending transactions, for example, downloading Apple hosted content.
460+
For more information read "Finish the transaction" paragraph here: https://developer.apple.com/library/archive/technotes/tn2387/_index.html
461+
462+
__Note__: Only use this method if you know what you are doing. Must be called before Apphud SDK initialization.
463+
*/
464+
@objc public static func setFinishAllTransactions(){
465+
ApphudUtils.shared.finishTransactions = true
466+
}
467+
412468
/**
413469
This method must be called before SDK initialization. Apphud will send all subscription events of current user to your test analytics, if test api keys are set in integrations dashboard.
414470
*/

Sources/ApphudSDK/ApphudHttpClient.swift

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@
88

99
import Foundation
1010

11-
typealias ApphudBoolDictionaryCallback = (Bool, [String : Any]?, Error?) -> Void
11+
typealias ApphudHTTPResponseCallback = (Bool, [String : Any]?, Error?, Int) -> Void
1212
typealias ApphudStringCallback = (String?, Error?) -> Void
1313
/**
1414
This is Apphud's internal class.
@@ -44,7 +44,7 @@ public class ApphudHttpClient {
4444
return URLSession.init(configuration: config)
4545
}()
4646

47-
internal func startRequest(path: String, apiVersion: ApphudApiVersion = .v1, params: [String : Any]?, method: ApphudHttpMethod, callback: ApphudBoolDictionaryCallback?) {
47+
internal func startRequest(path: String, apiVersion: ApphudApiVersion = .v1, params: [String : Any]?, method: ApphudHttpMethod, callback: ApphudHTTPResponseCallback?) {
4848
if let request = makeRequest(path: path, apiVersion: apiVersion, params: params, method: method) {
4949
start(request: request, callback: callback)
5050
}
@@ -145,7 +145,7 @@ public class ApphudHttpClient {
145145
task.resume()
146146
}
147147

148-
private func start(request: URLRequest, callback: ApphudBoolDictionaryCallback?){
148+
private func start(request: URLRequest, callback: ApphudHTTPResponseCallback?){
149149
let task = session.dataTask(with: request) { (data, response, error) in
150150

151151
var dictionary: [String : Any]?
@@ -172,15 +172,15 @@ public class ApphudHttpClient {
172172
apphudLog("Request \(method) \(request.url?.absoluteString ?? "") success with response: \n\(string)")
173173
}
174174

175-
callback?(true, dictionary, nil)
175+
callback?(true, dictionary, nil, code)
176176
return
177177
}
178178
apphudLog("Request \(method) \(request.url?.absoluteString ?? "") failed with code \(code), error: \(error?.localizedDescription ?? "") response: \(dictionary ?? [:])", forceDisplay: true)
179+
callback?(false, nil, error, code)
180+
} else {
181+
callback?(false, nil, error, 0)
179182
}
180-
181-
callback?(false, nil, error)
182-
}
183-
183+
}
184184
}
185185
task.resume()
186186
}

0 commit comments

Comments
 (0)