diff --git a/CHANGELOG.md b/CHANGELOG.md index b50620fc..56816316 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,6 @@ +## XX.XX.XX +* Mitigated an issue where feedback widgets could be closed automatically after about a minute even while the user was still interacting with them. + ## 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". diff --git a/CountlyTests/CountlyWebViewManager+Tests.h b/CountlyTests/CountlyWebViewManager+Tests.h index 236a6edd..df8547d7 100644 --- a/CountlyTests/CountlyWebViewManager+Tests.h +++ b/CountlyTests/CountlyWebViewManager+Tests.h @@ -23,6 +23,7 @@ - (void)cancelPendingReload; - (void)contentShownDeadlineReached; - (void)recordEventsWithJSONString:(NSString *)jsonString; +- (BOOL)isFeedbackWidgetURL:(NSURL *)url; @end #endif diff --git a/CountlyTests/CountlyWebViewManagerTests.swift b/CountlyTests/CountlyWebViewManagerTests.swift index cc1f840f..dc7ace47 100644 --- a/CountlyTests/CountlyWebViewManagerTests.swift +++ b/CountlyTests/CountlyWebViewManagerTests.swift @@ -765,6 +765,24 @@ class CountlyWebViewManagerTests: XCTestCase { XCTAssertFalse(timer.isValid) } + func testIsFeedbackWidgetURL_recognizesFeedbackWidgetURLs() { + // Feedback widget URLs (host/feedback/?...&widget_id=...) must be recognized so the + // content-shown deadline is NOT armed for them (widgets never emit [CLY]_content_shown). + XCTAssertTrue(manager.isFeedbackWidgetURL(URL(string: "https://example.count.ly/feedback/nps?app_key=k&widget_id=abc123")!)) + XCTAssertTrue(manager.isFeedbackWidgetURL(URL(string: "https://example.count.ly/feedback/survey?widget_id=x")!)) + XCTAssertTrue(manager.isFeedbackWidgetURL(URL(string: "https://example.count.ly/feedback/rating?widget_id=x")!)) + // Base-path host still matches (uses a path-segment contains check, not a prefix). + XCTAssertTrue(manager.isFeedbackWidgetURL(URL(string: "https://example.com/base/feedback/nps?widget_id=x")!)) + } + + func testIsFeedbackWidgetURL_rejectsContentURLs() { + // Content URLs (host/_external/content?...) must NOT be treated as feedback widgets, so + // the content-shown deadline stays armed for real content. + XCTAssertFalse(manager.isFeedbackWidgetURL(URL(string: "https://countly.teb.com.tr/_external/content?app_id=1&id=2&journeyId=3")!)) + XCTAssertFalse(manager.isFeedbackWidgetURL(URL(string: "https://example.count.ly/o/sdk/content?method=queue")!)) + XCTAssertFalse(manager.isFeedbackWidgetURL(URL(string: "about:blank")!)) + } + func testNonContentShownEvent_leavesDeadlineTimerRunning() { // A different event must NOT cancel the deadline. let timer = Timer.scheduledTimer(withTimeInterval: 60, repeats: false) { _ in } diff --git a/CountlyWebViewManager.m b/CountlyWebViewManager.m index a5105048..1f867ee0 100644 --- a/CountlyWebViewManager.m +++ b/CountlyWebViewManager.m @@ -181,10 +181,15 @@ - (void)createWebViewWithURL:(NSURL *)url // Arm the absolute content-shown deadline ONCE (not per navigation, so reloads cannot // extend it). If [CLY]_content_shown is not reported within kCLYContentShownDeadline, the // web view is closed regardless of retry/stall/verify state. - __weak typeof(self) weakSelf = self; - self.contentShownDeadlineTimer = [NSTimer scheduledTimerWithTimeInterval:kCLYContentShownDeadline repeats:NO block:^(NSTimer * _Nonnull timer) { - [weakSelf contentShownDeadlineReached]; - }]; + // Only for CONTENT: feedback widgets (survey/NPS/rating) load a different SDK-built URL and + // never report [CLY]_content_shown, so arming the deadline for them would force-close a + // widget the user is still filling out. Skip it for feedback widget URLs. + if (![self isFeedbackWidgetURL:url]) { + __weak typeof(self) weakSelf = self; + self.contentShownDeadlineTimer = [NSTimer scheduledTimerWithTimeInterval:kCLYContentShownDeadline repeats:NO block:^(NSTimer * _Nonnull timer) { + [weakSelf contentShownDeadlineReached]; + }]; + } CLYButton *dismissButton = [CLYButton dismissAlertButton:@"X"]; [self configureDismissButton:dismissButton forWebView:webView]; @@ -526,6 +531,20 @@ - (void)contentShownDeadlineReached { [self closeWebView]; } +// A feedback widget is presented through the same web view manager but loads a URL the SDK +// builds in the feedback module as "/feedback/?...&widget_id=&..." (see +// CountlyFeedbackWidget generateWidgetURL), whereas content loads "/_external/content?...". +// Feedback widgets never report [CLY]_content_shown, so we must not arm the content-shown +// deadline for them. Recognized by the SDK's own "/feedback/" endpoint path segment +// (kCountlyEndpointFeedback), which content URLs never contain. +- (BOOL)isFeedbackWidgetURL:(NSURL *)url { + if (!url) return NO; + NSString *path = url.path; + if (!path) return NO; + NSString *feedbackSegment = [kCountlyEndpointFeedback stringByAppendingString:@"/"]; + return [path rangeOfString:feedbackSegment].location != NSNotFound; +} + - (void)userContentController:(WKUserContentController *)userContentController didReceiveScriptMessage:(WKScriptMessage *)message {