Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -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
Expand Down
2 changes: 1 addition & 1 deletion Countly.m
Original file line number Diff line number Diff line change
Expand Up @@ -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];
Expand Down
2 changes: 1 addition & 1 deletion CountlyContentBuilderInternal.h
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
20 changes: 14 additions & 6 deletions CountlyContentConfig.h
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,12 @@ typedef enum: NSUInteger
} WebViewDisplayOption;

typedef void (^ContentCallback)(ContentStatus contentStatus, NSDictionary<NSString *, id>* 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
Expand Down Expand Up @@ -91,13 +97,15 @@ typedef void (^ContentCallback)(ContentStatus contentStatus, NSDictionary<NSStri

/**
* This is an experimental feature and it can have breaking changes.
* Routes user-tapped http(s) links in the content web view to the operating system instead of
* rendering them inside the content overlay: a link that matches one of the app's associated
* domains opens the app (Universal Link / deep link), and any other link opens in the system
* browser. This is a one-way switch: once enabled it stays enabled. Disabled by default.
* Sets a handler that is called when a link is opened from the content web view (an external
* link or the content "link" action), letting the host app take over instead of the SDK
* opening the system browser. This is how an app routes its own deep links (custom scheme or
* https) to the correct screen. The handler receives the URL and returns YES if it handled it;
* 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)enableUniversalLinkHandling;
- (BOOL)getEnableUniversalLinkHandling;
- (void)setContentURLHandler:(nullable ContentURLHandler)handler;
- (nullable ContentURLHandler)getContentURLHandler;
#endif

NS_ASSUME_NONNULL_END
Expand Down
10 changes: 5 additions & 5 deletions CountlyContentConfig.m
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ @interface CountlyContentConfig ()
@property (nonatomic) BOOL contentReloadOnStallEnabled;
@property (nonatomic) NSUInteger contentReloadOnStallTimeoutMs;
@property (nonatomic) BOOL zoomDisabled;
@property (nonatomic) BOOL universalLinkHandlingEnabled;
@property (nonatomic, copy) ContentURLHandler contentURLHandler;
#endif
@end

Expand Down Expand Up @@ -96,14 +96,14 @@ - (BOOL)getDisableZoom
return _zoomDisabled;
}

- (void)enableUniversalLinkHandling
- (void)setContentURLHandler:(ContentURLHandler)handler
{
_universalLinkHandlingEnabled = YES;
_contentURLHandler = handler;
}

- (BOOL)getEnableUniversalLinkHandling
- (ContentURLHandler)getContentURLHandler
{
return _universalLinkHandlingEnabled;
return _contentURLHandler;
}
#endif

Expand Down
21 changes: 11 additions & 10 deletions CountlyTests/CountlyContentBuilderTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand Down Expand Up @@ -355,26 +355,27 @@ class CountlyContentBuilderTests: CountlyBaseTestCase {

/**
* <pre>
* 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
* </pre>
*/
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)
}

/**
Expand Down
31 changes: 10 additions & 21 deletions CountlyWebViewManager.m
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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");
}];
}
Expand Down
Loading