diff --git a/Qonversion.xcodeproj/project.pbxproj b/Qonversion.xcodeproj/project.pbxproj index bd23baa9..e1952be9 100644 --- a/Qonversion.xcodeproj/project.pbxproj +++ b/Qonversion.xcodeproj/project.pbxproj @@ -38,6 +38,8 @@ 45FFA2EB24BEEA9A007EFB8F /* ProductCenterManagerTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 45FFA2EA24BEEA9A007EFB8F /* ProductCenterManagerTests.m */; }; 45FFA2ED24BEF379007EFB8F /* full_init.json in Resources */ = {isa = PBXBuildFile; fileRef = 45FFA2EC24BEF379007EFB8F /* full_init.json */; }; 4CADC5602759181A004FDC10 /* AuthViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CADC55F2759181A004FDC10 /* AuthViewController.swift */; }; + 6A121DAC2BB445AE0073B330 /* QONRemoteConfigList.m in Sources */ = {isa = PBXBuildFile; fileRef = 6A121DAB2BB445AE0073B330 /* QONRemoteConfigList.m */; }; + 6A121DAE2BB446740073B330 /* QONRemoteConfigList.h in Headers */ = {isa = PBXBuildFile; fileRef = 6A121DAD2BB446740073B330 /* QONRemoteConfigList.h */; settings = {ATTRIBUTES = (Public, ); }; }; 6A21BF4C2AB201A7005BDA7C /* QONRateLimiter.h in Headers */ = {isa = PBXBuildFile; fileRef = 6A21BF4B2AB201A7005BDA7C /* QONRateLimiter.h */; }; 6A21BF4E2AB20483005BDA7C /* QONRateLimiter.m in Sources */ = {isa = PBXBuildFile; fileRef = 6A21BF4D2AB20483005BDA7C /* QONRateLimiter.m */; }; 6A21BF532AB2059F005BDA7C /* QONRequest.h in Headers */ = {isa = PBXBuildFile; fileRef = 6A21BF522AB2059F005BDA7C /* QONRequest.h */; }; @@ -342,6 +344,9 @@ 45FFA2EA24BEEA9A007EFB8F /* ProductCenterManagerTests.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = ProductCenterManagerTests.m; sourceTree = ""; }; 45FFA2EC24BEF379007EFB8F /* full_init.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; path = full_init.json; sourceTree = ""; }; 4CADC55F2759181A004FDC10 /* AuthViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AuthViewController.swift; sourceTree = ""; }; + 6A121DAB2BB445AE0073B330 /* QONRemoteConfigList.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = QONRemoteConfigList.m; sourceTree = ""; }; + 6A121DAD2BB446740073B330 /* QONRemoteConfigList.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = QONRemoteConfigList.h; sourceTree = ""; }; + 6A121DAF2BB44D7B0073B330 /* QONRemoteConfigList+protected.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "QONRemoteConfigList+protected.h"; sourceTree = ""; }; 6A21BF4B2AB201A7005BDA7C /* QONRateLimiter.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = QONRateLimiter.h; sourceTree = ""; }; 6A21BF4D2AB20483005BDA7C /* QONRateLimiter.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = QONRateLimiter.m; sourceTree = ""; }; 6A21BF522AB2059F005BDA7C /* QONRequest.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = QONRequest.h; sourceTree = ""; }; @@ -991,6 +996,8 @@ 70D0E2B7291A9BE3004E8DE8 /* QONConfiguration.m */, 7094501829CD994200F55E68 /* QONRemoteConfig.h */, 7094501929CD994200F55E68 /* QONRemoteConfig.m */, + 6A121DAD2BB446740073B330 /* QONRemoteConfigList.h */, + 6A121DAB2BB445AE0073B330 /* QONRemoteConfigList.m */, 707734F22A9F607700CFF742 /* QONRemoteConfigurationSource.h */, 707734F32A9F607700CFF742 /* QONRemoteConfigurationSource.m */, 70A26ADC2A31BC44008CE4A7 /* QONExperiment.h */, @@ -1335,6 +1342,7 @@ children = ( 700EC171291277130032E205 /* QONExperimentGroup+Protected.h */, 7097C6BD2A38BFC800565DE4 /* QONRemoteConfig+Protected.h */, + 6A121DAF2BB44D7B0073B330 /* QONRemoteConfigList+protected.h */, 707734F62A9F6B8700CFF742 /* QONRemoteConfigurationSource+Protected.h */, 702DBDEB2A3216C900D590D0 /* QONExperiment+Protected.h */, 8957324226DD03A3009507A6 /* QONUser+Protected.h */, @@ -1595,6 +1603,7 @@ 6A21BF4C2AB201A7005BDA7C /* QONRateLimiter.h in Headers */, 70B917672B34284200BF0689 /* QONTransaction+Protected.h in Headers */, 895732B426DD03A3009507A6 /* QONAutomationsService.h in Headers */, + 6A121DAE2BB446740073B330 /* QONRemoteConfigList.h in Headers */, 895732CA26DD03A3009507A6 /* QNUtils.h in Headers */, 8957328126DD03A3009507A6 /* QONEntitlement.h in Headers */, 8957330526DD03A3009507A6 /* QNIdentityServiceInterface.h in Headers */, @@ -1983,6 +1992,7 @@ 8957329726DD03A3009507A6 /* QONStoreKitSugare.m in Sources */, 895732A226DD03A3009507A6 /* QONMacrosProcess.m in Sources */, 8957329E26DD03A3009507A6 /* QONProduct.m in Sources */, + 6A121DAC2BB445AE0073B330 /* QONRemoteConfigList.m in Sources */, 6ABCBE192B99A79E003DB107 /* QONRemoteConfigLoadingState.m in Sources */, 895732A026DD03A3009507A6 /* QONAutomationsConstants.m in Sources */, 7094501B29CD994200F55E68 /* QONRemoteConfig.m in Sources */, diff --git a/Sources/Qonversion/Public/QONLaunchResult.h b/Sources/Qonversion/Public/QONLaunchResult.h index 4f6070ec..fdc57f6b 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, QONUserProperties; +@class QONEntitlement, QONProduct, QONOfferings, QONIntroEligibility, QONUser, QONRemoteConfig, QONRemoteConfigList, QONUserProperties; typedef NS_ENUM(NSInteger, QONAttributionProvider) { QONAttributionProviderAppsFlyer = 0, @@ -62,6 +62,8 @@ typedef void (^QONEligibilityCompletionHandler)(NSDictionary +#import "QONRemoteConfig.h" + +NS_ASSUME_NONNULL_BEGIN + +NS_SWIFT_NAME(Qonversion.RemoteConfig) +@interface QONRemoteConfigList : NSObject + +@property (nonatomic, copy) NSArray *remoteConfigs; + +- (QONRemoteConfig *_Nullable)remoteConfigForContextKey:(NSString *)key; +- (QONRemoteConfig *_Nullable)remoteConfigForEmptyContextKey; + +@end + +NS_ASSUME_NONNULL_END diff --git a/Sources/Qonversion/Public/QONRemoteConfigList.m b/Sources/Qonversion/Public/QONRemoteConfigList.m new file mode 100644 index 00000000..af395850 --- /dev/null +++ b/Sources/Qonversion/Public/QONRemoteConfigList.m @@ -0,0 +1,40 @@ +// +// QONRemoteConfigList.m +// Qonversion +// +// Created by Kamo Spertsyan on 27.03.2024. +// Copyright © 2024 Qonversion Inc. All rights reserved. +// + +#import "QONRemoteConfigList.h" + +@implementation QONRemoteConfigList + +- (instancetype)initWithRemoteConfigs:(NSArray *)remoteConfigs { + self = [super init]; + + if (self) { + _remoteConfigs = [remoteConfigs copy]; + } + + return self; +} + +- (QONRemoteConfig *_Nullable)remoteConfigForContextKey:(NSString *)key { + return [self findRemoteConfigForContextKey:key]; +} + +- (QONRemoteConfig *_Nullable)remoteConfigForEmptyContextKey { + return [self findRemoteConfigForContextKey:nil]; +} + +- (QONRemoteConfig *)findRemoteConfigForContextKey:(NSString *_Nullable)key { + for (QONRemoteConfig *config in self.remoteConfigs) { + if ((key == nil && config.source.contextKey == nil) || [config.source.contextKey isEqualToString:key]) { + return config; + } + } + return nil; +} + +@end diff --git a/Sources/Qonversion/Public/Qonversion.h b/Sources/Qonversion/Public/Qonversion.h index eeb977c4..54c3872f 100644 --- a/Sources/Qonversion/Public/Qonversion.h +++ b/Sources/Qonversion/Public/Qonversion.h @@ -11,6 +11,7 @@ #import "QONExperimentGroup.h" #import "QONExperiment.h" #import "QONRemoteConfig.h" +#import "QONRemoteConfigList.h" #import "QONUser.h" #import "QONErrors.h" #import "QONStoreKitSugare.h" @@ -207,12 +208,29 @@ static NSString *const QonversionApiErrorDomain = @"com.qonversion.io.api"; /** Returns Qonversion remote config object by context key. Use this function to get the remote config with specific payload and experiment info. - @param contextKey context key for the requested remote config. + @param contextKey context key to load remote config for. @param completion completion block that includes information about the remote config. */ - (void)remoteConfig:(NSString *)contextKey completion:(QONRemoteConfigCompletionHandler)completion NS_SWIFT_NAME(remoteConfig(contextKey:completion:)); +/** + Returns Qonversion remote config objects by a list of context keys. + Use this function to get the remote configs with specific payload and experiment info. + @param contextKeys list of context keys to load remote configs for. + @param includeEmptyContextKey - set to true if you want to include remote config with empty context key to the result + @param completion completion block that includes information about the loaded remote configs. + */ +- (void)remoteConfigList:(NSArray *)contextKeys includeEmptyContextKey:(BOOL)includeEmptyContextKey completion:(QONRemoteConfigListCompletionHandler)completion +NS_SWIFT_NAME(remoteConfigList(contextKeys:includeEmptyContextKey:completion:)); + +/** + Returns Qonversion remote config objects for all existing context key (including empty one). + Use this function to get the remote configs with specific payload and experiment info. + @param completion completion block that includes information about the loaded remote configs. + */ +- (void)remoteConfigList:(QONRemoteConfigListCompletionHandler)completion; + /** This function should be used for the test purposes only. Do not forget to delete the usage of this function before the release. diff --git a/Sources/Qonversion/Public/Qonversion.m b/Sources/Qonversion/Public/Qonversion.m index 43bc6092..3fffd13a 100644 --- a/Sources/Qonversion/Public/Qonversion.m +++ b/Sources/Qonversion/Public/Qonversion.m @@ -203,6 +203,14 @@ - (void)remoteConfig:(NSString *)contextKey completion:(QONRemoteConfigCompletio [[[Qonversion sharedInstance] remoteConfigManager] obtainRemoteConfigWithContextKey:contextKey completion:completion]; } +- (void)remoteConfigList:(NSArray *)contextKeys includeEmptyContextKey:(BOOL)includeEmptyContextKey completion:(QONRemoteConfigListCompletionHandler)completion { + [[[Qonversion sharedInstance] remoteConfigManager] obtainRemoteConfigListWithContextKeys:contextKeys includeEmptyContextKey:includeEmptyContextKey completion:completion]; +} + +- (void)remoteConfigList:(QONRemoteConfigListCompletionHandler)completion { + [[[Qonversion sharedInstance] remoteConfigManager] obtainRemoteConfigList:completion]; +} + - (void)attachUserToExperiment:(NSString *)experimentId groupId:(NSString *)groupId completion:(QONExperimentAttachCompletionHandler)completion { [[[Qonversion sharedInstance] remoteConfigManager] attachUserToExperiment:experimentId groupId:groupId completion:completion]; } diff --git a/Sources/Qonversion/Qonversion/Constants/QNAPIConstants/QNAPIConstants.h b/Sources/Qonversion/Qonversion/Constants/QNAPIConstants/QNAPIConstants.h index 42d94151..0e9bcce4 100644 --- a/Sources/Qonversion/Qonversion/Constants/QNAPIConstants/QNAPIConstants.h +++ b/Sources/Qonversion/Qonversion/Constants/QNAPIConstants/QNAPIConstants.h @@ -26,10 +26,11 @@ extern NSString *const kSdkLogsEndpoint; extern NSString *const kSdkLogsBaseURL; extern NSString *const kStoredRequestsKey; extern NSString *const kRemoteConfigEndpoint; +extern NSString *const kRemoteConfigListEndpoint; extern NSString *const kEventEndpoint; extern NSString *const kAccessDeniedError; extern NSString *const kInternalServerError; -extern NSUInteger const kMaxSimilarRequestsPerSecond; \ No newline at end of file +extern NSUInteger const kMaxSimilarRequestsPerSecond; diff --git a/Sources/Qonversion/Qonversion/Constants/QNAPIConstants/QNAPIConstants.m b/Sources/Qonversion/Qonversion/Constants/QNAPIConstants/QNAPIConstants.m index 3b87bebd..ab254a9e 100644 --- a/Sources/Qonversion/Qonversion/Constants/QNAPIConstants/QNAPIConstants.m +++ b/Sources/Qonversion/Qonversion/Constants/QNAPIConstants/QNAPIConstants.m @@ -17,6 +17,7 @@ NSString * const kProductsEndpoint = @"v1/products/get"; NSString * const kPropertiesEndpoint = @"v3/users/%@/properties"; NSString * const kRemoteConfigEndpoint = @"v3/remote-config"; +NSString * const kRemoteConfigListEndpoint = @"v3/remote-configs"; NSString * const kAttachUserToExperimentEndpointFormat = @"v3/experiments/%@/users/%@"; NSString * const kAttachUserToRemoteConfigurationEndpointFormat = @"v3/remote-configurations/%@/users/%@"; diff --git a/Sources/Qonversion/Qonversion/Core/QNRequestBuilder/QNRequestBuilder.h b/Sources/Qonversion/Qonversion/Core/QNRequestBuilder/QNRequestBuilder.h index 73362f6b..8195a06d 100644 --- a/Sources/Qonversion/Qonversion/Core/QNRequestBuilder/QNRequestBuilder.h +++ b/Sources/Qonversion/Qonversion/Core/QNRequestBuilder/QNRequestBuilder.h @@ -23,7 +23,9 @@ typedef NS_ENUM(NSInteger, QONRequestType) { - (NSURLRequest *)makeCreateIdentityRequestWith:(NSDictionary *)parameters; - (NSURLRequest *)makeScreenShownRequestWith:(NSString *)parameter body:(NSDictionary *)body; - (NSURLRequest *)makeIntroTrialEligibilityRequestWithData:(NSDictionary *)parameters; -- (NSURLRequest *)remoteConfigRequestForUserId:(NSString *)userId contextKey:(NSString *)contextKey; +- (NSURLRequest *)makeRemoteConfigRequestForUserId:(NSString *)userId contextKey:(NSString *)contextKey; +- (NSURLRequest *)makeRemoteConfigListRequestForUserId:(NSString *)userId contextKeys:(NSArray *)contextKeys includeEmptyContextKey:(BOOL)includeEmptyContextKey; +- (NSURLRequest *)makeRemoteConfigListRequestForUserId:(NSString *)userId; - (NSURLRequest *)makeSdkLogsRequestWithBody:(NSDictionary *)body; - (NSURLRequest *)makeAttachUserToExperimentRequest:(NSString *)experimentId groupId:(NSString *)groupId userID:(NSString *)userID; - (NSURLRequest *)makeDetachUserFromExperimentRequest:(NSString *)experimentId userID:(NSString *)userID; diff --git a/Sources/Qonversion/Qonversion/Core/QNRequestBuilder/QNRequestBuilder.m b/Sources/Qonversion/Qonversion/Core/QNRequestBuilder/QNRequestBuilder.m index 30d36e15..73059baf 100644 --- a/Sources/Qonversion/Qonversion/Core/QNRequestBuilder/QNRequestBuilder.m +++ b/Sources/Qonversion/Qonversion/Core/QNRequestBuilder/QNRequestBuilder.m @@ -81,17 +81,34 @@ - (NSURLRequest *)makeIntroTrialEligibilityRequestWithData:(NSDictionary *)param return [self makeRequestWithDictBody:parameters baseURL:self.baseURL endpoint:kProductsEndpoint type:QONRequestTypePost]; } -- (NSURLRequest *)remoteConfigRequestForUserId:(NSString *)userId contextKey:(NSString *)contextKey { - NSURLRequest *request = [self makeGetRequestWithBaseURL:self.baseURL endpoint:kRemoteConfigEndpoint]; - - NSMutableURLRequest *mutableRequest = [request mutableCopy]; - NSString *updatedURLString = [mutableRequest.URL.absoluteString stringByAppendingString:[NSString stringWithFormat:@"?user_id=%@", userId]]; +- (NSURLRequest *)makeRemoteConfigRequestForUserId:(NSString *)userId contextKey:(NSString *)contextKey { + NSMutableURLRequest *request = [[self makeGetRequestWithBaseURL:self.baseURL endpoint:kRemoteConfigEndpoint] mutableCopy]; + NSString *updatedURLString = [request.URL.absoluteString stringByAppendingString:[NSString stringWithFormat:@"?user_id=%@", userId]]; if (contextKey) { updatedURLString = [updatedURLString stringByAppendingString:[NSString stringWithFormat:@"&context_key=%@", contextKey]]; } - [mutableRequest setURL:[NSURL URLWithString:updatedURLString]]; + [request setURL:[NSURL URLWithString:updatedURLString]]; + + return [request copy]; +} + +- (NSURLRequest *)makeRemoteConfigListRequestForUserId:(NSString *)userId contextKeys:(NSArray *)contextKeys includeEmptyContextKey:(BOOL)includeEmptyContextKey { + NSMutableURLRequest *request = [[self makeGetRequestWithBaseURL:self.baseURL endpoint:kRemoteConfigListEndpoint] mutableCopy]; + NSString *updatedURLString = [request.URL.absoluteString stringByAppendingString:[NSString stringWithFormat:@"?user_id=%@&with_empty_context_key=%@", userId, includeEmptyContextKey ? @"true" : @"false"]]; + for (NSString *contextKey in contextKeys) { + updatedURLString = [updatedURLString stringByAppendingString:[NSString stringWithFormat:@"&context_key=%@", contextKey]]; + } + [request setURL:[NSURL URLWithString:updatedURLString]]; + + return [request copy]; +} + +- (NSURLRequest *)makeRemoteConfigListRequestForUserId:(NSString *)userId { + NSMutableURLRequest *request = [[self makeGetRequestWithBaseURL:self.baseURL endpoint:kRemoteConfigListEndpoint] mutableCopy]; + NSString *updatedURLString = [request.URL.absoluteString stringByAppendingString:[NSString stringWithFormat:@"?user_id=%@&all_context_keys=true", userId]]; + [request setURL:[NSURL URLWithString:updatedURLString]]; - return [mutableRequest copy]; + return [request copy]; } - (NSURLRequest *)makeAttachUserToExperimentRequest:(NSString *)experimentId groupId:(NSString *)groupId userID:(NSString *)userID { diff --git a/Sources/Qonversion/Qonversion/Main/QONRemoteConfigManager/QONRemoteConfigManager.h b/Sources/Qonversion/Qonversion/Main/QONRemoteConfigManager/QONRemoteConfigManager.h index b9075558..610e37e6 100644 --- a/Sources/Qonversion/Qonversion/Main/QONRemoteConfigManager/QONRemoteConfigManager.h +++ b/Sources/Qonversion/Qonversion/Main/QONRemoteConfigManager/QONRemoteConfigManager.h @@ -22,6 +22,8 @@ NS_ASSUME_NONNULL_BEGIN - (void)userChangingRequestFailedWithError:(NSError *)error; - (void)handlePendingRequests; - (void)obtainRemoteConfigWithContextKey:(NSString * _Nullable)contextKey completion:(QONRemoteConfigCompletionHandler)completion; +- (void)obtainRemoteConfigListWithContextKeys:(NSArray *)contextKeys includeEmptyContextKey:(BOOL)includeEmptyContextKey completion:(QONRemoteConfigListCompletionHandler)completion; +- (void)obtainRemoteConfigList:(QONRemoteConfigListCompletionHandler)completion; - (void)attachUserToExperiment:(NSString *)experimentId groupId:(NSString *)groupId completion:(QONExperimentAttachCompletionHandler)completion; - (void)detachUserFromExperiment:(NSString *)experimentId completion:(QONExperimentAttachCompletionHandler)completion; - (void)attachUserToRemoteConfiguration:(NSString *)remoteConfigurationId completion:(QONRemoteConfigurationAttachCompletionHandler)completion; diff --git a/Sources/Qonversion/Qonversion/Main/QONRemoteConfigManager/QONRemoteConfigManager.m b/Sources/Qonversion/Qonversion/Main/QONRemoteConfigManager/QONRemoteConfigManager.m index acc7b5bf..7d2cb0ef 100644 --- a/Sources/Qonversion/Qonversion/Main/QONRemoteConfigManager/QONRemoteConfigManager.m +++ b/Sources/Qonversion/Qonversion/Main/QONRemoteConfigManager/QONRemoteConfigManager.m @@ -9,11 +9,12 @@ #import "QONRemoteConfigManager.h" #import "QONRemoteConfigService.h" #import "QONRemoteConfig.h" +#import "QONRemoteConfigList+protected.h" #import "QONExperiment.h" #import "QNProductCenterManager.h" #import "QONRemoteConfigLoadingState.h" -static NSString *const kDefaultLoadingStateKey = @""; +static NSString *const kEmptyContextKey = @""; @interface QONRemoteConfigManager () @@ -62,7 +63,7 @@ - (void)obtainRemoteConfigWithContextKey:(NSString * _Nullable)contextKey comple QONRemoteConfigLoadingState *loadingState = [self loadingStateForContextKey:contextKey]; if (loadingState == nil) { loadingState = [QONRemoteConfigLoadingState new]; - self.loadingStates[contextKey ?: kDefaultLoadingStateKey] = loadingState; + self.loadingStates[contextKey ?: kEmptyContextKey] = loadingState; } if (!isUserStable || loadingState.isInProgress) { @@ -91,23 +92,52 @@ - (void)obtainRemoteConfigWithContextKey:(NSString * _Nullable)contextKey comple }]; } +- (void)obtainRemoteConfigListWithContextKeys:(NSArray *)contextKeys includeEmptyContextKey:(BOOL)includeEmptyContextKey completion:(QONRemoteConfigListCompletionHandler)completion { + NSMutableArray *allKeys = [contextKeys mutableCopy]; + if (includeEmptyContextKey) { + [allKeys addObject:kEmptyContextKey]; + } + NSMutableArray *configs = [NSMutableArray new]; + for (NSString *contextKey in allKeys) { + QONRemoteConfigLoadingState *loadingState = [self loadingStateForContextKey:contextKey]; + if (loadingState && loadingState.loadedConfig) { + [configs addObject:loadingState.loadedConfig]; + } else { + break; + } + } + + if (configs.count == allKeys.count) { + QONRemoteConfigList *remoteConfigList = [[QONRemoteConfigList alloc] initWithRemoteConfigs:configs]; + return completion(remoteConfigList, nil); + } + + QONRemoteConfigListCompletionHandler completionWrapper = [self remoteConfigListCompletionWrapper:completion]; + [self.remoteConfigService loadRemoteConfigList:contextKeys includeEmptyContextKey:includeEmptyContextKey completion:completionWrapper]; +} + +- (void)obtainRemoteConfigList:(QONRemoteConfigListCompletionHandler)completion { + QONRemoteConfigListCompletionHandler completionWrapper = [self remoteConfigListCompletionWrapper:completion]; + [self.remoteConfigService loadRemoteConfigList:completionWrapper]; +} + - (void)attachUserToExperiment:(NSString *)experimentId groupId:(NSString *)groupId completion:(QONExperimentAttachCompletionHandler)completion { - self.loadingStates[kDefaultLoadingStateKey] = nil; + self.loadingStates[kEmptyContextKey] = nil; [self.remoteConfigService attachUserToExperiment:experimentId groupId:groupId completion:completion]; } - (void)detachUserFromExperiment:(NSString *)experimentId completion:(QONExperimentAttachCompletionHandler)completion { - self.loadingStates[kDefaultLoadingStateKey] = nil; + self.loadingStates[kEmptyContextKey] = nil; [self.remoteConfigService detachUserFromExperiment:experimentId completion:completion]; } - (void)attachUserToRemoteConfiguration:(NSString *)remoteConfigurationId completion:(QONRemoteConfigurationAttachCompletionHandler)completion { - self.loadingStates[kDefaultLoadingStateKey] = nil; + self.loadingStates[kEmptyContextKey] = nil; [self.remoteConfigService attachUserToRemoteConfiguration:remoteConfigurationId completion:completion]; } - (void)detachUserFromRemoteConfiguration:(NSString *)remoteConfigurationId completion:(QONRemoteConfigurationAttachCompletionHandler)completion { - self.loadingStates[kDefaultLoadingStateKey] = nil; + self.loadingStates[kEmptyContextKey] = nil; [self.remoteConfigService detachUserFromRemoteConfiguration:remoteConfigurationId completion:completion]; } @@ -124,8 +154,30 @@ - (void)executeRemoteConfigCompletionsWithContextKey:(NSString *)contextKey remo } - (QONRemoteConfigLoadingState *)loadingStateForContextKey:(NSString *)contextKey { - NSString *key = contextKey ?: kDefaultLoadingStateKey; + NSString *key = contextKey ?: kEmptyContextKey; return self.loadingStates[key]; } +- (QONRemoteConfigListCompletionHandler)remoteConfigListCompletionWrapper:(QONRemoteConfigListCompletionHandler)completion { + NSMutableDictionary *localLoadingStates = self.loadingStates; + + return ^(QONRemoteConfigList * _Nullable remoteConfigList, NSError * _Nullable error) { + if (error) { + completion(nil, error); + return; + } + + if (remoteConfigList) { + for (QONRemoteConfig *remoteConfig in remoteConfigList.remoteConfigs) { + NSString *contextKey = remoteConfig.source.contextKey ?: kEmptyContextKey; + QONRemoteConfigLoadingState *loadingState = localLoadingStates[contextKey] ?: [QONRemoteConfigLoadingState new]; + loadingState.loadedConfig = remoteConfig; + localLoadingStates[contextKey] = loadingState; + } + } + + completion(remoteConfigList, nil); + }; +} + @end diff --git a/Sources/Qonversion/Qonversion/Mappers/QONRemoteConfigMapper/QONRemoteConfigMapper.h b/Sources/Qonversion/Qonversion/Mappers/QONRemoteConfigMapper/QONRemoteConfigMapper.h index 0026f70c..d176b74f 100644 --- a/Sources/Qonversion/Qonversion/Mappers/QONRemoteConfigMapper/QONRemoteConfigMapper.h +++ b/Sources/Qonversion/Qonversion/Mappers/QONRemoteConfigMapper/QONRemoteConfigMapper.h @@ -8,7 +8,7 @@ #import -@class QONRemoteConfig; +@class QONRemoteConfig, QONRemoteConfigList; NS_ASSUME_NONNULL_BEGIN @@ -16,6 +16,8 @@ NS_ASSUME_NONNULL_BEGIN - (QONRemoteConfig * _Nullable)mapRemoteConfig:(NSDictionary *)remoteConfigData; +- (QONRemoteConfigList * _Nullable)mapRemoteConfigList:(NSArray *)remoteConfigListData; + @end NS_ASSUME_NONNULL_END diff --git a/Sources/Qonversion/Qonversion/Mappers/QONRemoteConfigMapper/QONRemoteConfigMapper.m b/Sources/Qonversion/Qonversion/Mappers/QONRemoteConfigMapper/QONRemoteConfigMapper.m index 06616646..417cd607 100644 --- a/Sources/Qonversion/Qonversion/Mappers/QONRemoteConfigMapper/QONRemoteConfigMapper.m +++ b/Sources/Qonversion/Qonversion/Mappers/QONRemoteConfigMapper/QONRemoteConfigMapper.m @@ -11,6 +11,7 @@ #import "QONExperiment+Protected.h" #import "QONExperimentGroup+Protected.h" #import "QONRemoteConfig+Protected.h" +#import "QONRemoteConfigList+Protected.h" #import "QONRemoteConfigurationSource+Protected.h" NSString *const kControlGroupType = @"control"; @@ -72,6 +73,24 @@ - (QONRemoteConfig * _Nullable)mapRemoteConfig:(NSDictionary *)remoteConfigData return [[QONRemoteConfig alloc] initWithPayload:payload experiment:experiment source:remoteConfigurationSource]; } +- (QONRemoteConfigList * _Nullable)mapRemoteConfigList:(NSArray *)remoteConfigListData { + if (![remoteConfigListData isKindOfClass:[NSArray class]]) { + return nil; + } + + NSMutableArray *remoteConfigs = [NSMutableArray new]; + for (NSDictionary *remoteConfigData in remoteConfigListData) { + if ([remoteConfigData isKindOfClass:[NSDictionary class]]) { + QONRemoteConfig *config = [self mapRemoteConfig:remoteConfigData]; + if (config != nil && config.source != nil) { + [remoteConfigs addObject:config]; + } + } + } + + return [[QONRemoteConfigList alloc] initWithRemoteConfigs:remoteConfigs]; +} + - (QONExperiment *)mapExperiment:(NSDictionary *)experimentData { if (![experimentData isKindOfClass:[NSDictionary class]]) { return nil; diff --git a/Sources/Qonversion/Qonversion/Models/Protected/QONRemoteConfigList+protected.h b/Sources/Qonversion/Qonversion/Models/Protected/QONRemoteConfigList+protected.h new file mode 100644 index 00000000..a1655311 --- /dev/null +++ b/Sources/Qonversion/Qonversion/Models/Protected/QONRemoteConfigList+protected.h @@ -0,0 +1,21 @@ +// +// QONRemoteConfigList+protected.h +// Qonversion +// +// Created by Kamo Spertsyan on 27.03.2024. +// Copyright © 2024 Qonversion Inc. All rights reserved. +// + +#import "QONRemoteConfigList.h" + +@class QONRemoteConfig; + +NS_ASSUME_NONNULL_BEGIN + +@interface QONRemoteConfigList () + +- (instancetype)initWithRemoteConfigs:(NSArray *)remoteConfigs; + +@end + +NS_ASSUME_NONNULL_END diff --git a/Sources/Qonversion/Qonversion/Services/QNAPIClient/QNAPIClient.h b/Sources/Qonversion/Qonversion/Services/QNAPIClient/QNAPIClient.h index e1496ad2..331b3f5d 100644 --- a/Sources/Qonversion/Qonversion/Services/QNAPIClient/QNAPIClient.h +++ b/Sources/Qonversion/Qonversion/Services/QNAPIClient/QNAPIClient.h @@ -56,6 +56,8 @@ NS_ASSUME_NONNULL_BEGIN - (void)storeRequestForRetry:(NSURLRequest *)request transactionId:(NSString *)transactionId; - (void)removeStoredRequestForTransactionId:(NSString *)transactionId; - (void)loadRemoteConfig:(NSString * _Nullable)contextKey completion:(QNAPIClientDictCompletionHandler)completion; +- (void)loadRemoteConfigList:(QNAPIClientArrayCompletionHandler)completion; +- (void)loadRemoteConfigListForContextKeys:(NSArray *)contextKeys includeEmptyContextKey:(BOOL)includeEmptyContextKey completion:(QNAPIClientArrayCompletionHandler)completion; - (void)attachUserToExperiment:(NSString *)experimentId groupId:(NSString *)groupId completion:(QNAPIClientEmptyCompletionHandler)completion; - (void)detachUserFromExperiment:(NSString *)experimentId completion:(QNAPIClientEmptyCompletionHandler)completion; - (void)attachUserToRemoteConfiguration:(NSString *)remoteConfigurationId completion:(QNAPIClientEmptyCompletionHandler)completion; diff --git a/Sources/Qonversion/Qonversion/Services/QNAPIClient/QNAPIClient.m b/Sources/Qonversion/Qonversion/Services/QNAPIClient/QNAPIClient.m index fba32ae3..d52fe97e 100644 --- a/Sources/Qonversion/Qonversion/Services/QNAPIClient/QNAPIClient.m +++ b/Sources/Qonversion/Qonversion/Services/QNAPIClient/QNAPIClient.m @@ -379,12 +379,44 @@ - (void)loadRemoteConfig:(NSString * _Nullable)contextKey completion:(QNAPIClien return; } - NSURLRequest *request = [self.requestBuilder remoteConfigRequestForUserId:self.userID contextKey:contextKey]; + NSURLRequest *request = [self.requestBuilder makeRemoteConfigRequestForUserId:self.userID contextKey:contextKey]; return [self processDictRequest:request completion:completion]; }]; } +- (void)loadRemoteConfigList:(QNAPIClientArrayCompletionHandler)completion { + [self.rateLimiter validateRateLimit:QONRateLimitedRequestTypeRemoteConfigList + hash:[self.userID hash] + completion:^(NSError *rateLimitError) { + if (rateLimitError != nil) { + completion(nil, rateLimitError); + return; + } + + NSURLRequest *request = [self.requestBuilder makeRemoteConfigListRequestForUserId:self.userID]; + + return [self processArrayRequest:request completion:completion]; + }]; +} + +- (void)loadRemoteConfigListForContextKeys:(NSArray *)contextKeys + includeEmptyContextKey:(BOOL)includeEmptyContextKey + completion:(QNAPIClientArrayCompletionHandler)completion { + [self.rateLimiter validateRateLimit:QONRateLimitedRequestTypeRemoteConfigList + params:@{@"contextKeys": contextKeys, @"userId": self.userID} + completion:^(NSError *rateLimitError) { + if (rateLimitError != nil) { + completion(nil, rateLimitError); + return; + } + + NSURLRequest *request = [self.requestBuilder makeRemoteConfigListRequestForUserId:self.userID contextKeys:contextKeys includeEmptyContextKey:includeEmptyContextKey]; + + return [self processArrayRequest:request completion:completion]; + }]; +} + - (void)attachUserToExperiment:(NSString *)experimentId groupId:(NSString *)groupId completion:(QNAPIClientEmptyCompletionHandler)completion { [self.rateLimiter validateRateLimit:QONRateLimitedRequestTypeAttachUserToExperiment hash:[[NSString stringWithFormat:@"%@%@%@", self.userID, experimentId, groupId] hash] diff --git a/Sources/Qonversion/Qonversion/Services/QONRemoteConfigService/QONRemoteConfigService.h b/Sources/Qonversion/Qonversion/Services/QONRemoteConfigService/QONRemoteConfigService.h index 098366b7..edfba1ea 100644 --- a/Sources/Qonversion/Qonversion/Services/QONRemoteConfigService/QONRemoteConfigService.h +++ b/Sources/Qonversion/Qonversion/Services/QONRemoteConfigService/QONRemoteConfigService.h @@ -20,6 +20,8 @@ NS_ASSUME_NONNULL_BEGIN @property (nonatomic, strong) QONRemoteConfigMapper *mapper; - (void)loadRemoteConfig:(NSString * _Nullable)contextKey completion:(QONRemoteConfigCompletionHandler)completion; +- (void)loadRemoteConfigList:(QONRemoteConfigListCompletionHandler)completion; +- (void)loadRemoteConfigList:(NSArray *)contextKeys includeEmptyContextKey:(BOOL)includeEmptyContextKey completion:(QONRemoteConfigListCompletionHandler)completion; - (void)attachUserToExperiment:(NSString *)experimentId groupId:(NSString *)groupId completion:(QONExperimentAttachCompletionHandler)completion; - (void)detachUserFromExperiment:(NSString *)experimentId completion:(QONExperimentAttachCompletionHandler)completion; - (void)attachUserToRemoteConfiguration:(NSString *)remoteConfiguration completion:(QONRemoteConfigurationAttachCompletionHandler)completion; diff --git a/Sources/Qonversion/Qonversion/Services/QONRemoteConfigService/QONRemoteConfigService.m b/Sources/Qonversion/Qonversion/Services/QONRemoteConfigService/QONRemoteConfigService.m index b9809426..cefb5c43 100644 --- a/Sources/Qonversion/Qonversion/Services/QONRemoteConfigService/QONRemoteConfigService.m +++ b/Sources/Qonversion/Qonversion/Services/QONRemoteConfigService/QONRemoteConfigService.m @@ -37,7 +37,7 @@ - (void)loadRemoteConfig:(NSString * _Nullable)contextKey completion:(QONRemoteC QONRemoteConfig *config = [weakSelf.mapper mapRemoteConfig:dict]; - if (config.payload.count == 0 && config.source == nil) { + if (config.source == nil) { NSError *error = [QONErrors errorWithCode:QONErrorRemoteConfigurationNotAvailable message:kNoRemoteConfigurationErrorMessage]; completion(nil, error); return; @@ -47,6 +47,32 @@ - (void)loadRemoteConfig:(NSString * _Nullable)contextKey completion:(QONRemoteC }]; } +- (void)loadRemoteConfigList:(QONRemoteConfigListCompletionHandler)completion { + __block __weak QONRemoteConfigService *weakSelf = self; + [self.apiClient loadRemoteConfigList:^(NSArray * _Nullable arr, NSError * _Nullable error) { + if (error) { + completion(nil, error); + return; + } + + QONRemoteConfigList *configList = [weakSelf.mapper mapRemoteConfigList:arr]; + completion(configList, error); + }]; +} + +- (void)loadRemoteConfigList:(NSArray *)contextKeys includeEmptyContextKey:(BOOL)includeEmptyContextKey completion:(QONRemoteConfigListCompletionHandler)completion { + __block __weak QONRemoteConfigService *weakSelf = self; + [self.apiClient loadRemoteConfigListForContextKeys:contextKeys includeEmptyContextKey:includeEmptyContextKey completion:^(NSArray * _Nullable arr, NSError * _Nullable error) { + if (error) { + completion(nil, error); + return; + } + + QONRemoteConfigList *configList = [weakSelf.mapper mapRemoteConfigList:arr]; + completion(configList, error); + }]; +} + - (void)attachUserToExperiment:(NSString *)experimentId groupId:(NSString *)groupId completion:(QONExperimentAttachCompletionHandler)completion { [self.apiClient attachUserToExperiment:experimentId groupId:groupId completion:^(NSError * _Nullable error) { if (error) { diff --git a/Sources/Qonversion/Qonversion/Utils/QNRateLimiter/QONRateLimiter.h b/Sources/Qonversion/Qonversion/Utils/QNRateLimiter/QONRateLimiter.h index 12b3aa49..152b43cc 100644 --- a/Sources/Qonversion/Qonversion/Utils/QNRateLimiter/QONRateLimiter.h +++ b/Sources/Qonversion/Qonversion/Utils/QNRateLimiter/QONRateLimiter.h @@ -16,16 +16,17 @@ typedef void (^QONRateLimiterCompletionHandler)(NSError * _Nullable rateLimitErr typedef NS_ENUM(NSInteger, QONRateLimitedRequestType) { QONRateLimitedRequestTypeInit = 0, QONRateLimitedRequestTypeRemoteConfig = 1, - QONRateLimitedRequestTypeAttachUserToExperiment = 2, - QONRateLimitedRequestTypeDetachUserFromExperiment = 3, - QONRateLimitedRequestTypePurchase = 4, - QONRateLimitedRequestTypeUserInfo = 5, - QONRateLimitedRequestTypeAttribution = 6, - QONRateLimitedRequestTypeGetProperties = 7, - QONRateLimitedRequestTypeEligibilityForProducts = 8, - QONRateLimitedRequestTypeIdentify = 9, - QONRateLimitedRequestTypeAttachUserToRemoteConfiguration = 10, - QONRateLimitedRequestTypeDetachUserFromRemoteConfiguration = 11, + QONRateLimitedRequestTypeRemoteConfigList = 2, + QONRateLimitedRequestTypeAttachUserToExperiment = 3, + QONRateLimitedRequestTypeDetachUserFromExperiment = 4, + QONRateLimitedRequestTypePurchase = 5, + QONRateLimitedRequestTypeUserInfo = 6, + QONRateLimitedRequestTypeAttribution = 7, + QONRateLimitedRequestTypeGetProperties = 8, + QONRateLimitedRequestTypeEligibilityForProducts = 9, + QONRateLimitedRequestTypeIdentify = 10, + QONRateLimitedRequestTypeAttachUserToRemoteConfiguration = 11, + QONRateLimitedRequestTypeDetachUserFromRemoteConfiguration = 12, }; @interface QONRateLimiter : NSObject