From cc209660b29337d054cc88e24ed708c674a7d96b Mon Sep 17 00:00:00 2001 From: Philip Niedertscheider Date: Thu, 18 Sep 2025 13:49:57 +0200 Subject: [PATCH 1/4] feat: Add toolbar button to display user feedback UI --- Targets/App/Sources/Generated/L10n.swift | 10 ++++++ .../Sources/Resources/Localizable.xcstrings | 36 +++++++++++++++++++ .../CreateLinkEditorRenderView.swift | 13 +++++++ .../CreateLinkListEditorRenderView.swift | 13 +++++++ .../UI/LinkDetail/LinkDetailRenderView.swift | 11 ++++++ .../UI/LinkInfo/LinkInfoRenderView.swift | 12 +++++++ .../LinkListDetailRenderView.swift | 12 +++++++ .../UI/LinkLists/LinkListsRenderView.swift | 23 ++++++++++++ 8 files changed, 130 insertions(+) diff --git a/Targets/App/Sources/Generated/L10n.swift b/Targets/App/Sources/Generated/L10n.swift index 63132c4..2602e83 100644 --- a/Targets/App/Sources/Generated/L10n.swift +++ b/Targets/App/Sources/Generated/L10n.swift @@ -217,6 +217,16 @@ internal enum L10n { internal static let label = L10n.tr("shared.button.edit.accessibility.label", fallback: "Edit") } } + internal enum Feedback { + /// Send Feedback + internal static let label = L10n.tr("shared.button.feedback.label", fallback: "Send Feedback") + internal enum Accessibility { + /// Send feedback to help improve the app + internal static let hint = L10n.tr("shared.button.feedback.accessibility.hint", fallback: "Send feedback to help improve the app") + /// Send Feedback + internal static let label = L10n.tr("shared.button.feedback.accessibility.label", fallback: "Send Feedback") + } + } internal enum NewLink { internal enum Accessibility { /// Create a new link in this list diff --git a/Targets/App/Sources/Resources/Localizable.xcstrings b/Targets/App/Sources/Resources/Localizable.xcstrings index c89ff60..58f0ac9 100644 --- a/Targets/App/Sources/Resources/Localizable.xcstrings +++ b/Targets/App/Sources/Resources/Localizable.xcstrings @@ -795,6 +795,42 @@ } } }, + "shared.button.feedback.accessibility.hint": { + "comment": "Feedback button accessibility hint", + "extractionState": "manual", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Send feedback to help improve the app" + } + } + } + }, + "shared.button.feedback.accessibility.label": { + "comment": "Feedback button accessibility label", + "extractionState": "manual", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Send Feedback" + } + } + } + }, + "shared.button.feedback.label": { + "comment": "Feedback button text", + "extractionState": "manual", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Send Feedback" + } + } + } + }, "shared.color-picker.accessibility.label": { "comment": "Color picker accessibility label", "extractionState": "manual", diff --git a/Targets/App/Sources/UI/CreateLinkEditor/CreateLinkEditorRenderView.swift b/Targets/App/Sources/UI/CreateLinkEditor/CreateLinkEditorRenderView.swift index bd8ccba..8b0609b 100644 --- a/Targets/App/Sources/UI/CreateLinkEditor/CreateLinkEditorRenderView.swift +++ b/Targets/App/Sources/UI/CreateLinkEditor/CreateLinkEditorRenderView.swift @@ -1,4 +1,6 @@ import SwiftUI +import SFSafeSymbols +import Sentry struct CreateLinkEditorRenderView: View { private enum CreateField { @@ -55,6 +57,17 @@ struct CreateLinkEditorRenderView: View { .accessibilityHint(L10n.Shared.Button.Cancel.Accessibility.hint) .accessibilityIdentifier("create-link.cancel.button") } + ToolbarItem(placement: .topBarTrailing) { + Button(action: { + // TODO: Display the feedback UI when available + // SentrySDK.feedback.presentUI() + }, label: { + Label(L10n.Shared.Button.Feedback.label, systemSymbol: .megaphone) + }) + .accessibilityLabel(L10n.Shared.Button.Feedback.Accessibility.label) + .accessibilityHint(L10n.Shared.Button.Feedback.Accessibility.hint) + .accessibilityIdentifier("create-link.feedback.button") + } ToolbarItem(placement: .confirmationAction) { if #available(iOS 26, *) { Button(role: .confirm) { diff --git a/Targets/App/Sources/UI/CreateListEditor/CreateLinkListEditorRenderView.swift b/Targets/App/Sources/UI/CreateListEditor/CreateLinkListEditorRenderView.swift index 20b0eff..41af154 100644 --- a/Targets/App/Sources/UI/CreateListEditor/CreateLinkListEditorRenderView.swift +++ b/Targets/App/Sources/UI/CreateListEditor/CreateLinkListEditorRenderView.swift @@ -1,3 +1,5 @@ +import SFSafeSymbols +import Sentry import SwiftUI struct CreateLinkListEditorRenderView: View { @@ -27,6 +29,17 @@ struct CreateLinkListEditorRenderView: View { .navigationTitle(L10n.CreateList.title) .accessibilityIdentifier("create-link-list.container") .toolbar { + ToolbarItem(placement: .topBarTrailing) { + Button(action: { + // TODO: Display the feedback UI when available + // SentrySDK.feedback.presentUI() + }, label: { + Label(L10n.Shared.Button.Feedback.label, systemSymbol: .megaphone) + }) + .accessibilityLabel(L10n.Shared.Button.Feedback.Accessibility.label) + .accessibilityHint(L10n.Shared.Button.Feedback.Accessibility.hint) + .accessibilityIdentifier("create-link-list.feedback.button") + } ToolbarItem(placement: .cancellationAction) { Button(role: .cancel) { dismiss() diff --git a/Targets/App/Sources/UI/LinkDetail/LinkDetailRenderView.swift b/Targets/App/Sources/UI/LinkDetail/LinkDetailRenderView.swift index cedb8b5..d506283 100644 --- a/Targets/App/Sources/UI/LinkDetail/LinkDetailRenderView.swift +++ b/Targets/App/Sources/UI/LinkDetail/LinkDetailRenderView.swift @@ -30,6 +30,17 @@ struct LinkDetailRenderView: View { } .background(Color(UIColor.systemGroupedBackground)) .toolbar { + ToolbarItemGroup(placement: .topBarLeading) { + Button(action: { + // TODO: Display the feedback UI when available + // SentrySDK.feedback.presentUI() + }, label: { + Label(L10n.Shared.Button.Feedback.label, systemSymbol: .megaphone) + }) + .accessibilityLabel(L10n.Shared.Button.Feedback.Accessibility.label) + .accessibilityHint(L10n.Shared.Button.Feedback.Accessibility.hint) + .accessibilityIdentifier("link-detail.feedback.button") + } ToolbarItemGroup(placement: .topBarLeading) { Menu { Button { diff --git a/Targets/App/Sources/UI/LinkInfo/LinkInfoRenderView.swift b/Targets/App/Sources/UI/LinkInfo/LinkInfoRenderView.swift index 75faa90..13f2f12 100644 --- a/Targets/App/Sources/UI/LinkInfo/LinkInfoRenderView.swift +++ b/Targets/App/Sources/UI/LinkInfo/LinkInfoRenderView.swift @@ -1,5 +1,6 @@ import FlinkyCore import SFSafeSymbols +import Sentry import SwiftUI struct LinkInfoRenderView: View { @@ -34,6 +35,17 @@ struct LinkInfoRenderView: View { .accessibilityHint(L10n.Shared.Button.Cancel.Accessibility.hint) .accessibilityIdentifier("link-info.cancel.button") } + ToolbarItem(placement: .topBarTrailing) { + Button(action: { + // TODO: Display the feedback UI when available + // SentrySDK.feedback.presentUI() + }, label: { + Label(L10n.Shared.Button.Feedback.label, systemSymbol: .megaphone) + }) + .accessibilityLabel(L10n.Shared.Button.Feedback.Accessibility.label) + .accessibilityHint(L10n.Shared.Button.Feedback.Accessibility.hint) + .accessibilityIdentifier("link-info.feedback.button") + } if #available(iOS 26, *) { ToolbarItem(placement: .confirmationAction) { Button(role: .confirm) { diff --git a/Targets/App/Sources/UI/LinkListDetail/LinkListDetailRenderView.swift b/Targets/App/Sources/UI/LinkListDetail/LinkListDetailRenderView.swift index 3c8bcb5..d948f4d 100644 --- a/Targets/App/Sources/UI/LinkListDetail/LinkListDetailRenderView.swift +++ b/Targets/App/Sources/UI/LinkListDetail/LinkListDetailRenderView.swift @@ -1,5 +1,6 @@ import FlinkyCore import SFSafeSymbols +import Sentry import SwiftUI struct LinkListDetailRenderView: View { @@ -30,6 +31,17 @@ struct LinkListDetailRenderView: View { emptyStateView } .toolbar { + ToolbarItem(placement: .topBarTrailing) { + Button(action: { + // TODO: Display the feedback UI when available + // SentrySDK.feedback.presentUI() + }, label: { + Label(L10n.Shared.Button.Feedback.label, systemSymbol: .megaphone) + }) + .accessibilityLabel(L10n.Shared.Button.Feedback.Accessibility.label) + .accessibilityHint(L10n.Shared.Button.Feedback.Accessibility.hint) + .accessibilityIdentifier("link-list-detail.feedback.button") + } ToolbarItem(placement: .navigationBarTrailing) { sortMenu } diff --git a/Targets/App/Sources/UI/LinkLists/LinkListsRenderView.swift b/Targets/App/Sources/UI/LinkLists/LinkListsRenderView.swift index fed4c47..1b6d13b 100644 --- a/Targets/App/Sources/UI/LinkLists/LinkListsRenderView.swift +++ b/Targets/App/Sources/UI/LinkLists/LinkListsRenderView.swift @@ -1,4 +1,5 @@ import SFSafeSymbols +import Sentry import SwiftUI struct LinkListsRenderView: View { @@ -71,6 +72,17 @@ struct LinkListsRenderView: View { } .ifAvailable(.iOS26, modify: { view in view.toolbar { + ToolbarItem(placement: .topBarTrailing) { + Button(action: { + // TODO: Display the feedback UI when available + // SentrySDK.feedback.presentUI() + }, label: { + Label(L10n.Shared.Button.Feedback.label, systemSymbol: .megaphone) + }) + .accessibilityLabel(L10n.Shared.Button.Feedback.Accessibility.label) + .accessibilityHint(L10n.Shared.Button.Feedback.Accessibility.hint) + .accessibilityIdentifier("link-lists.feedback.button") + } if !pinnedLists.isEmpty || !unpinnedLists.isEmpty { ToolbarItem(placement: .navigationBarTrailing) { EditButton() @@ -111,6 +123,17 @@ struct LinkListsRenderView: View { } }, elseModify: { view in view.toolbar { + ToolbarItem(placement: .topBarTrailing) { + Button(action: { + // TODO: Display the feedback UI when available + // SentrySDK.feedback.presentUI() + }, label: { + Label(L10n.Shared.Button.Feedback.label, systemSymbol: .megaphone) + }) + .accessibilityLabel(L10n.Shared.Button.Feedback.Accessibility.label) + .accessibilityHint(L10n.Shared.Button.Feedback.Accessibility.hint) + .accessibilityIdentifier("link-lists.feedback.button") + } if !pinnedLists.isEmpty || !unpinnedLists.isEmpty { ToolbarItem(placement: .topBarTrailing) { EditButton() From a81e60311c8cf07cb682a22d7a4448193d11f487 Mon Sep 17 00:00:00 2001 From: Philip Niedertscheider Date: Mon, 24 Nov 2025 14:55:59 +0100 Subject: [PATCH 2/4] WIP --- Flinky.xcodeproj/project.pbxproj | 8 ++++++++ .../xcshareddata/swiftpm/Package.resolved | 4 ++-- .../UI/CreateLinkEditor/CreateLinkEditorRenderView.swift | 3 +-- .../CreateLinkListEditorRenderView.swift | 3 +-- .../App/Sources/UI/LinkDetail/LinkDetailRenderView.swift | 3 +-- Targets/App/Sources/UI/LinkInfo/LinkInfoRenderView.swift | 9 ++++----- .../UI/LinkListDetail/LinkListDetailRenderView.swift | 3 +-- .../App/Sources/UI/LinkLists/LinkListsRenderView.swift | 6 ++---- 8 files changed, 20 insertions(+), 19 deletions(-) diff --git a/Flinky.xcodeproj/project.pbxproj b/Flinky.xcodeproj/project.pbxproj index eed6623..58f727e 100644 --- a/Flinky.xcodeproj/project.pbxproj +++ b/Flinky.xcodeproj/project.pbxproj @@ -1873,6 +1873,14 @@ version = 0.0.6; }; }; + D48F65902EC7A2630027AFC3 /* XCRemoteSwiftPackageReference "sentry-cocoa" */ = { + isa = XCRemoteSwiftPackageReference; + repositoryURL = "https://github.com/getsentry/sentry-cocoa"; + requirement = { + branch = "philprime/feedback-show-form-button"; + kind = branch; + }; + }; D4CFD1772E1E38F2001447E9 /* XCRemoteSwiftPackageReference "SFSafeSymbols" */ = { isa = XCRemoteSwiftPackageReference; repositoryURL = "https://github.com/SFSafeSymbols/SFSafeSymbols"; diff --git a/Flinky.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved b/Flinky.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved index e358642..cfb6300 100644 --- a/Flinky.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ b/Flinky.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -15,8 +15,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/getsentry/sentry-cocoa", "state" : { - "revision" : "cef29e94feb00b1b712514443d6d70b09ef20355", - "version" : "9.15.0" + "branch" : "philprime/feedback-show-form-button", + "revision" : "5c6740d29d13c13e7990906a0e72bae22573479b" } }, { diff --git a/Targets/App/Sources/UI/CreateLinkEditor/CreateLinkEditorRenderView.swift b/Targets/App/Sources/UI/CreateLinkEditor/CreateLinkEditorRenderView.swift index 8b0609b..aae8313 100644 --- a/Targets/App/Sources/UI/CreateLinkEditor/CreateLinkEditorRenderView.swift +++ b/Targets/App/Sources/UI/CreateLinkEditor/CreateLinkEditorRenderView.swift @@ -59,8 +59,7 @@ struct CreateLinkEditorRenderView: View { } ToolbarItem(placement: .topBarTrailing) { Button(action: { - // TODO: Display the feedback UI when available - // SentrySDK.feedback.presentUI() + SentrySDK.feedback.showForm() }, label: { Label(L10n.Shared.Button.Feedback.label, systemSymbol: .megaphone) }) diff --git a/Targets/App/Sources/UI/CreateListEditor/CreateLinkListEditorRenderView.swift b/Targets/App/Sources/UI/CreateListEditor/CreateLinkListEditorRenderView.swift index 41af154..7e7bb40 100644 --- a/Targets/App/Sources/UI/CreateListEditor/CreateLinkListEditorRenderView.swift +++ b/Targets/App/Sources/UI/CreateListEditor/CreateLinkListEditorRenderView.swift @@ -31,8 +31,7 @@ struct CreateLinkListEditorRenderView: View { .toolbar { ToolbarItem(placement: .topBarTrailing) { Button(action: { - // TODO: Display the feedback UI when available - // SentrySDK.feedback.presentUI() + SentrySDK.feedback.showForm() }, label: { Label(L10n.Shared.Button.Feedback.label, systemSymbol: .megaphone) }) diff --git a/Targets/App/Sources/UI/LinkDetail/LinkDetailRenderView.swift b/Targets/App/Sources/UI/LinkDetail/LinkDetailRenderView.swift index d506283..5144f24 100644 --- a/Targets/App/Sources/UI/LinkDetail/LinkDetailRenderView.swift +++ b/Targets/App/Sources/UI/LinkDetail/LinkDetailRenderView.swift @@ -32,8 +32,7 @@ struct LinkDetailRenderView: View { .toolbar { ToolbarItemGroup(placement: .topBarLeading) { Button(action: { - // TODO: Display the feedback UI when available - // SentrySDK.feedback.presentUI() + SentrySDK.feedback.showForm() }, label: { Label(L10n.Shared.Button.Feedback.label, systemSymbol: .megaphone) }) diff --git a/Targets/App/Sources/UI/LinkInfo/LinkInfoRenderView.swift b/Targets/App/Sources/UI/LinkInfo/LinkInfoRenderView.swift index 13f2f12..6dc1a90 100644 --- a/Targets/App/Sources/UI/LinkInfo/LinkInfoRenderView.swift +++ b/Targets/App/Sources/UI/LinkInfo/LinkInfoRenderView.swift @@ -37,8 +37,7 @@ struct LinkInfoRenderView: View { } ToolbarItem(placement: .topBarTrailing) { Button(action: { - // TODO: Display the feedback UI when available - // SentrySDK.feedback.presentUI() + SentrySDK.feedback.showForm() }, label: { Label(L10n.Shared.Button.Feedback.label, systemSymbol: .megaphone) }) @@ -183,8 +182,8 @@ extension LinkInfoRenderView { .font(.system(size: 22, weight: .medium)) .foregroundStyle( symbol.isEmoji - ? Color.blue - : (colorScheme == .light ? Color.gray.mix(with: Color.black, by: 0.3) : Color.gray) + ? Color.blue + : (colorScheme == .light ? Color.gray.mix(with: Color.black, by: 0.3) : Color.gray) ) .frame(maxWidth: .infinity, maxHeight: .infinity) .padding(6) @@ -192,7 +191,7 @@ extension LinkInfoRenderView { .frame(maxWidth: .infinity, maxHeight: .infinity) .background( symbol.isEmoji - ? Color.blue.opacity(0.15) : Color.gray.opacity(colorScheme == .light ? 0.1 : 0.2) + ? Color.blue.opacity(0.15) : Color.gray.opacity(colorScheme == .light ? 0.1 : 0.2) ) .clipShape(Circle()) .contentShape(Circle()) diff --git a/Targets/App/Sources/UI/LinkListDetail/LinkListDetailRenderView.swift b/Targets/App/Sources/UI/LinkListDetail/LinkListDetailRenderView.swift index d948f4d..db4a6d2 100644 --- a/Targets/App/Sources/UI/LinkListDetail/LinkListDetailRenderView.swift +++ b/Targets/App/Sources/UI/LinkListDetail/LinkListDetailRenderView.swift @@ -33,8 +33,7 @@ struct LinkListDetailRenderView: View { .toolbar { ToolbarItem(placement: .topBarTrailing) { Button(action: { - // TODO: Display the feedback UI when available - // SentrySDK.feedback.presentUI() + SentrySDK.feedback.presentUI() }, label: { Label(L10n.Shared.Button.Feedback.label, systemSymbol: .megaphone) }) diff --git a/Targets/App/Sources/UI/LinkLists/LinkListsRenderView.swift b/Targets/App/Sources/UI/LinkLists/LinkListsRenderView.swift index 1b6d13b..c23ffc1 100644 --- a/Targets/App/Sources/UI/LinkLists/LinkListsRenderView.swift +++ b/Targets/App/Sources/UI/LinkLists/LinkListsRenderView.swift @@ -74,8 +74,7 @@ struct LinkListsRenderView: View { view.toolbar { ToolbarItem(placement: .topBarTrailing) { Button(action: { - // TODO: Display the feedback UI when available - // SentrySDK.feedback.presentUI() + SentrySDK.feedback.presentUI() }, label: { Label(L10n.Shared.Button.Feedback.label, systemSymbol: .megaphone) }) @@ -125,8 +124,7 @@ struct LinkListsRenderView: View { view.toolbar { ToolbarItem(placement: .topBarTrailing) { Button(action: { - // TODO: Display the feedback UI when available - // SentrySDK.feedback.presentUI() + SentrySDK.feedback.presentUI() }, label: { Label(L10n.Shared.Button.Feedback.label, systemSymbol: .megaphone) }) From 13ada7452f7cab1a4033e61b70e069bd44f89946 Mon Sep 17 00:00:00 2001 From: Philip Niedertscheider Date: Tue, 2 Jun 2026 14:42:47 +0200 Subject: [PATCH 3/4] feat: Add feedback environment, adopt new Sentry feedback API, and add UI tests Introduce a FeedbackAction environment value so views call feedback.show() instead of importing SentrySwift directly. Remove the floating feedback widget in favor of toolbar buttons. Adopt the new SentryFeedbackFormConfig API from sentry-cocoa. Add UI tests verifying the feedback button exists and opens the form on all six screens. --- Flinky.xcodeproj/project.pbxproj | 12 +- .../xcshareddata/swiftpm/Package.resolved | 4 +- .../Environment/Environment+Feedback.swift | 24 ++++ Targets/App/Sources/Main/FlinkyApp.swift | 13 +- .../Licenses.latest_result.txt | 4 +- .../Resources/Settings.bundle/Licenses.plist | 2 +- .../CreateLinkEditorRenderView.swift | 6 +- .../CreateLinkListEditorRenderView.swift | 4 +- .../UI/LinkDetail/LinkDetailRenderView.swift | 3 +- .../UI/LinkInfo/LinkInfoRenderView.swift | 5 +- .../LinkListDetailRenderView.swift | 5 +- .../UI/LinkLists/LinkListsContainerView.swift | 5 - .../UI/LinkLists/LinkListsRenderView.swift | 7 +- Targets/UITests/Sources/UITests.swift | 132 +++++++++++++++++- 14 files changed, 182 insertions(+), 44 deletions(-) create mode 100644 Targets/App/Sources/Environment/Environment+Feedback.swift diff --git a/Flinky.xcodeproj/project.pbxproj b/Flinky.xcodeproj/project.pbxproj index 58f727e..fc7317f 100644 --- a/Flinky.xcodeproj/project.pbxproj +++ b/Flinky.xcodeproj/project.pbxproj @@ -1859,8 +1859,8 @@ isa = XCRemoteSwiftPackageReference; repositoryURL = "https://github.com/getsentry/sentry-cocoa"; requirement = { - kind = exactVersion; - version = 9.15.0; + branch = "ref/feedback-config-arg"; + kind = branch; }; traits = ( ); @@ -1873,14 +1873,6 @@ version = 0.0.6; }; }; - D48F65902EC7A2630027AFC3 /* XCRemoteSwiftPackageReference "sentry-cocoa" */ = { - isa = XCRemoteSwiftPackageReference; - repositoryURL = "https://github.com/getsentry/sentry-cocoa"; - requirement = { - branch = "philprime/feedback-show-form-button"; - kind = branch; - }; - }; D4CFD1772E1E38F2001447E9 /* XCRemoteSwiftPackageReference "SFSafeSymbols" */ = { isa = XCRemoteSwiftPackageReference; repositoryURL = "https://github.com/SFSafeSymbols/SFSafeSymbols"; diff --git a/Flinky.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved b/Flinky.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved index cfb6300..fc6e35d 100644 --- a/Flinky.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ b/Flinky.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -15,8 +15,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/getsentry/sentry-cocoa", "state" : { - "branch" : "philprime/feedback-show-form-button", - "revision" : "5c6740d29d13c13e7990906a0e72bae22573479b" + "branch" : "ref/feedback-config-arg", + "revision" : "7946700ddb51ecede53afb14283da63262eabba3" } }, { diff --git a/Targets/App/Sources/Environment/Environment+Feedback.swift b/Targets/App/Sources/Environment/Environment+Feedback.swift new file mode 100644 index 0000000..4e98523 --- /dev/null +++ b/Targets/App/Sources/Environment/Environment+Feedback.swift @@ -0,0 +1,24 @@ +import SwiftUI + +struct FeedbackAction { + private let handler: () -> Void + + init(handler: @escaping () -> Void = {}) { + self.handler = handler + } + + func show() { + handler() + } +} + +extension EnvironmentValues { + var feedback: FeedbackAction { + get { self[FeedbackKey.self] } + set { self[FeedbackKey.self] = newValue } + } + + private struct FeedbackKey: EnvironmentKey { + static let defaultValue = FeedbackAction() + } +} diff --git a/Targets/App/Sources/Main/FlinkyApp.swift b/Targets/App/Sources/Main/FlinkyApp.swift index 04966cf..cf0779f 100644 --- a/Targets/App/Sources/Main/FlinkyApp.swift +++ b/Targets/App/Sources/Main/FlinkyApp.swift @@ -117,15 +117,6 @@ struct FlinkyApp: App { // Configure User Feedback options.configureUserFeedback = { feedbackOptions in feedbackOptions.animations = true - feedbackOptions.configureWidget = { widgetOptions in - widgetOptions.autoInject = false // Disable automatic injection of the widget, because it's not supported in SwiftUI. - widgetOptions.labelText = "Send Feedback" - widgetOptions.showIcon = true - widgetOptions.widgetAccessibilityLabel = "Feedback Widget" - widgetOptions.windowLevel = UIWindow.Level.normal + 1 - widgetOptions.location = [.bottom, .trailing] - widgetOptions.layoutUIOffset = .init(horizontal: 18, vertical: 80) - } feedbackOptions.useShakeGesture = true feedbackOptions.showFormForScreenshots = true feedbackOptions.configureForm = { formOptions in @@ -222,6 +213,10 @@ struct FlinkyApp: App { var body: some Scene { WindowGroup { MainContainerView() + .environment(\.feedback, FeedbackAction { + let config = SentryFeedbackFormConfig() + SentrySDK.feedback.show(config: config) + }) .toaster(toastManager) .configureOnLaunch { options in options.publicKey = "30d2f7cc2fa469eaf8e4bdf958ad9d66bce491a7da1fb08ff0a7156a8e15a47d" diff --git a/Targets/App/Sources/Resources/Settings.bundle/Licenses.latest_result.txt b/Targets/App/Sources/Resources/Settings.bundle/Licenses.latest_result.txt index 8032d35..a7887ec 100644 --- a/Targets/App/Sources/Resources/Settings.bundle/Licenses.latest_result.txt +++ b/Targets/App/Sources/Resources/Settings.bundle/Licenses.latest_result.txt @@ -1,12 +1,12 @@ name: OnLaunch-iOS-Client, nameSpecified: OnLaunch-iOS-Client, owner: kula-app, version: 0.0.6, source: https://github.com/kula-app/OnLaunch-iOS-Client -name: sentry-cocoa, nameSpecified: Sentry, owner: getsentry, version: 9.15.0, source: https://github.com/getsentry/sentry-cocoa +name: sentry-cocoa, nameSpecified: sentry-cocoa, owner: getsentry, version: , source: https://github.com/getsentry/sentry-cocoa name: SFSafeSymbols, nameSpecified: SFSafeSymbols, owner: SFSafeSymbols, version: 7.0.0, source: https://github.com/SFSafeSymbols/SFSafeSymbols name: OnLaunch-iOS-Client, nameSpecified: OnLaunch-iOS-Client, owner: kula-app, version: 0.0.6, source: https://github.com/kula-app/OnLaunch-iOS-Client -name: sentry-cocoa, nameSpecified: Sentry, owner: getsentry, version: 9.15.0, source: https://github.com/getsentry/sentry-cocoa +name: sentry-cocoa, nameSpecified: sentry-cocoa, owner: getsentry, version: , source: https://github.com/getsentry/sentry-cocoa name: SFSafeSymbols, nameSpecified: SFSafeSymbols, owner: SFSafeSymbols, version: 7.0.0, source: https://github.com/SFSafeSymbols/SFSafeSymbols diff --git a/Targets/App/Sources/Resources/Settings.bundle/Licenses.plist b/Targets/App/Sources/Resources/Settings.bundle/Licenses.plist index 1829a0e..ffdd7ff 100644 --- a/Targets/App/Sources/Resources/Settings.bundle/Licenses.plist +++ b/Targets/App/Sources/Resources/Settings.bundle/Licenses.plist @@ -22,7 +22,7 @@ File Licenses/sentry-cocoa Title - Sentry (9.15.0) + sentry-cocoa Type PSChildPaneSpecifier diff --git a/Targets/App/Sources/UI/CreateLinkEditor/CreateLinkEditorRenderView.swift b/Targets/App/Sources/UI/CreateLinkEditor/CreateLinkEditorRenderView.swift index aae8313..44c055e 100644 --- a/Targets/App/Sources/UI/CreateLinkEditor/CreateLinkEditorRenderView.swift +++ b/Targets/App/Sources/UI/CreateLinkEditor/CreateLinkEditorRenderView.swift @@ -1,6 +1,5 @@ -import SwiftUI import SFSafeSymbols -import Sentry +import SwiftUI struct CreateLinkEditorRenderView: View { private enum CreateField { @@ -9,6 +8,7 @@ struct CreateLinkEditorRenderView: View { } @Environment(\.dismiss) private var dismiss + @Environment(\.feedback) private var feedback @Binding var name: String @Binding var url: String @@ -59,7 +59,7 @@ struct CreateLinkEditorRenderView: View { } ToolbarItem(placement: .topBarTrailing) { Button(action: { - SentrySDK.feedback.showForm() + feedback.show() }, label: { Label(L10n.Shared.Button.Feedback.label, systemSymbol: .megaphone) }) diff --git a/Targets/App/Sources/UI/CreateListEditor/CreateLinkListEditorRenderView.swift b/Targets/App/Sources/UI/CreateListEditor/CreateLinkListEditorRenderView.swift index 7e7bb40..82e46bc 100644 --- a/Targets/App/Sources/UI/CreateListEditor/CreateLinkListEditorRenderView.swift +++ b/Targets/App/Sources/UI/CreateListEditor/CreateLinkListEditorRenderView.swift @@ -1,5 +1,4 @@ import SFSafeSymbols -import Sentry import SwiftUI struct CreateLinkListEditorRenderView: View { @@ -8,6 +7,7 @@ struct CreateLinkListEditorRenderView: View { } @Environment(\.dismiss) private var dismiss + @Environment(\.feedback) private var feedback @Binding var name: String @FocusState private var focusedField: CreateField? @@ -31,7 +31,7 @@ struct CreateLinkListEditorRenderView: View { .toolbar { ToolbarItem(placement: .topBarTrailing) { Button(action: { - SentrySDK.feedback.showForm() + feedback.show() }, label: { Label(L10n.Shared.Button.Feedback.label, systemSymbol: .megaphone) }) diff --git a/Targets/App/Sources/UI/LinkDetail/LinkDetailRenderView.swift b/Targets/App/Sources/UI/LinkDetail/LinkDetailRenderView.swift index 5144f24..d6d63b6 100644 --- a/Targets/App/Sources/UI/LinkDetail/LinkDetailRenderView.swift +++ b/Targets/App/Sources/UI/LinkDetail/LinkDetailRenderView.swift @@ -7,6 +7,7 @@ import SwiftUI struct LinkDetailRenderView: View { @Environment(\.dismiss) private var dismiss + @Environment(\.feedback) private var feedback let linkId: UUID @@ -32,7 +33,7 @@ struct LinkDetailRenderView: View { .toolbar { ToolbarItemGroup(placement: .topBarLeading) { Button(action: { - SentrySDK.feedback.showForm() + feedback.show() }, label: { Label(L10n.Shared.Button.Feedback.label, systemSymbol: .megaphone) }) diff --git a/Targets/App/Sources/UI/LinkInfo/LinkInfoRenderView.swift b/Targets/App/Sources/UI/LinkInfo/LinkInfoRenderView.swift index 6dc1a90..4e1475a 100644 --- a/Targets/App/Sources/UI/LinkInfo/LinkInfoRenderView.swift +++ b/Targets/App/Sources/UI/LinkInfo/LinkInfoRenderView.swift @@ -1,9 +1,10 @@ import FlinkyCore import SFSafeSymbols -import Sentry import SwiftUI struct LinkInfoRenderView: View { + @Environment(\.feedback) private var feedback + @Binding var name: String @Binding var url: String @Binding var color: ListColor @@ -37,7 +38,7 @@ struct LinkInfoRenderView: View { } ToolbarItem(placement: .topBarTrailing) { Button(action: { - SentrySDK.feedback.showForm() + feedback.show() }, label: { Label(L10n.Shared.Button.Feedback.label, systemSymbol: .megaphone) }) diff --git a/Targets/App/Sources/UI/LinkListDetail/LinkListDetailRenderView.swift b/Targets/App/Sources/UI/LinkListDetail/LinkListDetailRenderView.swift index db4a6d2..16d2ffb 100644 --- a/Targets/App/Sources/UI/LinkListDetail/LinkListDetailRenderView.swift +++ b/Targets/App/Sources/UI/LinkListDetail/LinkListDetailRenderView.swift @@ -1,9 +1,10 @@ import FlinkyCore import SFSafeSymbols -import Sentry import SwiftUI struct LinkListDetailRenderView: View { + @Environment(\.feedback) private var feedback + let list: LinkListsDisplayItem let links: [LinkListDetailDisplayItem] @@ -33,7 +34,7 @@ struct LinkListDetailRenderView: View { .toolbar { ToolbarItem(placement: .topBarTrailing) { Button(action: { - SentrySDK.feedback.presentUI() + feedback.show() }, label: { Label(L10n.Shared.Button.Feedback.label, systemSymbol: .megaphone) }) diff --git a/Targets/App/Sources/UI/LinkLists/LinkListsContainerView.swift b/Targets/App/Sources/UI/LinkLists/LinkListsContainerView.swift index aa6e67f..77aeb00 100644 --- a/Targets/App/Sources/UI/LinkLists/LinkListsContainerView.swift +++ b/Targets/App/Sources/UI/LinkLists/LinkListsContainerView.swift @@ -194,11 +194,6 @@ struct LinkListsContainerView: View { } } .sentryTrace("LINK_LISTS_VIEW") - .onAppear { - // Auto-injecting Sentry feedback widget is currently not supported in SwiftUI. - // Therefore we manually trigger it when the view appears. - SentrySDK.feedback.showWidget() - } .onChange(of: searchText) { oldValue, newValue in // Track search when user starts searching (transitions from empty to non-empty) if oldValue.isEmpty && !newValue.isEmpty { diff --git a/Targets/App/Sources/UI/LinkLists/LinkListsRenderView.swift b/Targets/App/Sources/UI/LinkLists/LinkListsRenderView.swift index c23ffc1..b349365 100644 --- a/Targets/App/Sources/UI/LinkLists/LinkListsRenderView.swift +++ b/Targets/App/Sources/UI/LinkLists/LinkListsRenderView.swift @@ -1,8 +1,9 @@ import SFSafeSymbols -import Sentry import SwiftUI struct LinkListsRenderView: View { + @Environment(\.feedback) private var feedback + let pinnedLists: [LinkListsDisplayItem] let unpinnedLists: [LinkListsDisplayItem] @@ -74,7 +75,7 @@ struct LinkListsRenderView: View { view.toolbar { ToolbarItem(placement: .topBarTrailing) { Button(action: { - SentrySDK.feedback.presentUI() + feedback.show() }, label: { Label(L10n.Shared.Button.Feedback.label, systemSymbol: .megaphone) }) @@ -124,7 +125,7 @@ struct LinkListsRenderView: View { view.toolbar { ToolbarItem(placement: .topBarTrailing) { Button(action: { - SentrySDK.feedback.presentUI() + feedback.show() }, label: { Label(L10n.Shared.Button.Feedback.label, systemSymbol: .megaphone) }) diff --git a/Targets/UITests/Sources/UITests.swift b/Targets/UITests/Sources/UITests.swift index dcca258..fb3a12b 100644 --- a/Targets/UITests/Sources/UITests.swift +++ b/Targets/UITests/Sources/UITests.swift @@ -2,15 +2,143 @@ import XCTest final class UITests: XCTestCase { + private var app: XCUIApplication! + override func setUpWithError() throws { - // In UI tests it is usually best to stop immediately when a failure occurs. continueAfterFailure = false + app = XCUIApplication() + app.launchEnvironment["TESTING"] = "1" + app.launch() } @MainActor func testLaunchPerformance() throws { measure(metrics: [XCTApplicationLaunchMetric()]) { - XCUIApplication().launch() + let app = XCUIApplication() + app.launchEnvironment["TESTING"] = "1" + app.launch() } } + + // MARK: - Feedback Button Tests + + func testFeedbackButtonExistsOnListsView() throws { + // -- Act -- + let feedbackButton = app.buttons["link-lists.feedback.button"] + XCTAssertTrue(feedbackButton.waitForExistence(timeout: 5)) + XCTAssertTrue(feedbackButton.isHittable) + feedbackButton.tap() + + // -- Assert -- + assertFeedbackSubmitButtonExists() + } + + func testFeedbackButtonExistsOnListDetailView() throws { + // -- Arrange -- + navigateToListDetail() + + // -- Act -- + let feedbackButton = app.buttons["link-list-detail.feedback.button"] + XCTAssertTrue(feedbackButton.waitForExistence(timeout: 5)) + XCTAssertTrue(feedbackButton.isHittable) + feedbackButton.tap() + + // -- Assert -- + assertFeedbackSubmitButtonExists() + } + + func testFeedbackButtonExistsOnLinkDetailView() throws { + // -- Arrange -- + navigateToLinkDetail() + + // -- Act -- + let feedbackButton = app.buttons["link-detail.feedback.button"] + XCTAssertTrue(feedbackButton.waitForExistence(timeout: 5)) + XCTAssertTrue(feedbackButton.isHittable) + feedbackButton.tap() + + // -- Assert -- + assertFeedbackSubmitButtonExists() + } + + func testFeedbackButtonExistsOnLinkInfoView() throws { + // -- Arrange -- + navigateToLinkDetail() + + let moreMenu = app.buttons["link-detail.more-menu.button"] + XCTAssertTrue(moreMenu.waitForExistence(timeout: 5)) + moreMenu.tap() + + let editButton = app.buttons["Edit"] + XCTAssertTrue(editButton.waitForExistence(timeout: 5)) + editButton.tap() + + // -- Act -- + let feedbackButton = app.buttons["link-info.feedback.button"] + XCTAssertTrue(feedbackButton.waitForExistence(timeout: 5)) + XCTAssertTrue(feedbackButton.isHittable) + feedbackButton.tap() + + // -- Assert -- + assertFeedbackSubmitButtonExists() + } + + func testFeedbackButtonExistsOnCreateListView() throws { + // -- Arrange -- + let createListButton = app.buttons["link-lists.create-list.button"] + XCTAssertTrue(createListButton.waitForExistence(timeout: 5)) + createListButton.tap() + + // -- Act -- + let feedbackButton = app.buttons["create-link-list.feedback.button"] + XCTAssertTrue(feedbackButton.waitForExistence(timeout: 5)) + XCTAssertTrue(feedbackButton.isHittable) + feedbackButton.tap() + + // -- Assert -- + assertFeedbackSubmitButtonExists() + } + + func testFeedbackButtonExistsOnCreateLinkView() throws { + // -- Arrange -- + navigateToListDetail() + + let newLinkButton = app.buttons["link-list-detail.new-link.button"] + XCTAssertTrue(newLinkButton.waitForExistence(timeout: 5)) + newLinkButton.tap() + + // -- Act -- + let feedbackButton = app.buttons["create-link.feedback.button"] + XCTAssertTrue(feedbackButton.waitForExistence(timeout: 5)) + XCTAssertTrue(feedbackButton.isHittable) + feedbackButton.tap() + + // -- Assert -- + assertFeedbackSubmitButtonExists() + } + + // MARK: - Navigation Helpers + + private func navigateToListDetail() { + let myLinksRow = app.cells.containing(.staticText, identifier: "My Links").firstMatch + XCTAssertTrue(myLinksRow.waitForExistence(timeout: 5)) + myLinksRow.tap() + + XCTAssertTrue(app.navigationBars["My Links"].waitForExistence(timeout: 5)) + } + + private func navigateToLinkDetail() { + navigateToListDetail() + + let appleRow = app.cells.containing(.staticText, identifier: "Apple").firstMatch + XCTAssertTrue(appleRow.waitForExistence(timeout: 5)) + appleRow.tap() + } + + // MARK: - Assertion Helpers + + private func assertFeedbackSubmitButtonExists() { + let submitButton = app.buttons["io.sentry.feedback.form.submit"].firstMatch + XCTAssertTrue(submitButton.waitForExistence(timeout: 5)) + } } From 3be7bf21d6966227194206c6664dddfb8f3491da Mon Sep 17 00:00:00 2001 From: Philip Niedertscheider Date: Fri, 5 Jun 2026 12:15:56 +0200 Subject: [PATCH 4/4] feat: Add breadcrumbs and toast notifications for feedback submission Wire up onSubmitSuccess and onSubmitError callbacks with Sentry breadcrumbs and localized toast messages. Pass ToastManager lazily via closure to avoid accessing self before full initialization. --- Flinky.xcodeproj/project.pbxproj | 2 +- .../xcshareddata/swiftpm/Package.resolved | 4 +- Targets/App/Sources/Generated/L10n.swift | 10 +++++ Targets/App/Sources/Main/FlinkyApp.swift | 41 ++++++++++++++----- .../Sources/Resources/Localizable.xcstrings | 24 +++++++++++ 5 files changed, 68 insertions(+), 13 deletions(-) diff --git a/Flinky.xcodeproj/project.pbxproj b/Flinky.xcodeproj/project.pbxproj index fc7317f..81f3ceb 100644 --- a/Flinky.xcodeproj/project.pbxproj +++ b/Flinky.xcodeproj/project.pbxproj @@ -1859,7 +1859,7 @@ isa = XCRemoteSwiftPackageReference; repositoryURL = "https://github.com/getsentry/sentry-cocoa"; requirement = { - branch = "ref/feedback-config-arg"; + branch = "feat/feedback-presentation-api"; kind = branch; }; traits = ( diff --git a/Flinky.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved b/Flinky.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved index fc6e35d..2d7354e 100644 --- a/Flinky.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ b/Flinky.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -15,8 +15,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/getsentry/sentry-cocoa", "state" : { - "branch" : "ref/feedback-config-arg", - "revision" : "7946700ddb51ecede53afb14283da63262eabba3" + "branch" : "feat/feedback-presentation-api", + "revision" : "3fa7efdac738a631246f4c0ccf7f8217762ba9ef" } }, { diff --git a/Targets/App/Sources/Generated/L10n.swift b/Targets/App/Sources/Generated/L10n.swift index 2602e83..b752d78 100644 --- a/Targets/App/Sources/Generated/L10n.swift +++ b/Targets/App/Sources/Generated/L10n.swift @@ -39,6 +39,16 @@ internal enum L10n { /// New List internal static let title = L10n.tr("create-list.title", fallback: "New List") } + internal enum Feedback { + internal enum Toast { + internal enum Submit { + /// Failed to submit feedback + internal static let error = L10n.tr("feedback.toast.submit.error", fallback: "Failed to submit feedback") + /// Feedback submitted successfully + internal static let success = L10n.tr("feedback.toast.submit.success", fallback: "Feedback submitted successfully") + } + } + } internal enum LinkDetail { internal enum EditLink { /// Edit diff --git a/Targets/App/Sources/Main/FlinkyApp.swift b/Targets/App/Sources/Main/FlinkyApp.swift index cf0779f..88994e9 100644 --- a/Targets/App/Sources/Main/FlinkyApp.swift +++ b/Targets/App/Sources/Main/FlinkyApp.swift @@ -13,15 +13,10 @@ struct FlinkyApp: App { private let sharedModelContainer: ModelContainer init() { - SentrySDK.start { options in - Self.configureSentry(options: options) + SentrySDK.start { [_toastManager] options in + Self.configureSentry(options: options, toastManager: { _toastManager.wrappedValue }) } - // Start app health observation for system-level metrics - // (thermal state, network reachability, app state transitions) - // Reference: https://github.com/getsentry/sentry-cocoa/issues/7000 - AppHealthObserver.shared.startObserving() - do { sharedModelContainer = try SharedModelContainerFactory.make( isStoredInMemoryOnly: ProcessInfo.processInfo.isTestingEnabled @@ -30,6 +25,11 @@ struct FlinkyApp: App { fatalError("Failed to create shared ModelContainer: \(error)") } + // Start app health observation for system-level metrics + // (thermal state, network reachability, app state transitions) + // Reference: https://github.com/getsentry/sentry-cocoa/issues/7000 + AppHealthObserver.shared.startObserving() + // Seed if needed on first app launch DataSeedingService.seedDataIfNeeded(modelContext: sharedModelContainer.mainContext) } @@ -39,7 +39,11 @@ struct FlinkyApp: App { /// This method is defined as `private static` to because it is called from a non-mutating context. /// /// - Parameter options: Options structure to configure Sentry. - private static func configureSentry(options: Options) { // swiftlint:disable:this function_body_length + /// - Parameter toastManager: Closure providing lazy access to the toast manager for feedback callbacks. + private static func configureSentry( // swiftlint:disable:this function_body_length + options: Options, + toastManager: @escaping () -> ToastManager + ) { // Disable Sentry for tests because it produces a lot of noise. if ProcessInfo.processInfo.isTestingEnabled { Self.logger.warning("Sentry is disabled in test environment") @@ -194,6 +198,24 @@ struct FlinkyApp: App { // Track feedback form closing using metrics - better for aggregate counts than individual events SentryMetricsHelper.trackFeedbackFormClosed() } + feedbackOptions.onSubmitSuccess = { _ in + let breadcrumb = Breadcrumb(level: .info, category: "user_feedback") + breadcrumb.message = "User successfully submitted feedback" + SentrySDK.addBreadcrumb(breadcrumb) + + DispatchQueue.main.async { + toastManager().success(description: L10n.Feedback.Toast.Submit.success) + } + } + feedbackOptions.onSubmitError = { error in + let breadcrumb = Breadcrumb(level: .error, category: "user_feedback") + breadcrumb.message = "Feedback submission failed: \(error.localizedDescription)" + SentrySDK.addBreadcrumb(breadcrumb) + + DispatchQueue.main.async { + toastManager().error(description: L10n.Feedback.Toast.Submit.error) + } + } } // Configure Logs @@ -214,8 +236,7 @@ struct FlinkyApp: App { WindowGroup { MainContainerView() .environment(\.feedback, FeedbackAction { - let config = SentryFeedbackFormConfig() - SentrySDK.feedback.show(config: config) + SentrySDK.feedback.show() }) .toaster(toastManager) .configureOnLaunch { options in diff --git a/Targets/App/Sources/Resources/Localizable.xcstrings b/Targets/App/Sources/Resources/Localizable.xcstrings index 58f0ac9..9961efe 100644 --- a/Targets/App/Sources/Resources/Localizable.xcstrings +++ b/Targets/App/Sources/Resources/Localizable.xcstrings @@ -1502,6 +1502,30 @@ } } } + }, + "feedback.toast.submit.success": { + "comment": "Toast message shown when feedback is submitted successfully", + "extractionState": "manual", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Feedback submitted successfully" + } + } + } + }, + "feedback.toast.submit.error": { + "comment": "Toast message shown when feedback submission fails", + "extractionState": "manual", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Failed to submit feedback" + } + } + } } }, "version": "1.0"