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
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,10 @@

- Add SentryObjC User Feedback presentation APIs and a feedback form factory returning `UIViewController` instances. (#8027)

### Fixes

- Show feedback form from shake or screenshot without widget (#8050)

### Deprecations

- Deprecate the managed User Feedback custom button. It will be removed in v10. Present the feedback form from your own UI with `SentrySDK.feedback.show()`, `SentrySDK.FeedbackForm`, or `.sentryFeedback(isPresented:)` instead. (#8052)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ final class SentryUserFeedbackIntegrationDriver: NSObject {
* At the time this integration is being installed, if there is no UIApplicationDelegate and no connected UIScene, it is very likely we are in a SwiftUI app, but it's possible we could instead be in a UIKit app that has some nonstandard launch procedure or doesn't call SentrySDK.start in a place we expect/recommend, in which case they will need to manually display the widget when they're ready by calling SentrySDK.feedback.showWidget. The managed widget is deprecated; prefer presenting the feedback form from your own UI using SentrySDK.feedback.show(), SentrySDK.FeedbackForm, or sentryFeedback(isPresented:).
*/
if UIApplication.shared.connectedScenes.isEmpty && UIApplication.shared.delegate == nil {
observeScreenshots()
observeShakeGesture()
return
}
Expand Down Expand Up @@ -203,11 +204,15 @@ private extension SentryUserFeedbackIntegrationDriver {
}

var presenter: UIViewController? {
if let customButton = configuration.customButton {
return customButton.controller
if let customButton = configuration.customButton?.controller {
return customButton
}

if let widgetRootViewController = widget?.rootVC {
return widgetRootViewController
}

return widget?.rootVC
return SentryFeedbackFormPresenter.presentingViewController()
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -241,6 +241,9 @@ final class UserFeedbackIntegrationTests: XCTestCase {
}

func testShowForm_whenNoPresenterAvailable_shouldNotPresentForm() {
let application = TestSentryUIApplication()
application.windows = []
SentryDependencyContainer.sharedInstance().applicationOverride = application
let sut = SentryUserFeedbackIntegrationDriver(
configuration: SentryUserFeedbackConfiguration(),
screenshotSource: makeScreenshotSource())
Expand All @@ -250,6 +253,50 @@ final class UserFeedbackIntegrationTests: XCTestCase {
XCTAssertFalse(sut.displayingForm)
}

func testShakeGesture_whenNoWidgetOrCustomButton_shouldUseFallbackPresenter() throws {
let window = UIWindow(frame: UIScreen.main.bounds)
let viewController = TestPresentingViewController()
let config = SentryUserFeedbackConfiguration()
config.animations = false
config.useShakeGesture = true
let sut = SentryUserFeedbackIntegrationDriver(
configuration: config,
screenshotSource: makeScreenshotSource())
useFallbackPresenter(viewController, in: window)

NotificationCenter.default.post(name: .SentryShakeDetected, object: nil)

_ = try XCTUnwrap(viewController.lastPresentedViewController as? SentryUserFeedbackFormController)
XCTAssertTrue(sut.displayingForm)

withExtendedLifetime(window) { }
}

@available(*, deprecated, message: "Testing deprecated widget configuration")
func testScreenshotTrigger_whenWidgetAutoInjectionDisabled_shouldUseFallbackPresenter() throws {
let window = UIWindow(frame: UIScreen.main.bounds)
let viewController = TestPresentingViewController()
let screenshot = UIImage()
let config = SentryUserFeedbackConfiguration()
config.animations = false
config.showFormForScreenshots = true
config.configureWidget = { widget in
widget.autoInject = false
}
let sut = SentryUserFeedbackIntegrationDriver(
configuration: config,
screenshotSource: TestScreenshotSource(screenshots: [screenshot]))
useFallbackPresenter(viewController, in: window)

NotificationCenter.default.post(name: UIApplication.userDidTakeScreenshotNotification, object: nil)

let form = try XCTUnwrap(viewController.lastPresentedViewController as? SentryUserFeedbackFormController)
XCTAssertIdentical(form.screenshot, screenshot)
XCTAssertNil(widgetHost(for: sut))

withExtendedLifetime(window) { }
}

func testShowForm_whenConfigurationBuildersAreSet_shouldNotApplyBuildersAgain() throws {
let window = UIWindow(frame: UIScreen.main.bounds)
let viewController = TestPresentingViewController()
Expand Down Expand Up @@ -483,6 +530,29 @@ final class UserFeedbackIntegrationTests: XCTestCase {
viewController.view.addSubview(customButton)
}

private func useFallbackPresenter(_ viewController: UIViewController, in window: UIWindow) {
window.rootViewController = viewController
let application = TestSentryUIApplication()
application.windows = [window]
SentryDependencyContainer.sharedInstance().applicationOverride = application
}

private final class TestScreenshotSource: SentryScreenshotSource {
private let screenshots: [UIImage]

init(screenshots: [UIImage]) {
self.screenshots = screenshots
super.init(photographer: SentryViewPhotographer(
renderer: SentryDefaultViewRenderer(),
redactOptions: Options().screenshot,
enableMaskRendererV2: false))
}

override func appScreenshots() -> [UIImage] {
return screenshots
}
}

private final class TestPresentingViewController: UIViewController {
private(set) var lastPresentedViewController: UIViewController?
private(set) var presentCallCount = 0
Expand Down
Loading