diff --git a/CHANGELOG.md b/CHANGELOG.md index 25e9e4ab4d..bbe774d915 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,11 @@ # Changelog +## Unreleased + +### Features + +- Add `SentrySDK.feedback.enableShakeGesture()` and `SentrySDK.feedback.disableShakeGesture()` for runtime control of shake-to-report ([#7737](https://github.com/getsentry/sentry-cocoa/pull/7737)) + ## 9.8.0 ### Features diff --git a/Sources/Swift/Integrations/UserFeedback/SentryFeedbackAPI.swift b/Sources/Swift/Integrations/UserFeedback/SentryFeedbackAPI.swift index feeb14e4f0..5e76607a72 100644 --- a/Sources/Swift/Integrations/UserFeedback/SentryFeedbackAPI.swift +++ b/Sources/Swift/Integrations/UserFeedback/SentryFeedbackAPI.swift @@ -20,6 +20,22 @@ @objc public func hideWidget() { getIntegration()?.driver.hideWidget() } + + /// Enable shake gesture to show the feedback form. + /// Call this to dynamically enable shake-to-report at runtime. + /// - warning: This is an experimental feature and may still have bugs. + @available(iOSApplicationExtension, unavailable) + @objc public func enableShakeGesture() { + getIntegration()?.driver.enableShakeGesture() + } + + /// Disable shake gesture for the feedback form. + /// Call this to dynamically disable shake-to-report at runtime. + /// - warning: This is an experimental feature and may still have bugs. + @available(iOSApplicationExtension, unavailable) + @objc public func disableShakeGesture() { + getIntegration()?.driver.disableShakeGesture() + } private func getIntegration() -> UserFeedbackIntegration? { SentrySDKInternal.currentHub().getInstalledIntegration(UserFeedbackIntegration.self) as? UserFeedbackIntegration diff --git a/Sources/Swift/Integrations/UserFeedback/SentryShakeDetector.swift b/Sources/Swift/Integrations/UserFeedback/SentryShakeDetector.swift index a58746df8c..2a17857141 100644 --- a/Sources/Swift/Integrations/UserFeedback/SentryShakeDetector.swift +++ b/Sources/Swift/Integrations/UserFeedback/SentryShakeDetector.swift @@ -94,6 +94,13 @@ public final class SentryShakeDetector: NSObject { enabled = false SentrySDKLog.debug("Shake detector: disabled") } + + #if SENTRY_TEST || SENTRY_TEST_CI + /// Resets cooldown state for testing. Not available in production builds. + static func resetCooldownForTesting() { + lastShakeTimestamp = 0 + } + #endif #else /// No-op on non-iOS platforms. @objc public static func enable() {} diff --git a/Sources/Swift/Integrations/UserFeedback/SentryUserFeedbackIntegrationDriver.swift b/Sources/Swift/Integrations/UserFeedback/SentryUserFeedbackIntegrationDriver.swift index 7a4953ffc4..36db457972 100644 --- a/Sources/Swift/Integrations/UserFeedback/SentryUserFeedbackIntegrationDriver.swift +++ b/Sources/Swift/Integrations/UserFeedback/SentryUserFeedbackIntegrationDriver.swift @@ -75,6 +75,18 @@ final class SentryUserFeedbackIntegrationDriver: NSObject { widget?.rootVC.setWidget(visible: false, animated: configuration.animations) } + func enableShakeGesture() { + // Remove any existing observer first to prevent duplicate notifications + NotificationCenter.default.removeObserver(self, name: .SentryShakeDetected, object: nil) + SentryShakeDetector.enable() + NotificationCenter.default.addObserver(self, selector: #selector(handleShakeGesture), name: .SentryShakeDetected, object: nil) + } + + func disableShakeGesture() { + SentryShakeDetector.disable() + NotificationCenter.default.removeObserver(self, name: .SentryShakeDetected, object: nil) + } + @objc func showForm(sender: UIButton) { presenter?.present(SentryUserFeedbackFormController(config: configuration, delegate: self, screenshot: nil), animated: configuration.animations) { self.configuration.onFormOpen?() diff --git a/Tests/SentryTests/Integrations/Feedback/SentryShakeDetectorTests.swift b/Tests/SentryTests/Integrations/Feedback/SentryShakeDetectorTests.swift index 0213e4ea12..9349dfde5c 100644 --- a/Tests/SentryTests/Integrations/Feedback/SentryShakeDetectorTests.swift +++ b/Tests/SentryTests/Integrations/Feedback/SentryShakeDetectorTests.swift @@ -9,6 +9,7 @@ final class SentryShakeDetectorTests: XCTestCase { override func tearDown() { super.tearDown() SentryShakeDetector.disable() + SentryShakeDetector.resetCooldownForTesting() } func testEnable_whenShakeOccurs_shouldPostNotification() { @@ -82,6 +83,19 @@ final class SentryShakeDetectorTests: XCTestCase { NotificationCenter.default.removeObserver(observer) } + func testReEnable_afterDisable_shouldPostNotification() { + SentryShakeDetector.enable() + SentryShakeDetector.disable() + SentryShakeDetector.enable() + + let expectation = expectation(forNotification: .SentryShakeDetected, object: nil) + + let window = UIWindow() + window.motionEnded(.motionShake, with: nil) + + wait(for: [expectation], timeout: 1.0) + } + func testOriginalImplementation_shouldStillBeCalled() { SentryShakeDetector.enable() diff --git a/sdk_api.json b/sdk_api.json index 7d7b65d11f..474558d157 100644 --- a/sdk_api.json +++ b/sdk_api.json @@ -37230,6 +37230,50 @@ "printedName": "init()", "usr": "c:@M@Sentry@objc(cs)SentryFeedbackAPI(im)init" }, + { + "children": [ + { + "kind": "TypeNominal", + "name": "Void", + "printedName": "()" + } + ], + "declAttributes": [ + "Available", + "Final", + "ObjC" + ], + "declKind": "Func", + "funcSelfKind": "NonMutating", + "kind": "Function", + "mangledName": "$s6Sentry0A11FeedbackAPIC19disableShakeGestureyyF", + "moduleName": "Sentry", + "name": "disableShakeGesture", + "printedName": "disableShakeGesture()", + "usr": "c:@M@Sentry@objc(cs)SentryFeedbackAPI(im)disableShakeGesture" + }, + { + "children": [ + { + "kind": "TypeNominal", + "name": "Void", + "printedName": "()" + } + ], + "declAttributes": [ + "Available", + "Final", + "ObjC" + ], + "declKind": "Func", + "funcSelfKind": "NonMutating", + "kind": "Function", + "mangledName": "$s6Sentry0A11FeedbackAPIC18enableShakeGestureyyF", + "moduleName": "Sentry", + "name": "enableShakeGesture", + "printedName": "enableShakeGesture()", + "usr": "c:@M@Sentry@objc(cs)SentryFeedbackAPI(im)enableShakeGesture" + }, { "children": [ {