FIRIAMActionURLFollower.m 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230
  1. /*
  2. * Copyright 2018 Google
  3. *
  4. * Licensed under the Apache License, Version 2.0 (the "License");
  5. * you may not use this file except in compliance with the License.
  6. * You may obtain a copy of the License at
  7. *
  8. * http://www.apache.org/licenses/LICENSE-2.0
  9. *
  10. * Unless required by applicable law or agreed to in writing, software
  11. * distributed under the License is distributed on an "AS IS" BASIS,
  12. * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  13. * See the License for the specific language governing permissions and
  14. * limitations under the License.
  15. */
  16. #import <TargetConditionals.h>
  17. #if TARGET_OS_IOS || TARGET_OS_TV
  18. #import <Foundation/Foundation.h>
  19. #import <UIKit/UIKit.h>
  20. #import "FirebaseInAppMessaging/Sources/FIRCore+InAppMessaging.h"
  21. #import "FirebaseInAppMessaging/Sources/Private/Runtime/FIRIAMActionURLFollower.h"
  22. NS_EXTENSION_UNAVAILABLE("Firebase In App Messaging is not supported for iOS extensions.")
  23. @interface FIRIAMActionURLFollower ()
  24. @property(nonatomic, readonly, nonnull, copy) NSSet<NSString *> *appCustomURLSchemesSet;
  25. @property(nonatomic, readonly) BOOL isOldAppDelegateOpenURLDefined;
  26. @property(nonatomic, readonly) BOOL isNewAppDelegateOpenURLDefined;
  27. @property(nonatomic, readonly) BOOL isContinueUserActivityMethodDefined;
  28. @property(nonatomic, readonly, nullable) id<UIApplicationDelegate> appDelegate;
  29. @property(nonatomic, readonly, nonnull) UIApplication *mainApplication;
  30. @end
  31. NS_EXTENSION_UNAVAILABLE("Firebase In App Messaging is not supported for iOS extensions.")
  32. @implementation FIRIAMActionURLFollower
  33. + (FIRIAMActionURLFollower *)actionURLFollower {
  34. static FIRIAMActionURLFollower *URLFollower;
  35. static dispatch_once_t onceToken;
  36. dispatch_once(&onceToken, ^{
  37. NSMutableArray<NSString *> *customSchemeURLs = [[NSMutableArray alloc] init];
  38. // Reading the custom url list from the environment.
  39. NSBundle *appBundle = [NSBundle mainBundle];
  40. if (appBundle) {
  41. id URLTypesID = [appBundle objectForInfoDictionaryKey:@"CFBundleURLTypes"];
  42. if ([URLTypesID isKindOfClass:[NSArray class]]) {
  43. NSArray *urlTypesArray = (NSArray *)URLTypesID;
  44. for (id nextURLType in urlTypesArray) {
  45. if ([nextURLType isKindOfClass:[NSDictionary class]]) {
  46. NSDictionary *nextURLTypeDict = (NSDictionary *)nextURLType;
  47. id nextSchemeArray = nextURLTypeDict[@"CFBundleURLSchemes"];
  48. if (nextSchemeArray && [nextSchemeArray isKindOfClass:[NSArray class]]) {
  49. [customSchemeURLs addObjectsFromArray:nextSchemeArray];
  50. }
  51. }
  52. }
  53. }
  54. }
  55. FIRLogDebug(kFIRLoggerInAppMessaging, @"I-IAM300010",
  56. @"Detected %d custom URL schemes from environment", (int)customSchemeURLs.count);
  57. if ([NSThread isMainThread]) {
  58. // We can not dispatch sychronously to main queue if we are already in main queue. That
  59. // can cause deadlock.
  60. URLFollower = [[FIRIAMActionURLFollower alloc]
  61. initWithCustomURLSchemeArray:customSchemeURLs
  62. withApplication:UIApplication.sharedApplication];
  63. } else {
  64. // If we are not on main thread, dispatch it to main queue since it invovles calling UIKit
  65. // methods, which are required to be carried out on main queue.
  66. dispatch_sync(dispatch_get_main_queue(), ^{
  67. URLFollower = [[FIRIAMActionURLFollower alloc]
  68. initWithCustomURLSchemeArray:customSchemeURLs
  69. withApplication:UIApplication.sharedApplication];
  70. });
  71. }
  72. });
  73. return URLFollower;
  74. }
  75. - (instancetype)initWithCustomURLSchemeArray:(NSArray<NSString *> *)customURLScheme
  76. withApplication:(UIApplication *)application {
  77. if (self = [super init]) {
  78. _appCustomURLSchemesSet = [NSSet setWithArray:customURLScheme];
  79. _mainApplication = application;
  80. _appDelegate = [application delegate];
  81. if (_appDelegate) {
  82. _isOldAppDelegateOpenURLDefined = [_appDelegate
  83. respondsToSelector:@selector(application:openURL:sourceApplication:annotation:)];
  84. _isNewAppDelegateOpenURLDefined =
  85. [_appDelegate respondsToSelector:@selector(application:openURL:options:)];
  86. _isContinueUserActivityMethodDefined = [_appDelegate
  87. respondsToSelector:@selector(application:continueUserActivity:restorationHandler:)];
  88. }
  89. }
  90. return self;
  91. }
  92. - (void)followActionURL:(NSURL *)actionURL withCompletionBlock:(void (^)(BOOL success))completion {
  93. // So this is the logic of the url following flow
  94. // 1 If it's a http or https link
  95. // 1.1 If delegate implements application:continueUserActivity:restorationHandler: and calling
  96. // it returns YES: the flow stops here: we have finished the url-following action
  97. // 1.2 In other cases: fall through to step 3
  98. // 2 If the URL scheme matches any element in appCustomURLSchemes
  99. // 2.1 Triggers application:openURL:options: or
  100. // application:openURL:sourceApplication:annotation:
  101. // depending on their availability.
  102. // 3 Use UIApplication openURL: or openURL:options:completionHandler: to have iOS system to deal
  103. // with the url following.
  104. //
  105. // The rationale for doing step 1 and 2 instead of simply doing step 3 for all cases are:
  106. // I) calling UIApplication openURL with the universal link targeted for current app would
  107. // not cause the link being treated as a universal link. See apple doc at
  108. // https://developer.apple.com/library/content/documentation/General/Conceptual/AppSearch/UniversalLinks.html
  109. // So step 1 is trying to handle this gracefully
  110. // II) If there are other apps on the same device declaring the same custom url scheme as for
  111. // the current app, doing step 3 directly have the risk of triggering another app for
  112. // handling the custom scheme url: See the note about "If more than one third-party" from
  113. // https://developer.apple.com/library/content/documentation/iPhone/Conceptual/iPhoneOSProgrammingGuide/Inter-AppCommunication/Inter-AppCommunication.html
  114. // So step 2 is to optimize user experience by short-circuiting the engagement with iOS
  115. // system
  116. FIRLogDebug(kFIRLoggerInAppMessaging, @"I-IAM240007", @"Following action url %@", actionURL);
  117. if ([self.class isHttpOrHttpsScheme:actionURL]) {
  118. FIRLogDebug(kFIRLoggerInAppMessaging, @"I-IAM240001", @"Try to treat it as a universal link.");
  119. if ([self followURLWithContinueUserActivity:actionURL]) {
  120. completion(YES);
  121. return; // following the url has been fully handled by App Delegate's
  122. // continueUserActivity method
  123. }
  124. } else if ([self isCustomSchemeForCurrentApp:actionURL]) {
  125. FIRLogDebug(kFIRLoggerInAppMessaging, @"I-IAM240002", @"Custom URL scheme matches.");
  126. if ([self followURLWithAppDelegateOpenURLActivity:actionURL]) {
  127. completion(YES);
  128. return; // following the url has been fully handled by App Delegate's openURL method
  129. }
  130. }
  131. FIRLogDebug(kFIRLoggerInAppMessaging, @"I-IAM240003", @"Open the url via iOS.");
  132. [self followURLViaIOS:actionURL withCompletionBlock:completion];
  133. }
  134. // Try to handle the url as a custom scheme url link by triggering
  135. // application:openURL:options: on App's delegate object directly.
  136. // @returns YES if that delegate method is defined and returns YES.
  137. - (BOOL)followURLWithAppDelegateOpenURLActivity:(NSURL *)url {
  138. if (self.isNewAppDelegateOpenURLDefined) {
  139. FIRLogDebug(kFIRLoggerInAppMessaging, @"I-IAM210008",
  140. @"iOS 9+ version of App Delegate's application:openURL:options: method detected");
  141. return [self.appDelegate application:self.mainApplication openURL:url options:@{}];
  142. }
  143. FIRLogDebug(kFIRLoggerInAppMessaging, @"I-IAM240010",
  144. @"No approriate openURL method defined for App Delegate");
  145. return NO;
  146. }
  147. // Try to handle the url as a universal link by triggering
  148. // application:continueUserActivity:restorationHandler: on App's delegate object directly.
  149. // @returns YES if that delegate method is defined and seeing a YES being returned from
  150. // trigging it
  151. - (BOOL)followURLWithContinueUserActivity:(NSURL *)url {
  152. if (self.isContinueUserActivityMethodDefined) {
  153. FIRLogDebug(kFIRLoggerInAppMessaging, @"I-IAM240004",
  154. @"App delegate responds to application:continueUserActivity:restorationHandler:."
  155. "Simulating action url opening from a web browser.");
  156. NSUserActivity *userActivity =
  157. [[NSUserActivity alloc] initWithActivityType:NSUserActivityTypeBrowsingWeb];
  158. userActivity.webpageURL = url;
  159. BOOL handled = [self.appDelegate application:self.mainApplication
  160. continueUserActivity:userActivity
  161. restorationHandler:^(NSArray *restorableObjects) {
  162. // mimic system behavior of triggering restoreUserActivityState:
  163. // method on each element of restorableObjects
  164. for (id nextRestoreObject in restorableObjects) {
  165. if ([nextRestoreObject isKindOfClass:[UIResponder class]]) {
  166. UIResponder *responder = (UIResponder *)nextRestoreObject;
  167. [responder restoreUserActivityState:userActivity];
  168. }
  169. }
  170. }];
  171. if (handled) {
  172. FIRLogDebug(kFIRLoggerInAppMessaging, @"I-IAM240005",
  173. @"App handling acton URL returns YES, no more further action taken");
  174. } else {
  175. FIRLogDebug(kFIRLoggerInAppMessaging, @"I-IAM240004", @"App handling acton URL returns NO.");
  176. }
  177. return handled;
  178. } else {
  179. return NO;
  180. }
  181. }
  182. - (void)followURLViaIOS:(NSURL *)url withCompletionBlock:(void (^)(BOOL success))completion {
  183. if ([self.mainApplication respondsToSelector:@selector(openURL:options:completionHandler:)]) {
  184. NSDictionary *options = @{};
  185. [self.mainApplication
  186. openURL:url
  187. options:options
  188. completionHandler:^(BOOL success) {
  189. FIRLogDebug(kFIRLoggerInAppMessaging, @"I-IAM240006", @"openURL result is %d", success);
  190. completion(success);
  191. }];
  192. }
  193. }
  194. - (BOOL)isCustomSchemeForCurrentApp:(NSURL *)url {
  195. NSString *schemeInLowerCase = [url.scheme lowercaseString];
  196. return [self.appCustomURLSchemesSet containsObject:schemeInLowerCase];
  197. }
  198. + (BOOL)isHttpOrHttpsScheme:(NSURL *)url {
  199. NSString *schemeInLowerCase = [url.scheme lowercaseString];
  200. return
  201. [schemeInLowerCase isEqualToString:@"https"] || [schemeInLowerCase isEqualToString:@"http"];
  202. }
  203. @end
  204. #endif // TARGET_OS_IOS || TARGET_OS_TV