From 919f2ba0bec2c9604daf8d9c177146dc53e18232 Mon Sep 17 00:00:00 2001 From: Surik Date: Fri, 15 Jan 2021 18:10:03 +0800 Subject: [PATCH 01/17] Added intro eligibility and trial duration logic --- Framework/QonversionFramework.h | 1 + Qonversion.xcodeproj/project.pbxproj | 16 +++ Sources/Qonversion/QNAPIClient.h | 3 + Sources/Qonversion/QNAPIClient.m | 9 ++ Sources/Qonversion/QNAPIConstants.h | 1 + Sources/Qonversion/QNAPIConstants.m | 1 + .../Qonversion/QNIntroEligibility+Protected.h | 19 +++ .../Qonversion/QNIntroEligibility+Protected.m | 13 ++ Sources/Qonversion/QNIntroEligibility.h | 27 ++++ Sources/Qonversion/QNIntroEligibility.m | 53 ++++++++ Sources/Qonversion/QNLaunchResult.h | 4 +- Sources/Qonversion/QNMapper.h | 4 +- Sources/Qonversion/QNMapper.m | 30 +++++ Sources/Qonversion/QNProduct.h | 23 +++- Sources/Qonversion/QNProduct.m | 105 ++++++++++++++- Sources/Qonversion/QNProductCenterManager.h | 1 + Sources/Qonversion/QNProductCenterManager.m | 123 ++++++++++++------ Sources/Qonversion/QNRequestBuilder.h | 1 + Sources/Qonversion/QNRequestBuilder.m | 4 + Sources/Qonversion/QNRequestSerializer.h | 2 + Sources/Qonversion/QNRequestSerializer.m | 22 ++++ Sources/Qonversion/QNStoreKitService.m | 4 + Sources/Qonversion/Qonversion.h | 1 + Sources/Qonversion/Qonversion.m | 4 + 24 files changed, 423 insertions(+), 48 deletions(-) create mode 100644 Sources/Qonversion/QNIntroEligibility+Protected.h create mode 100644 Sources/Qonversion/QNIntroEligibility+Protected.m create mode 100644 Sources/Qonversion/QNIntroEligibility.h create mode 100644 Sources/Qonversion/QNIntroEligibility.m diff --git a/Framework/QonversionFramework.h b/Framework/QonversionFramework.h index 1679f6d2..7dba9562 100644 --- a/Framework/QonversionFramework.h +++ b/Framework/QonversionFramework.h @@ -9,3 +9,4 @@ #import #import #import +#import diff --git a/Qonversion.xcodeproj/project.pbxproj b/Qonversion.xcodeproj/project.pbxproj index 6eb4bce1..8522a9b9 100644 --- a/Qonversion.xcodeproj/project.pbxproj +++ b/Qonversion.xcodeproj/project.pbxproj @@ -94,6 +94,10 @@ 8911823925222C2300EBCDFA /* QNAPIConstants.h in Headers */ = {isa = PBXBuildFile; fileRef = 8911823725222C2300EBCDFA /* QNAPIConstants.h */; }; 8911823A25222C2300EBCDFA /* QNAPIConstants.m in Sources */ = {isa = PBXBuildFile; fileRef = 8911823825222C2300EBCDFA /* QNAPIConstants.m */; }; 893045EC252DE8B500E22F75 /* QNPromoPurchasesDelegate.h in Headers */ = {isa = PBXBuildFile; fileRef = 893045EB252DE8B500E22F75 /* QNPromoPurchasesDelegate.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 896D2D6025B0585B009905A0 /* QNIntroEligibility.h in Headers */ = {isa = PBXBuildFile; fileRef = 896D2D5E25B0585B009905A0 /* QNIntroEligibility.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 896D2D6125B0585B009905A0 /* QNIntroEligibility.m in Sources */ = {isa = PBXBuildFile; fileRef = 896D2D5F25B0585B009905A0 /* QNIntroEligibility.m */; }; + 896D2D6625B06447009905A0 /* QNIntroEligibility+Protected.h in Headers */ = {isa = PBXBuildFile; fileRef = 896D2D6425B06447009905A0 /* QNIntroEligibility+Protected.h */; }; + 896D2D6725B06447009905A0 /* QNIntroEligibility+Protected.m in Sources */ = {isa = PBXBuildFile; fileRef = 896D2D6525B06447009905A0 /* QNIntroEligibility+Protected.m */; }; 89861D2C2501563B00E5D36B /* Qonversion.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 459DAB69243E329F0011ECF3 /* Qonversion.framework */; }; 89861D2D2501563B00E5D36B /* Qonversion.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = 459DAB69243E329F0011ECF3 /* Qonversion.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; 89ADA76C250696A400EB2E54 /* ActivePermissionsTableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 89ADA76B250696A400EB2E54 /* ActivePermissionsTableViewCell.swift */; }; @@ -248,6 +252,10 @@ 8911823725222C2300EBCDFA /* QNAPIConstants.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = QNAPIConstants.h; sourceTree = ""; }; 8911823825222C2300EBCDFA /* QNAPIConstants.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = QNAPIConstants.m; sourceTree = ""; }; 893045EB252DE8B500E22F75 /* QNPromoPurchasesDelegate.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = QNPromoPurchasesDelegate.h; sourceTree = ""; }; + 896D2D5E25B0585B009905A0 /* QNIntroEligibility.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = QNIntroEligibility.h; sourceTree = ""; }; + 896D2D5F25B0585B009905A0 /* QNIntroEligibility.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = QNIntroEligibility.m; sourceTree = ""; }; + 896D2D6425B06447009905A0 /* QNIntroEligibility+Protected.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "QNIntroEligibility+Protected.h"; sourceTree = ""; }; + 896D2D6525B06447009905A0 /* QNIntroEligibility+Protected.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = "QNIntroEligibility+Protected.m"; sourceTree = ""; }; 89ADA769250693C400EB2E54 /* ActivePermissionsViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ActivePermissionsViewController.swift; sourceTree = ""; }; 89ADA76B250696A400EB2E54 /* ActivePermissionsTableViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ActivePermissionsTableViewCell.swift; sourceTree = ""; }; 89EA13AB25A42BB90065BCED /* QNOfferings.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = QNOfferings.h; sourceTree = ""; }; @@ -400,6 +408,10 @@ 45F4C2EA2517E85E00AD8FF0 /* QNPermission.m */, 456ECD41249C76D400D2BC40 /* QNProduct.h */, 456ECD44249C8B2000D2BC40 /* QNProduct.m */, + 896D2D5E25B0585B009905A0 /* QNIntroEligibility.h */, + 896D2D5F25B0585B009905A0 /* QNIntroEligibility.m */, + 896D2D6425B06447009905A0 /* QNIntroEligibility+Protected.h */, + 896D2D6525B06447009905A0 /* QNIntroEligibility+Protected.m */, 89EA13AB25A42BB90065BCED /* QNOfferings.h */, 89EA13AC25A42BB90065BCED /* QNOfferings.m */, 89EA13B725A443070065BCED /* QNOfferings+Protected.h */, @@ -612,6 +624,7 @@ isa = PBXHeadersBuildPhase; buildActionMask = 2147483647; files = ( + 896D2D6025B0585B009905A0 /* QNIntroEligibility.h in Headers */, 89EA13B325A42BC90065BCED /* QNOffering.h in Headers */, 89EA13AD25A42BB90065BCED /* QNOfferings.h in Headers */, 893045EC252DE8B500E22F75 /* QNPromoPurchasesDelegate.h in Headers */, @@ -642,6 +655,7 @@ 458A8CD124BAC5F500637130 /* QNUserDefaultsStorage.h in Headers */, 4572FE1C24BC40F900A17AC4 /* QNProductCenterManager.h in Headers */, 458A8CC124B5E50A00637130 /* QNProduct.h in Headers */, + 896D2D6625B06447009905A0 /* QNIntroEligibility+Protected.h in Headers */, 458A8CD824BAC61E00637130 /* QNRequestSerializer.h in Headers */, 458A8CC824B8407C00637130 /* QNStoreKitService.h in Headers */, 458A8CBE24B4B88F00637130 /* QNStoreKitSugare.h in Headers */, @@ -893,10 +907,12 @@ 459DABFC243E35BC0011ECF3 /* QNMapper.m in Sources */, 456ECD34249B901900D2BC40 /* QNRequestBuilder.m in Sources */, 4572FE2124BC4B5B00A17AC4 /* QNUserPropertiesManager.m in Sources */, + 896D2D6125B0585B009905A0 /* QNIntroEligibility.m in Sources */, 458A8CBF24B4B88F00637130 /* QNStoreKitSugare.m in Sources */, 4531CBC424577D770022C422 /* QNConstants.m in Sources */, 45FFA2D424BDFCE8007EFB8F /* QNAttributionManager.m in Sources */, 458A8CC924B8407C00637130 /* QNStoreKitService.m in Sources */, + 896D2D6725B06447009905A0 /* QNIntroEligibility+Protected.m in Sources */, 459DABF9243E35BC0011ECF3 /* QNKeychain.m in Sources */, 458A8CD224BAC5F500637130 /* QNUserDefaultsStorage.m in Sources */, ); diff --git a/Sources/Qonversion/QNAPIClient.h b/Sources/Qonversion/QNAPIClient.h index 073e34db..a04c8448 100644 --- a/Sources/Qonversion/QNAPIClient.h +++ b/Sources/Qonversion/QNAPIClient.h @@ -24,6 +24,9 @@ NS_ASSUME_NONNULL_BEGIN receipt:(nullable NSString *)receipt completion:(QNAPIClientCompletionHandler)completion; +- (void)checkTrialIntroEligibilityParamsForProducts:(NSArray *)products + completion:(QNAPIClientCompletionHandler)completion; + - (void)properties:(NSDictionary *)properties completion:(QNAPIClientCompletionHandler)completion; - (void)attributionRequest:(QNAttributionProvider)provider diff --git a/Sources/Qonversion/QNAPIClient.m b/Sources/Qonversion/QNAPIClient.m index 5c4eead5..6bb1692e 100644 --- a/Sources/Qonversion/QNAPIClient.m +++ b/Sources/Qonversion/QNAPIClient.m @@ -76,6 +76,15 @@ - (void)purchaseRequestWith:(SKProduct *)product return [self dataTaskWithRequest:request completion:completion]; } +- (void)checkTrialIntroEligibilityParamsForProducts:(NSArray *)products + completion:(QNAPIClientCompletionHandler)completion { + NSDictionary *requestData = [self.requestSerializer introTrialEligibilityDataForProducts:products]; + NSDictionary *resultBody = [self enrichParameters:requestData]; + NSURLRequest *request = [self.requestBuilder makeIntroTrialEligibilityRequestWithData:resultBody]; + + return [self dataTaskWithRequest:request completion:completion]; +} + - (void)properties:(NSDictionary *)properties completion:(QNAPIClientCompletionHandler)completion { NSDictionary *body = [self enrichParameters:@{@"properties": properties}]; NSURLRequest *request = [self.requestBuilder makePropertiesRequestWith:body]; diff --git a/Sources/Qonversion/QNAPIConstants.h b/Sources/Qonversion/QNAPIConstants.h index 0ffcfefe..e2c18aad 100644 --- a/Sources/Qonversion/QNAPIConstants.h +++ b/Sources/Qonversion/QNAPIConstants.h @@ -11,6 +11,7 @@ extern NSString *const kAPIBase; extern NSString *const kInitEndpoint; extern NSString *const kPurchaseEndpoint; +extern NSString *const kProductsEndpoint; extern NSString *const kPropertiesEndpoint; extern NSString *const kAttributionEndpoint; extern NSString *const kStoredRequestsKey; diff --git a/Sources/Qonversion/QNAPIConstants.m b/Sources/Qonversion/QNAPIConstants.m index f96f9dce..793e2622 100644 --- a/Sources/Qonversion/QNAPIConstants.m +++ b/Sources/Qonversion/QNAPIConstants.m @@ -11,6 +11,7 @@ NSString * const kAPIBase = @"https://api.qonversion.io/"; NSString * const kInitEndpoint = @"v1/user/init"; NSString * const kPurchaseEndpoint = @"v1/user/purchase"; +NSString * const kProductsEndpoint = @"v1/products/get"; NSString * const kPropertiesEndpoint = @"v1/properties"; diff --git a/Sources/Qonversion/QNIntroEligibility+Protected.h b/Sources/Qonversion/QNIntroEligibility+Protected.h new file mode 100644 index 00000000..98c7916d --- /dev/null +++ b/Sources/Qonversion/QNIntroEligibility+Protected.h @@ -0,0 +1,19 @@ +// +// QNIntroEligibility+Protected.h +// Qonversion +// +// Created by Surik Sarkisyan on 14.01.2021. +// Copyright © 2021 Qonversion Inc. All rights reserved. +// + +#import "QNIntroEligibility.h" + +NS_ASSUME_NONNULL_BEGIN + +@interface QNIntroEligibility (Protected) + +- (instancetype)initWithStatus:(QNIntroEligibilityStatus)status; + +@end + +NS_ASSUME_NONNULL_END diff --git a/Sources/Qonversion/QNIntroEligibility+Protected.m b/Sources/Qonversion/QNIntroEligibility+Protected.m new file mode 100644 index 00000000..e7c87fad --- /dev/null +++ b/Sources/Qonversion/QNIntroEligibility+Protected.m @@ -0,0 +1,13 @@ +// +// QNIntroEligibility+Protected.m +// Qonversion +// +// Created by Surik Sarkisyan on 14.01.2021. +// Copyright © 2021 Qonversion Inc. All rights reserved. +// + +#import "QNIntroEligibility+Protected.h" + +@implementation QNIntroEligibility (Protected) + +@end diff --git a/Sources/Qonversion/QNIntroEligibility.h b/Sources/Qonversion/QNIntroEligibility.h new file mode 100644 index 00000000..9341c222 --- /dev/null +++ b/Sources/Qonversion/QNIntroEligibility.h @@ -0,0 +1,27 @@ +// +// QNIntroEligibility.h +// Qonversion +// +// Created by Surik Sarkisyan on 14.01.2021. +// Copyright © 2021 Qonversion Inc. All rights reserved. +// + +#import + +typedef NS_ENUM(NSInteger, QNIntroEligibilityStatus) { + QNIntroEligibilityStatusUnknown = 0, + QNIntroEligibilityStatusNonIntroProduct, + QNIntroEligibilityStatusIneligible, + QNIntroEligibilityStatusEligible +} NS_SWIFT_NAME(Qonversion.IntroEligibilityStatus); + +NS_ASSUME_NONNULL_BEGIN + +NS_SWIFT_NAME(Qonversion.IntroEligibility) +@interface QNIntroEligibility : NSObject + +@property (nonatomic, assign, readonly) QNIntroEligibilityStatus status; + +@end + +NS_ASSUME_NONNULL_END diff --git a/Sources/Qonversion/QNIntroEligibility.m b/Sources/Qonversion/QNIntroEligibility.m new file mode 100644 index 00000000..d5da8ccb --- /dev/null +++ b/Sources/Qonversion/QNIntroEligibility.m @@ -0,0 +1,53 @@ +// +// QNIntroEligibility.m +// Qonversion +// +// Created by Surik Sarkisyan on 14.01.2021. +// Copyright © 2021 Qonversion Inc. All rights reserved. +// + +#import "QNIntroEligibility.h" + +@implementation QNIntroEligibility + +- (instancetype)initWithStatus:(QNIntroEligibilityStatus)status { + self = [super init]; + + if (self) { + _status = status; + } + + return self; +} + +- (NSString *)description { + NSMutableString *description = [NSMutableString stringWithFormat:@"<%@: ", NSStringFromClass([self class])]; + + [description appendFormat:@"status=%@ (enum value = %li),\n", [self prettyStatus], (long) self.status]; + [description appendString:@">"]; + + return [description copy]; +} + +- (NSString *)prettyStatus { + NSString *result; + + switch (self.status) { + case QNIntroEligibilityStatusNonIntroProduct: + result = @"non intro product"; break; + + case QNIntroEligibilityStatusIneligible: + result = @"intro ineligible"; break; + + case QNIntroEligibilityStatusEligible: + result = @"intro eligible"; break; + + default: + result = @"unknown"; + break; + } + + return result; +} + +@end diff --git a/Sources/Qonversion/QNLaunchResult.h b/Sources/Qonversion/QNLaunchResult.h index fdc60847..c4f44d08 100644 --- a/Sources/Qonversion/QNLaunchResult.h +++ b/Sources/Qonversion/QNLaunchResult.h @@ -2,7 +2,7 @@ NS_ASSUME_NONNULL_BEGIN -@class QNPermission, QNProduct, QNOfferings; +@class QNPermission, QNProduct, QNOfferings, QNIntroEligibility; typedef NS_ENUM(NSInteger, QNAttributionProvider) { QNAttributionProviderAppsFlyer = 0, @@ -71,6 +71,8 @@ typedef void (^QNRestoreCompletionHandler)(NSDictionary *result, NSError *_Nullable error) NS_SWIFT_NAME(Qonversion.ProductsCompletionHandler); +typedef void (^QNEligibilityCompletionHandler)(NSDictionary *result, NSError *_Nullable error) NS_SWIFT_NAME(Qonversion.EligibilityCompletionHandler); + typedef void (^QNOfferingsCompletionHandler)(QNOfferings *_Nullable offerings, NSError *_Nullable error) NS_SWIFT_NAME(Qonversion.OfferingsCompletionHandler); NS_ASSUME_NONNULL_END diff --git a/Sources/Qonversion/QNMapper.h b/Sources/Qonversion/QNMapper.h index 68bb2363..f188ad89 100644 --- a/Sources/Qonversion/QNMapper.h +++ b/Sources/Qonversion/QNMapper.h @@ -1,6 +1,6 @@ #import -@class QNMapperObject, QNLaunchResult, QNPermission; +@class QNMapperObject, QNLaunchResult, QNPermission, QNProduct, QNIntroEligibility; @interface QNMapper : NSObject @@ -8,6 +8,8 @@ + (QNLaunchResult * _Nonnull)fillLaunchResult:(NSDictionary * _Nullable)dict; ++ (NSDictionary *)mapProductsEligibility:(NSDictionary * _Nullable)dict; + + (NSInteger)mapInteger:(NSObject * _Nullable)object orReturn:(NSInteger)defaultValue; @end diff --git a/Sources/Qonversion/QNMapper.m b/Sources/Qonversion/QNMapper.m index ed9ff3c4..03ea7d60 100644 --- a/Sources/Qonversion/QNMapper.m +++ b/Sources/Qonversion/QNMapper.m @@ -6,10 +6,12 @@ #import "QNMapperObject.h" #import "QNOfferings.h" #import "QNOffering.h" +#import "QNIntroEligibility.h" #import "QNLaunchResult+Protected.h" #import "QNOfferings+Protected.h" #import "QNOffering+Protected.h" +#import "QNIntroEligibility+Protected.h" @implementation QNMapper @@ -63,6 +65,34 @@ + (QNLaunchResult * _Nonnull)fillLaunchResult:(NSDictionary *)dict { return [products copy]; } ++ (NSDictionary *)mapProductsEligibility:(NSDictionary * _Nullable)dict { + NSDictionary *introEligibilityStatuses = @{@"non_intro_or_trial_product": @(QNIntroEligibilityStatusNonIntroProduct), + @"intro_or_trial_eligible": @(QNIntroEligibilityStatusEligible), + @"intro_or_trial_ineligible": @(QNIntroEligibilityStatusIneligible)}; + + NSArray *enrichedProducts = dict[@"products_enriched"]; + + NSMutableDictionary *products = [NSMutableDictionary new]; + + for (NSDictionary *item in enrichedProducts) { + NSDictionary *productData = item[@"product"]; + if (!productData) { + continue; + } + + QNProduct *product = [self fillProduct:productData]; + NSString *eligibilityStatusString = item[@"intro_eligibility_status"]; + + NSNumber *eligibilityValue = introEligibilityStatuses[eligibilityStatusString]; + QNIntroEligibilityStatus eligibilityStatus = eligibilityValue ? eligibilityValue.integerValue : QNIntroEligibilityStatusUnknown; + QNIntroEligibility *eligibility = [[QNIntroEligibility alloc] initWithStatus:eligibilityStatus]; + + products[product.qonversionID] = eligibility; + } + + return [products copy]; +} + + (QNPermission * _Nonnull)fillPermission:(NSDictionary *)dict { QNPermission *result = [[QNPermission alloc] init]; result.permissionID = dict[@"id"]; diff --git a/Sources/Qonversion/QNProduct.h b/Sources/Qonversion/QNProduct.h index 1c8e4209..364bc072 100644 --- a/Sources/Qonversion/QNProduct.h +++ b/Sources/Qonversion/QNProduct.h @@ -1,6 +1,6 @@ #import -typedef NS_ENUM(NSInteger, QNProductType){ +typedef NS_ENUM(NSInteger, QNProductType) { QNProductTypeUnknown = -1, /** Provides access to content on a recurring basis with a free introductory offer @@ -18,7 +18,7 @@ typedef NS_ENUM(NSInteger, QNProductType){ QNProductTypeOneTime = 2 } NS_SWIFT_NAME(Qonversion.ProductType); -typedef NS_ENUM(NSInteger, QNProductDuration){ +typedef NS_ENUM(NSInteger, QNProductDuration) { QNProductDurationUnknown = -1, QNProductDurationWeekly = 0, QNProductDurationMonthly = 1, @@ -28,6 +28,19 @@ typedef NS_ENUM(NSInteger, QNProductDuration){ QNProductDurationLifetime = 5 } NS_SWIFT_NAME(Qonversion.ProductDuration); +typedef NS_ENUM(NSInteger, QNTrialDuration) { + QNTrialDurationNotAvailable = -1, + QNTrialDurationThreeDays = 1, + QNTrialDurationWeek = 2, + QNTrialDurationTwoWeeks = 3, + QNTrialDurationMonth = 4, + QNTrialDurationTwoMonths = 5, + QNTrialDurationThreeMonths = 6, + QNTrialDurationSixMonths = 7, + QNTrialDurationYear = 8, + QNTrialDurationOther = 9 +} NS_SWIFT_NAME(Qonversion.TrialDuration); + NS_SWIFT_NAME(Qonversion.Product) @interface QNProduct : NSObject @@ -48,13 +61,15 @@ NS_SWIFT_NAME(Qonversion.Product) Trial, Subscription or one-time purchase @see [Products types](https://qonversion.io/docs/product-types) */ -@property (nonatomic) QNProductType type; +@property (nonatomic, assign) QNProductType type; /** Product duration @see [Products durations](https://qonversion.io/docs/product-durations) */ -@property (nonatomic) QNProductDuration duration; +@property (nonatomic, assign) QNProductDuration duration; + +@property (nonatomic, assign) QNTrialDuration trialDuration; /** Associated StoreKit Product diff --git a/Sources/Qonversion/QNProduct.m b/Sources/Qonversion/QNProduct.m index 228707de..7421e44d 100644 --- a/Sources/Qonversion/QNProduct.m +++ b/Sources/Qonversion/QNProduct.m @@ -22,6 +22,65 @@ - (void)encodeWithCoder:(NSCoder *)coder { [coder encodeInteger:_duration forKey:NSStringFromSelector(@selector(duration))]; } +- (void)setSkProduct:(SKProduct *)skProduct { + _skProduct = skProduct; +} + +- (QNTrialDuration)trialDuration { + if (_trialDuration) { + return _trialDuration; + } + + QNTrialDuration duration = QNTrialDurationNotAvailable; + + if (@available(iOS 11.2, macOS 10.13.2, watchOS 6.2, *) && self.skProduct.introductoryPrice) { + duration = QNTrialDurationOther; + + SKProductPeriodUnit unit = self.skProduct.introductoryPrice.subscriptionPeriod.unit; + NSUInteger numberOfUnits = self.skProduct.introductoryPrice.subscriptionPeriod.numberOfUnits; + switch (unit) { + case SKProductPeriodUnitDay: + if (numberOfUnits == 3) { + duration = QNTrialDurationThreeDays; + } + break; + + case SKProductPeriodUnitWeek: + if (numberOfUnits == 1) { + duration = QNTrialDurationWeek; + } else if (numberOfUnits == 2) { + duration = QNTrialDurationTwoWeeks; + } + break; + + case SKProductPeriodUnitMonth: + if (numberOfUnits == 1) { + duration = QNTrialDurationMonth; + } else if (numberOfUnits == 2) { + duration = QNTrialDurationTwoMonths; + } else if (numberOfUnits == 3) { + duration = QNTrialDurationThreeMonths; + } else if (numberOfUnits == 6) { + duration = QNTrialDurationSixMonths; + } + break; + + case SKProductPeriodUnitYear: + if (numberOfUnits == 1) { + duration = QNTrialDurationYear; + } + break; + + default: + break; + } + } + + _trialDuration = duration; + + return _trialDuration; +} + - (NSString *)description { NSMutableString *description = [NSMutableString stringWithFormat:@"<%@: ", NSStringFromClass([self class])]; @@ -29,16 +88,13 @@ - (NSString *)description { [description appendFormat:@"storeID=%@,\n", self.storeID]; [description appendFormat:@"type=%@ (enum value = %li),\n", [self prettyType], (long) self.type]; [description appendFormat:@"duration=%@ (enum value = %li),\n", [self prettyDuration], (long) self.duration]; + [description appendFormat:@"trial duration=%@ (enum value = %li),\n", [self prettyTrialDuration], (long) self.trialDuration]; [description appendFormat:@"skProduct=%@,\n", self.skProduct]; [description appendString:@">"]; return [description copy]; } -- (void)setSkProduct:(SKProduct *)skProduct { - _skProduct = skProduct; -} - - (NSString *)prettyPrice { if (_skProduct) { return _skProduct.prettyPrice; @@ -76,6 +132,47 @@ - (NSString *)prettyDuration { return result; } +- (NSString *)prettyTrialDuration { + NSString *result = @"unknown"; + + switch (self.trialDuration) { + case QNTrialDurationNotAvailable: + result = @"not available"; break; + + case QNTrialDurationThreeDays: + result = @"three days"; break; + + case QNTrialDurationWeek: + result = @"week"; break; + + case QNTrialDurationTwoWeeks: + result = @"two weeks"; break; + + case QNTrialDurationMonth: + result = @"month"; break; + + case QNTrialDurationTwoMonths: + result = @"two months"; break; + + case QNTrialDurationThreeMonths: + result = @"three months"; break; + + case QNTrialDurationSixMonths: + result = @"six months"; break; + + case QNTrialDurationYear: + result = @"year"; break; + + case QNTrialDurationOther: + result = @"other"; break; + + default: + break; + } + + return result; +} + - (NSString *)prettyType { NSString *result = @"unknown"; diff --git a/Sources/Qonversion/QNProductCenterManager.h b/Sources/Qonversion/QNProductCenterManager.h index bc95c17f..61d0501b 100644 --- a/Sources/Qonversion/QNProductCenterManager.h +++ b/Sources/Qonversion/QNProductCenterManager.h @@ -16,6 +16,7 @@ NS_ASSUME_NONNULL_BEGIN - (void)restoreWithCompletion:(QNRestoreCompletionHandler)completion; - (void)products:(QNProductsCompletionHandler)completion; +- (void)checkTrialIntroEligibilityForProductIds:(NSArray *)productIds completion:(QNEligibilityCompletionHandler)completion; - (void)offerings:(QNOfferingsCompletionHandler)completion; - (void)launch:(void (^)(QNLaunchResult * _Nullable result, NSError * _Nullable error))completion; diff --git a/Sources/Qonversion/QNProductCenterManager.m b/Sources/Qonversion/QNProductCenterManager.m index 5e9f0518..462c1c7d 100644 --- a/Sources/Qonversion/QNProductCenterManager.m +++ b/Sources/Qonversion/QNProductCenterManager.m @@ -12,6 +12,7 @@ #import "QNPromoPurchasesDelegate.h" #import "QNOfferings.h" #import "QNOffering.h" +#import "QNIntroEligibility.h" static NSString * const kLaunchResult = @"qonversion.launch.result"; static NSString * const kUserDefaultsSuiteName = @"qonversion.product-center.suite"; @@ -166,6 +167,7 @@ - (void)preparDelayedPurchase:(NSString *)productID completion:(QNPurchaseComple - (void)processPurchase:(NSString *)productID completion:(QNPurchaseCompletionHandler)completion { QNProduct *product = [self QNProduct:productID]; if (!product) { + QONVERSION_LOG([NSString stringWithFormat:@"❌ product with id: %@ not found", product.qonversionID]); run_block_on_main(completion, @{}, [QNErrors errorWithQNErrorCode:QNErrorProductNotFound], NO); return; } @@ -180,6 +182,7 @@ - (void)processPurchase:(NSString *)productID completion:(QNPurchaseCompletionHa return; } + QONVERSION_LOG([NSString stringWithFormat:@"❌ product with id: %@ not found", product.qonversionID]); run_block_on_main(completion, @{}, [QNErrors errorWithQNErrorCode:QNErrorProductNotFound], NO); } @@ -255,18 +258,8 @@ - (void)executeProductsBlocksWithError:(NSError * _Nullable)error { [_productsBlocks removeAllObjects]; NSArray *products = [(_launchResult.products ?: @{}) allValues];; - NSMutableDictionary *resultProducts = [[NSMutableDictionary alloc] init]; - for (QNProduct *_product in products) { - if (!_product.qonversionID) { - continue; - } - - QNProduct *qnProduct = [self productAt:_product.qonversionID]; - if (qnProduct) { - [resultProducts setValue:qnProduct forKey:_product.qonversionID]; - } - } + NSDictionary *resultProducts = [self enrichProductsWithStoreProducts:products]; NSError *resultError = error ?: _launchError; NSDictionary *result = resultError ? @{} : [resultProducts copy]; for (QNProductsCompletionHandler _block in _blocks) { @@ -275,6 +268,22 @@ - (void)executeProductsBlocksWithError:(NSError * _Nullable)error { } } +- (NSDictionary *)enrichProductsWithStoreProducts:(NSArray *)products { + NSMutableDictionary *resultProducts = [[NSMutableDictionary alloc] init]; + for (QNProduct *_product in products) { + if (!_product.qonversionID) { + continue; + } + + QNProduct *qnProduct = [self productAt:_product.qonversionID]; + if (qnProduct) { + [resultProducts setValue:qnProduct forKey:_product.qonversionID]; + } + } + + return [resultProducts copy]; +} + - (void)loadProducts { if (!self.launchResult || self.productsLoading) { return; @@ -310,6 +319,43 @@ - (void)products:(QNProductsCompletionHandler)completion { } } +- (void)checkTrialIntroEligibilityForProductIds:(NSArray *)productIds completion:(QNEligibilityCompletionHandler)completion { + NSArray *uniqueProductIdentifiers = [NSSet setWithArray:productIds].allObjects; + + __block __weak QNProductCenterManager *weakSelf = self; + [self products:^(NSDictionary * _Nonnull result, NSError * _Nullable error) { + for (NSString *identifier in uniqueProductIdentifiers) { + QNProduct *product = result[identifier]; + if (!product) { + QONVERSION_LOG([NSString stringWithFormat:@"❌ product with id: %@ not found", product.qonversionID]); + run_block_on_main(completion, @{}, [QNErrors errorWithQNErrorCode:QNErrorProductNotFound]); + return; + } + } + + [weakSelf.apiClient checkTrialIntroEligibilityParamsForProducts:result.allValues completion:^(NSDictionary * _Nullable dict, NSError * _Nullable error) { + QNMapperObject *result = [QNMapper mapperObjectFrom:dict]; + if (result.error) { + run_block_on_main(completion, @{}, result.error); + completion(nil, result.error); + return; + } + + NSDictionary *eligibilityData = [QNMapper mapProductsEligibility:result.data]; + NSMutableDictionary *resultEligibility = [NSMutableDictionary new]; + + for (NSString *identifier in uniqueProductIdentifiers) { + QNIntroEligibility *item = eligibilityData[identifier]; + if (item) { + resultEligibility[identifier] = item; + } + } + + run_block_on_main(completion, [resultEligibility copy], nil); + }]; + }]; +} + - (void)retryLaunchFlowWithCompletion:(void(^)(void))completion { if (self.launchError) { __block __weak QNProductCenterManager *weakSelf = self; @@ -364,35 +410,36 @@ - (QNProduct * _Nullable)QNProduct:(NSString *)productID { - (void)launch:(void (^)(QNLaunchResult * _Nullable result, NSError * _Nullable error))completion { __block __weak QNProductCenterManager *weakSelf = self; - - [_apiClient launchRequest:^(NSDictionary * _Nullable dict, NSError * _Nullable error) { - @synchronized (weakSelf) { - weakSelf.launchingFinished = YES; - } +// [self.storeKitService receipt:^(NSString * _Nonnull receipt) { + [weakSelf.apiClient launchRequest:^(NSDictionary * _Nullable dict, NSError * _Nullable error) { + @synchronized (weakSelf) { + weakSelf.launchingFinished = YES; + } - if (!completion) { - return; - } + if (!completion) { + return; + } - if (error) { - completion([[QNLaunchResult alloc] init], error); - return; - } - - QNMapperObject *result = [QNMapper mapperObjectFrom:dict]; - if (result.error) { - completion([[QNLaunchResult alloc] init], result.error); - return; - } - - QNLaunchResult *launchResult = [QNMapper fillLaunchResult:result.data]; - completion(launchResult, nil); - - static dispatch_once_t onceToken; - dispatch_once(&onceToken, ^{ - [weakSelf.apiClient processStoredRequests]; - }); - }]; + if (error) { + completion([[QNLaunchResult alloc] init], error); + return; + } + + QNMapperObject *result = [QNMapper mapperObjectFrom:dict]; + if (result.error) { + completion([[QNLaunchResult alloc] init], result.error); + return; + } + + QNLaunchResult *launchResult = [QNMapper fillLaunchResult:result.data]; + completion(launchResult, nil); + + static dispatch_once_t onceToken; + dispatch_once(&onceToken, ^{ + [weakSelf.apiClient processStoredRequests]; + }); + }]; +// }]; } - (void)process:(NSDictionary * _Nullable)dict error:(NSError *)error diff --git a/Sources/Qonversion/QNRequestBuilder.h b/Sources/Qonversion/QNRequestBuilder.h index 33ca878f..7bc30755 100644 --- a/Sources/Qonversion/QNRequestBuilder.h +++ b/Sources/Qonversion/QNRequestBuilder.h @@ -6,5 +6,6 @@ - (NSURLRequest *)makePropertiesRequestWith:(NSDictionary *)parameters; - (NSURLRequest *)makeAttributionRequestWith:(NSDictionary *)parameters; - (NSURLRequest *)makePurchaseRequestWith:(NSDictionary *)parameters; +- (NSURLRequest *)makeIntroTrialEligibilityRequestWithData:(NSDictionary *)parameters; @end diff --git a/Sources/Qonversion/QNRequestBuilder.m b/Sources/Qonversion/QNRequestBuilder.m index bee8f52d..5e06856a 100644 --- a/Sources/Qonversion/QNRequestBuilder.m +++ b/Sources/Qonversion/QNRequestBuilder.m @@ -19,6 +19,10 @@ - (NSURLRequest *)makePurchaseRequestWith:(NSDictionary *)parameters { return [self makePostRequestWith:kPurchaseEndpoint andBody:parameters]; } +- (NSURLRequest *)makeIntroTrialEligibilityRequestWithData:(NSDictionary *)parameters { + return [self makePostRequestWith:kProductsEndpoint andBody:parameters]; +} + // MARK: Private - (NSURLRequest *)makePostRequestWith:(NSString *)endpoint andBody:(NSDictionary *)body { diff --git a/Sources/Qonversion/QNRequestSerializer.h b/Sources/Qonversion/QNRequestSerializer.h index d6ba6e00..4f2b3338 100644 --- a/Sources/Qonversion/QNRequestSerializer.h +++ b/Sources/Qonversion/QNRequestSerializer.h @@ -11,6 +11,8 @@ NS_ASSUME_NONNULL_BEGIN transaction:(SKPaymentTransaction *)transaction receipt:(nullable NSString *)receipt; +- (NSDictionary *)introTrialEligibilityDataForProducts:(NSArray *)products; + - (NSDictionary *)attributionDataWithDict:(NSDictionary *)data fromProvider:(QNAttributionProvider)provider; @end diff --git a/Sources/Qonversion/QNRequestSerializer.m b/Sources/Qonversion/QNRequestSerializer.m index 74f04822..80ec482c 100644 --- a/Sources/Qonversion/QNRequestSerializer.m +++ b/Sources/Qonversion/QNRequestSerializer.m @@ -2,6 +2,7 @@ #import "QNUserInfo.h" #import "QNDevice.h" #import "QNStoreKitSugare.h" +#import "QNProduct.h" @interface QNRequestSerializer () @@ -68,6 +69,27 @@ - (NSDictionary *)purchaseData:(SKProduct *)product return result; } +- (NSDictionary *)introTrialEligibilityDataForProducts:(NSArray *)products { + NSMutableDictionary *result = [[self mainData] mutableCopy]; + + NSMutableArray *productsLocalData = [NSMutableArray new]; + + for (QNProduct *product in products) { + NSMutableDictionary *param = [NSMutableDictionary new]; + param[@"store_id"] = product.storeID; + + if (@available(iOS 12.0, tvOS 10.0, watchOS 6.2, *)) { + param[@"subscription_group_identifier"] = product.skProduct.subscriptionGroupIdentifier; + } + + [productsLocalData addObject:param]; + } + + result[@"products_local_data"] = productsLocalData; + + return [result copy]; +} + - (NSDictionary *)attributionDataWithDict:(NSDictionary *)data fromProvider:(QNAttributionProvider)provider { NSMutableDictionary *body = @{@"d": self.mainData}.mutableCopy; NSMutableDictionary *providerData = [NSMutableDictionary new]; diff --git a/Sources/Qonversion/QNStoreKitService.m b/Sources/Qonversion/QNStoreKitService.m index ed58623f..23025fbe 100644 --- a/Sources/Qonversion/QNStoreKitService.m +++ b/Sources/Qonversion/QNStoreKitService.m @@ -178,6 +178,10 @@ - (void)handleTransaction:(nonnull SKPaymentTransaction *)transaction { // MARK: - SKProductsRequestDelegate - (void)productsRequest:(nonnull SKProductsRequest *)request didReceiveResponse:(nonnull SKProductsResponse *)response { + if (response.invalidProductIdentifiers.count > 0) { + QONVERSION_LOG([NSString stringWithFormat:@"❌ Invalid store products identifiers: %@", response.invalidProductIdentifiers]); + } + BOOL autoTracked = NO; for (SKProduct *product in response.products) { [_products setValue:product forKey:product.productIdentifier]; diff --git a/Sources/Qonversion/Qonversion.h b/Sources/Qonversion/Qonversion.h index d5880830..9107fd54 100644 --- a/Sources/Qonversion/Qonversion.h +++ b/Sources/Qonversion/Qonversion.h @@ -89,6 +89,7 @@ NS_ASSUME_NONNULL_BEGIN */ + (void)products:(QNProductsCompletionHandler)completion; ++ (void)checkTrialIntroEligibilityForProductIds:(NSArray *)productIds completion:(QNEligibilityCompletionHandler)completion; /** Return Qonversion Offerings Object diff --git a/Sources/Qonversion/Qonversion.m b/Sources/Qonversion/Qonversion.m index f45484aa..20e1f129 100644 --- a/Sources/Qonversion/Qonversion.m +++ b/Sources/Qonversion/Qonversion.m @@ -81,6 +81,10 @@ + (void)products:(QNProductsCompletionHandler)completion { return [[Qonversion sharedInstance].productCenterManager products:completion]; } ++ (void)checkTrialIntroEligibilityForProductIds:(NSArray *)productIds completion:(QNEligibilityCompletionHandler)completion { + [[Qonversion sharedInstance].productCenterManager checkTrialIntroEligibilityForProductIds:productIds completion:completion]; +} + + (void)offerings:(QNOfferingsCompletionHandler)completion { return [[Qonversion sharedInstance].productCenterManager offerings:completion]; } From 9915dbd5c6690c198d6dc3d87f7c832a5c68a815 Mon Sep 17 00:00:00 2001 From: Surik Sarkisyan Date: Fri, 15 Jan 2021 18:15:38 +0800 Subject: [PATCH 02/17] Update Sources/Qonversion/QNMapper.h --- Sources/Qonversion/QNMapper.h | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Sources/Qonversion/QNMapper.h b/Sources/Qonversion/QNMapper.h index f188ad89..1ae307b3 100644 --- a/Sources/Qonversion/QNMapper.h +++ b/Sources/Qonversion/QNMapper.h @@ -1,6 +1,6 @@ #import -@class QNMapperObject, QNLaunchResult, QNPermission, QNProduct, QNIntroEligibility; +@class QNMapperObject, QNLaunchResult, QNPermission, QNIntroEligibility; @interface QNMapper : NSObject From 23d0f2eea5a3af3b49fe8ac71b5fe9d44e6920c9 Mon Sep 17 00:00:00 2001 From: Surik Date: Fri, 15 Jan 2021 18:18:11 +0800 Subject: [PATCH 03/17] Added comment --- Sources/Qonversion/QNProduct.h | 3 +++ 1 file changed, 3 insertions(+) diff --git a/Sources/Qonversion/QNProduct.h b/Sources/Qonversion/QNProduct.h index 364bc072..4114e89f 100644 --- a/Sources/Qonversion/QNProduct.h +++ b/Sources/Qonversion/QNProduct.h @@ -69,6 +69,9 @@ NS_SWIFT_NAME(Qonversion.Product) */ @property (nonatomic, assign) QNProductDuration duration; +/** + Trial duration + */ @property (nonatomic, assign) QNTrialDuration trialDuration; /** From 56d0070b7e9f007c9f87c2fdc296fe4be44ee3d8 Mon Sep 17 00:00:00 2001 From: Surik Date: Fri, 15 Jan 2021 18:21:26 +0800 Subject: [PATCH 04/17] Removed extra completion --- Sources/Qonversion/QNProductCenterManager.m | 1 - 1 file changed, 1 deletion(-) diff --git a/Sources/Qonversion/QNProductCenterManager.m b/Sources/Qonversion/QNProductCenterManager.m index 462c1c7d..d2eab3d5 100644 --- a/Sources/Qonversion/QNProductCenterManager.m +++ b/Sources/Qonversion/QNProductCenterManager.m @@ -337,7 +337,6 @@ - (void)checkTrialIntroEligibilityForProductIds:(NSArray *)productId QNMapperObject *result = [QNMapper mapperObjectFrom:dict]; if (result.error) { run_block_on_main(completion, @{}, result.error); - completion(nil, result.error); return; } From a180a316f332ef87dcc810feb1002b496584951e Mon Sep 17 00:00:00 2001 From: Surik Date: Fri, 15 Jan 2021 18:24:03 +0800 Subject: [PATCH 05/17] Removed commented code --- Sources/Qonversion/QNProductCenterManager.m | 54 ++++++++++----------- 1 file changed, 26 insertions(+), 28 deletions(-) diff --git a/Sources/Qonversion/QNProductCenterManager.m b/Sources/Qonversion/QNProductCenterManager.m index d2eab3d5..334f6a84 100644 --- a/Sources/Qonversion/QNProductCenterManager.m +++ b/Sources/Qonversion/QNProductCenterManager.m @@ -409,36 +409,34 @@ - (QNProduct * _Nullable)QNProduct:(NSString *)productID { - (void)launch:(void (^)(QNLaunchResult * _Nullable result, NSError * _Nullable error))completion { __block __weak QNProductCenterManager *weakSelf = self; -// [self.storeKitService receipt:^(NSString * _Nonnull receipt) { - [weakSelf.apiClient launchRequest:^(NSDictionary * _Nullable dict, NSError * _Nullable error) { - @synchronized (weakSelf) { - weakSelf.launchingFinished = YES; - } + [self.apiClient launchRequest:^(NSDictionary * _Nullable dict, NSError * _Nullable error) { + @synchronized (weakSelf) { + weakSelf.launchingFinished = YES; + } - if (!completion) { - return; - } + if (!completion) { + return; + } - if (error) { - completion([[QNLaunchResult alloc] init], error); - return; - } - - QNMapperObject *result = [QNMapper mapperObjectFrom:dict]; - if (result.error) { - completion([[QNLaunchResult alloc] init], result.error); - return; - } - - QNLaunchResult *launchResult = [QNMapper fillLaunchResult:result.data]; - completion(launchResult, nil); - - static dispatch_once_t onceToken; - dispatch_once(&onceToken, ^{ - [weakSelf.apiClient processStoredRequests]; - }); - }]; -// }]; + if (error) { + completion([[QNLaunchResult alloc] init], error); + return; + } + + QNMapperObject *result = [QNMapper mapperObjectFrom:dict]; + if (result.error) { + completion([[QNLaunchResult alloc] init], result.error); + return; + } + + QNLaunchResult *launchResult = [QNMapper fillLaunchResult:result.data]; + completion(launchResult, nil); + + static dispatch_once_t onceToken; + dispatch_once(&onceToken, ^{ + [weakSelf.apiClient processStoredRequests]; + }); + }]; } - (void)process:(NSDictionary * _Nullable)dict error:(NSError *)error From 06453db2896cc53b7b5640393297c93121424fb2 Mon Sep 17 00:00:00 2001 From: Surik Date: Fri, 15 Jan 2021 20:11:10 +0800 Subject: [PATCH 06/17] Added exeriments logic --- Qonversion.xcodeproj/project.pbxproj | 24 +++++++++++ .../Qonversion/QNExperimentGroup+Protected.h | 19 ++++++++ Sources/Qonversion/QNExperimentGroup.h | 25 +++++++++++ Sources/Qonversion/QNExperimentGroup.m | 23 ++++++++++ .../Qonversion/QNExperimentInfo+Protected.h | 19 ++++++++ Sources/Qonversion/QNExperimentInfo.h | 22 ++++++++++ Sources/Qonversion/QNExperimentInfo.m | 24 +++++++++++ Sources/Qonversion/QNLaunchResult+Protected.h | 1 + Sources/Qonversion/QNLaunchResult.h | 8 +++- Sources/Qonversion/QNMapper.m | 43 ++++++++++++++++++- Sources/Qonversion/QNProductCenterManager.h | 1 + Sources/Qonversion/QNProductCenterManager.m | 35 ++++++++++++++- Sources/Qonversion/Qonversion.h | 2 + Sources/Qonversion/Qonversion.m | 4 ++ 14 files changed, 247 insertions(+), 3 deletions(-) create mode 100644 Sources/Qonversion/QNExperimentGroup+Protected.h create mode 100644 Sources/Qonversion/QNExperimentGroup.h create mode 100644 Sources/Qonversion/QNExperimentGroup.m create mode 100644 Sources/Qonversion/QNExperimentInfo+Protected.h create mode 100644 Sources/Qonversion/QNExperimentInfo.h create mode 100644 Sources/Qonversion/QNExperimentInfo.m diff --git a/Qonversion.xcodeproj/project.pbxproj b/Qonversion.xcodeproj/project.pbxproj index 8522a9b9..1b5cb795 100644 --- a/Qonversion.xcodeproj/project.pbxproj +++ b/Qonversion.xcodeproj/project.pbxproj @@ -98,6 +98,12 @@ 896D2D6125B0585B009905A0 /* QNIntroEligibility.m in Sources */ = {isa = PBXBuildFile; fileRef = 896D2D5F25B0585B009905A0 /* QNIntroEligibility.m */; }; 896D2D6625B06447009905A0 /* QNIntroEligibility+Protected.h in Headers */ = {isa = PBXBuildFile; fileRef = 896D2D6425B06447009905A0 /* QNIntroEligibility+Protected.h */; }; 896D2D6725B06447009905A0 /* QNIntroEligibility+Protected.m in Sources */ = {isa = PBXBuildFile; fileRef = 896D2D6525B06447009905A0 /* QNIntroEligibility+Protected.m */; }; + 896D2D7D25B1A82B009905A0 /* QNExperimentInfo.h in Headers */ = {isa = PBXBuildFile; fileRef = 896D2D7B25B1A82B009905A0 /* QNExperimentInfo.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 896D2D7E25B1A82B009905A0 /* QNExperimentInfo.m in Sources */ = {isa = PBXBuildFile; fileRef = 896D2D7C25B1A82B009905A0 /* QNExperimentInfo.m */; }; + 896D2D8925B1AF05009905A0 /* QNExperimentGroup.h in Headers */ = {isa = PBXBuildFile; fileRef = 896D2D8725B1AF05009905A0 /* QNExperimentGroup.h */; }; + 896D2D8A25B1AF05009905A0 /* QNExperimentGroup.m in Sources */ = {isa = PBXBuildFile; fileRef = 896D2D8825B1AF05009905A0 /* QNExperimentGroup.m */; }; + 896D2D8F25B1B41A009905A0 /* QNExperimentGroup+Protected.h in Headers */ = {isa = PBXBuildFile; fileRef = 896D2D8D25B1B41A009905A0 /* QNExperimentGroup+Protected.h */; }; + 896D2D9525B1B42E009905A0 /* QNExperimentInfo+Protected.h in Headers */ = {isa = PBXBuildFile; fileRef = 896D2D9325B1B42E009905A0 /* QNExperimentInfo+Protected.h */; }; 89861D2C2501563B00E5D36B /* Qonversion.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 459DAB69243E329F0011ECF3 /* Qonversion.framework */; }; 89861D2D2501563B00E5D36B /* Qonversion.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = 459DAB69243E329F0011ECF3 /* Qonversion.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; 89ADA76C250696A400EB2E54 /* ActivePermissionsTableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 89ADA76B250696A400EB2E54 /* ActivePermissionsTableViewCell.swift */; }; @@ -256,6 +262,12 @@ 896D2D5F25B0585B009905A0 /* QNIntroEligibility.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = QNIntroEligibility.m; sourceTree = ""; }; 896D2D6425B06447009905A0 /* QNIntroEligibility+Protected.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "QNIntroEligibility+Protected.h"; sourceTree = ""; }; 896D2D6525B06447009905A0 /* QNIntroEligibility+Protected.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = "QNIntroEligibility+Protected.m"; sourceTree = ""; }; + 896D2D7B25B1A82B009905A0 /* QNExperimentInfo.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = QNExperimentInfo.h; sourceTree = ""; }; + 896D2D7C25B1A82B009905A0 /* QNExperimentInfo.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = QNExperimentInfo.m; sourceTree = ""; }; + 896D2D8725B1AF05009905A0 /* QNExperimentGroup.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = QNExperimentGroup.h; sourceTree = ""; }; + 896D2D8825B1AF05009905A0 /* QNExperimentGroup.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = QNExperimentGroup.m; sourceTree = ""; }; + 896D2D8D25B1B41A009905A0 /* QNExperimentGroup+Protected.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "QNExperimentGroup+Protected.h"; sourceTree = ""; }; + 896D2D9325B1B42E009905A0 /* QNExperimentInfo+Protected.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "QNExperimentInfo+Protected.h"; sourceTree = ""; }; 89ADA769250693C400EB2E54 /* ActivePermissionsViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ActivePermissionsViewController.swift; sourceTree = ""; }; 89ADA76B250696A400EB2E54 /* ActivePermissionsTableViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ActivePermissionsTableViewCell.swift; sourceTree = ""; }; 89EA13AB25A42BB90065BCED /* QNOfferings.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = QNOfferings.h; sourceTree = ""; }; @@ -401,6 +413,12 @@ children = ( 45239D702517D5CD0085D0A8 /* QNLaunchResult.h */, 456ECD3E249C737500D2BC40 /* QNLaunchResult.m */, + 896D2D7B25B1A82B009905A0 /* QNExperimentInfo.h */, + 896D2D7C25B1A82B009905A0 /* QNExperimentInfo.m */, + 896D2D9325B1B42E009905A0 /* QNExperimentInfo+Protected.h */, + 896D2D8725B1AF05009905A0 /* QNExperimentGroup.h */, + 896D2D8825B1AF05009905A0 /* QNExperimentGroup.m */, + 896D2D8D25B1B41A009905A0 /* QNExperimentGroup+Protected.h */, 456ECD40249C75D400D2BC40 /* QNLaunchResult+Protected.h */, 45FFA2E424BED563007EFB8F /* QNMapperObject.h */, 45FFA2E524BED563007EFB8F /* QNMapperObject.m */, @@ -625,10 +643,14 @@ buildActionMask = 2147483647; files = ( 896D2D6025B0585B009905A0 /* QNIntroEligibility.h in Headers */, + 896D2D7D25B1A82B009905A0 /* QNExperimentInfo.h in Headers */, 89EA13B325A42BC90065BCED /* QNOffering.h in Headers */, 89EA13AD25A42BB90065BCED /* QNOfferings.h in Headers */, 893045EC252DE8B500E22F75 /* QNPromoPurchasesDelegate.h in Headers */, + 896D2D8925B1AF05009905A0 /* QNExperimentGroup.h in Headers */, 45FFA2CE24BDE5FE007EFB8F /* QNAPIClient.h in Headers */, + 896D2D9525B1B42E009905A0 /* QNExperimentInfo+Protected.h in Headers */, + 896D2D8F25B1B41A009905A0 /* QNExperimentGroup+Protected.h in Headers */, 454BE230247C4AD8001FE771 /* QNUtils.h in Headers */, 45239D722517D9270085D0A8 /* QNLaunchResult+Protected.h in Headers */, 45FFA2D924BE029E007EFB8F /* QNRequestBuilder.h in Headers */, @@ -904,12 +926,14 @@ 458A8CD924BAC61E00637130 /* QNRequestSerializer.m in Sources */, 45FFA2CF24BDE5FE007EFB8F /* QNAPIClient.m in Sources */, 45FFA2E724BED563007EFB8F /* QNMapperObject.m in Sources */, + 896D2D7E25B1A82B009905A0 /* QNExperimentInfo.m in Sources */, 459DABFC243E35BC0011ECF3 /* QNMapper.m in Sources */, 456ECD34249B901900D2BC40 /* QNRequestBuilder.m in Sources */, 4572FE2124BC4B5B00A17AC4 /* QNUserPropertiesManager.m in Sources */, 896D2D6125B0585B009905A0 /* QNIntroEligibility.m in Sources */, 458A8CBF24B4B88F00637130 /* QNStoreKitSugare.m in Sources */, 4531CBC424577D770022C422 /* QNConstants.m in Sources */, + 896D2D8A25B1AF05009905A0 /* QNExperimentGroup.m in Sources */, 45FFA2D424BDFCE8007EFB8F /* QNAttributionManager.m in Sources */, 458A8CC924B8407C00637130 /* QNStoreKitService.m in Sources */, 896D2D6725B06447009905A0 /* QNIntroEligibility+Protected.m in Sources */, diff --git a/Sources/Qonversion/QNExperimentGroup+Protected.h b/Sources/Qonversion/QNExperimentGroup+Protected.h new file mode 100644 index 00000000..9b1d340d --- /dev/null +++ b/Sources/Qonversion/QNExperimentGroup+Protected.h @@ -0,0 +1,19 @@ +// +// QNExperimentGroup+Protected.h +// Qonversion +// +// Created by Surik Sarkisyan on 15.01.2021. +// Copyright © 2021 Qonversion Inc. All rights reserved. +// + +#import "QNExperimentGroup.h" + +NS_ASSUME_NONNULL_BEGIN + +@interface QNExperimentGroup (Protected) + +- (instancetype)initWithType:(QNExperimentGroupType)type; + +@end + +NS_ASSUME_NONNULL_END diff --git a/Sources/Qonversion/QNExperimentGroup.h b/Sources/Qonversion/QNExperimentGroup.h new file mode 100644 index 00000000..64218973 --- /dev/null +++ b/Sources/Qonversion/QNExperimentGroup.h @@ -0,0 +1,25 @@ +// +// QNExperimentGroup.h +// Qonversion +// +// Created by Surik Sarkisyan on 15.01.2021. +// Copyright © 2021 Qonversion Inc. All rights reserved. +// + +#import + +NS_ASSUME_NONNULL_BEGIN + +typedef NS_ENUM(NSInteger, QNExperimentGroupType) { + QNExperimentGroupTypeA = 0, + QNExperimentGroupTypB +} NS_SWIFT_NAME(Qonversion.ExperimentGroupType); + +NS_SWIFT_NAME(Qonversion.ExperimentGroup) +@interface QNExperimentGroup : NSObject + +@property (nonatomic, assign) QNExperimentGroupType type; + +@end + +NS_ASSUME_NONNULL_END diff --git a/Sources/Qonversion/QNExperimentGroup.m b/Sources/Qonversion/QNExperimentGroup.m new file mode 100644 index 00000000..34759311 --- /dev/null +++ b/Sources/Qonversion/QNExperimentGroup.m @@ -0,0 +1,23 @@ +// +// QNExperimentGroup.m +// Qonversion +// +// Created by Surik Sarkisyan on 15.01.2021. +// Copyright © 2021 Qonversion Inc. All rights reserved. +// + +#import "QNExperimentGroup.h" + +@implementation QNExperimentGroup + +- (instancetype)initWithType:(QNExperimentGroupType)type { + self = [super init]; + + if (self) { + _type = type; + } + + return self; +} + +@end diff --git a/Sources/Qonversion/QNExperimentInfo+Protected.h b/Sources/Qonversion/QNExperimentInfo+Protected.h new file mode 100644 index 00000000..64bada75 --- /dev/null +++ b/Sources/Qonversion/QNExperimentInfo+Protected.h @@ -0,0 +1,19 @@ +// +// QNExperimentInfo+Protected.h +// Qonversion +// +// Created by Surik Sarkisyan on 15.01.2021. +// Copyright © 2021 Qonversion Inc. All rights reserved. +// + +#import + +NS_ASSUME_NONNULL_BEGIN + +@interface QNExperimentInfo (Protected) + +- (instancetype)initWithIdentifier:(NSString *)identifier group:(QNExperimentGroup *)group; + +@end + +NS_ASSUME_NONNULL_END diff --git a/Sources/Qonversion/QNExperimentInfo.h b/Sources/Qonversion/QNExperimentInfo.h new file mode 100644 index 00000000..9849f4ce --- /dev/null +++ b/Sources/Qonversion/QNExperimentInfo.h @@ -0,0 +1,22 @@ +// +// QNExperimentInfo.h +// Qonversion +// +// Created by Surik Sarkisyan on 15.01.2021. +// Copyright © 2021 Qonversion Inc. All rights reserved. +// + +#import + +NS_ASSUME_NONNULL_BEGIN + +@class QNExperimentGroup; + +@interface QNExperimentInfo : NSObject + +@property (nonatomic, copy) NSString *identifier; +@property (nonatomic, strong) QNExperimentGroup *group; + +@end + +NS_ASSUME_NONNULL_END diff --git a/Sources/Qonversion/QNExperimentInfo.m b/Sources/Qonversion/QNExperimentInfo.m new file mode 100644 index 00000000..17ab3239 --- /dev/null +++ b/Sources/Qonversion/QNExperimentInfo.m @@ -0,0 +1,24 @@ +// +// QNExperimentInfo.m +// Qonversion +// +// Created by Surik Sarkisyan on 15.01.2021. +// Copyright © 2021 Qonversion Inc. All rights reserved. +// + +#import "QNExperimentInfo.h" + +@implementation QNExperimentInfo + +- (instancetype)initWithIdentifier:(NSString *)identifier group:(QNExperimentGroup *)group { + self = [super init]; + + if (self) { + _identifier = identifier; + _group = group; + } + + return self; +} + +@end diff --git a/Sources/Qonversion/QNLaunchResult+Protected.h b/Sources/Qonversion/QNLaunchResult+Protected.h index 8a2ea7e3..591fb0e3 100644 --- a/Sources/Qonversion/QNLaunchResult+Protected.h +++ b/Sources/Qonversion/QNLaunchResult+Protected.h @@ -4,6 +4,7 @@ @property (nonatomic, copy) NSString *uid; @property (nonatomic, assign) NSUInteger timestamp; +@property (nonatomic, copy) NSArray *experiments; @property (nonatomic, copy) NSDictionary *permissions; @property (nonatomic, copy) NSDictionary *products; @property (nonatomic, copy) NSDictionary *userProducts; diff --git a/Sources/Qonversion/QNLaunchResult.h b/Sources/Qonversion/QNLaunchResult.h index c4f44d08..b1419a31 100644 --- a/Sources/Qonversion/QNLaunchResult.h +++ b/Sources/Qonversion/QNLaunchResult.h @@ -2,7 +2,7 @@ NS_ASSUME_NONNULL_BEGIN -@class QNPermission, QNProduct, QNOfferings, QNIntroEligibility; +@class QNPermission, QNProduct, QNOfferings, QNIntroEligibility, QNExperimentInfo; typedef NS_ENUM(NSInteger, QNAttributionProvider) { QNAttributionProviderAppsFlyer = 0, @@ -38,6 +38,10 @@ NS_SWIFT_NAME(Qonversion.LaunchResult) */ @property (nonatomic, assign, readonly) NSUInteger timestamp; +/** + User A/B-test experiments + */ +@property (nonatomic, copy, readonly) NSDictionary *experiments; /** User permissions */ @@ -73,6 +77,8 @@ typedef void (^QNProductsCompletionHandler)(NSDictionary *result, NSError *_Nullable error) NS_SWIFT_NAME(Qonversion.EligibilityCompletionHandler); +typedef void (^QNExperimentsCompletionHandler)(NSDictionary *result, NSError *_Nullable error) NS_SWIFT_NAME(Qonversion.ExperimentsCompletionHandler); + typedef void (^QNOfferingsCompletionHandler)(QNOfferings *_Nullable offerings, NSError *_Nullable error) NS_SWIFT_NAME(Qonversion.OfferingsCompletionHandler); NS_ASSUME_NONNULL_END diff --git a/Sources/Qonversion/QNMapper.m b/Sources/Qonversion/QNMapper.m index 03ea7d60..ec6dd84e 100644 --- a/Sources/Qonversion/QNMapper.m +++ b/Sources/Qonversion/QNMapper.m @@ -7,11 +7,15 @@ #import "QNOfferings.h" #import "QNOffering.h" #import "QNIntroEligibility.h" +#import "QNExperimentInfo.h" +#import "QNExperimentGroup.h" #import "QNLaunchResult+Protected.h" #import "QNOfferings+Protected.h" #import "QNOffering+Protected.h" #import "QNIntroEligibility+Protected.h" +#import "QNExperimentInfo+Protected.h" +#import "QNExperimentGroup+Protected.h" @implementation QNMapper @@ -22,6 +26,7 @@ + (QNLaunchResult * _Nonnull)fillLaunchResult:(NSDictionary *)dict { NSArray *productsArray = dict[@"products"] ?: @[]; NSArray *userProductsArray = dict[@"user_products"] ?: @[]; NSArray *offeringsArray = dict[@"offerings"]; + NSArray *experiments = dict[@"experiments"] ?: @[]; NSNumber *timestamp = dict[@"timestamp"] ?: @0; @@ -30,6 +35,7 @@ + (QNLaunchResult * _Nonnull)fillLaunchResult:(NSDictionary *)dict { [result setPermissions:[self fillPermissions:permissionsArray]]; [result setProducts:[self fillProducts:productsArray]]; [result setUserProducts:[self fillProducts:userProductsArray]]; + [result setExperiments:[self fillExperiments:experiments]]; if (offeringsArray.count > 0) { QNOfferings *offerings = [self fillOfferingsObject:offeringsArray]; @@ -52,12 +58,47 @@ + (QNLaunchResult * _Nonnull)fillLaunchResult:(NSDictionary *)dict { return [[NSDictionary alloc] initWithDictionary:permissions]; } ++ (NSDictionary *)fillExperiments:(NSArray *)data { + NSMutableDictionary *experiments = [NSMutableDictionary new]; + + for (NSDictionary* itemDict in data) { + QNExperimentInfo *item = [self fillExperiment:itemDict]; + if (item.identifier) { + experiments[item.identifier] = item; + } + } + + return [experiments copy]; +} + ++ (QNExperimentInfo * _Nullable)fillExperiment:(NSDictionary *)dict { + NSString *identifier = dict[@"id"]; + if (!identifier) { + return nil; + } + + NSDictionary *experimentGroupData = dict[@"group"]; + + QNExperimentGroup *group = [QNMapper fillExperimentGroup:experimentGroupData]; + + QNExperimentInfo *experiment = [[QNExperimentInfo alloc] initWithIdentifier:identifier group:group]; + + return experiment; +} + ++ (QNExperimentGroup * _Nonnull)fillExperimentGroup:(NSDictionary * _Nullable)dict { + QNExperimentGroupType type = [self mapInteger:dict[@"type"] orReturn:0]; + QNExperimentGroup *group = [[QNExperimentGroup alloc] initWithType:type]; + + return group; +} + + (NSDictionary *)fillProducts:(NSArray *)data { NSMutableDictionary *products = [NSMutableDictionary new]; for (NSDictionary* itemDict in data) { QNProduct *item = [self fillProduct:itemDict]; - if (item && item.qonversionID) { + if (item.qonversionID) { products[item.qonversionID] = item; } } diff --git a/Sources/Qonversion/QNProductCenterManager.h b/Sources/Qonversion/QNProductCenterManager.h index 61d0501b..d5bb8b06 100644 --- a/Sources/Qonversion/QNProductCenterManager.h +++ b/Sources/Qonversion/QNProductCenterManager.h @@ -18,6 +18,7 @@ NS_ASSUME_NONNULL_BEGIN - (void)products:(QNProductsCompletionHandler)completion; - (void)checkTrialIntroEligibilityForProductIds:(NSArray *)productIds completion:(QNEligibilityCompletionHandler)completion; - (void)offerings:(QNOfferingsCompletionHandler)completion; +- (void)experimentInfo:(QNExperimentsCompletionHandler)completion; - (void)launch:(void (^)(QNLaunchResult * _Nullable result, NSError * _Nullable error))completion; diff --git a/Sources/Qonversion/QNProductCenterManager.m b/Sources/Qonversion/QNProductCenterManager.m index 334f6a84..f2325587 100644 --- a/Sources/Qonversion/QNProductCenterManager.m +++ b/Sources/Qonversion/QNProductCenterManager.m @@ -30,6 +30,7 @@ @interface QNProductCenterManager() @property (nonatomic, strong) NSMutableArray *permissionsBlocks; @property (nonatomic, strong) NSMutableArray *productsBlocks; @property (nonatomic, strong) NSMutableArray *offeringsBlocks; +@property (nonatomic, strong) NSMutableArray *experimentsBlocks; @property (nonatomic) QNAPIClient *apiClient; @property (nonatomic) QNLaunchResult *launchResult; @@ -56,10 +57,11 @@ - (instancetype)init { _persistentStorage = [[QNUserDefaultsStorage alloc] init]; [_persistentStorage setUserDefaults:[[NSUserDefaults alloc] initWithSuiteName:kUserDefaultsSuiteName]]; - _purchasingBlocks = [[NSMutableDictionary alloc] init]; + _purchasingBlocks = [NSMutableDictionary new]; _permissionsBlocks = [NSMutableArray new]; _productsBlocks = [NSMutableArray new]; _offeringsBlocks = [NSMutableArray new]; + _experimentsBlocks = [NSMutableArray new]; } return self; @@ -83,6 +85,7 @@ - (void)launchWithCompletion:(QNLaunchCompletionHandler)completion { weakSelf.launchError = error; [weakSelf executePermissionBlocks]; + [weakSelf executeExperimentsBlocks]; NSArray *storeProducts = [weakSelf.storeKitService getLoadedProducts]; if (!weakSelf.productsLoading && storeProducts.count == 0) { @@ -206,6 +209,21 @@ - (void)executePermissionBlocks { } } +- (void)executeExperimentsBlocks { + @synchronized (self) { + NSArray *blocks = [self.experimentsBlocks copy]; + if (blocks.count == 0) { + return; + } + + [self.experimentsBlocks removeAllObjects]; + + for (QNExperimentsCompletionHandler block in blocks) { + run_block_on_main(block, self.launchResult.experiments, self.launchError); + } + } +} + - (void)executeOfferingsBlocks { [self executeOfferingsBlocksWithError:nil]; } @@ -389,6 +407,21 @@ - (void)offerings:(QNOfferingsCompletionHandler)completion { } } +- (void)experimentInfo:(QNExperimentsCompletionHandler)completion { + @synchronized (self) { + if (!self.launchingFinished) { + [self.experimentsBlocks addObject:completion]; + return; + } + + if (self.launchResult) { + [self executeExperimentsBlocks]; + } else { + [self launchWithCompletion:nil]; + } + } +} + - (QNProduct *)productAt:(NSString *)productID { QNProduct *product = [self QNProduct:productID]; if (product) { diff --git a/Sources/Qonversion/Qonversion.h b/Sources/Qonversion/Qonversion.h index 9107fd54..be6b446f 100644 --- a/Sources/Qonversion/Qonversion.h +++ b/Sources/Qonversion/Qonversion.h @@ -103,6 +103,8 @@ NS_ASSUME_NONNULL_BEGIN */ + (void)offerings:(QNOfferingsCompletionHandler)completion; ++ (void)experimentInfo:(QNExperimentsCompletionHandler)completion; + + (void)resetUser; /** diff --git a/Sources/Qonversion/Qonversion.m b/Sources/Qonversion/Qonversion.m index 20e1f129..0bc6cb59 100644 --- a/Sources/Qonversion/Qonversion.m +++ b/Sources/Qonversion/Qonversion.m @@ -89,6 +89,10 @@ + (void)offerings:(QNOfferingsCompletionHandler)completion { return [[Qonversion sharedInstance].productCenterManager offerings:completion]; } ++ (void)experimentInfo:(QNExperimentsCompletionHandler)completion { + [[Qonversion sharedInstance].productCenterManager experimentInfo:completion]; +} + + (void)setAppleSearchAdsAttributionEnabled:(BOOL)enable { if (enable) { [[Qonversion sharedInstance].attributionManager addAppleSearchAttributionData]; From c8e26bbf158b0fc918a5a5a2be0e0d473288058f Mon Sep 17 00:00:00 2001 From: Surik Date: Fri, 15 Jan 2021 20:11:40 +0800 Subject: [PATCH 07/17] Removed useless file --- Qonversion.xcodeproj/project.pbxproj | 4 ---- Sources/Qonversion/QNIntroEligibility+Protected.m | 13 ------------- 2 files changed, 17 deletions(-) delete mode 100644 Sources/Qonversion/QNIntroEligibility+Protected.m diff --git a/Qonversion.xcodeproj/project.pbxproj b/Qonversion.xcodeproj/project.pbxproj index 8522a9b9..a2374c78 100644 --- a/Qonversion.xcodeproj/project.pbxproj +++ b/Qonversion.xcodeproj/project.pbxproj @@ -97,7 +97,6 @@ 896D2D6025B0585B009905A0 /* QNIntroEligibility.h in Headers */ = {isa = PBXBuildFile; fileRef = 896D2D5E25B0585B009905A0 /* QNIntroEligibility.h */; settings = {ATTRIBUTES = (Public, ); }; }; 896D2D6125B0585B009905A0 /* QNIntroEligibility.m in Sources */ = {isa = PBXBuildFile; fileRef = 896D2D5F25B0585B009905A0 /* QNIntroEligibility.m */; }; 896D2D6625B06447009905A0 /* QNIntroEligibility+Protected.h in Headers */ = {isa = PBXBuildFile; fileRef = 896D2D6425B06447009905A0 /* QNIntroEligibility+Protected.h */; }; - 896D2D6725B06447009905A0 /* QNIntroEligibility+Protected.m in Sources */ = {isa = PBXBuildFile; fileRef = 896D2D6525B06447009905A0 /* QNIntroEligibility+Protected.m */; }; 89861D2C2501563B00E5D36B /* Qonversion.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 459DAB69243E329F0011ECF3 /* Qonversion.framework */; }; 89861D2D2501563B00E5D36B /* Qonversion.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = 459DAB69243E329F0011ECF3 /* Qonversion.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; 89ADA76C250696A400EB2E54 /* ActivePermissionsTableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 89ADA76B250696A400EB2E54 /* ActivePermissionsTableViewCell.swift */; }; @@ -255,7 +254,6 @@ 896D2D5E25B0585B009905A0 /* QNIntroEligibility.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = QNIntroEligibility.h; sourceTree = ""; }; 896D2D5F25B0585B009905A0 /* QNIntroEligibility.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = QNIntroEligibility.m; sourceTree = ""; }; 896D2D6425B06447009905A0 /* QNIntroEligibility+Protected.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "QNIntroEligibility+Protected.h"; sourceTree = ""; }; - 896D2D6525B06447009905A0 /* QNIntroEligibility+Protected.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = "QNIntroEligibility+Protected.m"; sourceTree = ""; }; 89ADA769250693C400EB2E54 /* ActivePermissionsViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ActivePermissionsViewController.swift; sourceTree = ""; }; 89ADA76B250696A400EB2E54 /* ActivePermissionsTableViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ActivePermissionsTableViewCell.swift; sourceTree = ""; }; 89EA13AB25A42BB90065BCED /* QNOfferings.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = QNOfferings.h; sourceTree = ""; }; @@ -411,7 +409,6 @@ 896D2D5E25B0585B009905A0 /* QNIntroEligibility.h */, 896D2D5F25B0585B009905A0 /* QNIntroEligibility.m */, 896D2D6425B06447009905A0 /* QNIntroEligibility+Protected.h */, - 896D2D6525B06447009905A0 /* QNIntroEligibility+Protected.m */, 89EA13AB25A42BB90065BCED /* QNOfferings.h */, 89EA13AC25A42BB90065BCED /* QNOfferings.m */, 89EA13B725A443070065BCED /* QNOfferings+Protected.h */, @@ -912,7 +909,6 @@ 4531CBC424577D770022C422 /* QNConstants.m in Sources */, 45FFA2D424BDFCE8007EFB8F /* QNAttributionManager.m in Sources */, 458A8CC924B8407C00637130 /* QNStoreKitService.m in Sources */, - 896D2D6725B06447009905A0 /* QNIntroEligibility+Protected.m in Sources */, 459DABF9243E35BC0011ECF3 /* QNKeychain.m in Sources */, 458A8CD224BAC5F500637130 /* QNUserDefaultsStorage.m in Sources */, ); diff --git a/Sources/Qonversion/QNIntroEligibility+Protected.m b/Sources/Qonversion/QNIntroEligibility+Protected.m deleted file mode 100644 index e7c87fad..00000000 --- a/Sources/Qonversion/QNIntroEligibility+Protected.m +++ /dev/null @@ -1,13 +0,0 @@ -// -// QNIntroEligibility+Protected.m -// Qonversion -// -// Created by Surik Sarkisyan on 14.01.2021. -// Copyright © 2021 Qonversion Inc. All rights reserved. -// - -#import "QNIntroEligibility+Protected.h" - -@implementation QNIntroEligibility (Protected) - -@end From d9d4f2062a0ca7c8280656f8b9d047c3f31b2422 Mon Sep 17 00:00:00 2001 From: Surik Date: Fri, 15 Jan 2021 20:31:27 +0800 Subject: [PATCH 08/17] Updated functon name --- Sources/Qonversion/QNProductCenterManager.h | 2 +- Sources/Qonversion/QNProductCenterManager.m | 2 +- Sources/Qonversion/Qonversion.h | 2 +- Sources/Qonversion/Qonversion.m | 4 ++-- 4 files changed, 5 insertions(+), 5 deletions(-) diff --git a/Sources/Qonversion/QNProductCenterManager.h b/Sources/Qonversion/QNProductCenterManager.h index d5bb8b06..43b1ad04 100644 --- a/Sources/Qonversion/QNProductCenterManager.h +++ b/Sources/Qonversion/QNProductCenterManager.h @@ -18,7 +18,7 @@ NS_ASSUME_NONNULL_BEGIN - (void)products:(QNProductsCompletionHandler)completion; - (void)checkTrialIntroEligibilityForProductIds:(NSArray *)productIds completion:(QNEligibilityCompletionHandler)completion; - (void)offerings:(QNOfferingsCompletionHandler)completion; -- (void)experimentInfo:(QNExperimentsCompletionHandler)completion; +- (void)experiments:(QNExperimentsCompletionHandler)completion; - (void)launch:(void (^)(QNLaunchResult * _Nullable result, NSError * _Nullable error))completion; diff --git a/Sources/Qonversion/QNProductCenterManager.m b/Sources/Qonversion/QNProductCenterManager.m index f2325587..48bb1ae3 100644 --- a/Sources/Qonversion/QNProductCenterManager.m +++ b/Sources/Qonversion/QNProductCenterManager.m @@ -407,7 +407,7 @@ - (void)offerings:(QNOfferingsCompletionHandler)completion { } } -- (void)experimentInfo:(QNExperimentsCompletionHandler)completion { +- (void)experiments:(QNExperimentsCompletionHandler)completion { @synchronized (self) { if (!self.launchingFinished) { [self.experimentsBlocks addObject:completion]; diff --git a/Sources/Qonversion/Qonversion.h b/Sources/Qonversion/Qonversion.h index be6b446f..d96788d5 100644 --- a/Sources/Qonversion/Qonversion.h +++ b/Sources/Qonversion/Qonversion.h @@ -103,7 +103,7 @@ NS_ASSUME_NONNULL_BEGIN */ + (void)offerings:(QNOfferingsCompletionHandler)completion; -+ (void)experimentInfo:(QNExperimentsCompletionHandler)completion; ++ (void)experiments:(QNExperimentsCompletionHandler)completion; + (void)resetUser; diff --git a/Sources/Qonversion/Qonversion.m b/Sources/Qonversion/Qonversion.m index 0bc6cb59..99b5f085 100644 --- a/Sources/Qonversion/Qonversion.m +++ b/Sources/Qonversion/Qonversion.m @@ -89,8 +89,8 @@ + (void)offerings:(QNOfferingsCompletionHandler)completion { return [[Qonversion sharedInstance].productCenterManager offerings:completion]; } -+ (void)experimentInfo:(QNExperimentsCompletionHandler)completion { - [[Qonversion sharedInstance].productCenterManager experimentInfo:completion]; ++ (void)experiments:(QNExperimentsCompletionHandler)completion { + [[Qonversion sharedInstance].productCenterManager experiments:completion]; } + (void)setAppleSearchAdsAttributionEnabled:(BOOL)enable { From f0e45ca2ef0108a145ff06b05e23144b12d9c4b6 Mon Sep 17 00:00:00 2001 From: Surik Date: Fri, 15 Jan 2021 20:35:22 +0800 Subject: [PATCH 09/17] Updated experiments completion call logic --- Sources/Qonversion/QNProductCenterManager.m | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/Sources/Qonversion/QNProductCenterManager.m b/Sources/Qonversion/QNProductCenterManager.m index 48bb1ae3..9324676e 100644 --- a/Sources/Qonversion/QNProductCenterManager.m +++ b/Sources/Qonversion/QNProductCenterManager.m @@ -409,8 +409,9 @@ - (void)offerings:(QNOfferingsCompletionHandler)completion { - (void)experiments:(QNExperimentsCompletionHandler)completion { @synchronized (self) { + [self.experimentsBlocks addObject:completion]; + if (!self.launchingFinished) { - [self.experimentsBlocks addObject:completion]; return; } From 6f7d901a189e07f20bfe6c71912e4bf869a206d3 Mon Sep 17 00:00:00 2001 From: Surik Date: Mon, 18 Jan 2021 14:43:21 +0800 Subject: [PATCH 10/17] Fixed mac os version check for subscriptionGroupIdentifier --- Sources/Qonversion/QNRequestSerializer.m | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Sources/Qonversion/QNRequestSerializer.m b/Sources/Qonversion/QNRequestSerializer.m index 80ec482c..4ac9624e 100644 --- a/Sources/Qonversion/QNRequestSerializer.m +++ b/Sources/Qonversion/QNRequestSerializer.m @@ -78,7 +78,7 @@ - (NSDictionary *)introTrialEligibilityDataForProducts:(NSArray *)p NSMutableDictionary *param = [NSMutableDictionary new]; param[@"store_id"] = product.storeID; - if (@available(iOS 12.0, tvOS 10.0, watchOS 6.2, *)) { + if (@available(iOS 12.0, macOS 10.14, watchOS 6.2, *)) { param[@"subscription_group_identifier"] = product.skProduct.subscriptionGroupIdentifier; } From 78c47d3caf3cf1bb16c69c549d9395b4f2628009 Mon Sep 17 00:00:00 2001 From: Surik Date: Mon, 18 Jan 2021 14:43:51 +0800 Subject: [PATCH 11/17] Revert "Fixed mac os version check for subscriptionGroupIdentifier" This reverts commit 6f7d901a189e07f20bfe6c71912e4bf869a206d3. --- Sources/Qonversion/QNRequestSerializer.m | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Sources/Qonversion/QNRequestSerializer.m b/Sources/Qonversion/QNRequestSerializer.m index 4ac9624e..80ec482c 100644 --- a/Sources/Qonversion/QNRequestSerializer.m +++ b/Sources/Qonversion/QNRequestSerializer.m @@ -78,7 +78,7 @@ - (NSDictionary *)introTrialEligibilityDataForProducts:(NSArray *)p NSMutableDictionary *param = [NSMutableDictionary new]; param[@"store_id"] = product.storeID; - if (@available(iOS 12.0, macOS 10.14, watchOS 6.2, *)) { + if (@available(iOS 12.0, tvOS 10.0, watchOS 6.2, *)) { param[@"subscription_group_identifier"] = product.skProduct.subscriptionGroupIdentifier; } From 9688b76936cf365795db4084241134aab205b6d7 Mon Sep 17 00:00:00 2001 From: Surik Date: Mon, 18 Jan 2021 14:45:27 +0800 Subject: [PATCH 12/17] Updated mac os version check for subscriptionGroupIdentifier --- Sources/Qonversion/QNRequestSerializer.m | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Sources/Qonversion/QNRequestSerializer.m b/Sources/Qonversion/QNRequestSerializer.m index 80ec482c..4ac9624e 100644 --- a/Sources/Qonversion/QNRequestSerializer.m +++ b/Sources/Qonversion/QNRequestSerializer.m @@ -78,7 +78,7 @@ - (NSDictionary *)introTrialEligibilityDataForProducts:(NSArray *)p NSMutableDictionary *param = [NSMutableDictionary new]; param[@"store_id"] = product.storeID; - if (@available(iOS 12.0, tvOS 10.0, watchOS 6.2, *)) { + if (@available(iOS 12.0, macOS 10.14, watchOS 6.2, *)) { param[@"subscription_group_identifier"] = product.skProduct.subscriptionGroupIdentifier; } From f648c8c297f1db3bd2677c402fcd00198c47cf4d Mon Sep 17 00:00:00 2001 From: Surik Sarkisyan Date: Mon, 18 Jan 2021 20:35:59 +0800 Subject: [PATCH 13/17] Apply suggestions from code review Co-authored-by: Sam Mejl --- Sources/Qonversion/QNProduct.m | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/Sources/Qonversion/QNProduct.m b/Sources/Qonversion/QNProduct.m index 7421e44d..0b9dae55 100644 --- a/Sources/Qonversion/QNProduct.m +++ b/Sources/Qonversion/QNProduct.m @@ -140,13 +140,13 @@ - (NSString *)prettyTrialDuration { result = @"not available"; break; case QNTrialDurationThreeDays: - result = @"three days"; break; + result = @"3 days"; break; case QNTrialDurationWeek: - result = @"week"; break; + result = @"7 days"; break; case QNTrialDurationTwoWeeks: - result = @"two weeks"; break; + result = @"14 days"; break; case QNTrialDurationMonth: result = @"month"; break; From 5e1411825874c1657b9cf038629d6cac2f53955a Mon Sep 17 00:00:00 2001 From: Surik Date: Mon, 18 Jan 2021 22:14:38 +0800 Subject: [PATCH 14/17] Updated logic --- Framework/QonversionFramework.h | 2 ++ Qonversion.xcodeproj/project.pbxproj | 2 +- Sources/Qonversion/QNExperimentInfo.h | 1 + Sources/Qonversion/Qonversion.h | 12 ++++++++++++ 4 files changed, 16 insertions(+), 1 deletion(-) diff --git a/Framework/QonversionFramework.h b/Framework/QonversionFramework.h index 7dba9562..4a35231e 100644 --- a/Framework/QonversionFramework.h +++ b/Framework/QonversionFramework.h @@ -10,3 +10,5 @@ #import #import #import +#import +#import diff --git a/Qonversion.xcodeproj/project.pbxproj b/Qonversion.xcodeproj/project.pbxproj index bbab93ca..7295e9d9 100644 --- a/Qonversion.xcodeproj/project.pbxproj +++ b/Qonversion.xcodeproj/project.pbxproj @@ -99,7 +99,7 @@ 896D2D6625B06447009905A0 /* QNIntroEligibility+Protected.h in Headers */ = {isa = PBXBuildFile; fileRef = 896D2D6425B06447009905A0 /* QNIntroEligibility+Protected.h */; }; 896D2D7D25B1A82B009905A0 /* QNExperimentInfo.h in Headers */ = {isa = PBXBuildFile; fileRef = 896D2D7B25B1A82B009905A0 /* QNExperimentInfo.h */; settings = {ATTRIBUTES = (Public, ); }; }; 896D2D7E25B1A82B009905A0 /* QNExperimentInfo.m in Sources */ = {isa = PBXBuildFile; fileRef = 896D2D7C25B1A82B009905A0 /* QNExperimentInfo.m */; }; - 896D2D8925B1AF05009905A0 /* QNExperimentGroup.h in Headers */ = {isa = PBXBuildFile; fileRef = 896D2D8725B1AF05009905A0 /* QNExperimentGroup.h */; }; + 896D2D8925B1AF05009905A0 /* QNExperimentGroup.h in Headers */ = {isa = PBXBuildFile; fileRef = 896D2D8725B1AF05009905A0 /* QNExperimentGroup.h */; settings = {ATTRIBUTES = (Public, ); }; }; 896D2D8A25B1AF05009905A0 /* QNExperimentGroup.m in Sources */ = {isa = PBXBuildFile; fileRef = 896D2D8825B1AF05009905A0 /* QNExperimentGroup.m */; }; 896D2D8F25B1B41A009905A0 /* QNExperimentGroup+Protected.h in Headers */ = {isa = PBXBuildFile; fileRef = 896D2D8D25B1B41A009905A0 /* QNExperimentGroup+Protected.h */; }; 896D2D9525B1B42E009905A0 /* QNExperimentInfo+Protected.h in Headers */ = {isa = PBXBuildFile; fileRef = 896D2D9325B1B42E009905A0 /* QNExperimentInfo+Protected.h */; }; diff --git a/Sources/Qonversion/QNExperimentInfo.h b/Sources/Qonversion/QNExperimentInfo.h index 9849f4ce..28b65735 100644 --- a/Sources/Qonversion/QNExperimentInfo.h +++ b/Sources/Qonversion/QNExperimentInfo.h @@ -12,6 +12,7 @@ NS_ASSUME_NONNULL_BEGIN @class QNExperimentGroup; +NS_SWIFT_NAME(Qonversion.ExperimentInfo) @interface QNExperimentInfo : NSObject @property (nonatomic, copy) NSString *identifier; diff --git a/Sources/Qonversion/Qonversion.h b/Sources/Qonversion/Qonversion.h index d96788d5..67dad67f 100644 --- a/Sources/Qonversion/Qonversion.h +++ b/Sources/Qonversion/Qonversion.h @@ -89,6 +89,13 @@ NS_ASSUME_NONNULL_BEGIN */ + (void)products:(QNProductsCompletionHandler)completion; +/** + You can check if a user is eligible for an introductory offer, including a free trial. On the Apple platform, users who have not previously used an introductory offer for any products in the same subscription group are eligible for an introductory offer. Use this method to determine eligibility. + + You can show only a regular price for users who are not eligible for an introductory offer. + @param productIds products identifiers that must be checked + @param completion Completion block that include trial eligibility check result dictionary and error + */ + (void)checkTrialIntroEligibilityForProductIds:(NSArray *)productIds completion:(QNEligibilityCompletionHandler)completion; /** @@ -103,6 +110,11 @@ NS_ASSUME_NONNULL_BEGIN */ + (void)offerings:(QNOfferingsCompletionHandler)completion; +/** + Qonversion A/B tests help you grow your app revenue by making it easy to run and analyze paywall and promoted in-app product experiments. It gives you the power to measure your paywalls' performance before you roll them out widely. It is an out-of-the-box solution that does not require any third-party service. + + @param completion Completion block that include user experiments check result dictionary and error + */ + (void)experiments:(QNExperimentsCompletionHandler)completion; + (void)resetUser; From 067ead687540a71aed6d4c37b0dad439733c7d99 Mon Sep 17 00:00:00 2001 From: Surik Date: Tue, 19 Jan 2021 12:13:36 +0800 Subject: [PATCH 15/17] Fixed wrong type --- Sources/Qonversion/QNLaunchResult+Protected.h | 2 +- Sources/Qonversion/QNLaunchResult.h | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Sources/Qonversion/QNLaunchResult+Protected.h b/Sources/Qonversion/QNLaunchResult+Protected.h index 591fb0e3..b7264a3b 100644 --- a/Sources/Qonversion/QNLaunchResult+Protected.h +++ b/Sources/Qonversion/QNLaunchResult+Protected.h @@ -4,7 +4,7 @@ @property (nonatomic, copy) NSString *uid; @property (nonatomic, assign) NSUInteger timestamp; -@property (nonatomic, copy) NSArray *experiments; +@property (nonatomic, copy) NSDictionary *experiments; @property (nonatomic, copy) NSDictionary *permissions; @property (nonatomic, copy) NSDictionary *products; @property (nonatomic, copy) NSDictionary *userProducts; diff --git a/Sources/Qonversion/QNLaunchResult.h b/Sources/Qonversion/QNLaunchResult.h index b1419a31..4920294b 100644 --- a/Sources/Qonversion/QNLaunchResult.h +++ b/Sources/Qonversion/QNLaunchResult.h @@ -41,7 +41,7 @@ NS_SWIFT_NAME(Qonversion.LaunchResult) /** User A/B-test experiments */ -@property (nonatomic, copy, readonly) NSDictionary *experiments; +@property (nonatomic, copy) NSDictionary *experiments; /** User permissions */ From 8a3219c9f21b27c7afc1382b7d0822c1360a2cd1 Mon Sep 17 00:00:00 2001 From: Surik Date: Tue, 19 Jan 2021 14:52:51 +0800 Subject: [PATCH 16/17] Fixed warnings --- .../Qonversion/QNExperimentInfo+Protected.h | 2 +- Sources/Qonversion/QNMapper.h | 2 +- Sources/Qonversion/QNMapper.m | 8 +- Sources/Qonversion/QNProduct.m | 82 ++++++++++--------- Sources/Qonversion/QNProductCenterManager.h | 2 +- Sources/Qonversion/QNProductCenterManager.m | 10 +-- Sources/Qonversion/QNRequestSerializer.m | 2 +- Sources/Qonversion/QNStoreKitService.m | 2 +- 8 files changed, 56 insertions(+), 54 deletions(-) diff --git a/Sources/Qonversion/QNExperimentInfo+Protected.h b/Sources/Qonversion/QNExperimentInfo+Protected.h index 64bada75..ec002476 100644 --- a/Sources/Qonversion/QNExperimentInfo+Protected.h +++ b/Sources/Qonversion/QNExperimentInfo+Protected.h @@ -6,7 +6,7 @@ // Copyright © 2021 Qonversion Inc. All rights reserved. // -#import +#import "QNExperimentInfo.h" NS_ASSUME_NONNULL_BEGIN diff --git a/Sources/Qonversion/QNMapper.h b/Sources/Qonversion/QNMapper.h index 1ae307b3..15572003 100644 --- a/Sources/Qonversion/QNMapper.h +++ b/Sources/Qonversion/QNMapper.h @@ -8,7 +8,7 @@ + (QNLaunchResult * _Nonnull)fillLaunchResult:(NSDictionary * _Nullable)dict; -+ (NSDictionary *)mapProductsEligibility:(NSDictionary * _Nullable)dict; ++ (NSDictionary * _Nonnull)mapProductsEligibility:(NSDictionary * _Nullable)dict; + (NSInteger)mapInteger:(NSObject * _Nullable)object orReturn:(NSInteger)defaultValue; diff --git a/Sources/Qonversion/QNMapper.m b/Sources/Qonversion/QNMapper.m index ec6dd84e..0520df90 100644 --- a/Sources/Qonversion/QNMapper.m +++ b/Sources/Qonversion/QNMapper.m @@ -106,14 +106,14 @@ + (QNExperimentGroup * _Nonnull)fillExperimentGroup:(NSDictionary * _Nullable)di return [products copy]; } -+ (NSDictionary *)mapProductsEligibility:(NSDictionary * _Nullable)dict { ++ (NSDictionary * _Nonnull)mapProductsEligibility:(NSDictionary * _Nullable)dict { NSDictionary *introEligibilityStatuses = @{@"non_intro_or_trial_product": @(QNIntroEligibilityStatusNonIntroProduct), @"intro_or_trial_eligible": @(QNIntroEligibilityStatusEligible), @"intro_or_trial_ineligible": @(QNIntroEligibilityStatusIneligible)}; NSArray *enrichedProducts = dict[@"products_enriched"]; - NSMutableDictionary *products = [NSMutableDictionary new]; + NSMutableDictionary *eligibilityInfo = [NSMutableDictionary new]; for (NSDictionary *item in enrichedProducts) { NSDictionary *productData = item[@"product"]; @@ -128,10 +128,10 @@ + (QNExperimentGroup * _Nonnull)fillExperimentGroup:(NSDictionary * _Nullable)di QNIntroEligibilityStatus eligibilityStatus = eligibilityValue ? eligibilityValue.integerValue : QNIntroEligibilityStatusUnknown; QNIntroEligibility *eligibility = [[QNIntroEligibility alloc] initWithStatus:eligibilityStatus]; - products[product.qonversionID] = eligibility; + eligibilityInfo[product.qonversionID] = eligibility; } - return [products copy]; + return [eligibilityInfo copy]; } + (QNPermission * _Nonnull)fillPermission:(NSDictionary *)dict { diff --git a/Sources/Qonversion/QNProduct.m b/Sources/Qonversion/QNProduct.m index 0b9dae55..40bc2e0e 100644 --- a/Sources/Qonversion/QNProduct.m +++ b/Sources/Qonversion/QNProduct.m @@ -33,46 +33,48 @@ - (QNTrialDuration)trialDuration { QNTrialDuration duration = QNTrialDurationNotAvailable; - if (@available(iOS 11.2, macOS 10.13.2, watchOS 6.2, *) && self.skProduct.introductoryPrice) { - duration = QNTrialDurationOther; - - SKProductPeriodUnit unit = self.skProduct.introductoryPrice.subscriptionPeriod.unit; - NSUInteger numberOfUnits = self.skProduct.introductoryPrice.subscriptionPeriod.numberOfUnits; - switch (unit) { - case SKProductPeriodUnitDay: - if (numberOfUnits == 3) { - duration = QNTrialDurationThreeDays; - } - break; - - case SKProductPeriodUnitWeek: - if (numberOfUnits == 1) { - duration = QNTrialDurationWeek; - } else if (numberOfUnits == 2) { - duration = QNTrialDurationTwoWeeks; - } - break; - - case SKProductPeriodUnitMonth: - if (numberOfUnits == 1) { - duration = QNTrialDurationMonth; - } else if (numberOfUnits == 2) { - duration = QNTrialDurationTwoMonths; - } else if (numberOfUnits == 3) { - duration = QNTrialDurationThreeMonths; - } else if (numberOfUnits == 6) { - duration = QNTrialDurationSixMonths; - } - break; - - case SKProductPeriodUnitYear: - if (numberOfUnits == 1) { - duration = QNTrialDurationYear; - } - break; - - default: - break; + if (@available(iOS 11.2, macOS 10.13.2, watchOS 6.2, tvOS 11.2, *)) { + if (self.skProduct.introductoryPrice) { + duration = QNTrialDurationOther; + + SKProductPeriodUnit unit = self.skProduct.introductoryPrice.subscriptionPeriod.unit; + NSUInteger numberOfUnits = self.skProduct.introductoryPrice.subscriptionPeriod.numberOfUnits; + switch (unit) { + case SKProductPeriodUnitDay: + if (numberOfUnits == 3) { + duration = QNTrialDurationThreeDays; + } + break; + + case SKProductPeriodUnitWeek: + if (numberOfUnits == 1) { + duration = QNTrialDurationWeek; + } else if (numberOfUnits == 2) { + duration = QNTrialDurationTwoWeeks; + } + break; + + case SKProductPeriodUnitMonth: + if (numberOfUnits == 1) { + duration = QNTrialDurationMonth; + } else if (numberOfUnits == 2) { + duration = QNTrialDurationTwoMonths; + } else if (numberOfUnits == 3) { + duration = QNTrialDurationThreeMonths; + } else if (numberOfUnits == 6) { + duration = QNTrialDurationSixMonths; + } + break; + + case SKProductPeriodUnitYear: + if (numberOfUnits == 1) { + duration = QNTrialDurationYear; + } + break; + + default: + break; + } } } diff --git a/Sources/Qonversion/QNProductCenterManager.h b/Sources/Qonversion/QNProductCenterManager.h index 43b1ad04..9d38f5ac 100644 --- a/Sources/Qonversion/QNProductCenterManager.h +++ b/Sources/Qonversion/QNProductCenterManager.h @@ -10,7 +10,7 @@ NS_ASSUME_NONNULL_BEGIN - (void)setPromoPurchasesDelegate:(id)delegate; -- (void)launchWithCompletion:(QNLaunchCompletionHandler)completion; +- (void)launchWithCompletion:(nullable QNLaunchCompletionHandler)completion; - (void)checkPermissions:(QNPermissionCompletionHandler)completion; - (void)purchase:(NSString *)productID completion:(QNPurchaseCompletionHandler)completion; - (void)restoreWithCompletion:(QNRestoreCompletionHandler)completion; diff --git a/Sources/Qonversion/QNProductCenterManager.m b/Sources/Qonversion/QNProductCenterManager.m index 9324676e..e277a782 100644 --- a/Sources/Qonversion/QNProductCenterManager.m +++ b/Sources/Qonversion/QNProductCenterManager.m @@ -71,7 +71,7 @@ - (QNLaunchResult *)launchModel { return [self.persistentStorage loadObjectForKey:kLaunchResult]; } -- (void)launchWithCompletion:(QNLaunchCompletionHandler)completion { +- (void)launchWithCompletion:(nullable QNLaunchCompletionHandler)completion { _launchingFinished = NO; __block __weak QNProductCenterManager *weakSelf = self; @@ -170,7 +170,7 @@ - (void)preparDelayedPurchase:(NSString *)productID completion:(QNPurchaseComple - (void)processPurchase:(NSString *)productID completion:(QNPurchaseCompletionHandler)completion { QNProduct *product = [self QNProduct:productID]; if (!product) { - QONVERSION_LOG([NSString stringWithFormat:@"❌ product with id: %@ not found", product.qonversionID]); + QONVERSION_LOG(@"❌ product with id: %@ not found", product.qonversionID); run_block_on_main(completion, @{}, [QNErrors errorWithQNErrorCode:QNErrorProductNotFound], NO); return; } @@ -185,7 +185,7 @@ - (void)processPurchase:(NSString *)productID completion:(QNPurchaseCompletionHa return; } - QONVERSION_LOG([NSString stringWithFormat:@"❌ product with id: %@ not found", product.qonversionID]); + QONVERSION_LOG(@"❌ product with id: %@ not found", product.qonversionID); run_block_on_main(completion, @{}, [QNErrors errorWithQNErrorCode:QNErrorProductNotFound], NO); } @@ -345,7 +345,7 @@ - (void)checkTrialIntroEligibilityForProductIds:(NSArray *)productId for (NSString *identifier in uniqueProductIdentifiers) { QNProduct *product = result[identifier]; if (!product) { - QONVERSION_LOG([NSString stringWithFormat:@"❌ product with id: %@ not found", product.qonversionID]); + QONVERSION_LOG(@"❌ product with id: %@ not found", product.qonversionID); run_block_on_main(completion, @{}, [QNErrors errorWithQNErrorCode:QNErrorProductNotFound]); return; } @@ -359,7 +359,7 @@ - (void)checkTrialIntroEligibilityForProductIds:(NSArray *)productId } NSDictionary *eligibilityData = [QNMapper mapProductsEligibility:result.data]; - NSMutableDictionary *resultEligibility = [NSMutableDictionary new]; + NSMutableDictionary *resultEligibility = [NSMutableDictionary new]; for (NSString *identifier in uniqueProductIdentifiers) { QNIntroEligibility *item = eligibilityData[identifier]; diff --git a/Sources/Qonversion/QNRequestSerializer.m b/Sources/Qonversion/QNRequestSerializer.m index 4ac9624e..cc2f73a3 100644 --- a/Sources/Qonversion/QNRequestSerializer.m +++ b/Sources/Qonversion/QNRequestSerializer.m @@ -78,7 +78,7 @@ - (NSDictionary *)introTrialEligibilityDataForProducts:(NSArray *)p NSMutableDictionary *param = [NSMutableDictionary new]; param[@"store_id"] = product.storeID; - if (@available(iOS 12.0, macOS 10.14, watchOS 6.2, *)) { + if (@available(iOS 12.0, macOS 10.14, watchOS 6.2, tvOS 12.0, *)) { param[@"subscription_group_identifier"] = product.skProduct.subscriptionGroupIdentifier; } diff --git a/Sources/Qonversion/QNStoreKitService.m b/Sources/Qonversion/QNStoreKitService.m index 23025fbe..eab543c3 100644 --- a/Sources/Qonversion/QNStoreKitService.m +++ b/Sources/Qonversion/QNStoreKitService.m @@ -179,7 +179,7 @@ - (void)handleTransaction:(nonnull SKPaymentTransaction *)transaction { - (void)productsRequest:(nonnull SKProductsRequest *)request didReceiveResponse:(nonnull SKProductsResponse *)response { if (response.invalidProductIdentifiers.count > 0) { - QONVERSION_LOG([NSString stringWithFormat:@"❌ Invalid store products identifiers: %@", response.invalidProductIdentifiers]); + QONVERSION_LOG(@"❌ Invalid store products identifiers: %@", response.invalidProductIdentifiers); } BOOL autoTracked = NO; From c2f6b3c01b418b335fb5bd7db9d6f196a208f814 Mon Sep 17 00:00:00 2001 From: Surik Date: Tue, 19 Jan 2021 14:53:02 +0800 Subject: [PATCH 17/17] Bump version to 2.8.0 --- Qonversion.podspec | 2 +- Qonversion.xcodeproj/project.pbxproj | 4 ++-- Sources/Qonversion/QNConstants.m | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/Qonversion.podspec b/Qonversion.podspec index df9dcb2d..b16dcb3f 100644 --- a/Qonversion.podspec +++ b/Qonversion.podspec @@ -1,6 +1,6 @@ Pod::Spec.new do |s| s.name = 'Qonversion' - s.version = '2.7.0' + s.version = '2.8.0' s.summary = 'qonversion.io' s.description = <<-DESC Deep Analytics for iOS Subscriptions diff --git a/Qonversion.xcodeproj/project.pbxproj b/Qonversion.xcodeproj/project.pbxproj index 7295e9d9..b5be369d 100644 --- a/Qonversion.xcodeproj/project.pbxproj +++ b/Qonversion.xcodeproj/project.pbxproj @@ -1185,7 +1185,7 @@ "@loader_path/Frameworks", ); MACOSX_DEPLOYMENT_TARGET = 10.12; - MARKETING_VERSION = 2.7.0; + MARKETING_VERSION = 2.8.0; MODULEMAP_FILE = Framework/Qonversion.modulemap; PRODUCT_BUNDLE_IDENTIFIER = com.qonversion.Qonversion; PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)"; @@ -1217,7 +1217,7 @@ "@loader_path/Frameworks", ); MACOSX_DEPLOYMENT_TARGET = 10.12; - MARKETING_VERSION = 2.7.0; + MARKETING_VERSION = 2.8.0; MODULEMAP_FILE = Framework/Qonversion.modulemap; PRODUCT_BUNDLE_IDENTIFIER = com.qonversion.Qonversion; PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)"; diff --git a/Sources/Qonversion/QNConstants.m b/Sources/Qonversion/QNConstants.m index bee21a93..3d742fc5 100644 --- a/Sources/Qonversion/QNConstants.m +++ b/Sources/Qonversion/QNConstants.m @@ -1,6 +1,6 @@ #import "QNConstants.h" -NSString *const keyQVersion = @"2.7.0"; +NSString *const keyQVersion = @"2.8.0"; NSString *const keyQUnknownLibrary = @"unknown"; NSString *const keyQUnknownVersion = @"unknown"; NSString *const keyQInternalUserID = @"keyQInternalUserID";