From 9701844e7bd0f379308506ca63710323a56b56ad Mon Sep 17 00:00:00 2001 From: Arif Burak Demiray Date: Thu, 9 Jul 2026 16:44:59 +0300 Subject: [PATCH 1/3] feat: content URL handler --- CHANGELOG.md | 2 +- Countly.m | 2 +- CountlyContentBuilderInternal.h | 2 +- CountlyContentConfig.h | 20 ++++++++++++++------ CountlyContentConfig.m | 10 +++++----- CountlyWebViewManager.m | 29 +++++++++-------------------- 6 files changed, 31 insertions(+), 34 deletions(-) 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..8d7d5612 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) ContentURLHandler contentURLHandler; @property (nonatomic, assign) int contentInitialDelay; + (instancetype)sharedInstance; diff --git a/CountlyContentConfig.h b/CountlyContentConfig.h index 207a165a..fef983a9 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 Date: Thu, 9 Jul 2026 16:48:53 +0300 Subject: [PATCH 2/3] feat: content URL handler: test --- CountlyTests/CountlyContentBuilderTests.swift | 21 ++++++++++--------- CountlyWebViewManager.m | 2 +- 2 files changed, 12 insertions(+), 11 deletions(-) diff --git a/CountlyTests/CountlyContentBuilderTests.swift b/CountlyTests/CountlyContentBuilderTests.swift index 09b3fbeb..4c465dfd 100644 --- a/CountlyTests/CountlyContentBuilderTests.swift +++ b/CountlyTests/CountlyContentBuilderTests.swift @@ -24,7 +24,7 @@ class CountlyContentBuilderTests: CountlyBaseTestCase { CountlyContentBuilderInternal.sharedInstance().enableContentReloadOnStall = false CountlyContentBuilderInternal.sharedInstance().contentReloadOnStallTimeout = 0 CountlyContentBuilderInternal.sharedInstance().disableZoom = false - CountlyContentBuilderInternal.sharedInstance().enableUniversalLinkHandling = false + CountlyContentBuilderInternal.sharedInstance().contentURLHandler = nil Countly.sharedInstance().halt(true) MockURLProtocol.requestHandler = nil super.tearDown() @@ -355,26 +355,27 @@ class CountlyContentBuilderTests: CountlyBaseTestCase { /** *
-     * 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 eb4c0b47..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; From 54bcde3c8e1021d9c65e0bf723b3a86846dfce96 Mon Sep 17 00:00:00 2001 From: Arif Burak Demiray Date: Thu, 9 Jul 2026 16:51:31 +0300 Subject: [PATCH 3/3] feat: content URL handler: test --- CountlyContentBuilderInternal.h | 2 +- CountlyContentConfig.h | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/CountlyContentBuilderInternal.h b/CountlyContentBuilderInternal.h index 8d7d5612..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, copy) ContentURLHandler contentURLHandler; +@property (nonatomic, copy, nullable) ContentURLHandler contentURLHandler; @property (nonatomic, assign) int contentInitialDelay; + (instancetype)sharedInstance; diff --git a/CountlyContentConfig.h b/CountlyContentConfig.h index fef983a9..b07f5209 100644 --- a/CountlyContentConfig.h +++ b/CountlyContentConfig.h @@ -104,8 +104,8 @@ typedef BOOL (^ContentURLHandler)(NSURL *url); * returning NO (or not setting a handler) makes the SDK open the URL in the system browser as * before. Called on the main thread. */ -- (void)setContentURLHandler:(ContentURLHandler)handler; -- (ContentURLHandler)getContentURLHandler; +- (void)setContentURLHandler:(nullable ContentURLHandler)handler; +- (nullable ContentURLHandler)getContentURLHandler; #endif NS_ASSUME_NONNULL_END