/* * Copyright 2018 Google * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ #import #if TARGET_OS_IOS #import "FirebaseDynamicLinks/Sources/FIRDynamicLinkNetworking+Private.h" #import "FirebaseDynamicLinks/Sources/GINInvocation/GINArgument.h" #import "FirebaseDynamicLinks/Sources/GINInvocation/GINInvocation.h" #import "FirebaseDynamicLinks/Sources/Utilities/FDLDeviceHeuristicsHelper.h" #import "FirebaseDynamicLinks/Sources/Utilities/FDLUtilities.h" NS_ASSUME_NONNULL_BEGIN NSString *const kApiaryRestBaseUrl = @"https://appinvite-pa.googleapis.com/v1"; static NSString *const kiOSReopenRestBaseUrl = @"https://firebasedynamiclinks.googleapis.com/v1"; // Endpoint for default retrieval process V2. (Endpoint version is V1) static NSString *const kIosPostInstallAttributionRestBaseUrl = @"https://firebasedynamiclinks.googleapis.com/v1"; static NSString *const kReasonString = @"reason"; static NSString *const kiOSInviteReason = @"ios_invite"; NSString *const kFDLResolvedLinkDeepLinkURLKey = @"deepLink"; NSString *const kFDLResolvedLinkMinAppVersionKey = @"iosMinAppVersion"; static NSString *const kFDLAnalyticsDataSourceKey = @"utmSource"; static NSString *const kFDLAnalyticsDataMediumKey = @"utmMedium"; static NSString *const kFDLAnalyticsDataCampaignKey = @"utmCampaign"; static NSString *const kFDLAnalyticsDataTermKey = @"utmTerm"; static NSString *const kFDLAnalyticsDataContentKey = @"utmContent"; static NSString *const kHeaderIosBundleIdentifier = @"X-Ios-Bundle-Identifier"; static NSString *const kGenericErrorDomain = @"com.firebase.dynamicLinks"; typedef NSDictionary *_Nullable (^FIRDLNetworkingParserBlock)( NSString *requestURLString, NSData *data, NSString *_Nullable *_Nonnull matchMessagePtr, NSError *_Nullable *_Nullable errorPtr); NSString *FIRURLParameterString(NSString *key, NSString *value) { if (key.length > 0) { return [NSString stringWithFormat:@"?%@=%@", key, value]; } return @""; } NSString *_Nullable FIRDynamicLinkAPIKeyParameter(NSString *apiKey) { return apiKey ? FIRURLParameterString(@"key", apiKey) : nil; } void FIRMakeHTTPRequest(NSURLRequest *request, FIRNetworkRequestCompletionHandler completion) { NSURLSessionConfiguration *sessionConfig = [NSURLSessionConfiguration defaultSessionConfiguration]; NSURLSession *session = [NSURLSession sessionWithConfiguration:sessionConfig]; NSURLSessionDataTask *dataTask = [session dataTaskWithRequest:request completionHandler:^(NSData *_Nullable data, NSURLResponse *_Nullable response, NSError *_Nullable error) { completion(data, response, error); }]; [dataTask resume]; } NSData *_Nullable FIRDataWithDictionary(NSDictionary *dictionary, NSError **_Nullable error) { return [NSJSONSerialization dataWithJSONObject:dictionary options:0 error:error]; } @implementation FIRDynamicLinkNetworking { NSString *_APIKey; NSString *_URLScheme; } - (instancetype)initWithAPIKey:(NSString *)APIKey URLScheme:(NSString *)URLScheme { NSParameterAssert(APIKey); NSParameterAssert(URLScheme); if (self = [super init]) { _APIKey = [APIKey copy]; _URLScheme = [URLScheme copy]; } return self; } + (nullable NSError *)extractErrorForShortLink:(NSURL *)url data:(NSData *)data response:(NSURLResponse *)response error:(nullable NSError *)error { if (error) { return error; } NSInteger statusCode = [(NSHTTPURLResponse *)response statusCode]; NSError *customError = nil; if (![response isKindOfClass:[NSHTTPURLResponse class]]) { customError = [NSError errorWithDomain:kGenericErrorDomain code:0 userInfo:@{@"message" : @"Response should be of type NSHTTPURLResponse."}]; } else if ((statusCode < 200 || statusCode >= 300) && data) { NSDictionary *result = [NSJSONSerialization JSONObjectWithData:data options:0 error:nil]; if ([result isKindOfClass:[NSDictionary class]] && [result objectForKey:@"error"]) { id err = [result objectForKey:@"error"]; customError = [NSError errorWithDomain:kGenericErrorDomain code:statusCode userInfo:err]; } else { customError = [NSError errorWithDomain:kGenericErrorDomain code:0 userInfo:@{ @"message" : [NSString stringWithFormat:@"Failed to resolve link: %@", url.absoluteString] }]; } } return customError; } #pragma mark - Public interface - (void)resolveShortLink:(NSURL *)url FDLSDKVersion:(NSString *)FDLSDKVersion completion:(FIRDynamicLinkResolverHandler)handler { NSParameterAssert(handler); if (!url) { handler(nil, nil); return; } NSDictionary *requestBody = @{ @"requestedLink" : url.absoluteString, @"bundle_id" : [NSBundle mainBundle].bundleIdentifier, @"sdk_version" : FDLSDKVersion }; FIRNetworkRequestCompletionHandler resolveLinkCallback = ^(NSData *data, NSURLResponse *response, NSError *error) { NSURL *resolvedURL = nil; NSError *extractedError = [FIRDynamicLinkNetworking extractErrorForShortLink:url data:data response:response error:error]; if (!extractedError && data) { NSDictionary *result = [NSJSONSerialization JSONObjectWithData:data options:0 error:nil]; if ([result isKindOfClass:[NSDictionary class]]) { id invitationIDObject = [result objectForKey:@"invitationId"]; NSString *invitationIDString; if ([invitationIDObject isKindOfClass:[NSDictionary class]]) { NSDictionary *invitationIDDictionary = invitationIDObject; invitationIDString = invitationIDDictionary[@"id"]; } else if ([invitationIDObject isKindOfClass:[NSString class]]) { invitationIDString = invitationIDObject; } NSString *deepLinkString = result[kFDLResolvedLinkDeepLinkURLKey]; NSString *minAppVersion = result[kFDLResolvedLinkMinAppVersionKey]; NSString *utmSource = result[kFDLAnalyticsDataSourceKey]; NSString *utmMedium = result[kFDLAnalyticsDataMediumKey]; NSString *utmCampaign = result[kFDLAnalyticsDataCampaignKey]; NSString *utmContent = result[kFDLAnalyticsDataContentKey]; NSString *utmTerm = result[kFDLAnalyticsDataTermKey]; resolvedURL = FIRDLDeepLinkURLWithInviteID( invitationIDString, deepLinkString, utmSource, utmMedium, utmCampaign, utmContent, utmTerm, NO, nil, minAppVersion, self->_URLScheme, nil); } } handler(resolvedURL, extractedError); }; NSString *requestURLString = [NSString stringWithFormat:@"%@/reopenAttribution%@", kiOSReopenRestBaseUrl, FIRDynamicLinkAPIKeyParameter(_APIKey)]; [self executeOnePlatformRequest:requestBody forURL:requestURLString completionHandler:resolveLinkCallback]; } - (void)retrievePendingDynamicLinkWithIOSVersion:(NSString *)IOSVersion resolutionHeight:(NSInteger)resolutionHeight resolutionWidth:(NSInteger)resolutionWidth locale:(NSString *)locale localeRaw:(NSString *)localeRaw localeFromWebView:(NSString *)localeFromWebView timezone:(NSString *)timezone modelName:(NSString *)modelName FDLSDKVersion:(NSString *)FDLSDKVersion appInstallationDate:(NSDate *_Nullable)appInstallationDate uniqueMatchVisualStyle: (FIRDynamicLinkNetworkingUniqueMatchVisualStyle)uniqueMatchVisualStyle retrievalProcessType: (FIRDynamicLinkNetworkingRetrievalProcessType)retrievalProcessType uniqueMatchLinkToCheck:(NSURL *)uniqueMatchLinkToCheck handler: (FIRPostInstallAttributionCompletionHandler)handler { NSParameterAssert(handler); NSMutableDictionary *requestBody = [@{ @"bundleId" : [NSBundle mainBundle].bundleIdentifier, @"device" : [FDLDeviceHeuristicsHelper FDLDeviceInfoDictionaryFromResolutionHeight:resolutionHeight resolutionWidth:resolutionWidth locale:locale localeRaw:localeRaw localeFromWebview:localeFromWebView timeZone:timezone modelName:modelName], @"iosVersion" : IOSVersion, @"sdkVersion" : FDLSDKVersion, @"visualStyle" : @(uniqueMatchVisualStyle), @"retrievalMethod" : @(retrievalProcessType), } mutableCopy]; if (appInstallationDate) { requestBody[@"appInstallationTime"] = @((NSInteger)[appInstallationDate timeIntervalSince1970]); } if (uniqueMatchLinkToCheck) { requestBody[@"uniqueMatchLinkToCheck"] = uniqueMatchLinkToCheck.absoluteString; } FIRDLNetworkingParserBlock responseParserBlock = ^NSDictionary *_Nullable( NSString *requestURLString, NSData *data, NSString **matchMessagePtr, NSError **errorPtr) { NSError *serializationError; NSDictionary *result = [NSJSONSerialization JSONObjectWithData:data options:0 error:&serializationError]; if (serializationError) { if (errorPtr != nil) { *errorPtr = serializationError; } return nil; } NSString *matchMessage = result[@"matchMessage"]; if (matchMessage.length) { *matchMessagePtr = matchMessage; } // Create the dynamic link parameters NSMutableDictionary *dynamicLinkParameters = [[NSMutableDictionary alloc] init]; dynamicLinkParameters[kFIRDLParameterInviteId] = result[@"invitationId"]; dynamicLinkParameters[kFIRDLParameterDeepLinkIdentifier] = result[@"deepLink"]; if (result[@"deepLink"]) { dynamicLinkParameters[kFIRDLParameterMatchType] = FIRDLMatchTypeStringFromServerString(result[@"attributionConfidence"]); } dynamicLinkParameters[kFIRDLParameterSource] = result[@"utmSource"]; dynamicLinkParameters[kFIRDLParameterMedium] = result[@"utmMedium"]; dynamicLinkParameters[kFIRDLParameterCampaign] = result[@"utmCampaign"]; dynamicLinkParameters[kFIRDLParameterMinimumAppVersion] = result[@"appMinimumVersion"]; dynamicLinkParameters[kFIRDLParameterRequestIPVersion] = result[@"requestIpVersion"]; dynamicLinkParameters[kFIRDLParameterMatchMessage] = matchMessage; return [dynamicLinkParameters copy]; }; [self sendRequestWithBaseURLString:kIosPostInstallAttributionRestBaseUrl requestBody:requestBody endpointPath:@"installAttribution" parserBlock:responseParserBlock completion:handler]; } - (void)convertInvitation:(NSString *)invitationID handler:(nullable FIRDynamicLinkNetworkingErrorHandler)handler { if (!invitationID) { return; } NSDictionary *requestBody = @{ @"invitationId" : @{@"id" : invitationID}, @"containerClientId" : @{ @"type" : @"IOS", } }; FIRNetworkRequestCompletionHandler convertInvitationCallback = ^(NSData *data, NSURLResponse *response, NSError *error) { if (handler) { dispatch_async(dispatch_get_main_queue(), ^{ handler(error); }); } }; NSString *requestURL = [NSString stringWithFormat:@"%@/convertInvitation%@", kApiaryRestBaseUrl, FIRDynamicLinkAPIKeyParameter(_APIKey)]; [self executeOnePlatformRequest:requestBody forURL:requestURL completionHandler:convertInvitationCallback]; } #pragma mark - Internal methods - (void)sendRequestWithBaseURLString:(NSString *)baseURL requestBody:(NSDictionary *)requestBody endpointPath:(NSString *)endpointPath parserBlock:(FIRDLNetworkingParserBlock)parserBlock completion:(FIRPostInstallAttributionCompletionHandler)handler { NSParameterAssert(handler); NSString *requestURLString = [NSString stringWithFormat:@"%@/%@%@", baseURL, endpointPath, FIRDynamicLinkAPIKeyParameter(_APIKey)]; FIRNetworkRequestCompletionHandler completeInvitationByDeviceCallback = ^(NSData *data, NSURLResponse *response, NSError *error) { if (error || !data) { dispatch_async(dispatch_get_main_queue(), ^{ handler(nil, nil, error); }); return; } NSString *matchMessage = nil; NSError *parsingError = nil; NSDictionary *parsedDynamicLinkParameters = parserBlock(requestURLString, data, &matchMessage, &parsingError); dispatch_async(dispatch_get_main_queue(), ^{ handler(parsedDynamicLinkParameters, matchMessage, parsingError); }); }; [self executeOnePlatformRequest:requestBody forURL:requestURLString completionHandler:completeInvitationByDeviceCallback]; } - (void)executeOnePlatformRequest:(NSDictionary *)requestBody forURL:(NSString *)requestURLString completionHandler:(FIRNetworkRequestCompletionHandler)handler { NSURL *requestURL = [NSURL URLWithString:requestURLString]; NSMutableURLRequest *request = [NSMutableURLRequest requestWithURL:requestURL]; // TODO: Verify that HTTPBody and HTTPMethod are iOS 8+ and find an alternative. request.HTTPBody = FIRDataWithDictionary(requestBody, nil); request.HTTPMethod = @"POST"; [request setValue:@"application/json; charset=utf-8" forHTTPHeaderField:@"Content-Type"]; // Set the iOS bundleID as a request header. NSString *bundleID = [[NSBundle mainBundle] bundleIdentifier]; if (bundleID) { [request setValue:bundleID forHTTPHeaderField:kHeaderIosBundleIdentifier]; } FIRMakeHTTPRequest(request, handler); } @end NS_ASSUME_NONNULL_END #endif // TARGET_OS_IOS