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
3 changes: 3 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -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".
Expand Down
1 change: 1 addition & 0 deletions CountlyTests/CountlyWebViewManager+Tests.h
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@
- (void)cancelPendingReload;
- (void)contentShownDeadlineReached;
- (void)recordEventsWithJSONString:(NSString *)jsonString;
- (BOOL)isFeedbackWidgetURL:(NSURL *)url;

@end
#endif
18 changes: 18 additions & 0 deletions CountlyTests/CountlyWebViewManagerTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -765,6 +765,24 @@ class CountlyWebViewManagerTests: XCTestCase {
XCTAssertFalse(timer.isValid)
}

func testIsFeedbackWidgetURL_recognizesFeedbackWidgetURLs() {
// Feedback widget URLs (host/feedback/<type>?...&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 }
Expand Down
27 changes: 23 additions & 4 deletions CountlyWebViewManager.m
Original file line number Diff line number Diff line change
Expand Up @@ -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];
Expand Down Expand Up @@ -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 "<host>/feedback/<type>?...&widget_id=<id>&..." (see
// CountlyFeedbackWidget generateWidgetURL), whereas content loads "<host>/_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
{
Expand Down
Loading