Skip to content

Commit

Permalink
Merge pull request #558 from qonversion/feature/promoOffers
Browse files Browse the repository at this point in the history
Updated promo offers logic
  • Loading branch information
suriksarkisyan authored Nov 8, 2024
2 parents 72b6286 + bed52d9 commit ba07c1d
Show file tree
Hide file tree
Showing 28 changed files with 217 additions and 55 deletions.
2 changes: 1 addition & 1 deletion Sources/Qonversion/Public/QONPromotionalOffer.h
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
21 changes: 21 additions & 0 deletions Sources/Qonversion/Public/QONPurchaseOptions.h
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@

#import <Foundation/Foundation.h>

@class QONPromotionalOffer;

NS_ASSUME_NONNULL_BEGIN

NS_SWIFT_NAME(Qonversion.PurchaseOptions)
Expand All @@ -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<NSString *> *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.
Expand All @@ -44,6 +49,22 @@ NS_SWIFT_NAME(Qonversion.PurchaseOptions)
*/
- (instancetype)initWithContextKeys:(NSArray<NSString *> * _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<NSString *> * _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
16 changes: 16 additions & 0 deletions Sources/Qonversion/Public/QONPurchaseOptions.m
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,22 @@ - (instancetype)initWithQuantity:(NSInteger)quantity contextKeys:(NSArray<NSStri
return self;
}

- (instancetype)initWithQuantity:(NSInteger)quantity contextKeys:(NSArray<NSString *> * _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) {
Expand Down
6 changes: 6 additions & 0 deletions Sources/Qonversion/Public/QONStoreKit2PurchaseModel.h
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
27 changes: 27 additions & 0 deletions Sources/Qonversion/Public/QONStoreKit2PurchaseModel.m
Original file line number Diff line number Diff line change
Expand Up @@ -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
5 changes: 5 additions & 0 deletions Sources/Qonversion/Public/QONTransaction.h
Original file line number Diff line number Diff line change
Expand Up @@ -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.
*/
Expand Down
5 changes: 5 additions & 0 deletions Sources/Qonversion/Public/QONTransaction.m
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand All @@ -28,6 +29,7 @@ - (instancetype)initWithOriginalTransactionId:(NSString *)originalTransactionId
_transactionDate = transactionDate;
_expirationDate = expirationDate;
_transactionRevocationDate = transactionRevocationDate;
_promoOfferId = promoOfferId;
_environment = environment;
_ownershipType = ownershipType;
_type = type;
Expand All @@ -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))];
Expand All @@ -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))];
Expand All @@ -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];
Expand Down
12 changes: 12 additions & 0 deletions Sources/Qonversion/Public/Qonversion.h
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
7 changes: 2 additions & 5 deletions Sources/Qonversion/Public/Qonversion.m
Original file line number Diff line number Diff line change
Expand Up @@ -244,13 +244,10 @@ - (void)handlePurchases:(NSArray<QONStoreKit2PurchaseModel *> *)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];

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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));

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
Expand All @@ -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];
}
Expand Down Expand Up @@ -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;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand Down Expand Up @@ -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;
Expand Down
Loading

0 comments on commit ba07c1d

Please sign in to comment.