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 @@
-
+
-
+