diff --git a/CHANGELOG.md b/CHANGELOG.md index 7bdb4b0f..b50620fc 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,7 +1,7 @@ ## 26.1.3 * Added a content configuration option to reload the content web view when its load stalls, enabled via "enableContentReloadOnStall" on "CountlyContentConfig", with a configurable stall timeout "setContentReloadOnStallTimeout:" in milliseconds (default 1000). * Added a content configuration option to disable pinch zoom, disabled via "disableZoom". -* Added a content configuration option to open external content links as Universal Links (app deep links) when possible instead of always forcing them into the system browser, enabled via "enableUniversalLinkHandling". +* Added a content configuration option to provide a handler for links opened from the content web view, so the app can route its own deep links instead of the SDK opening the system browser, set via "setContentURLHandler:". * Improved link handling for content and feedback widgets, so links that carry their own query parameters, such as deep links, are parsed correctly. ## 26.1.2 diff --git a/Countly.m b/Countly.m index 9d7d72ea..18c34838 100644 --- a/Countly.m +++ b/Countly.m @@ -298,7 +298,7 @@ - (void)startWithConfig:(CountlyConfig *)config CountlyContentBuilderInternal.sharedInstance.enableContentReloadOnStall = config.content.getEnableContentReloadOnStall; CountlyContentBuilderInternal.sharedInstance.contentReloadOnStallTimeout = config.content.getContentReloadOnStallTimeout / 1000.0; CountlyContentBuilderInternal.sharedInstance.disableZoom = config.content.getDisableZoom; - CountlyContentBuilderInternal.sharedInstance.enableUniversalLinkHandling = config.content.getEnableUniversalLinkHandling; + CountlyContentBuilderInternal.sharedInstance.contentURLHandler = config.content.getContentURLHandler; #endif [CountlyPerformanceMonitoring.sharedInstance startWithConfig:config.apm]; diff --git a/CountlyContentBuilderInternal.h b/CountlyContentBuilderInternal.h index c78d5410..8314d2b6 100644 --- a/CountlyContentBuilderInternal.h +++ b/CountlyContentBuilderInternal.h @@ -19,7 +19,7 @@ NS_ASSUME_NONNULL_BEGIN @property (nonatomic, assign) BOOL enableContentReloadOnStall; @property (nonatomic, assign) NSTimeInterval contentReloadOnStallTimeout; // seconds @property (nonatomic, assign) BOOL disableZoom; -@property (nonatomic, assign) BOOL enableUniversalLinkHandling; +@property (nonatomic, copy, nullable) ContentURLHandler contentURLHandler; @property (nonatomic, assign) int contentInitialDelay; + (instancetype)sharedInstance; diff --git a/CountlyContentConfig.h b/CountlyContentConfig.h index 207a165a..b07f5209 100644 --- a/CountlyContentConfig.h +++ b/CountlyContentConfig.h @@ -22,6 +22,12 @@ typedef enum: NSUInteger } WebViewDisplayOption; typedef void (^ContentCallback)(ContentStatus contentStatus, NSDictionary* contentData); + +// Handler the host app can provide to take over opening a link tapped in the content web view +// (e.g. to route the app's own deep link to the right screen). Return YES if the app handled +// the URL; return NO to let the SDK open it in the system browser as usual. Called on the main +// thread. +typedef BOOL (^ContentURLHandler)(NSURL *url); #endif @interface CountlyContentConfig : NSObject @@ -91,13 +97,15 @@ typedef void (^ContentCallback)(ContentStatus contentStatus, NSDictionary - * enableUniversalLinkHandling is a one-way config switch that plumbs to the internal builder. + * A content URL handler set on the config plumbs through to the internal builder. * - * 1- A fresh CountlyContentConfig has it disabled by default - * 2- enableUniversalLinkHandling() turns it on and round-trips on the config - * 3- After start, the internal builder reflects it + * 1- A fresh CountlyContentConfig has no handler by default + * 2- setContentURLHandler: round-trips on the config + * 3- After start, the internal builder holds the handler * */ - func test_universalLinkHandling_configDefaultsAndPlumbing() { - XCTAssertFalse(CountlyContentConfig().getEnableUniversalLinkHandling()) + func test_contentURLHandler_configDefaultsAndPlumbing() { + XCTAssertNil(CountlyContentConfig().getContentURLHandler()) let config = createContentTestConfig() - config.content().enableUniversalLinkHandling() - XCTAssertTrue(config.content().getEnableUniversalLinkHandling()) + let handler: (URL) -> Bool = { _ in true } + config.content().setContentURLHandler(handler) + XCTAssertNotNil(config.content().getContentURLHandler()) MockURLProtocol.requestHandler = { request in let response = HTTPURLResponse(url: request.url!, statusCode: 200, httpVersion: "HTTP/1.1", headerFields: nil)! return ("{}".data(using: .utf8)!, response, nil) } Countly.sharedInstance().start(with: config) - XCTAssertTrue(CountlyContentBuilderInternal.sharedInstance().enableUniversalLinkHandling) + XCTAssertNotNil(CountlyContentBuilderInternal.sharedInstance().contentURLHandler) } /** diff --git a/CountlyWebViewManager.m b/CountlyWebViewManager.m index ca4e1676..a5105048 100644 --- a/CountlyWebViewManager.m +++ b/CountlyWebViewManager.m @@ -244,7 +244,7 @@ - (void)webView:(WKWebView *)webView decidePolicyForNavigationAction:(WKNavigati if ([url containsString:@"cly_x_int=1"]) { CLY_LOG_I(@"%s Opening external url [%@]", __FUNCTION__, url); - // Prefers the app (Universal Link) when enableUniversalLinkHandling is on, else browser. + // Routed through the app's content URL handler if one is set, else the system browser. [self openExternalURL:navigationAction.request.URL]; decisionHandler(WKNavigationActionPolicyCancel); return; @@ -270,32 +270,21 @@ - (void)webView:(WKWebView *)webView decidePolicyForNavigationAction:(WKNavigati decisionHandler(WKNavigationActionPolicyAllow); } -// Opens an external URL from the content. When universal-link handling is enabled -// (CountlyContentConfig enableUniversalLinkHandling), the URL is first offered to the OS as a -// Universal Link (UIApplicationOpenURLOptionUniversalLinksOnly), so a link matching the host -// app's own associated domains opens the app (deep link) rather than being forced into Safari -// -- which is what a plain openURL: does for an app's own Universal Link. Only if it is not a -// Universal Link does it fall back to the system browser. When the option is off, it opens in -// the browser as before. +// Opens an external URL from the content. If the host app has provided a content URL handler +// (CountlyContentConfig setContentURLHandler:), the URL is offered to it first so the app can +// route its own deep link (custom scheme or https) to the right screen; the handler returns +// YES if it took over. If there is no handler, or it returns NO, the SDK opens the URL in the +// system browser as before. - (void)openExternalURL:(NSURL *)url { if (!url) return; - UIApplication *application = UIApplication.sharedApplication; - if (CountlyContentBuilderInternal.sharedInstance.enableUniversalLinkHandling) { - [application openURL:url options:@{UIApplicationOpenURLOptionUniversalLinksOnly: @YES} completionHandler:^(BOOL openedInApp) { - if (openedInApp) { - CLY_LOG_I(@"%s URL [%@] opened in the app via Universal Link.", __FUNCTION__, url.absoluteString); - return; - } - // Not a registered Universal Link: fall back to the system browser. - [application openURL:url options:@{} completionHandler:^(BOOL openedInBrowser) { - CLY_LOG_I(@"%s URL [%@] is not a Universal Link; opened in browser: %@.", __FUNCTION__, url.absoluteString, openedInBrowser ? @"YES" : @"NO"); - }]; - }]; + ContentURLHandler handler = CountlyContentBuilderInternal.sharedInstance.contentURLHandler; + if (handler && handler(url)) { + CLY_LOG_I(@"%s URL [%@] handled by the app's content URL handler.", __FUNCTION__, url.absoluteString); return; } - [application openURL:url options:@{} completionHandler:^(BOOL success) { + [UIApplication.sharedApplication openURL:url options:@{} completionHandler:^(BOOL success) { CLY_LOG_I(@"%s URL [%@] opened in browser: %@.", __FUNCTION__, url.absoluteString, success ? @"YES" : @"NO"); }]; }