diff --git a/Framework/Info.plist b/Framework/Info.plist index e2e7f910..c0353421 100644 --- a/Framework/Info.plist +++ b/Framework/Info.plist @@ -15,7 +15,7 @@ CFBundlePackageType FMWK CFBundleShortVersionString - 5.11.1 + 5.12.0 CFBundleSignature ???? CFBundleVersion diff --git a/Framework/QonversionFramework.h b/Framework/QonversionFramework.h index 5c3e0f68..81ea4104 100644 --- a/Framework/QonversionFramework.h +++ b/Framework/QonversionFramework.h @@ -24,5 +24,6 @@ #import #import #import +#import #import diff --git a/Qonversion.podspec b/Qonversion.podspec index e2f5f324..c7565dcc 100644 --- a/Qonversion.podspec +++ b/Qonversion.podspec @@ -3,7 +3,7 @@ Pod::Spec.new do |s| idfa_exclude_files = ['Sources/Qonversion/IDFA'] s.name = 'Qonversion' s.swift_version = '5.5' - s.version = '5.11.1' + s.version = '5.12.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 a44040d9..48957334 100644 --- a/Qonversion.xcodeproj/project.pbxproj +++ b/Qonversion.xcodeproj/project.pbxproj @@ -63,6 +63,8 @@ 701922732B10981200724926 /* QONSubscriptionPeriod.h in Headers */ = {isa = PBXBuildFile; fileRef = 701922712B10981200724926 /* QONSubscriptionPeriod.h */; settings = {ATTRIBUTES = (Public, ); }; }; 701922742B10981200724926 /* QONSubscriptionPeriod.m in Sources */ = {isa = PBXBuildFile; fileRef = 701922722B10981200724926 /* QONSubscriptionPeriod.m */; }; 701922762B10AB3300724926 /* QONSubscriptionPeriod+Protected.h in Headers */ = {isa = PBXBuildFile; fileRef = 701922752B10AB3300724926 /* QONSubscriptionPeriod+Protected.h */; }; + 701BAC102C524626009B16FB /* QONPurchaseOptions.m in Sources */ = {isa = PBXBuildFile; fileRef = 701BAC0F2C524626009B16FB /* QONPurchaseOptions.m */; }; + 701BAC112C524626009B16FB /* QONPurchaseOptions.h in Headers */ = {isa = PBXBuildFile; fileRef = 701BAC0E2C524626009B16FB /* QONPurchaseOptions.h */; settings = {ATTRIBUTES = (Public, ); }; }; 702394912923EBF3003126D5 /* QONNotificationsService.h in Headers */ = {isa = PBXBuildFile; fileRef = 7023948F2923EBF3003126D5 /* QONNotificationsService.h */; }; 702394922923EBF3003126D5 /* QONNotificationsService.m in Sources */ = {isa = PBXBuildFile; fileRef = 702394902923EBF3003126D5 /* QONNotificationsService.m */; }; 70283DF729F66FAC00D138BC /* PurchasesMapper.swift in Sources */ = {isa = PBXBuildFile; fileRef = 70283DF629F66FAC00D138BC /* PurchasesMapper.swift */; }; @@ -72,6 +74,7 @@ 7042E7832C1C5DB700C5AECF /* QONFallbackService.m in Sources */ = {isa = PBXBuildFile; fileRef = 7042E7802C1C5DB700C5AECF /* QONFallbackService.m */; }; 7042E7872C1C5DDE00C5AECF /* QONFallbackMapper.h in Headers */ = {isa = PBXBuildFile; fileRef = 7042E7842C1C5DDE00C5AECF /* QONFallbackMapper.h */; }; 7042E7882C1C5DDE00C5AECF /* QONFallbackMapper.m in Sources */ = {isa = PBXBuildFile; fileRef = 7042E7852C1C5DDE00C5AECF /* QONFallbackMapper.m */; }; + 70454FFF2C5FD4FA00B03017 /* Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 70454FFE2C5FD4FA00B03017 /* Extensions.swift */; }; 707734F42A9F607700CFF742 /* QONRemoteConfigurationSource.h in Headers */ = {isa = PBXBuildFile; fileRef = 707734F22A9F607700CFF742 /* QONRemoteConfigurationSource.h */; settings = {ATTRIBUTES = (Public, ); }; }; 707734F52A9F607700CFF742 /* QONRemoteConfigurationSource.m in Sources */ = {isa = PBXBuildFile; fileRef = 707734F32A9F607700CFF742 /* QONRemoteConfigurationSource.m */; }; 707734F72A9F6B8700CFF742 /* QONRemoteConfigurationSource+Protected.h in Headers */ = {isa = PBXBuildFile; fileRef = 707734F62A9F6B8700CFF742 /* QONRemoteConfigurationSource+Protected.h */; }; @@ -91,6 +94,9 @@ 70B9A9F1297AB8A700BD30FD /* QONAutomationsNavigationController.h in Headers */ = {isa = PBXBuildFile; fileRef = 70B9A9EF297AB8A700BD30FD /* QONAutomationsNavigationController.h */; }; 70B9A9F2297AB8A700BD30FD /* QONAutomationsNavigationController.m in Sources */ = {isa = PBXBuildFile; fileRef = 70B9A9F0297AB8A700BD30FD /* QONAutomationsNavigationController.m */; }; 70BAB0642B58306E00D19A6A /* expected_entitlements.json in Resources */ = {isa = PBXBuildFile; fileRef = 70893C992B3EC136002C6B82 /* expected_entitlements.json */; }; + 70CB7CDD2C246DF200241FF1 /* QONPromotionalOffer.h in Headers */ = {isa = PBXBuildFile; fileRef = 70CB7CDB2C246DF200241FF1 /* QONPromotionalOffer.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 70CB7CDE2C246DF200241FF1 /* QONPromotionalOffer.m in Sources */ = {isa = PBXBuildFile; fileRef = 70CB7CDC2C246DF200241FF1 /* QONPromotionalOffer.m */; }; + 70CB7CE12C247A8A00241FF1 /* QONPromotionalOffer+Protected.h in Headers */ = {isa = PBXBuildFile; fileRef = 70CB7CDF2C247A8A00241FF1 /* QONPromotionalOffer+Protected.h */; }; 70CB7CE42C2DB37A00241FF1 /* qonversion_ios_fallbacks.json in Resources */ = {isa = PBXBuildFile; fileRef = 70CB7CE32C2DB37900241FF1 /* qonversion_ios_fallbacks.json */; }; 70D05A8E29C9FC1600EA5DDF /* QONRemoteConfigManager.h in Headers */ = {isa = PBXBuildFile; fileRef = 70D05A8C29C9FC1600EA5DDF /* QONRemoteConfigManager.h */; }; 70D05A8F29C9FC1600EA5DDF /* QONRemoteConfigManager.m in Sources */ = {isa = PBXBuildFile; fileRef = 70D05A8D29C9FC1600EA5DDF /* QONRemoteConfigManager.m */; }; @@ -336,7 +342,7 @@ 459DAB79243E329F0011ECF3 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 459DAB97243E333C0011ECF3 /* README.md */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = net.daringfireball.markdown; path = README.md; sourceTree = ""; }; 459DAB98243E333C0011ECF3 /* LICENSE */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text; path = LICENSE; sourceTree = ""; }; - 459DAB99243E333C0011ECF3 /* Qonversion.podspec */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text; path = Qonversion.podspec; sourceTree = ""; }; + 459DAB99243E333C0011ECF3 /* Qonversion.podspec */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text; path = Qonversion.podspec; sourceTree = ""; xcLanguageSpecificationIdentifier = xcode.lang.ruby; }; 459DAB9F243E33470011ECF3 /* report.html */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.html; path = report.html; sourceTree = ""; }; 459DABA0243E33470011ECF3 /* report.junit */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xml; path = report.junit; sourceTree = ""; }; 459DABA1243E33470011ECF3 /* Appfile */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text; path = Appfile; sourceTree = ""; }; @@ -385,6 +391,8 @@ 701922712B10981200724926 /* QONSubscriptionPeriod.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = QONSubscriptionPeriod.h; sourceTree = ""; }; 701922722B10981200724926 /* QONSubscriptionPeriod.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = QONSubscriptionPeriod.m; sourceTree = ""; }; 701922752B10AB3300724926 /* QONSubscriptionPeriod+Protected.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "QONSubscriptionPeriod+Protected.h"; sourceTree = ""; }; + 701BAC0E2C524626009B16FB /* QONPurchaseOptions.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = QONPurchaseOptions.h; sourceTree = ""; }; + 701BAC0F2C524626009B16FB /* QONPurchaseOptions.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = QONPurchaseOptions.m; sourceTree = ""; }; 7023948F2923EBF3003126D5 /* QONNotificationsService.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = QONNotificationsService.h; sourceTree = ""; }; 702394902923EBF3003126D5 /* QONNotificationsService.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = QONNotificationsService.m; sourceTree = ""; }; 70283DF629F66FAC00D138BC /* PurchasesMapper.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PurchasesMapper.swift; sourceTree = ""; }; @@ -394,6 +402,7 @@ 7042E7802C1C5DB700C5AECF /* QONFallbackService.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = QONFallbackService.m; sourceTree = ""; }; 7042E7842C1C5DDE00C5AECF /* QONFallbackMapper.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = QONFallbackMapper.h; sourceTree = ""; }; 7042E7852C1C5DDE00C5AECF /* QONFallbackMapper.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = QONFallbackMapper.m; sourceTree = ""; }; + 70454FFE2C5FD4FA00B03017 /* Extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Extensions.swift; sourceTree = ""; }; 7052136F29F1807A00164AAF /* PurchasesMapper.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PurchasesMapper.swift; sourceTree = ""; }; 707734F22A9F607700CFF742 /* QONRemoteConfigurationSource.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = QONRemoteConfigurationSource.h; sourceTree = ""; }; 707734F32A9F607700CFF742 /* QONRemoteConfigurationSource.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = QONRemoteConfigurationSource.m; sourceTree = ""; }; @@ -413,6 +422,9 @@ 70B917662B34284200BF0689 /* QONTransaction+Protected.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "QONTransaction+Protected.h"; sourceTree = ""; }; 70B9A9EF297AB8A700BD30FD /* QONAutomationsNavigationController.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = QONAutomationsNavigationController.h; sourceTree = ""; }; 70B9A9F0297AB8A700BD30FD /* QONAutomationsNavigationController.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = QONAutomationsNavigationController.m; sourceTree = ""; }; + 70CB7CDB2C246DF200241FF1 /* QONPromotionalOffer.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = QONPromotionalOffer.h; sourceTree = ""; }; + 70CB7CDC2C246DF200241FF1 /* QONPromotionalOffer.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = QONPromotionalOffer.m; sourceTree = ""; }; + 70CB7CDF2C247A8A00241FF1 /* QONPromotionalOffer+Protected.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "QONPromotionalOffer+Protected.h"; sourceTree = ""; }; 70CB7CE32C2DB37900241FF1 /* qonversion_ios_fallbacks.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; path = qonversion_ios_fallbacks.json; sourceTree = ""; }; 70D05A8C29C9FC1600EA5DDF /* QONRemoteConfigManager.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = QONRemoteConfigManager.h; sourceTree = ""; }; 70D05A8D29C9FC1600EA5DDF /* QONRemoteConfigManager.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = QONRemoteConfigManager.m; sourceTree = ""; }; @@ -909,6 +921,7 @@ 70ED967029FAC8D3005F5D00 /* QonversionSwift.swift */, 70EC019C29EEE94300E686E2 /* StoreKit2Service.swift */, 70283DF629F66FAC00D138BC /* PurchasesMapper.swift */, + 70454FFE2C5FD4FA00B03017 /* Extensions.swift */, ); name = Swift; path = Sources/Swift; @@ -1046,6 +1059,8 @@ 895731D526DD03A2009507A6 /* QONStoreKitSugare.m */, 895731D626DD03A2009507A6 /* QONAutomationsEventType.h */, 895731D726DD03A2009507A6 /* Qonversion.h */, + 701BAC0E2C524626009B16FB /* QONPurchaseOptions.h */, + 701BAC0F2C524626009B16FB /* QONPurchaseOptions.m */, 895731D826DD03A2009507A6 /* QONErrors.m */, 895731D926DD03A2009507A6 /* QONAutomationsEvent.m */, 895731DA26DD03A2009507A6 /* QONOffering.m */, @@ -1069,6 +1084,8 @@ A1839793226FD80F320A246F /* QONUserProperty.h */, A1839ACEB01696C50FEBB4F4 /* QONUserProperties.m */, A1839DCD5FA33E85193A2684 /* QONUserProperties.h */, + 70CB7CDB2C246DF200241FF1 /* QONPromotionalOffer.h */, + 70CB7CDC2C246DF200241FF1 /* QONPromotionalOffer.m */, ); path = Public; sourceTree = ""; @@ -1411,6 +1428,7 @@ 707734F62A9F6B8700CFF742 /* QONRemoteConfigurationSource+Protected.h */, 702DBDEB2A3216C900D590D0 /* QONExperiment+Protected.h */, 8957324226DD03A3009507A6 /* QONUser+Protected.h */, + 70CB7CDF2C247A8A00241FF1 /* QONPromotionalOffer+Protected.h */, 701922752B10AB3300724926 /* QONSubscriptionPeriod+Protected.h */, 70B917662B34284200BF0689 /* QONTransaction+Protected.h */, 8957324326DD03A3009507A6 /* QONOfferings+Protected.h */, @@ -1679,6 +1697,7 @@ 70E99384291BC60A006E0A64 /* QONEnvironment.h in Headers */, 8957330226DD03A3009507A6 /* QNUserInfoService.h in Headers */, 8957329D26DD03A3009507A6 /* QONActionResult.h in Headers */, + 70CB7CE12C247A8A00241FF1 /* QONPromotionalOffer+Protected.h in Headers */, 895732B626DD03A3009507A6 /* QONAutomationsScreenProcessor.h in Headers */, 895732AB26DD03A3009507A6 /* QONAutomationsEventsMapper.h in Headers */, 70D0E2B8291A9BE3004E8DE8 /* QONConfiguration.h in Headers */, @@ -1735,6 +1754,7 @@ 70B917642B3314BD00BF0689 /* QONTransaction.h in Headers */, 8957328926DD03A3009507A6 /* QONProduct.h in Headers */, 702DBDEC2A3216C900D590D0 /* QONExperiment+Protected.h in Headers */, + 70CB7CDD2C246DF200241FF1 /* QONPromotionalOffer.h in Headers */, 8957328D26DD03A3009507A6 /* QONOfferings.h in Headers */, 895732F926DD03A3009507A6 /* QNIdentityManagerInterface.h in Headers */, 7042E7822C1C5DB700C5AECF /* QONFallbackService.h in Headers */, @@ -1747,6 +1767,7 @@ 895732C726DD03A3009507A6 /* QNAPIConstants.h in Headers */, 8957327E26DD03A3009507A6 /* QONIntroEligibility.h in Headers */, 8957328226DD03A3009507A6 /* QONLaunchResult.h in Headers */, + 701BAC112C524626009B16FB /* QONPurchaseOptions.h in Headers */, 895732C526DD03A3009507A6 /* QNRequestBuilder.h in Headers */, A1839332BA452C481BDC499C /* QONExceptionManager.h in Headers */, 7042E7872C1C5DDE00C5AECF /* QONFallbackMapper.h in Headers */, @@ -2051,6 +2072,7 @@ 8957329A26DD03A3009507A6 /* QONErrors.m in Sources */, 895732CE26DD03A3009507A6 /* QNProperties.m in Sources */, 8957327A26DD03A3009507A6 /* QNDevice+Advertising.m in Sources */, + 70CB7CDE2C246DF200241FF1 /* QONPromotionalOffer.m in Sources */, 895732C626DD03A3009507A6 /* QNAPIConstants.m in Sources */, 6A21BF552AB205AC005BDA7C /* QONRequest.m in Sources */, 70B917652B3314BD00BF0689 /* QONTransaction.m in Sources */, @@ -2095,6 +2117,7 @@ 6A121DB32BBB10AA0073B330 /* QONRemoteConfigListRequestData.m in Sources */, 895732A426DD03A3009507A6 /* QONUserActionPoint.m in Sources */, 8957330A26DE5532009507A6 /* QNKeyedArchiver.m in Sources */, + 70454FFF2C5FD4FA00B03017 /* Extensions.swift in Sources */, 895732FC26DD03A3009507A6 /* QNStoreKitService.m in Sources */, 895732FA26DD03A3009507A6 /* QNIdentityManager.m in Sources */, 8957330026DD03A3009507A6 /* QNUserInfoService.m in Sources */, @@ -2115,6 +2138,7 @@ 702394922923EBF3003126D5 /* QONNotificationsService.m in Sources */, 8957328A26DD03A3009507A6 /* QONActionResult.m in Sources */, 70283DF729F66FAC00D138BC /* PurchasesMapper.swift in Sources */, + 701BAC102C524626009B16FB /* QONPurchaseOptions.m in Sources */, 8957328F26DD03A3009507A6 /* QONUser.m in Sources */, 895732B926DD03A3009507A6 /* QNUserDefaultsStorage.m in Sources */, A1839D7723FB8E18EC0294C9 /* QONExceptionManager.m in Sources */, diff --git a/Sources/Qonversion/Public/QONConfiguration.m b/Sources/Qonversion/Public/QONConfiguration.m index 54240cda..d3096115 100644 --- a/Sources/Qonversion/Public/QONConfiguration.m +++ b/Sources/Qonversion/Public/QONConfiguration.m @@ -9,7 +9,7 @@ #import "QONConfiguration.h" #import "QNAPIConstants.h" -static NSString *const kSDKVersion = @"5.11.1"; +static NSString *const kSDKVersion = @"5.12.0"; @interface QONConfiguration () diff --git a/Sources/Qonversion/Public/QONErrors.h b/Sources/Qonversion/Public/QONErrors.h index a375e230..5d2ce103 100644 --- a/Sources/Qonversion/Public/QONErrors.h +++ b/Sources/Qonversion/Public/QONErrors.h @@ -67,7 +67,7 @@ typedef NS_ERROR_ENUM(QONErrorDomain, QONError) { QONErrorStorePaymentDeferred = 18, // No remote configuration for the current user - QONErrorRemoteConfigurationNotAvailable = 19, + QONErrorRemoteConfigurationNotAvailable = 19 } NS_SWIFT_NAME(Qonversion.Error); diff --git a/Sources/Qonversion/Public/QONLaunchResult.h b/Sources/Qonversion/Public/QONLaunchResult.h index d785fa8d..d861ff2f 100644 --- a/Sources/Qonversion/Public/QONLaunchResult.h +++ b/Sources/Qonversion/Public/QONLaunchResult.h @@ -2,7 +2,7 @@ NS_ASSUME_NONNULL_BEGIN -@class QONEntitlement, QONProduct, QONOfferings, QONIntroEligibility, QONUser, QONRemoteConfig, QONRemoteConfigList, QONUserProperties; +@class QONEntitlement, QONProduct, QONOfferings, QONIntroEligibility, QONUser, QONRemoteConfig, QONRemoteConfigList, QONUserProperties, QONPromotionalOffer; typedef NS_ENUM(NSInteger, QONAttributionProvider) { QONAttributionProviderAppsFlyer = 0, @@ -74,6 +74,8 @@ typedef void (^QONOfferingsCompletionHandler)(QONOfferings *_Nullable offerings, typedef void (^QONUserPropertiesCompletionHandler)(QONUserProperties *_Nullable userProperties, NSError *_Nullable error) NS_SWIFT_NAME(Qonversion.UserPropertiesCompletionHandler); -typedef void (^QONDefaultCompletionHandler)(BOOL success, NSError *_Nullable error) NS_SWIFT_NAME(Qonversion.DefaulthCompletionHandler); +typedef void (^QONDefaultCompletionHandler)(BOOL success, NSError *_Nullable error) NS_SWIFT_NAME(Qonversion.DefaultCompletionHandler); + +typedef void (^QONPromotionalOfferCompletionHandler)(QONPromotionalOffer * _Nullable promotionalOffer, NSError *_Nullable error) NS_SWIFT_NAME(Qonversion.PromotionalOfferCompletionHandler); NS_ASSUME_NONNULL_END diff --git a/Sources/Qonversion/Public/QONPromotionalOffer.h b/Sources/Qonversion/Public/QONPromotionalOffer.h new file mode 100644 index 00000000..d81b8cda --- /dev/null +++ b/Sources/Qonversion/Public/QONPromotionalOffer.h @@ -0,0 +1,23 @@ +// +// QONPromotionalOffer.h +// Qonversion +// +// Created by Suren Sarkisyan on 20.06.2024. +// Copyright © 2024 Qonversion Inc. All rights reserved. +// + +#import +#import + +NS_ASSUME_NONNULL_BEGIN + +NS_SWIFT_NAME(Qonversion.PromotionalOffer) +API_AVAILABLE(ios(12.2), macos(10.14.4), watchos(6.2), visionos(1.0)) +@interface QONPromotionalOffer : NSObject + +@property (nonatomic, strong) SKProductDiscount *productDiscount; +@property (nonatomic, strong) SKPaymentDiscount *paymentDiscount; + +@end + +NS_ASSUME_NONNULL_END diff --git a/Sources/Qonversion/Public/QONPromotionalOffer.m b/Sources/Qonversion/Public/QONPromotionalOffer.m new file mode 100644 index 00000000..777fa753 --- /dev/null +++ b/Sources/Qonversion/Public/QONPromotionalOffer.m @@ -0,0 +1,24 @@ +// +// QONPromotionalOffer.m +// Qonversion +// +// Created by Suren Sarkisyan on 20.06.2024. +// Copyright © 2024 Qonversion Inc. All rights reserved. +// + +#import "QONPromotionalOffer.h" + +@implementation QONPromotionalOffer + +- (instancetype)initWithProductDiscount:(SKProductDiscount *)productDiscount paymentDiscount:(SKPaymentDiscount *)paymentDiscount { + self = [super init]; + + if (self) { + _productDiscount = productDiscount; + _paymentDiscount = paymentDiscount; + } + + return self; +} + +@end diff --git a/Sources/Qonversion/Public/QONPurchaseOptions.h b/Sources/Qonversion/Public/QONPurchaseOptions.h new file mode 100644 index 00000000..180a7b4d --- /dev/null +++ b/Sources/Qonversion/Public/QONPurchaseOptions.h @@ -0,0 +1,49 @@ +// +// QONPurchaseOptions.h +// Qonversion +// +// Created by Suren Sarkisyan on 25.07.2024. +// Copyright © 2024 Qonversion Inc. All rights reserved. +// + +#import + +NS_ASSUME_NONNULL_BEGIN + +NS_SWIFT_NAME(Qonversion.PurchaseOptions) +/** + Instances of this class should be used to add additional options to the purchase process. + */ +@interface QONPurchaseOptions : NSObject + +// Quantity of product purchasing. Use for consumable in-app products. +@property (nonatomic, assign) NSInteger quantity; + +// Context keys associated with a purchase. Use this field to associate a purchase with a concrete remote config. +@property (nonatomic, copy, nullable) NSArray *contextKeys; + +/** + Initialize purchase options with quantity. + @param quantity quantity of product purchasing. Use for consumable in-app products. + @return QONPurchaseOptions instance + */ +- (instancetype)initWithQuantity:(NSInteger)quantity NS_SWIFT_UNAVAILABLE("Use swift style initializer instead."); + +/** + Initialize purchase options with quantity and context keys. + @param quantity quantity of product purchasing. Use for consumable in-app products. + @param contextKeys context keys associated with a purchase. Use this field to associate a purchase with a concrete remote config. + @return QONPurchaseOptions instance + */ +- (instancetype)initWithQuantity:(NSInteger)quantity contextKeys:(NSArray * _Nullable)contextKeys NS_SWIFT_UNAVAILABLE("Use swift style initializer instead."); + +/** + Initialize purchase options with context keys. + @param contextKeys context keys associated with a purchase. Use this field to associate a purchase with a concrete remote config. + @return QONPurchaseOptions instance + */ +- (instancetype)initWithContextKeys:(NSArray * _Nullable)contextKeys NS_SWIFT_UNAVAILABLE("Use swift style initializer instead."); + +@end + +NS_ASSUME_NONNULL_END diff --git a/Sources/Qonversion/Public/QONPurchaseOptions.m b/Sources/Qonversion/Public/QONPurchaseOptions.m new file mode 100644 index 00000000..62199de6 --- /dev/null +++ b/Sources/Qonversion/Public/QONPurchaseOptions.m @@ -0,0 +1,46 @@ +// +// QONPurchaseOptions.m +// Qonversion +// +// Created by Suren Sarkisyan on 25.07.2024. +// Copyright © 2024 Qonversion Inc. All rights reserved. +// + +#import "QONPurchaseOptions.h" + +@implementation QONPurchaseOptions + +- (instancetype)initWithQuantity:(NSInteger)quantity { + return [self initWithQuantity:quantity contextKeys:nil]; +} + +- (instancetype)initWithContextKeys:(NSArray * _Nullable)contextKeys { + return [self initWithQuantity:1 contextKeys:contextKeys]; +} + +- (instancetype)initWithQuantity:(NSInteger)quantity contextKeys:(NSArray * _Nullable)contextKeys { + self = [super init]; + + if (self) { + _quantity = quantity; + _contextKeys = contextKeys; + } + + return self; +} + +- (instancetype)initWithCoder:(NSCoder *)coder { + self = [super init]; + if (self) { + _quantity = [coder decodeIntForKey:NSStringFromSelector(@selector(quantity))]; + _contextKeys = [coder decodeObjectForKey:NSStringFromSelector(@selector(contextKeys))]; + } + return self; +} + +- (void)encodeWithCoder:(NSCoder *)coder { + [coder encodeInteger:_quantity forKey:NSStringFromSelector(@selector(quantity))]; + [coder encodeObject:_contextKeys forKey:NSStringFromSelector(@selector(contextKeys))]; +} + +@end diff --git a/Sources/Qonversion/Public/Qonversion.h b/Sources/Qonversion/Public/Qonversion.h index 837e4abd..6833635d 100644 --- a/Sources/Qonversion/Public/Qonversion.h +++ b/Sources/Qonversion/Public/Qonversion.h @@ -21,6 +21,7 @@ #import "QONUserProperties.h" #import "QONUserProperty.h" #import "QONSubscriptionPeriod.h" +#import "QONPurchaseOptions.h" #if TARGET_OS_IOS #import "QONAutomationsDelegate.h" @@ -140,9 +141,19 @@ static NSString *const QonversionApiErrorDomain = @"com.qonversion.io.api"; Make a purchase and validate that through server-to-server using Qonversion's Backend @param product Product create in Qonversion Dash + @param completion Completion block that includes entitlements dictionary and error */ - (void)purchaseProduct:(QONProduct *)product completion:(QONPurchaseCompletionHandler)completion; +/** + Make a purchase and validate that through server-to-server using Qonversion's Backend + + @param product Product created in Qonversion Dash + @param options Purchase process additional options: quantity / context keys / etc. + @param completion Completion block that includes entitlements dictionary and error + */ +- (void)purchaseProduct:(QONProduct *)product options:(QONPurchaseOptions *)options completion:(QONPurchaseCompletionHandler)completion; + /** Make a purchase and validate that through server-to-server using Qonversion's Backend diff --git a/Sources/Qonversion/Public/Qonversion.m b/Sources/Qonversion/Public/Qonversion.m index 5c5137d8..0992e4dc 100644 --- a/Sources/Qonversion/Public/Qonversion.m +++ b/Sources/Qonversion/Public/Qonversion.m @@ -169,11 +169,15 @@ - (void)checkEntitlements:(QONEntitlementsCompletionHandler)completion { } - (void)purchaseProduct:(QONProduct *)product completion:(QONPurchaseCompletionHandler)completion { - [self.productCenterManager purchaseProduct:product completion:completion]; + [self.productCenterManager purchase:product options:nil completion:completion]; +} + +- (void)purchaseProduct:(QONProduct *)product options:(QONPurchaseOptions *)options completion:(QONPurchaseCompletionHandler)completion { + [self.productCenterManager purchase:product options:options completion:completion]; } - (void)purchase:(NSString *)productID completion:(QONPurchaseCompletionHandler)completion { - [self.productCenterManager purchase:productID completion:completion]; + [self.productCenterManager purchase:productID purchaseOptions:nil completion:completion]; } - (void)restore:(QNRestoreCompletionHandler)completion { @@ -197,39 +201,39 @@ - (void)collectAppleSearchAdsAttribution { } - (void)userInfo:(QONUserInfoCompletionHandler)completion { - [[self productCenterManager] userInfo:completion]; + [self.productCenterManager userInfo:completion]; } - (void)remoteConfig:(QONRemoteConfigCompletionHandler)completion { - [[self remoteConfigManager] obtainRemoteConfigWithContextKey:nil completion:completion]; + [self.remoteConfigManager obtainRemoteConfigWithContextKey:nil completion:completion]; } - (void)remoteConfig:(NSString *)contextKey completion:(QONRemoteConfigCompletionHandler)completion { - [[self remoteConfigManager] obtainRemoteConfigWithContextKey:contextKey completion:completion]; + [self.remoteConfigManager obtainRemoteConfigWithContextKey:contextKey completion:completion]; } - (void)remoteConfigList:(NSArray *)contextKeys includeEmptyContextKey:(BOOL)includeEmptyContextKey completion:(QONRemoteConfigListCompletionHandler)completion { - [[self remoteConfigManager] obtainRemoteConfigListWithContextKeys:contextKeys includeEmptyContextKey:includeEmptyContextKey completion:completion]; + [self.remoteConfigManager obtainRemoteConfigListWithContextKeys:contextKeys includeEmptyContextKey:includeEmptyContextKey completion:completion]; } - (void)remoteConfigList:(QONRemoteConfigListCompletionHandler)completion { - [[self remoteConfigManager] obtainRemoteConfigList:completion]; + [self.remoteConfigManager obtainRemoteConfigList:completion]; } - (void)attachUserToExperiment:(NSString *)experimentId groupId:(NSString *)groupId completion:(QONExperimentAttachCompletionHandler)completion { - [[self remoteConfigManager] attachUserToExperiment:experimentId groupId:groupId completion:completion]; + [self.remoteConfigManager attachUserToExperiment:experimentId groupId:groupId completion:completion]; } - (void)detachUserFromExperiment:(NSString *)experimentId completion:(QONExperimentAttachCompletionHandler)completion { - [[self remoteConfigManager] detachUserFromExperiment:experimentId completion:completion]; + [self.remoteConfigManager detachUserFromExperiment:experimentId completion:completion]; } - (void)attachUserToRemoteConfiguration:(NSString *)remoteConfigurationId completion:(QONRemoteConfigurationAttachCompletionHandler)completion { - [[self remoteConfigManager] attachUserToRemoteConfiguration:remoteConfigurationId completion:completion]; + [self.remoteConfigManager attachUserToRemoteConfiguration:remoteConfigurationId completion:completion]; } - (void)detachUserFromRemoteConfiguration:(NSString *)remoteConfigurationId completion:(QONRemoteConfigurationAttachCompletionHandler)completion { - [[self remoteConfigManager] detachUserFromRemoteConfiguration:remoteConfigurationId completion:completion]; + [self.remoteConfigManager detachUserFromRemoteConfiguration:remoteConfigurationId completion:completion]; } - (void)handlePurchases:(NSArray *)purchasesInfo { @@ -237,9 +241,16 @@ - (void)handlePurchases:(NSArray *)purchasesInfo { } - (void)handlePurchases:(NSArray *)purchasesInfo completion:(nullable QONDefaultCompletionHandler)completion { - [[self productCenterManager] handlePurchases:purchasesInfo completion:completion]; + [self.productCenterManager handlePurchases:purchasesInfo completion:completion]; } +#pragma GCC diagnostic push +#pragma GCC diagnostic ignored "-Wmissing-declarations" +NS_SWIFT_NAME(getPromotionalOfferForProduct(product: discount: completion:)); +- (void)getPromotionalOfferForProduct:(QONProduct * _Nonnull)product discount:(SKProductDiscount * _Nonnull)discount completion:(nonnull QONPromotionalOfferCompletionHandler)completion API_AVAILABLE(ios(12.2), macos(10.14.4), watchos(6.2), visionos(1.0)) { + [self.productCenterManager getPromotionalOfferForProduct:product discount:discount completion:completion]; +} +#pragma GCC diagnostic pop - (BOOL)isFallbackFileAccessible { QONFallbackObject *fallbackData = [self.fallbackService obtainFallbackData]; diff --git a/Sources/Qonversion/Qonversion/Constants/QNAPIConstants/QNAPIConstants.h b/Sources/Qonversion/Qonversion/Constants/QNAPIConstants/QNAPIConstants.h index 0e9bcce4..2eba64db 100644 --- a/Sources/Qonversion/Qonversion/Constants/QNAPIConstants/QNAPIConstants.h +++ b/Sources/Qonversion/Qonversion/Constants/QNAPIConstants/QNAPIConstants.h @@ -12,6 +12,7 @@ extern NSString *const kAPIBase; extern NSString *const kInitEndpoint; extern NSString *const kSendPushTokenEndpoint; extern NSString *const kPurchaseEndpoint; +extern NSString *const kGetPromoOfferDetailsEndpoint; extern NSString *const kProductsEndpoint; extern NSString *const kPropertiesEndpoint; extern NSString *const kActionPointsEndpointFormat; diff --git a/Sources/Qonversion/Qonversion/Constants/QNAPIConstants/QNAPIConstants.m b/Sources/Qonversion/Qonversion/Constants/QNAPIConstants/QNAPIConstants.m index ab254a9e..78b0ddbc 100644 --- a/Sources/Qonversion/Qonversion/Constants/QNAPIConstants/QNAPIConstants.m +++ b/Sources/Qonversion/Qonversion/Constants/QNAPIConstants/QNAPIConstants.m @@ -14,6 +14,8 @@ NSString * const kInitEndpoint = @"v1/user/init"; NSString * const kSendPushTokenEndpoint = @"v1/user/push-token"; NSString * const kPurchaseEndpoint = @"v1/user/purchase"; +// TODO: Update endpoint +NSString * const kGetPromoOfferDetailsEndpoint = @"update_promo_offer_endpoint_here"; NSString * const kProductsEndpoint = @"v1/products/get"; NSString * const kPropertiesEndpoint = @"v3/users/%@/properties"; NSString * const kRemoteConfigEndpoint = @"v3/remote-config"; diff --git a/Sources/Qonversion/Qonversion/Constants/QNInternalConstants/QNInternalConstants.h b/Sources/Qonversion/Qonversion/Constants/QNInternalConstants/QNInternalConstants.h index e73ec452..aed0e5c9 100644 --- a/Sources/Qonversion/Qonversion/Constants/QNInternalConstants/QNInternalConstants.h +++ b/Sources/Qonversion/Qonversion/Constants/QNInternalConstants/QNInternalConstants.h @@ -43,6 +43,7 @@ extern NSString *const kKeyQUserDefaultsPermissions; extern NSString *const kKeyQPermissionsTransfered; extern NSString *const kKeyQUserDefaultsPermissionsTimestamp; extern NSString *const kKeyQUserDefaultsProductsPermissionsRelation; +extern NSString *const kKeyQUserDefaultsPurchaseOptions; extern NSString *const kMainUserDefaultsSuiteName; extern NSString *const kKeyQUserDefaultsStoredPurchasesRequests; extern NSString *const kKeyQExperimentStartedEventName; diff --git a/Sources/Qonversion/Qonversion/Constants/QNInternalConstants/QNInternalConstants.m b/Sources/Qonversion/Qonversion/Constants/QNInternalConstants/QNInternalConstants.m index 2d91f529..83f3a841 100644 --- a/Sources/Qonversion/Qonversion/Constants/QNInternalConstants/QNInternalConstants.m +++ b/Sources/Qonversion/Qonversion/Constants/QNInternalConstants/QNInternalConstants.m @@ -42,6 +42,7 @@ NSString *const kKeyQPermissionsTransfered = @"com.qonversion.keys.entitlements.transfered"; NSString *const kKeyQUserDefaultsPermissionsTimestamp = @"com.qonversion.keys.permissions.timestamp"; NSString *const kKeyQUserDefaultsProductsPermissionsRelation = @"com.qonversion.keys.products.permissions.relation"; +NSString *const kKeyQUserDefaultsPurchaseOptions = @"com.qonversion.keys.purchases.options"; NSString *const kMainUserDefaultsSuiteName = @"qonversion.localstorage.main"; NSString *const kKeyQUserDefaultsStoredPurchasesRequests = @"com.qonversion.keys.requests.stored.purchases"; diff --git a/Sources/Qonversion/Qonversion/Core/QNRequestBuilder/QNRequestBuilder.h b/Sources/Qonversion/Qonversion/Core/QNRequestBuilder/QNRequestBuilder.h index 8195a06d..fd477cb4 100644 --- a/Sources/Qonversion/Qonversion/Core/QNRequestBuilder/QNRequestBuilder.h +++ b/Sources/Qonversion/Qonversion/Core/QNRequestBuilder/QNRequestBuilder.h @@ -31,5 +31,6 @@ typedef NS_ENUM(NSInteger, QONRequestType) { - (NSURLRequest *)makeDetachUserFromExperimentRequest:(NSString *)experimentId userID:(NSString *)userID; - (NSURLRequest *)makeAttachUserToRemoteConfigurationRequest:(NSString *)remoteConfigurationId userID:(NSString *)userID; - (NSURLRequest *)makeDetachUserFromRemoteConfigurationRequest:(NSString *)remoteConfigurationId userID:(NSString *)userID; +- (NSURLRequest *)makeGetPromotionalOfferRequestWithBody:(NSDictionary *)body; @end diff --git a/Sources/Qonversion/Qonversion/Core/QNRequestBuilder/QNRequestBuilder.m b/Sources/Qonversion/Qonversion/Core/QNRequestBuilder/QNRequestBuilder.m index 75c21690..2311607d 100644 --- a/Sources/Qonversion/Qonversion/Core/QNRequestBuilder/QNRequestBuilder.m +++ b/Sources/Qonversion/Qonversion/Core/QNRequestBuilder/QNRequestBuilder.m @@ -58,6 +58,10 @@ - (NSURLRequest *)makePurchaseRequestWith:(NSDictionary *)parameters { return [self makeRequestWithDictBody:parameters baseURL:self.baseURL endpoint:kPurchaseEndpoint type:QONRequestTypePost]; } +- (NSURLRequest *)makeGetPromotionalOfferRequestWithBody:(NSDictionary *)body { + return [self makeRequestWithDictBody:body baseURL:self.baseURL endpoint:kGetPromoOfferDetailsEndpoint type:QONRequestTypePost]; +} + - (NSURLRequest *)makeUserActionPointsRequestWith:(NSString *)parameter { NSString *endpoint = [NSString stringWithFormat:kActionPointsEndpointFormat, parameter]; return [self makeGetRequestWithBaseURL:self.baseURL endpoint:endpoint]; diff --git a/Sources/Qonversion/Qonversion/Core/QNRequestSerializer/QNRequestSerializer.h b/Sources/Qonversion/Qonversion/Core/QNRequestSerializer/QNRequestSerializer.h index 3b014ac5..32b8c229 100644 --- a/Sources/Qonversion/Qonversion/Core/QNRequestSerializer/QNRequestSerializer.h +++ b/Sources/Qonversion/Qonversion/Core/QNRequestSerializer/QNRequestSerializer.h @@ -1,6 +1,6 @@ #import "QONLaunchResult.h" -@class SKProduct, SKPaymentTransaction, QNProductPurchaseModel, QONProduct, QONStoreKit2PurchaseModel; +@class SKProduct, SKPaymentTransaction, SKProductDiscount, QNProductPurchaseModel, QONProduct, QONStoreKit2PurchaseModel, QONPurchaseOptions; NS_ASSUME_NONNULL_BEGIN @interface QNRequestSerializer : NSObject @@ -9,7 +9,8 @@ NS_ASSUME_NONNULL_BEGIN - (NSDictionary *)purchaseData:(SKProduct *)product transaction:(SKPaymentTransaction *)transaction - receipt:(nullable NSString *)receipt; + receipt:(nullable NSString *)receipt + purchaseOptions:(nullable QONPurchaseOptions *)purchaseOptions; - (NSDictionary *)introTrialEligibilityDataForProducts:(NSArray *)products; - (NSDictionary *)pushTokenData; @@ -18,6 +19,11 @@ NS_ASSUME_NONNULL_BEGIN - (NSDictionary *)purchaseInfo:(QONStoreKit2PurchaseModel *)purchaseInfo receipt:(nullable NSString *)receipt; +- (NSDictionary *)promotionalOfferInfoForProduct:(QONProduct *)product + discount:(SKProductDiscount *)productDiscount + identityId:(NSString *)identityId + receipt:(nullable NSString *)receipt API_AVAILABLE(ios(11.2), macos(10.13.2), visionos(1.0)); + @end NS_ASSUME_NONNULL_END diff --git a/Sources/Qonversion/Qonversion/Core/QNRequestSerializer/QNRequestSerializer.m b/Sources/Qonversion/Qonversion/Core/QNRequestSerializer/QNRequestSerializer.m index 15c9692d..442fb231 100644 --- a/Sources/Qonversion/Qonversion/Core/QNRequestSerializer/QNRequestSerializer.m +++ b/Sources/Qonversion/Qonversion/Core/QNRequestSerializer/QNRequestSerializer.m @@ -6,6 +6,7 @@ #import "QONExperiment.h" #import "QONExperimentGroup.h" #import "QONStoreKit2PurchaseModel.h" +#import "QONPurchaseOptions.h" @interface QNRequestSerializer () @@ -35,7 +36,8 @@ - (NSDictionary *)pushTokenData { - (NSDictionary *)purchaseData:(SKProduct *)product transaction:(SKPaymentTransaction *)transaction - receipt:(nullable NSString *)receipt { + receipt:(nullable NSString *)receipt + purchaseOptions:(nullable QONPurchaseOptions *)purchaseOptions { NSMutableDictionary *result = [[NSMutableDictionary alloc] initWithDictionary:self.mainData]; NSMutableDictionary *purchaseDict = [[NSMutableDictionary alloc] init]; @@ -71,6 +73,10 @@ - (NSDictionary *)purchaseData:(SKProduct *)product } } + if (purchaseOptions.contextKeys.count > 0) { + purchaseDict[@"context_keys"] = purchaseOptions.contextKeys; + } + if (@available(iOS 13.0, macos 10.15, tvOS 13.0, *)) { NSString *countryCode = SKPaymentQueue.defaultQueue.storefront.countryCode ?: @""; purchaseDict[@"country"] = countryCode; @@ -81,6 +87,22 @@ - (NSDictionary *)purchaseData:(SKProduct *)product return result; } +- (NSDictionary *)promotionalOfferInfoForProduct:(QONProduct *)product + discount:(SKProductDiscount *)productDiscount + identityId:(NSString *)identityId + receipt:(nullable NSString *)receipt { + NSMutableDictionary *result = [NSMutableDictionary new]; + + result[@"productIdentifier"] = product.storeID; + if (@available(iOS 12.2, macOS 10.14.4, watchOS 6.2, tvOS 12.2, visionOS 1.0, *)) { + result[@"discountIdentifier"] = productDiscount.identifier; + } + result[@"idetntityId"] = identityId; + result[@"receipt"] = receipt; + + return [result copy]; +} + - (NSDictionary *)purchaseInfo:(QONStoreKit2PurchaseModel *)purchaseModel receipt:(nullable NSString *)receipt { NSMutableDictionary *result = [[NSMutableDictionary alloc] initWithDictionary:self.mainData]; diff --git a/Sources/Qonversion/Qonversion/Main/QNProductCenterManager/QNProductCenterManager.h b/Sources/Qonversion/Qonversion/Main/QNProductCenterManager/QNProductCenterManager.h index f4ec3578..3185133e 100644 --- a/Sources/Qonversion/Qonversion/Main/QNProductCenterManager/QNProductCenterManager.h +++ b/Sources/Qonversion/Qonversion/Main/QNProductCenterManager/QNProductCenterManager.h @@ -4,7 +4,7 @@ #import "QONLaunchMode.h" #import "QONRemoteConfigManager.h" -@class QONLaunchResult, QONStoreKit2PurchaseModel, QONFallbackService; +@class QONLaunchResult, QONStoreKit2PurchaseModel, QONFallbackService, QONPromotionalOffer, QONPurchaseOptions, SKProductDiscount; @protocol QONPromoPurchasesDelegate, QONEntitlementsUpdateListener, QNUserInfoServiceInterface, QNIdentityManagerInterface, QNLocalStorage; NS_ASSUME_NONNULL_BEGIN @@ -27,8 +27,8 @@ NS_ASSUME_NONNULL_BEGIN - (void)launchWithCompletion:(nullable QONLaunchCompletionHandler)completion; - (void)checkEntitlements:(QONEntitlementsCompletionHandler)completion; -- (void)purchaseProduct:(QONProduct *)product completion:(QONPurchaseCompletionHandler)completion; -- (void)purchase:(NSString *)productID completion:(QONPurchaseCompletionHandler)completion; +- (void)purchase:(QONProduct * _Nonnull)product options:(QONPurchaseOptions * _Nullable)options completion:(nonnull QONPurchaseCompletionHandler)completion; +- (void)purchase:(NSString * _Nonnull)productID purchaseOptions:(QONPurchaseOptions * _Nullable)options completion:(nonnull QONPurchaseCompletionHandler)completion; - (void)restore:(QNRestoreCompletionHandler)completion; - (void)products:(QONProductsCompletionHandler)completion; @@ -40,6 +40,9 @@ NS_ASSUME_NONNULL_BEGIN - (void)handlePurchases:(NSArray *)purchasesInfo completion:(QONDefaultCompletionHandler)completion; - (void)launch:(void (^)(QONLaunchResult * _Nullable result, NSError * _Nullable error))completion; +- (void)getPromotionalOfferForProduct:(QONProduct *)product + discount:(SKProductDiscount *)discount + completion:(QONPromotionalOfferCompletionHandler)completion API_AVAILABLE(ios(12.2), macos(10.14.4), watchos(6.2), visionos(1.0)); @end diff --git a/Sources/Qonversion/Qonversion/Main/QNProductCenterManager/QNProductCenterManager.m b/Sources/Qonversion/Qonversion/Main/QNProductCenterManager/QNProductCenterManager.m index 1abffac7..ff13371c 100644 --- a/Sources/Qonversion/Qonversion/Main/QNProductCenterManager/QNProductCenterManager.m +++ b/Sources/Qonversion/Qonversion/Main/QNProductCenterManager/QNProductCenterManager.m @@ -21,6 +21,9 @@ #import "QONStoreKit2PurchaseModel.h" #import "QONFallbackService.h" #import "QONFallbackObject.h" +#import "QONPromotionalOffer.h" +#import "QONPurchaseOptions.h" +#import #if TARGET_OS_IOS #import "QONAutomations.h" @@ -59,6 +62,8 @@ @interface QNProductCenterManager() @property (nonatomic, strong) NSError *launchError; @property (nonatomic, strong) QONUser *user; +@property (nonatomic, copy) NSDictionary *processingPurchaseOptions; + @property (nonatomic, assign) BOOL launchingFinished; @property (nonatomic, assign) BOOL productsLoading; @property (nonatomic, assign) BOOL restoreInProgress; @@ -109,6 +114,33 @@ - (instancetype)initWithUserInfoService:(id)userInfo return self; } +- (void)updatePurchaseOptions:(QONPurchaseOptions *)purchaseOptions storeProductId:(NSString *)productId { + NSMutableDictionary *actualPurchaseOptions = [[self actualPurchaseOptions] mutableCopy]; + actualPurchaseOptions[productId] = purchaseOptions; + + self.processingPurchaseOptions = [actualPurchaseOptions copy]; + + [self.persistentStorage storeObject:self.processingPurchaseOptions forKey:kKeyQUserDefaultsPurchaseOptions]; +} + +- (void)removePurchaseOptionsForStoreProductId:(NSString *)productId { + [self updatePurchaseOptions:nil storeProductId:productId]; +} + +- (NSDictionary *)actualPurchaseOptions { + if (_processingPurchaseOptions) { + return self.processingPurchaseOptions; + } + + self.processingPurchaseOptions = [_persistentStorage loadObjectForKey:kKeyQUserDefaultsPurchaseOptions]; + + if (!self.processingPurchaseOptions) { + self.processingPurchaseOptions = @{}; + } + + return self.processingPurchaseOptions; +} + - (void)transferCachedPermissionsIfNeeded { BOOL alreadyTransfered = [self.persistentStorage loadBoolforKey:kKeyQPermissionsTransfered]; if (!alreadyTransfered) { @@ -344,20 +376,11 @@ - (void)handleLogout { [self launchWithCompletion:nil]; } -- (void)purchaseProduct:(QONProduct *)product completion:(QONPurchaseCompletionHandler)completion { - if (product.offeringID.length > 0) { - QONOffering *offering = [self.launchResult.offerings offeringForIdentifier:product.offeringID]; - [self purchase:product.qonversionID offeringID:offering.identifier completion:completion]; - } else { - [self purchase:product.qonversionID offeringID:nil completion:completion]; - } -} - -- (void)purchase:(NSString *)productID completion:(QONPurchaseCompletionHandler)completion { - [self purchase:productID offeringID:nil completion:completion]; +- (void)purchase:(QONProduct *)product options:(QONPurchaseOptions *)options completion:(QONPurchaseCompletionHandler)completion { + [self purchase:product.qonversionID purchaseOptions:options completion:completion]; } -- (void)purchase:(NSString *)productID offeringID:(NSString *)offeringID completion:(QONPurchaseCompletionHandler)completion { +- (void)purchase:(NSString *)productID purchaseOptions:(QONPurchaseOptions *)options completion:(QONPurchaseCompletionHandler)completion { if (self.launchMode == QONLaunchModeAnalytics) { QONVERSION_LOG(@"⚠️ Making purchases via Qonversion in the Analytics mode can lead to an inconsistent state in the store. Consider switching to the Subscription management mode."); } @@ -375,47 +398,36 @@ - (void)purchase:(NSString *)productID offeringID:(NSString *)offeringID complet } if (weakSelf.productsLoading) { - [weakSelf prepareDelayedPurchase:productID offeringID:offeringID completion:completion]; + [weakSelf prepareDelayedPurchase:productID options:options completion:completion]; } else { - [weakSelf processPurchase:productID offeringID:offeringID completion:completion]; + [weakSelf processPurchase:productID options:options completion:completion]; } }]; } else if (!self.productsLoading && storeProducts.count == 0) { - [self prepareDelayedPurchase:productID offeringID:offeringID completion:completion]; + [self prepareDelayedPurchase:productID options:options completion:completion]; [self loadProducts]; } else { - [self processPurchase:productID offeringID:offeringID completion:completion]; + [self processPurchase:productID options:options completion:completion]; } } } -- (void)prepareDelayedPurchase:(NSString *)productID offeringID:offeringID completion:(QONPurchaseCompletionHandler)completion { +- (void)prepareDelayedPurchase:(NSString *)productID options:(QONPurchaseOptions *)options completion:(QONPurchaseCompletionHandler)completion { QONProductsCompletionHandler productsCompletion = ^(NSDictionary *result, NSError *_Nullable error) { if (error) { run_block_on_main(completion, @{}, error, NO); return; } - [self processPurchase:productID offeringID:offeringID completion:completion]; + [self processPurchase:productID options:options completion:completion]; }; [self.productsBlocks addObject:productsCompletion]; } -- (void)processPurchase:(NSString *)productID offeringID:(NSString *)offeringID completion:(QONPurchaseCompletionHandler)completion { - QONProduct *product; - if (offeringID.length > 0) { - QONOffering *offering = [self.launchResult.offerings offeringForIdentifier:offeringID]; - - for (QONProduct *tempProduct in offering.products) { - if ([tempProduct.qonversionID isEqualToString:productID]) { - product = tempProduct; - } - } - } else { - product = [self QNProduct:productID]; - } +- (void)processPurchase:(NSString *)productID options:(QONPurchaseOptions *)options completion:(QONPurchaseCompletionHandler)completion { + QONProduct *product = [self QNProduct:productID]; if (!product) { QONVERSION_LOG(@"❌ product with id: %@ not found", productID); @@ -423,16 +435,17 @@ - (void)processPurchase:(NSString *)productID offeringID:(NSString *)offeringID return; } - [self processProductPurchase:product completion:completion]; + [self processProductPurchase:product options:options completion:completion]; } -- (void)processProductPurchase:(QONProduct *)product completion:(QONPurchaseCompletionHandler)completion { +- (void)processProductPurchase:(QONProduct *)product options:(QONPurchaseOptions *)options completion:(QONPurchaseCompletionHandler)completion { if (self.purchasingBlocks[product.storeID]) { QONVERSION_LOG(@"Purchasing in process"); return; } - if (product && [_storeKitService purchase:product.storeID]) { + if (product && [_storeKitService purchase:product.storeID options:options]) { + [self updatePurchaseOptions:options storeProductId:product.storeID]; self.purchasingBlocks[product.storeID] = completion; return; @@ -869,12 +882,16 @@ - (void)handlePurchasedTransaction:(SKPaymentTransaction *)transaction forProduc __block __weak QNProductCenterManager *weakSelf = self; [self.storeKitService receipt:^(NSString * receipt) { - __block NSURLRequest *request = [weakSelf.apiClient purchaseRequestWith:product transaction:transaction receipt:receipt completion:^(NSDictionary * _Nullable dict, NSError * _Nullable error) { + NSDictionary *allPurchaseOptions = [weakSelf actualPurchaseOptions]; + QONPurchaseOptions *purchaseOptions = allPurchaseOptions[product.productIdentifier]; + __block NSURLRequest *request = [weakSelf.apiClient purchaseRequestWith:product transaction:transaction receipt:receipt purchaseOptions:purchaseOptions completion:^(NSDictionary * _Nullable dict, NSError * _Nullable error) { QONPurchaseCompletionHandler _purchasingBlock = weakSelf.purchasingBlocks[product.productIdentifier]; @synchronized (weakSelf) { [weakSelf.purchasingBlocks removeObjectForKey:product.productIdentifier]; } + [weakSelf removePurchaseOptionsForStoreProductId:product.productIdentifier]; + if (error && [QNUtils shouldPurchaseRequestBeRetried:error]) { [weakSelf.apiClient storeRequestForRetry:request transactionId:transaction.transactionIdentifier]; } else { @@ -996,6 +1013,31 @@ - (void)executeRestoreBlocksWithResult:(NSDictionary *)products { @synchronized (self) { self->_productsLoading = NO; diff --git a/Sources/Qonversion/Qonversion/Mappers/QNMapper/QNMapper.h b/Sources/Qonversion/Qonversion/Mappers/QNMapper/QNMapper.h index 4198e18c..b3118e72 100644 --- a/Sources/Qonversion/Qonversion/Mappers/QNMapper/QNMapper.h +++ b/Sources/Qonversion/Qonversion/Mappers/QNMapper/QNMapper.h @@ -1,6 +1,6 @@ #import -@class QNMapperObject, QONLaunchResult, QONEntitlement, QONIntroEligibility, QONUser, QONFallbackObject, QONOfferings, QONProduct; +@class QNMapperObject, QONLaunchResult, QONEntitlement, QONIntroEligibility, QONUser, QONFallbackObject, QONOfferings, QONProduct, QONPromotionalOffer, SKProductDiscount; @interface QNMapper : NSObject @@ -22,4 +22,6 @@ - (NSDictionary * _Nullable)mapProductsEntitlementsRelations:(NSDictionary * _Nullable)dict; ++ (QONPromotionalOffer * _Nullable)mapPromoOffer:(NSDictionary * _Nullable)rawData productDiscount:(SKProductDiscount * _Nonnull)productDiscount mappingError:(NSError * _Nullable * _Nullable)error API_AVAILABLE(ios(12.2), macos(10.14.4), watchos(6.2), visionos(1.0)); + @end diff --git a/Sources/Qonversion/Qonversion/Mappers/QNMapper/QNMapper.m b/Sources/Qonversion/Qonversion/Mappers/QNMapper/QNMapper.m index 9a8df70d..4eb945bb 100644 --- a/Sources/Qonversion/Qonversion/Mappers/QNMapper/QNMapper.m +++ b/Sources/Qonversion/Qonversion/Mappers/QNMapper/QNMapper.m @@ -17,8 +17,11 @@ #import "QONExperimentGroup+Protected.h" #import "QONUser+Protected.h" #import "QONTransaction+Protected.h" +#import "QONPromotionalOffer+Protected.h" #import "QONFallbackObject.h" +#import + @implementation QNMapper + (QONLaunchResult * _Nonnull)fillLaunchResult:(NSDictionary *)dict { @@ -58,6 +61,40 @@ - (NSDictionary * _Nullable)mapProductsEntitlementsRelations:(NSDictionary * _Nu return [QNMapper mapProductsEntitlementsRelation:dict]; } ++ (NSError *)promoOfferMappingError { + return [QONErrors errorWithCode:QONAPIErrorFailedParseResponse message:@"Failed to map promotional offer" failureReason:nil]; +} + ++ (QONPromotionalOffer * _Nullable)mapPromoOffer:(NSDictionary * _Nullable)rawData productDiscount:(SKProductDiscount * _Nonnull)productDiscount mappingError:(NSError ** _Nullable)error { + if (![rawData isKindOfClass:[NSDictionary class]]) { + *error = [self promoOfferMappingError]; + + return nil; + } + + NSString *identifier = rawData[@"identifier"]; + NSString *keyIdentifier = rawData[@"keyIdentifier"]; + NSString *uuidString = rawData[@"uuid"]; + NSUUID *nonce = [[NSUUID alloc] initWithUUIDString:uuidString]; + NSString *signature = rawData[@"signature"]; + NSTimeInterval timestamp = [self mapInteger:rawData[@"timestamp"] orReturn:0]; + timestamp = timestamp != 0 ? timestamp : [NSDate date].timeIntervalSince1970; + + NSNumber *timestampNumber = [NSNumber numberWithDouble:timestamp]; + + if (identifier.length == 0 || keyIdentifier.length == 0 || uuidString.length == 0 || signature.length == 0) { + *error = [self promoOfferMappingError]; + + return nil; + } + + SKPaymentDiscount *paymentDiscount = [[SKPaymentDiscount alloc] initWithIdentifier:identifier keyIdentifier:keyIdentifier nonce:nonce signature:signature timestamp:timestampNumber]; + + QONPromotionalOffer *offer = [[QONPromotionalOffer alloc] initWithProductDiscount:productDiscount paymentDiscount:paymentDiscount]; + + return offer; +} + + (QONUser *)fillUser:(NSDictionary * _Nullable)dict { NSString *userID = dict[@"uid"]; NSString *originalAppVersion = dict[@"apple_extra"][@"original_application_version"]; diff --git a/Sources/Qonversion/Qonversion/Models/Protected/QONPromotionalOffer+Protected.h b/Sources/Qonversion/Qonversion/Models/Protected/QONPromotionalOffer+Protected.h new file mode 100644 index 00000000..9ef8d8ba --- /dev/null +++ b/Sources/Qonversion/Qonversion/Models/Protected/QONPromotionalOffer+Protected.h @@ -0,0 +1,20 @@ +// +// QONPromotionalOffer+Protected.h +// Qonversion +// +// Created by Suren Sarkisyan on 20.06.2024. +// Copyright © 2024 Qonversion Inc. All rights reserved. +// + +#import "QONPromotionalOffer.h" +#import + +NS_ASSUME_NONNULL_BEGIN + +@interface QONPromotionalOffer (Protected) + +- (instancetype)initWithProductDiscount:(SKProductDiscount *)productDiscount paymentDiscount:(SKPaymentDiscount *)paymentDiscount; + +@end + +NS_ASSUME_NONNULL_END diff --git a/Sources/Qonversion/Qonversion/Services/QNAPIClient/QNAPIClient.h b/Sources/Qonversion/Qonversion/Services/QNAPIClient/QNAPIClient.h index 331b3f5d..729df968 100644 --- a/Sources/Qonversion/Qonversion/Services/QNAPIClient/QNAPIClient.h +++ b/Sources/Qonversion/Qonversion/Services/QNAPIClient/QNAPIClient.h @@ -2,7 +2,7 @@ #import "QONLaunchResult.h" @protocol QNLocalStorage; -@class SKProduct, SKPaymentTransaction, QONOffering, QONProduct, QONStoreKit2PurchaseModel; +@class SKProduct, SKProductDiscount, SKPaymentTransaction, QONOffering, QONProduct, QONStoreKit2PurchaseModel, QONPurchaseOptions; typedef void (^QNAPIClientEmptyCompletionHandler)(NSError * _Nullable error); typedef void (^QNAPIClientDictCompletionHandler)(NSDictionary * _Nullable dict, NSError * _Nullable error); @@ -30,6 +30,7 @@ NS_ASSUME_NONNULL_BEGIN - (NSURLRequest *)purchaseRequestWith:(SKProduct *)product transaction:(SKPaymentTransaction *)transaction receipt:(nullable NSString *)receipt + purchaseOptions:(nullable QONPurchaseOptions *)purchaseOptions completion:(QNAPIClientDictCompletionHandler)completion; - (NSURLRequest *)purchaseRequestWith:(NSDictionary *) body completion:(QNAPIClientDictCompletionHandler)completion; @@ -62,6 +63,12 @@ NS_ASSUME_NONNULL_BEGIN - (void)detachUserFromExperiment:(NSString *)experimentId completion:(QNAPIClientEmptyCompletionHandler)completion; - (void)attachUserToRemoteConfiguration:(NSString *)remoteConfigurationId completion:(QNAPIClientEmptyCompletionHandler)completion; - (void)detachUserFromRemoteConfiguration:(NSString *)remoteConfigurationId completion:(QNAPIClientEmptyCompletionHandler)completion; +- (void)getPromotionalOfferForProduct:(QONProduct *)product + discount:(SKProductDiscount *)discount + identityId:(NSString *)identityId + receipt:(nullable NSString *)receipt + completion:(QNAPIClientDictCompletionHandler)completion API_AVAILABLE(ios(11.2), macos(10.13.2), visionos(1.0)); + - (NSURLRequest *)handlePurchase:(QONStoreKit2PurchaseModel *)purchaseInfo receipt:(nullable NSString *)receipt completion:(QNAPIClientDictCompletionHandler)completion; diff --git a/Sources/Qonversion/Qonversion/Services/QNAPIClient/QNAPIClient.m b/Sources/Qonversion/Qonversion/Services/QNAPIClient/QNAPIClient.m index d52fe97e..e35fa6fc 100644 --- a/Sources/Qonversion/Qonversion/Services/QNAPIClient/QNAPIClient.m +++ b/Sources/Qonversion/Qonversion/Services/QNAPIClient/QNAPIClient.m @@ -12,6 +12,7 @@ #import "QONRateLimiter.h" #import "QNLocalStorage.h" #import "Qonversion.h" +#import "QONPurchaseOptions.h" NSUInteger const kUnableToParseEmptyDataDefaultCode = 3840; @@ -171,8 +172,9 @@ - (NSURLRequest *)handlePurchase:(QONStoreKit2PurchaseModel *)purchaseInfo - (NSURLRequest *)purchaseRequestWith:(SKProduct *)product transaction:(SKPaymentTransaction *)transaction receipt:(nullable NSString *)receipt + purchaseOptions:(nullable QONPurchaseOptions *)purchaseOptions completion:(QNAPIClientDictCompletionHandler)completion { - NSDictionary *body = [self.requestSerializer purchaseData:product transaction:transaction receipt:receipt]; + NSDictionary *body = [self.requestSerializer purchaseData:product transaction:transaction receipt:receipt purchaseOptions:purchaseOptions]; return [self purchaseRequestWith:body completion:completion]; } @@ -370,6 +372,18 @@ - (void)sendCrashReport:(NSDictionary *)data completion:(QNAPIClientEmptyComplet [self processRequestWithoutResponse:mutableRequest completion:completion]; } +- (void)getPromotionalOfferForProduct:(QONProduct *)product + discount:(SKProductDiscount *)discount + identityId:(NSString *)identityId + receipt:(nullable NSString *)receipt + completion:(QNAPIClientDictCompletionHandler)completion { + NSDictionary *body = [self.requestSerializer promotionalOfferInfoForProduct:product discount:discount identityId:identityId receipt:receipt]; + + NSURLRequest *request = [self.requestBuilder makeGetPromotionalOfferRequestWithBody:body]; + + [self processDictRequest:request completion:completion]; +} + - (void)loadRemoteConfig:(NSString * _Nullable)contextKey completion:(QNAPIClientDictCompletionHandler)completion { [self.rateLimiter validateRateLimit:QONRateLimitedRequestTypeRemoteConfig hash:[self.userID hash] diff --git a/Sources/Qonversion/Qonversion/Services/QNStoreKitService/QNStoreKitService.h b/Sources/Qonversion/Qonversion/Services/QNStoreKitService/QNStoreKitService.h index b307b5c9..02728aac 100644 --- a/Sources/Qonversion/Qonversion/Services/QNStoreKitService/QNStoreKitService.h +++ b/Sources/Qonversion/Qonversion/Services/QNStoreKitService/QNStoreKitService.h @@ -5,6 +5,7 @@ NS_ASSUME_NONNULL_BEGIN typedef void(^QNStoreKitServiceReceiptFetchCompletionHandler)(void); typedef void(^QNStoreKitServiceReceiptFetchWithReceiptCompletionHandler)(NSString *); +@class QONPromotionalOffer, QONPurchaseOptions; @protocol QNStoreKitServiceDelegate; @interface QNStoreKitService : NSObject @@ -14,7 +15,7 @@ typedef void(^QNStoreKitServiceReceiptFetchWithReceiptCompletionHandler)(NSStrin - (instancetype)initWithDelegate:(id )delegate; - (void)loadProducts:(NSSet *)products; -- (nullable SKProduct *)purchase:(NSString *)productID; +- (SKProduct *)purchase:(NSString *)productID options:(QONPurchaseOptions * _Nullable)options; - (void)purchaseProduct:(SKProduct *)product; - (void)presentCodeRedemptionSheet; - (void)restore; diff --git a/Sources/Qonversion/Qonversion/Services/QNStoreKitService/QNStoreKitService.m b/Sources/Qonversion/Qonversion/Services/QNStoreKitService/QNStoreKitService.m index 3d4a6a54..d8e1cada 100644 --- a/Sources/Qonversion/Qonversion/Services/QNStoreKitService/QNStoreKitService.m +++ b/Sources/Qonversion/Qonversion/Services/QNStoreKitService/QNStoreKitService.m @@ -1,6 +1,8 @@ #import "QNStoreKitService.h" #import "QNUtils.h" #import "QNUserInfo.h" +#import "QONPromotionalOffer.h" +#import "QONPurchaseOptions.h" @interface QNStoreKitService() @@ -51,11 +53,12 @@ - (instancetype)init { return self; } -- (SKProduct *)purchase:(NSString *)productID { +- (SKProduct *)purchase:(NSString *)productID options:(QONPurchaseOptions * _Nullable)options { SKProduct *skProduct = self->_products[productID]; if (skProduct) { - [self purchaseProduct:skProduct]; + // TODO: get promo offer from purchase options + [self purchaseProduct:skProduct options:options]; return skProduct; } else { @@ -64,12 +67,21 @@ - (SKProduct *)purchase:(NSString *)productID { } - (void)purchaseProduct:(SKProduct *)product { + [self purchaseProduct:product options:nil]; +} + +- (void)purchaseProduct:(SKProduct *)product options:(QONPurchaseOptions * _Nullable)options { @synchronized (self) { self->_purchasingCurrently = product.productIdentifier; } - SKPayment *payment = [SKPayment paymentWithProduct:product]; - [[SKPaymentQueue defaultQueue] addPayment:payment]; + SKMutablePayment *payment = [SKMutablePayment paymentWithProduct:product]; + + if (options.quantity > 1) { + payment.quantity = options.quantity; + } + + [[SKPaymentQueue defaultQueue] addPayment:[payment copy]]; } - (void)presentCodeRedemptionSheet { diff --git a/Sources/Swift/Extensions.swift b/Sources/Swift/Extensions.swift new file mode 100644 index 00000000..b75bd4a4 --- /dev/null +++ b/Sources/Swift/Extensions.swift @@ -0,0 +1,19 @@ +// +// Extensions.swift +// Qonversion +// +// Created by Suren Sarkisyan on 04.08.2024. +// Copyright © 2024 Qonversion Inc. All rights reserved. +// + +import Foundation + +extension Qonversion.PurchaseOptions { + + public convenience init(quantity: Int = 1, contextKeys: [String]? = nil) { + self.init() + self.quantity = quantity + self.contextKeys = contextKeys + } + +} diff --git a/fastlane/report.xml b/fastlane/report.xml index 378994a8..5fc4ed47 100644 --- a/fastlane/report.xml +++ b/fastlane/report.xml @@ -5,12 +5,12 @@ - + - +