diff --git a/Sources/Qonversion/Public/QONPromotionalOffer.h b/Sources/Qonversion/Public/QONPromotionalOffer.h index d81b8cda..7f0f772d 100644 --- a/Sources/Qonversion/Public/QONPromotionalOffer.h +++ b/Sources/Qonversion/Public/QONPromotionalOffer.h @@ -12,7 +12,7 @@ NS_ASSUME_NONNULL_BEGIN NS_SWIFT_NAME(Qonversion.PromotionalOffer) -API_AVAILABLE(ios(12.2), macos(10.14.4), watchos(6.2), visionos(1.0)) +API_AVAILABLE(ios(12.2), macos(10.14.4), watchos(6.2), tvos(12.2), visionos(1.0)) @interface QONPromotionalOffer : NSObject @property (nonatomic, strong) SKProductDiscount *productDiscount; diff --git a/Sources/Qonversion/Public/QONPurchaseOptions.h b/Sources/Qonversion/Public/QONPurchaseOptions.h index 180a7b4d..5a347af4 100644 --- a/Sources/Qonversion/Public/QONPurchaseOptions.h +++ b/Sources/Qonversion/Public/QONPurchaseOptions.h @@ -8,6 +8,8 @@ #import +@class QONPromotionalOffer; + NS_ASSUME_NONNULL_BEGIN NS_SWIFT_NAME(Qonversion.PurchaseOptions) @@ -22,6 +24,9 @@ NS_SWIFT_NAME(Qonversion.PurchaseOptions) // Context keys associated with a purchase. Use this field to associate a purchase with a concrete remote config. @property (nonatomic, copy, nullable) NSArray *contextKeys; +// Promo offer details. Use to make a purchase with a promo offer. +@property (nonatomic, strong, nullable) QONPromotionalOffer *promoOffer API_AVAILABLE(ios(12.2), macos(10.14.4), watchos(6.2), tvos(12.2), visionos(1.0)); + /** Initialize purchase options with quantity. @param quantity quantity of product purchasing. Use for consumable in-app products. @@ -44,6 +49,22 @@ NS_SWIFT_NAME(Qonversion.PurchaseOptions) */ - (instancetype)initWithContextKeys:(NSArray * _Nullable)contextKeys NS_SWIFT_UNAVAILABLE("Use swift style initializer instead."); +/** + Initialize purchase options with quantity, context keys, and promo offer details. + @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. + @param promoOffer promo offer details. + @return QONPurchaseOptions instance + */ +- (instancetype)initWithQuantity:(NSInteger)quantity contextKeys:(NSArray * _Nullable)contextKeys promoOffer:(QONPromotionalOffer * _Nullable)promoOffer API_AVAILABLE(ios(12.2), macos(10.14.4), watchos(6.2), tvos(12.2), visionos(1.0)) NS_SWIFT_UNAVAILABLE("Use swift style initializer instead."); + +/** + Initialize purchase options with promo offer details. + @param promoOffer promo offer details. + @return QONPurchaseOptions instance + */ +- (instancetype)initWithPromoOffer:(QONPromotionalOffer * _Nullable)promoOffer API_AVAILABLE(ios(12.2), macos(10.14.4), watchos(6.2), tvos(12.2), visionos(1.0)) 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 index 62199de6..85855a88 100644 --- a/Sources/Qonversion/Public/QONPurchaseOptions.m +++ b/Sources/Qonversion/Public/QONPurchaseOptions.m @@ -29,6 +29,22 @@ - (instancetype)initWithQuantity:(NSInteger)quantity contextKeys:(NSArray * _Nullable)contextKeys promoOffer:(QONPromotionalOffer * _Nullable)promoOffer { + self = [super init]; + + if (self) { + _quantity = quantity; + _contextKeys = contextKeys; + _promoOffer = promoOffer; + } + + return self; +} + +- (instancetype)initWithPromoOffer:(QONPromotionalOffer *)promoOffer { + return [self initWithQuantity:1 contextKeys:nil promoOffer:promoOffer]; +} + - (instancetype)initWithCoder:(NSCoder *)coder { self = [super init]; if (self) { diff --git a/Sources/Qonversion/Public/QONStoreKit2PurchaseModel.h b/Sources/Qonversion/Public/QONStoreKit2PurchaseModel.h index c1c315ef..6f8531b3 100644 --- a/Sources/Qonversion/Public/QONStoreKit2PurchaseModel.h +++ b/Sources/Qonversion/Public/QONStoreKit2PurchaseModel.h @@ -25,6 +25,12 @@ NS_SWIFT_NAME(Qonversion.StoreKit2PurchaseModel) @property (nonatomic, copy, nullable) NSString *introductoryPeriodUnit; @property (nonatomic, copy, nullable) NSString *introductoryPeriodNumberOfUnits; @property (nonatomic, copy, nullable) NSString *introductoryPaymentMode; +@property (nonatomic, copy, nullable) NSString *promoOfferId; +@property (nonatomic, copy, nullable) NSString *promoOfferPrice; +@property (nonatomic, copy, nullable) NSString *promoOfferNumberOfPeriods; +@property (nonatomic, copy, nullable) NSString *promoOfferPeriodUnit; +@property (nonatomic, copy, nullable) NSString *promoOfferPeriodNumberOfUnits; +@property (nonatomic, copy, nullable) NSString *promoOfferPaymentMode; @property (nonatomic, copy, nullable) NSString *storefrontCountryCode; @end diff --git a/Sources/Qonversion/Public/QONStoreKit2PurchaseModel.m b/Sources/Qonversion/Public/QONStoreKit2PurchaseModel.m index cf8dc163..15efa688 100644 --- a/Sources/Qonversion/Public/QONStoreKit2PurchaseModel.m +++ b/Sources/Qonversion/Public/QONStoreKit2PurchaseModel.m @@ -10,4 +10,31 @@ @implementation QONStoreKit2PurchaseModel +- (NSString *)description { + NSMutableString *description = [NSMutableString stringWithFormat:@"<%@: ", NSStringFromClass([self class])]; + + [description appendFormat:@"productId=%@,\n", self.productId]; + [description appendFormat:@"price=%@,\n", self.price]; + [description appendFormat:@"currency=%@,\n", self.currency]; + [description appendFormat:@"transactionId=%@,\n", self.transactionId]; + [description appendFormat:@"originalTransactionId=%@,\n", self.originalTransactionId]; + [description appendFormat:@"subscriptionPeriodUnit=%@,\n", self.subscriptionPeriodUnit]; + [description appendFormat:@"subscriptionPeriodNumberOfUnits=%@,\n", self.subscriptionPeriodNumberOfUnits]; + [description appendFormat:@"introductoryPrice=%@,\n", self.introductoryPrice]; + [description appendFormat:@"introductoryNumberOfPeriods=%@,\n", self.introductoryNumberOfPeriods]; + [description appendFormat:@"introductoryPeriodUnit=%@,\n", self.introductoryPeriodUnit]; + [description appendFormat:@"introductoryPeriodNumberOfUnits=%@,\n", self.introductoryPeriodNumberOfUnits]; + [description appendFormat:@"introductoryPaymentMode=%@,\n", self.introductoryPaymentMode]; + [description appendFormat:@"promoOfferId=%@,\n", self.promoOfferId]; + [description appendFormat:@"promoOfferPrice=%@,\n", self.promoOfferPrice]; + [description appendFormat:@"promoOfferNumberOfPeriods=%@,\n", self.promoOfferNumberOfPeriods]; + [description appendFormat:@"promoOfferPeriodUnit=%@,\n", self.promoOfferPeriodUnit]; + [description appendFormat:@"promoOfferPeriodNumberOfUnits=%@,\n", self.promoOfferPeriodNumberOfUnits]; + [description appendFormat:@"promoOfferPaymentMode=%@,\n", self.promoOfferPaymentMode]; + [description appendFormat:@"storefrontCountryCode=%@,\n", self.storefrontCountryCode]; + [description appendString:@">"]; + + return [description copy]; +} + @end diff --git a/Sources/Qonversion/Public/QONTransaction.h b/Sources/Qonversion/Public/QONTransaction.h index 6870d90d..8fc18996 100644 --- a/Sources/Qonversion/Public/QONTransaction.h +++ b/Sources/Qonversion/Public/QONTransaction.h @@ -63,6 +63,11 @@ NS_SWIFT_NAME(Qonversion.Transaction) */ @property (nonatomic, strong, nullable) NSDate *transactionRevocationDate; +/** + The identifier for the promotional offer if this transaction was made using it. + */ +@property (nonatomic, copy, nullable) NSString *promoOfferId; + /** Environment of the transaction. */ diff --git a/Sources/Qonversion/Public/QONTransaction.m b/Sources/Qonversion/Public/QONTransaction.m index db881d3f..f02ed947 100644 --- a/Sources/Qonversion/Public/QONTransaction.m +++ b/Sources/Qonversion/Public/QONTransaction.m @@ -16,6 +16,7 @@ - (instancetype)initWithOriginalTransactionId:(NSString *)originalTransactionId transactionDate:(NSDate *)transactionDate expirationDate:(NSDate *)expirationDate transactionRevocationDate:(NSDate *)transactionRevocationDate + promoOfferId:(NSString *)promoOfferId environment:(QONTransactionEnvironment)environment ownershipType:(QONTransactionOwnershipType)ownershipType type:(QONTransactionType)type { @@ -28,6 +29,7 @@ - (instancetype)initWithOriginalTransactionId:(NSString *)originalTransactionId _transactionDate = transactionDate; _expirationDate = expirationDate; _transactionRevocationDate = transactionRevocationDate; + _promoOfferId = promoOfferId; _environment = environment; _ownershipType = ownershipType; _type = type; @@ -45,6 +47,7 @@ - (instancetype)initWithCoder:(NSCoder *)coder { _transactionDate = [coder decodeObjectForKey:NSStringFromSelector(@selector(transactionDate))]; _expirationDate = [coder decodeObjectForKey:NSStringFromSelector(@selector(expirationDate))]; _transactionRevocationDate = [coder decodeObjectForKey:NSStringFromSelector(@selector(transactionRevocationDate))]; + _promoOfferId = [coder decodeObjectForKey:NSStringFromSelector(@selector(promoOfferId))]; _environment = [coder decodeIntegerForKey:NSStringFromSelector(@selector(environment))]; _ownershipType = [coder decodeIntegerForKey:NSStringFromSelector(@selector(ownershipType))]; _type = [coder decodeIntegerForKey:NSStringFromSelector(@selector(type))]; @@ -59,6 +62,7 @@ - (void)encodeWithCoder:(NSCoder *)coder { [coder encodeObject:_transactionDate forKey:NSStringFromSelector(@selector(transactionDate))]; [coder encodeObject:_expirationDate forKey:NSStringFromSelector(@selector(expirationDate))]; [coder encodeObject:_transactionRevocationDate forKey:NSStringFromSelector(@selector(transactionRevocationDate))]; + [coder encodeObject:_promoOfferId forKey:NSStringFromSelector(@selector(promoOfferId))]; [coder encodeInteger:_environment forKey:NSStringFromSelector(@selector(environment))]; [coder encodeInteger:_ownershipType forKey:NSStringFromSelector(@selector(ownershipType))]; [coder encodeInteger:_type forKey:NSStringFromSelector(@selector(type))]; @@ -72,6 +76,7 @@ - (NSString *)description { [description appendFormat:@"transactionDate=%@,\n", self.transactionDate]; [description appendFormat:@"expirationDate=%@,\n", self.expirationDate]; [description appendFormat:@"transactionRevocationDate=%@,\n", self.transactionRevocationDate]; + [description appendFormat:@"promoOfferId=%@,\n", self.promoOfferId]; [description appendFormat:@"environment=%@ (enum value = %li),\n", [self prettyEnvironment], (long) self.environment]; [description appendFormat:@"ownershipType=%@ (enum value = %li),\n", [self prettyOwnershipType], (long) self.ownershipType]; [description appendFormat:@"type=%@ (enum value = %li),\n", [self prettyType], (long) self.type]; diff --git a/Sources/Qonversion/Public/Qonversion.h b/Sources/Qonversion/Public/Qonversion.h index 35c923e4..59ff6a33 100644 --- a/Sources/Qonversion/Public/Qonversion.h +++ b/Sources/Qonversion/Public/Qonversion.h @@ -132,6 +132,18 @@ static NSString *const QonversionApiErrorDomain = @"com.qonversion.io.api"; */ - (void)attribution:(NSDictionary *)data fromProvider:(QONAttributionProvider)provider DEPRECATED_MSG_ATTRIBUTE("This function shouldn't be called anymore. All attribution logic continues to work as usual."); +/** + Retrieve the promotional offer for the product if it exists. + Make sure to call this function before displaying product details to the user. + The generated signature for the promotional offer is valid for a single transaction. + If the purchase fails, you need to call this function again to obtain a new promotional offer signature. + Use this signature to complete the purchase through the purchase function, along with the purchase options object. + @param product - product you want to purchase. + @param discount - discount to create promotional offer signature. + @param completion - completion block that will be called when response is received. + */ +- (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), tvos(12.2), visionos(1.0)); + /** Check user entitlements @param completion Completion block that includes entitlements dictionary and error diff --git a/Sources/Qonversion/Public/Qonversion.m b/Sources/Qonversion/Public/Qonversion.m index 506afe75..4f436634 100644 --- a/Sources/Qonversion/Public/Qonversion.m +++ b/Sources/Qonversion/Public/Qonversion.m @@ -244,13 +244,10 @@ - (void)handlePurchases:(NSArray *)purchasesInfo co [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)) { +- (void)getPromotionalOfferForProduct:(QONProduct * _Nonnull)product discount:(SKProductDiscount * _Nonnull)discount completion:(nonnull QONPromotionalOfferCompletionHandler)completion { [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 80a8d3ea..3710a570 100644 --- a/Sources/Qonversion/Qonversion/Constants/QNAPIConstants/QNAPIConstants.h +++ b/Sources/Qonversion/Qonversion/Constants/QNAPIConstants/QNAPIConstants.h @@ -11,7 +11,7 @@ extern NSString *const kAPIBase; extern NSString *const kInitEndpoint; extern NSString *const kPurchaseEndpoint; -extern NSString *const kGetPromoOfferDetailsEndpoint; +extern NSString *const kPostPromoOfferDetailsEndpoint; 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 e9dcf2ec..98089d35 100644 --- a/Sources/Qonversion/Qonversion/Constants/QNAPIConstants/QNAPIConstants.m +++ b/Sources/Qonversion/Qonversion/Constants/QNAPIConstants/QNAPIConstants.m @@ -13,8 +13,7 @@ NSString * const kInitEndpoint = @"v1/user/init"; NSString * const kPurchaseEndpoint = @"v1/user/purchase"; -// TODO: Update endpoint -NSString * const kGetPromoOfferDetailsEndpoint = @"update_promo_offer_endpoint_here"; +NSString * const kPostPromoOfferDetailsEndpoint = @"v3/users/%@/offers/%@/signatures"; NSString * const kProductsEndpoint = @"v1/products/get"; NSString * const kPropertiesEndpoint = @"v3/users/%@/properties"; NSString * const kRemoteConfigEndpoint = @"v3/remote-config"; diff --git a/Sources/Qonversion/Qonversion/Core/QNRequestBuilder/QNRequestBuilder.h b/Sources/Qonversion/Qonversion/Core/QNRequestBuilder/QNRequestBuilder.h index 6a3a8823..bbe81f89 100644 --- a/Sources/Qonversion/Qonversion/Core/QNRequestBuilder/QNRequestBuilder.h +++ b/Sources/Qonversion/Qonversion/Core/QNRequestBuilder/QNRequestBuilder.h @@ -30,6 +30,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; +- (NSURLRequest *)makePostPromotionalOfferRequestWithBody:(NSDictionary *)body userId:(NSString *)userId offerId:(NSString *)offerId; @end diff --git a/Sources/Qonversion/Qonversion/Core/QNRequestBuilder/QNRequestBuilder.m b/Sources/Qonversion/Qonversion/Core/QNRequestBuilder/QNRequestBuilder.m index 736469c3..6c5ea1f7 100644 --- a/Sources/Qonversion/Qonversion/Core/QNRequestBuilder/QNRequestBuilder.m +++ b/Sources/Qonversion/Qonversion/Core/QNRequestBuilder/QNRequestBuilder.m @@ -54,8 +54,8 @@ - (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 *)makePostPromotionalOfferRequestWithBody:(NSDictionary *)body userId:(NSString *)userId offerId:(NSString *)offerId { + return [self makeRequestWithDictBody:body baseURL:self.baseURL endpoint:[NSString stringWithFormat:kPostPromoOfferDetailsEndpoint, userId, offerId] type:QONRequestTypePost]; } - (NSURLRequest *)makeUserActionPointsRequestWith:(NSString *)parameter { diff --git a/Sources/Qonversion/Qonversion/Core/QNRequestSerializer/QNRequestSerializer.h b/Sources/Qonversion/Qonversion/Core/QNRequestSerializer/QNRequestSerializer.h index 58d032ec..67a48086 100644 --- a/Sources/Qonversion/Qonversion/Core/QNRequestSerializer/QNRequestSerializer.h +++ b/Sources/Qonversion/Qonversion/Core/QNRequestSerializer/QNRequestSerializer.h @@ -19,7 +19,6 @@ NS_ASSUME_NONNULL_BEGIN 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)); diff --git a/Sources/Qonversion/Qonversion/Core/QNRequestSerializer/QNRequestSerializer.m b/Sources/Qonversion/Qonversion/Core/QNRequestSerializer/QNRequestSerializer.m index 568a5bc3..f6cad20f 100644 --- a/Sources/Qonversion/Qonversion/Core/QNRequestSerializer/QNRequestSerializer.m +++ b/Sources/Qonversion/Qonversion/Core/QNRequestSerializer/QNRequestSerializer.m @@ -65,6 +65,30 @@ - (NSDictionary *)purchaseData:(SKProduct *)product } } + if (@available(iOS 12.2, macOS 10.14.4, watchOS 6.2, visionOS 1.0, tvOS 12.2, *)) { + NSString *offerId = transaction.payment.paymentDiscount.identifier; + if (offerId.length > 0) { + NSMutableDictionary *promoOffer = [[NSMutableDictionary alloc] init]; + SKProductDiscount *purchasedDiscount = nil; + for (SKProductDiscount *discount in product.discounts) { + if ([discount.identifier isEqualToString:offerId]) { + purchasedDiscount = discount; + } + } + + if (purchasedDiscount) { + promoOffer[@"id"] = offerId; + promoOffer[@"value"] = purchasedDiscount.price.stringValue; + promoOffer[@"number_of_periods"] = @(purchasedDiscount.numberOfPeriods).stringValue; + promoOffer[@"period_number_of_units"] = @(purchasedDiscount.subscriptionPeriod.numberOfUnits).stringValue; + promoOffer[@"period_unit"] = @(purchasedDiscount.subscriptionPeriod.unit).stringValue; + promoOffer[@"payment_mode"] = @(purchasedDiscount.paymentMode).stringValue; + + result[@"promo_offer"] = [promoOffer copy]; + } + } + } + if (purchaseOptions.contextKeys.count > 0) { purchaseDict[@"context_keys"] = purchaseOptions.contextKeys; } @@ -80,17 +104,13 @@ - (NSDictionary *)purchaseData:(SKProduct *)product } - (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; + result[@"product"] = product.storeID; + result[@"app_account_token"] = identityId; + result[@"app_bundle_id"] = [NSBundle mainBundle].bundleIdentifier; return [result copy]; } @@ -122,6 +142,16 @@ - (NSDictionary *)purchaseInfo:(QONStoreKit2PurchaseModel *)purchaseModel result[@"introductory_offer"] = introOffer.count > 0 ? introOffer : nil; + NSMutableDictionary *promoOffer = [[NSMutableDictionary alloc] init]; + promoOffer[@"id"] = purchaseModel.promoOfferId; + promoOffer[@"value"] = purchaseModel.promoOfferPrice; + promoOffer[@"number_of_periods"] = purchaseModel.promoOfferNumberOfPeriods; + promoOffer[@"period_number_of_units"] = purchaseModel.promoOfferPeriodNumberOfUnits; + promoOffer[@"period_unit"] = purchaseModel.promoOfferPeriodUnit; + promoOffer[@"payment_mode"] = purchaseModel.promoOfferPaymentMode; + + result[@"promo_offer"] = [promoOffer copy]; + purchaseDict[@"country"] = purchaseModel.storefrontCountryCode; result[@"purchase"] = purchaseDict; diff --git a/Sources/Qonversion/Qonversion/Main/QNProductCenterManager/QNProductCenterManager.h b/Sources/Qonversion/Qonversion/Main/QNProductCenterManager/QNProductCenterManager.h index 2de1ca52..42a8cb4f 100644 --- a/Sources/Qonversion/Qonversion/Main/QNProductCenterManager/QNProductCenterManager.h +++ b/Sources/Qonversion/Qonversion/Main/QNProductCenterManager/QNProductCenterManager.h @@ -42,7 +42,7 @@ NS_ASSUME_NONNULL_BEGIN - (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)); + completion:(QONPromotionalOfferCompletionHandler)completion API_AVAILABLE(ios(12.2), macos(10.14.4), watchos(6.2), tvos(12.2), visionos(1.0)); @end diff --git a/Sources/Qonversion/Qonversion/Main/QNProductCenterManager/QNProductCenterManager.m b/Sources/Qonversion/Qonversion/Main/QNProductCenterManager/QNProductCenterManager.m index 0a443f63..4977a0a2 100644 --- a/Sources/Qonversion/Qonversion/Main/QNProductCenterManager/QNProductCenterManager.m +++ b/Sources/Qonversion/Qonversion/Main/QNProductCenterManager/QNProductCenterManager.m @@ -446,8 +446,8 @@ - (void)processProductPurchase:(QONProduct *)product options:(QONPurchaseOptions QONVERSION_LOG(@"Purchasing in process"); return; } - - if (product && [_storeKitService purchase:product.storeID options:options]) { + NSString *identityId = [self.userInfoService obtainCustomIdentityUserID]; + if (product && [_storeKitService purchase:product.storeID options:options identityId:identityId]) { [self updatePurchaseOptions:options storeProductId:product.storeID]; self.purchasingBlocks[product.storeID] = completion; @@ -1053,8 +1053,9 @@ - (void)getPromotionalOfferForProduct:(QONProduct *)product __block __weak QNProductCenterManager *weakSelf = self; [self.storeKitService receipt:^(NSString * receipt) { NSString *identityId = [weakSelf.userInfoService obtainCustomIdentityUserID]; + NSString *userId = [weakSelf.userInfoService obtainUserID]; - [self.apiClient getPromotionalOfferForProduct:product discount:discount identityId:identityId receipt:receipt completion:^(NSDictionary * _Nullable dict, NSError * _Nullable error) { + [self.apiClient getPromotionalOfferForProduct:product discount:discount userId:userId identityId:identityId receipt:receipt completion:^(NSDictionary * _Nullable dict, NSError * _Nullable error) { if (error) { run_block_on_main(completion, nil, error); return; diff --git a/Sources/Qonversion/Qonversion/Mappers/QNMapper/QNMapper.h b/Sources/Qonversion/Qonversion/Mappers/QNMapper/QNMapper.h index b3118e72..5fa4bc3c 100644 --- a/Sources/Qonversion/Qonversion/Mappers/QNMapper/QNMapper.h +++ b/Sources/Qonversion/Qonversion/Mappers/QNMapper/QNMapper.h @@ -22,6 +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)); ++ (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), tvos(12.2), visionos(1.0)); @end diff --git a/Sources/Qonversion/Qonversion/Mappers/QNMapper/QNMapper.m b/Sources/Qonversion/Qonversion/Mappers/QNMapper/QNMapper.m index 1cdabebc..15460c76 100644 --- a/Sources/Qonversion/Qonversion/Mappers/QNMapper/QNMapper.m +++ b/Sources/Qonversion/Qonversion/Mappers/QNMapper/QNMapper.m @@ -72,17 +72,17 @@ + (QONPromotionalOffer * _Nullable)mapPromoOffer:(NSDictionary * _Nullable)rawDa return nil; } - NSString *identifier = rawData[@"identifier"]; - NSString *keyIdentifier = rawData[@"keyIdentifier"]; - NSString *uuidString = rawData[@"uuid"]; - NSUUID *nonce = [[NSUUID alloc] initWithUUIDString:uuidString]; + NSString *identifier = productDiscount.identifier; + NSString *keyIdentifier = rawData[@"key_identifier"]; + NSString *nonceString = rawData[@"nonce"]; + NSUUID *nonce = [[NSUUID alloc] initWithUUIDString:nonceString]; 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) { + if (identifier.length == 0 || keyIdentifier.length == 0 || nonceString.length == 0 || signature.length == 0) { *error = [self promoOfferMappingError]; return nil; @@ -276,6 +276,7 @@ + (QONTransaction *)mapEntitlementTransaction:(NSDictionary *)rawTransaction { NSString *originalTransactionId = rawTransaction[@"original_transaction_id"]; NSString *transactionId = rawTransaction[@"transaction_id"]; NSString *offerCode = rawTransaction[@"offer_code"]; + NSString *promoOfferId = rawTransaction[@"promo_offer_id"]; NSDate *transactionDate = [self mapDateFromSource:rawTransaction key:@"transaction_timestamp"]; NSDate *expirationDate = [self mapDateFromSource:rawTransaction key:@"expiration_timestamp"]; @@ -293,7 +294,7 @@ + (QONTransaction *)mapEntitlementTransaction:(NSDictionary *)rawTransaction { NSNumber *transactionTypeNumber = transactionTypes[typeRaw]; QONTransactionType transactionType = transactionTypes ? transactionTypeNumber.integerValue : QONTransactionTypeUnknown; - QONTransaction *transaction = [[QONTransaction alloc] initWithOriginalTransactionId:originalTransactionId transactionId:transactionId offerCode:offerCode transactionDate:transactionDate expirationDate:expirationDate transactionRevocationDate:transactionRevocationDate environment:environment ownershipType:ownershipType type:transactionType]; + QONTransaction *transaction = [[QONTransaction alloc] initWithOriginalTransactionId:originalTransactionId transactionId:transactionId offerCode:offerCode transactionDate:transactionDate expirationDate:expirationDate transactionRevocationDate:transactionRevocationDate promoOfferId:promoOfferId environment:environment ownershipType:ownershipType type:transactionType]; return transaction; } diff --git a/Sources/Qonversion/Qonversion/Models/Protected/QONTransaction+Protected.h b/Sources/Qonversion/Qonversion/Models/Protected/QONTransaction+Protected.h index d5bb56e7..56bb632a 100644 --- a/Sources/Qonversion/Qonversion/Models/Protected/QONTransaction+Protected.h +++ b/Sources/Qonversion/Qonversion/Models/Protected/QONTransaction+Protected.h @@ -18,6 +18,7 @@ NS_ASSUME_NONNULL_BEGIN transactionDate:(NSDate *)transactionDate expirationDate:(NSDate *)expirationDate transactionRevocationDate:(NSDate *)transactionRevocationDate + promoOfferId:(NSString *)promoOfferId environment:(QONTransactionEnvironment)environment ownershipType:(QONTransactionOwnershipType)ownershipType type:(QONTransactionType)type; diff --git a/Sources/Qonversion/Qonversion/Services/QNAPIClient/QNAPIClient.h b/Sources/Qonversion/Qonversion/Services/QNAPIClient/QNAPIClient.h index 410998e4..96c88e76 100644 --- a/Sources/Qonversion/Qonversion/Services/QNAPIClient/QNAPIClient.h +++ b/Sources/Qonversion/Qonversion/Services/QNAPIClient/QNAPIClient.h @@ -64,9 +64,10 @@ NS_ASSUME_NONNULL_BEGIN - (void)detachUserFromRemoteConfiguration:(NSString *)remoteConfigurationId completion:(QNAPIClientEmptyCompletionHandler)completion; - (void)getPromotionalOfferForProduct:(QONProduct *)product discount:(SKProductDiscount *)discount + userId:(NSString *)userId identityId:(NSString *)identityId receipt:(nullable NSString *)receipt - completion:(QNAPIClientDictCompletionHandler)completion API_AVAILABLE(ios(11.2), macos(10.13.2), visionos(1.0)); + completion:(QNAPIClientDictCompletionHandler)completion API_AVAILABLE(ios(12.2), macos(10.14.4), watchos(6.2), tvos(12.2), visionos(1.0)); - (NSURLRequest *)handlePurchase:(QONStoreKit2PurchaseModel *)purchaseInfo receipt:(nullable NSString *)receipt diff --git a/Sources/Qonversion/Qonversion/Services/QNAPIClient/QNAPIClient.m b/Sources/Qonversion/Qonversion/Services/QNAPIClient/QNAPIClient.m index 470036d4..f374cf22 100644 --- a/Sources/Qonversion/Qonversion/Services/QNAPIClient/QNAPIClient.m +++ b/Sources/Qonversion/Qonversion/Services/QNAPIClient/QNAPIClient.m @@ -363,12 +363,13 @@ - (void)sendCrashReport:(NSDictionary *)data completion:(QNAPIClientEmptyComplet - (void)getPromotionalOfferForProduct:(QONProduct *)product discount:(SKProductDiscount *)discount + userId:(NSString *)userId identityId:(NSString *)identityId receipt:(nullable NSString *)receipt completion:(QNAPIClientDictCompletionHandler)completion { - NSDictionary *body = [self.requestSerializer promotionalOfferInfoForProduct:product discount:discount identityId:identityId receipt:receipt]; + NSDictionary *body = [self.requestSerializer promotionalOfferInfoForProduct:product identityId:identityId receipt:receipt]; - NSURLRequest *request = [self.requestBuilder makeGetPromotionalOfferRequestWithBody:body]; + NSURLRequest *request = [self.requestBuilder makePostPromotionalOfferRequestWithBody:body userId:userId offerId: discount.identifier]; [self processDictRequest:request completion:completion]; } diff --git a/Sources/Qonversion/Qonversion/Services/QNStoreKitService/QNStoreKitService.h b/Sources/Qonversion/Qonversion/Services/QNStoreKitService/QNStoreKitService.h index 02728aac..ae9eaa1e 100644 --- a/Sources/Qonversion/Qonversion/Services/QNStoreKitService/QNStoreKitService.h +++ b/Sources/Qonversion/Qonversion/Services/QNStoreKitService/QNStoreKitService.h @@ -15,7 +15,7 @@ typedef void(^QNStoreKitServiceReceiptFetchWithReceiptCompletionHandler)(NSStrin - (instancetype)initWithDelegate:(id )delegate; - (void)loadProducts:(NSSet *)products; -- (SKProduct *)purchase:(NSString *)productID options:(QONPurchaseOptions * _Nullable)options; +- (SKProduct *)purchase:(NSString *)productID options:(QONPurchaseOptions * _Nullable)options identityId:(NSString *)identityId; - (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 004458d1..1f2409ba 100644 --- a/Sources/Qonversion/Qonversion/Services/QNStoreKitService/QNStoreKitService.m +++ b/Sources/Qonversion/Qonversion/Services/QNStoreKitService/QNStoreKitService.m @@ -53,12 +53,11 @@ - (instancetype)init { return self; } -- (SKProduct *)purchase:(NSString *)productID options:(QONPurchaseOptions * _Nullable)options { +- (SKProduct *)purchase:(NSString *)productID options:(QONPurchaseOptions * _Nullable)options identityId:(NSString *)identityId { SKProduct *skProduct = self->_products[productID]; if (skProduct) { - // TODO: get promo offer from purchase options - [self purchaseProduct:skProduct options:options]; + [self purchaseProduct:skProduct options:options identityId:identityId]; return skProduct; } else { @@ -67,10 +66,10 @@ - (SKProduct *)purchase:(NSString *)productID options:(QONPurchaseOptions * _Nul } - (void)purchaseProduct:(SKProduct *)product { - [self purchaseProduct:product options:nil]; + [self purchaseProduct:product options:nil identityId:nil]; } -- (void)purchaseProduct:(SKProduct *)product options:(QONPurchaseOptions * _Nullable)options { +- (void)purchaseProduct:(SKProduct *)product options:(QONPurchaseOptions * _Nullable)options identityId:(NSString *)identityId { @synchronized (self) { self->_purchasingCurrently = product.productIdentifier; } @@ -81,6 +80,13 @@ - (void)purchaseProduct:(SKProduct *)product options:(QONPurchaseOptions * _Null payment.quantity = options.quantity; } + if (@available(iOS 12.2, macOS 10.14.4, watchOS 6.2, visionOS 1.0, tvOS 12.2, *)) { + if (options.promoOffer) { + payment.paymentDiscount = options.promoOffer.paymentDiscount; + payment.applicationUsername = identityId; + } + } + [[SKPaymentQueue defaultQueue] addPayment:[payment copy]]; } @@ -246,7 +252,12 @@ - (void)paymentQueue:(nonnull SKPaymentQueue *)queue for (SKPaymentTransaction *transaction in groupedTransactions) { BOOL isTheSameProductId = [previousHandledProductId isEqualToString:transaction.payment.productIdentifier]; - if (!isTheSameProductId) { + if (@available(iOS 12.2, macOS 10.14.4, watchOS 6.2, visionOS 1.0, tvOS 12.2, *)) { + if (!isTheSameProductId || transaction.payment.paymentDiscount) { + [resultTransactions addObject:transaction]; + previousHandledProductId = transaction.payment.productIdentifier; + } + } else if (!isTheSameProductId) { [resultTransactions addObject:transaction]; previousHandledProductId = transaction.payment.productIdentifier; } diff --git a/Sources/Swift/Extensions.swift b/Sources/Swift/Extensions.swift index b75bd4a4..fdd4afc8 100644 --- a/Sources/Swift/Extensions.swift +++ b/Sources/Swift/Extensions.swift @@ -16,4 +16,12 @@ extension Qonversion.PurchaseOptions { self.contextKeys = contextKeys } + @available(iOS 12.2, macOS 10.14.4, watchOS 6.2, visionOS 1.0, tvOS 12.2, *) + public convenience init(quantity: Int = 1, contextKeys: [String]? = nil, promoOffer: Qonversion.PromotionalOffer? = nil) { + self.init() + self.quantity = quantity + self.contextKeys = contextKeys + self.promoOffer = promoOffer + } + } diff --git a/Sources/Swift/PurchasesMapper.swift b/Sources/Swift/PurchasesMapper.swift index 56451dd7..62a4289f 100644 --- a/Sources/Swift/PurchasesMapper.swift +++ b/Sources/Swift/PurchasesMapper.swift @@ -10,7 +10,7 @@ import Foundation import StoreKit @_exported import Qonversion -@available(iOS 15.0, macOS 12.0, watchOS 8.0, tvOS 15.0, *) +@available(iOS 15.0, macOS 12.0, watchOS 8.0, tvOS 15.0, visionOS 1.0, *) final class PurchasesMapper { func map(transactions: [Transaction], with products:[Product]) async -> [Qonversion.StoreKit2PurchaseModel] { var result: [Qonversion.StoreKit2PurchaseModel] = [] @@ -50,10 +50,23 @@ final class PurchasesMapper { purchaseInfo.introductoryPeriodUnit = convert(periodUnit: introductoryOffer.period.unit) purchaseInfo.introductoryPeriodNumberOfUnits = String(introductoryOffer.period.value) purchaseInfo.introductoryPaymentMode = convert(paymentMode: introductoryOffer.paymentMode) - purchaseInfo.storefrontCountryCode = await Storefront.current?.countryCode } } + if #available(iOS 17.2, macOS 14.2, tvOS 17.2, watchOS 10.2, visionOS 1.1, *) { + if let offer: Transaction.Offer = transaction.offer, offer.type == .promotional, let offerId = offer.id, let promoOffer: Product.SubscriptionOffer = product.subscription?.promotionalOffers.first(where: {$0.id == offerId}) { + purchaseInfo.promoOfferId = offerId + purchaseInfo.promoOfferPrice = "\(promoOffer.price)" + purchaseInfo.promoOfferNumberOfPeriods = String(promoOffer.periodCount) + purchaseInfo.promoOfferPeriodUnit = convert(periodUnit: promoOffer.period.unit) + purchaseInfo.promoOfferPeriodNumberOfUnits = String(promoOffer.period.value) + purchaseInfo.promoOfferPaymentMode = convert(paymentMode: promoOffer.paymentMode) + + } + } + + purchaseInfo.storefrontCountryCode = await Storefront.current?.countryCode + return purchaseInfo } diff --git a/Sources/Swift/QonversionSwift.swift b/Sources/Swift/QonversionSwift.swift index 37bac26c..063d4611 100644 --- a/Sources/Swift/QonversionSwift.swift +++ b/Sources/Swift/QonversionSwift.swift @@ -22,7 +22,7 @@ public class QonversionSwift { /// Contact us before you start using this function. /// Call this function to sync purchases if you are using StoreKit2. public func syncStoreKit2Purchases() { - if #available(iOS 15.0, macOS 12.0, tvOS 15.0, watchOS 8.0, *) { + if #available(iOS 15.0, macOS 12.0, tvOS 15.0, watchOS 8.0, visionOS 1.0, *) { Task.init { try? await storeKitService?.syncTransactions() } @@ -31,7 +31,7 @@ public class QonversionSwift { /// Contact us before you start using this function. /// Call this function to sync purchases if you are using StoreKit2. - @available(iOS 15.0, macOS 12.0, tvOS 15.0, watchOS 8.0, *) + @available(iOS 15.0, macOS 12.0, tvOS 15.0, watchOS 8.0, visionOS 1.0, *) public func syncStoreKit2Transactions() async { try? await storeKitService?.syncTransactions() } @@ -39,7 +39,7 @@ public class QonversionSwift { /// Call this function to sync StoreKit2 transaction with Qonversion. /// - Parameters: /// - transaction: StoreKit2 transaction - @available(iOS 15.0, macOS 12.0, tvOS 15.0, watchOS 8.0, *) + @available(iOS 15.0, macOS 12.0, tvOS 15.0, watchOS 8.0, visionOS 1.0, *) public func handleTransaction(_ transaction: Transaction) async { try? await storeKitService?.handleTransaction(transaction) } @@ -47,7 +47,7 @@ public class QonversionSwift { /// Call this function to sync StoreKit2 transactions with Qonversion. /// - Parameters: /// - transactions: an array of StoreKit2 transactions - @available(iOS 15.0, macOS 12.0, tvOS 15.0, watchOS 8.0, *) + @available(iOS 15.0, macOS 12.0, tvOS 15.0, watchOS 8.0, visionOS 1.0, *) public func handleTransactions(_ transactions: [Transaction]) async { try? await storeKitService?.handleTransactions(transactions) } diff --git a/Sources/Swift/StoreKit2Service.swift b/Sources/Swift/StoreKit2Service.swift index 3e80e624..fb97c55b 100644 --- a/Sources/Swift/StoreKit2Service.swift +++ b/Sources/Swift/StoreKit2Service.swift @@ -12,17 +12,17 @@ import StoreKit protocol StoreKit2ServiceInterface { - @available(iOS 15.0, macOS 12.0, tvOS 15.0, watchOS 8.0, *) + @available(iOS 15.0, macOS 12.0, tvOS 15.0, watchOS 8.0, visionOS 1.0, *) func syncTransactions() async throws - @available(iOS 15.0, macOS 12.0, tvOS 15.0, watchOS 8.0, *) + @available(iOS 15.0, macOS 12.0, tvOS 15.0, watchOS 8.0, visionOS 1.0, *) func handleTransaction(_ transaction: Transaction) async throws - @available(iOS 15.0, macOS 12.0, tvOS 15.0, watchOS 8.0, *) + @available(iOS 15.0, macOS 12.0, tvOS 15.0, watchOS 8.0, visionOS 1.0, *) func handleTransactions(_ transactions: [Transaction]) async throws } -@available(iOS 15.0, macOS 12.0, tvOS 15.0, watchOS 8.0, *) +@available(iOS 15.0, macOS 12.0, tvOS 15.0, watchOS 8.0, visionOS 1.0, *) public class StoreKit2Service: StoreKit2ServiceInterface { let mapper = PurchasesMapper() @@ -112,10 +112,18 @@ public class StoreKit2Service: StoreKit2ServiceInterface { var previousHandledProductId = "" for transaction in transactions { - // here we detect another product purchase - if previousHandledProductId != transaction.productID { - result.append(transaction) - previousHandledProductId = transaction.productID + if #available(iOS 17.2, macOS 14.2, tvOS 17.2, watchOS 10.2, visionOS 1.1, *) { + // here we detect another product purchase or offer available + if transaction.offer != nil || previousHandledProductId != transaction.productID { + result.append(transaction) + previousHandledProductId = transaction.productID + } + } else { + // here we detect another product purchase + if previousHandledProductId != transaction.productID { + result.append(transaction) + previousHandledProductId = transaction.productID + } } } }