diff --git a/CHANGELOG.md b/CHANGELOG.md index 81fe2de..ecb1a48 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,15 @@ All notable changes to MacTorn will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## [1.4.7] - 2026-01-27 + +### Added +- In-app feedback prompt with smart timing (1 hour, 1 week, 1 month thresholds) +- Positive feedback links to Torn forums thread +- Negative feedback opens email for direct developer contact +- 5-minute cooldown between prompt dismissals +- Comprehensive test coverage for feedback logic + ## [1.4.6] - 2025-01-25 ### Fixed diff --git a/MacTorn-v1.4.7.zip b/MacTorn-v1.4.7.zip new file mode 100644 index 0000000..d7295d8 Binary files /dev/null and b/MacTorn-v1.4.7.zip differ diff --git a/MacTorn/MacTorn.xcodeproj/project.pbxproj b/MacTorn/MacTorn.xcodeproj/project.pbxproj index e50e3d8..8c80a87 100644 --- a/MacTorn/MacTorn.xcodeproj/project.pbxproj +++ b/MacTorn/MacTorn.xcodeproj/project.pbxproj @@ -31,6 +31,7 @@ AAA00022 /* TravelView.swift in Sources */ = {isa = PBXBuildFile; fileRef = AAA10023 /* TravelView.swift */; }; AAA00023 /* CreditsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = AAA10024 /* CreditsView.swift */; }; AAA00024 /* TransparencyEnvironment.swift in Sources */ = {isa = PBXBuildFile; fileRef = AAA10025 /* TransparencyEnvironment.swift */; }; + AAA00025 /* FeedbackPromptView.swift in Sources */ = {isa = PBXBuildFile; fileRef = AAA10026 /* FeedbackPromptView.swift */; }; /* Unit Tests */ BBB00001 /* MockNetworkSession.swift in Sources */ = {isa = PBXBuildFile; fileRef = BBB10001 /* MockNetworkSession.swift */; }; BBB00002 /* TestHelpers.swift in Sources */ = {isa = PBXBuildFile; fileRef = BBB10002 /* TestHelpers.swift */; }; @@ -44,6 +45,7 @@ BBB00010 /* MoneyDataTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = BBB10010 /* MoneyDataTests.swift */; }; BBB00011 /* AppStateTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = BBB10011 /* AppStateTests.swift */; }; BBB00012 /* AppStateWatchlistTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = BBB10012 /* AppStateWatchlistTests.swift */; }; + BBB00013 /* AppStateFeedbackTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = BBB10013 /* AppStateFeedbackTests.swift */; }; /* UI Tests */ CCC00001 /* MacTornUITests.swift in Sources */ = {isa = PBXBuildFile; fileRef = CCC10001 /* MacTornUITests.swift */; }; /* End PBXBuildFile section */ @@ -91,6 +93,7 @@ AAA10023 /* TravelView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TravelView.swift; sourceTree = ""; }; AAA10024 /* CreditsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CreditsView.swift; sourceTree = ""; }; AAA10025 /* TransparencyEnvironment.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TransparencyEnvironment.swift; sourceTree = ""; }; + AAA10026 /* FeedbackPromptView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FeedbackPromptView.swift; sourceTree = ""; }; AAA10000 /* MacTorn.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = MacTorn.app; sourceTree = BUILT_PRODUCTS_DIR; }; /* Unit Test Files */ BBB10001 /* MockNetworkSession.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MockNetworkSession.swift; sourceTree = ""; }; @@ -105,6 +108,7 @@ BBB10010 /* MoneyDataTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MoneyDataTests.swift; sourceTree = ""; }; BBB10011 /* AppStateTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppStateTests.swift; sourceTree = ""; }; BBB10012 /* AppStateWatchlistTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppStateWatchlistTests.swift; sourceTree = ""; }; + BBB10013 /* AppStateFeedbackTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppStateFeedbackTests.swift; sourceTree = ""; }; BBB10000 /* MacTornTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = MacTornTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; /* UI Test Files */ CCC10001 /* MacTornUITests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MacTornUITests.swift; sourceTree = ""; }; @@ -213,6 +217,7 @@ AAA10013 /* ChainView.swift */, AAA10014 /* StatusBadgesView.swift */, AAA10015 /* EventsView.swift */, + AAA10026 /* FeedbackPromptView.swift */, ); path = Components; sourceTree = ""; @@ -292,6 +297,7 @@ children = ( BBB10011 /* AppStateTests.swift */, BBB10012 /* AppStateWatchlistTests.swift */, + BBB10013 /* AppStateFeedbackTests.swift */, ); path = ViewModels; sourceTree = ""; @@ -457,6 +463,7 @@ AAA00022 /* TravelView.swift in Sources */, AAA00023 /* CreditsView.swift in Sources */, AAA00024 /* TransparencyEnvironment.swift in Sources */, + AAA00025 /* FeedbackPromptView.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -476,6 +483,7 @@ BBB00010 /* MoneyDataTests.swift in Sources */, BBB00011 /* AppStateTests.swift in Sources */, BBB00012 /* AppStateWatchlistTests.swift in Sources */, + BBB00013 /* AppStateFeedbackTests.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -639,7 +647,7 @@ "$(inherited)", "@executable_path/../Frameworks", ); - MARKETING_VERSION = 1.4.6; + MARKETING_VERSION = 1.4.7; PRODUCT_BUNDLE_IDENTIFIER = com.mactorn.app; PRODUCT_NAME = "$(TARGET_NAME)"; SWIFT_EMIT_LOC_STRINGS = YES; @@ -666,7 +674,7 @@ "$(inherited)", "@executable_path/../Frameworks", ); - MARKETING_VERSION = 1.4.6; + MARKETING_VERSION = 1.4.7; PRODUCT_BUNDLE_IDENTIFIER = com.mactorn.app; PRODUCT_NAME = "$(TARGET_NAME)"; SWIFT_EMIT_LOC_STRINGS = YES; @@ -684,7 +692,7 @@ DEVELOPMENT_TEAM = ""; GENERATE_INFOPLIST_FILE = YES; MACOSX_DEPLOYMENT_TARGET = 13.0; - MARKETING_VERSION = 1.4.6; + MARKETING_VERSION = 1.4.7; PRODUCT_BUNDLE_IDENTIFIER = com.mactorn.MacTornTests; PRODUCT_NAME = "$(TARGET_NAME)"; SWIFT_EMIT_LOC_STRINGS = NO; @@ -702,7 +710,7 @@ DEVELOPMENT_TEAM = ""; GENERATE_INFOPLIST_FILE = YES; MACOSX_DEPLOYMENT_TARGET = 13.0; - MARKETING_VERSION = 1.4.6; + MARKETING_VERSION = 1.4.7; PRODUCT_BUNDLE_IDENTIFIER = com.mactorn.MacTornTests; PRODUCT_NAME = "$(TARGET_NAME)"; SWIFT_EMIT_LOC_STRINGS = NO; @@ -720,7 +728,7 @@ DEVELOPMENT_TEAM = ""; GENERATE_INFOPLIST_FILE = YES; MACOSX_DEPLOYMENT_TARGET = 13.0; - MARKETING_VERSION = 1.4.6; + MARKETING_VERSION = 1.4.7; PRODUCT_BUNDLE_IDENTIFIER = com.mactorn.MacTornUITests; PRODUCT_NAME = "$(TARGET_NAME)"; SWIFT_EMIT_LOC_STRINGS = NO; @@ -737,7 +745,7 @@ DEVELOPMENT_TEAM = ""; GENERATE_INFOPLIST_FILE = YES; MACOSX_DEPLOYMENT_TARGET = 13.0; - MARKETING_VERSION = 1.4.6; + MARKETING_VERSION = 1.4.7; PRODUCT_BUNDLE_IDENTIFIER = com.mactorn.MacTornUITests; PRODUCT_NAME = "$(TARGET_NAME)"; SWIFT_EMIT_LOC_STRINGS = NO; diff --git a/MacTorn/MacTorn/Models/TornModels.swift b/MacTorn/MacTorn/Models/TornModels.swift index 80e3f80..800f61d 100644 --- a/MacTorn/MacTorn/Models/TornModels.swift +++ b/MacTorn/MacTorn/Models/TornModels.swift @@ -734,6 +734,14 @@ enum NotificationSound: String, CaseIterable { } } +// MARK: - App Feedback State +struct AppFeedbackState: Codable { + var firstLaunchDate: Date + var hasResponded: Bool + var dismissCount: Int + var lastDismissedDate: Date? +} + // MARK: - Keyboard Shortcuts struct KeyboardShortcut: Identifiable, Codable, Equatable { let id: String diff --git a/MacTorn/MacTorn/ViewModels/AppState.swift b/MacTorn/MacTorn/ViewModels/AppState.swift index b7caede..9a26980 100644 --- a/MacTorn/MacTorn/ViewModels/AppState.swift +++ b/MacTorn/MacTorn/ViewModels/AppState.swift @@ -46,6 +46,11 @@ class AppState: ObservableObject { // MARK: - Update State @Published var updateAvailable: GitHubRelease? + // MARK: - Feedback State + @Published var feedbackState: AppFeedbackState? + @Published var showFeedbackPrompt: Bool = false + static let feedbackThresholds: [TimeInterval] = [3600, 7 * 86400, 30 * 86400] + // MARK: - Fetch Time (for live countdown calculations) @Published var lastFetchTime: Date = Date() @@ -76,6 +81,7 @@ class AppState: ObservableObject { loadNotificationRules() loadTravelNotificationSettings() loadWatchlist() + loadFeedbackState() // Polling and permissions moved to onAppear in UI } @@ -572,6 +578,9 @@ class AppState: ObservableObject { // Manage travel timer after data is set self.manageTravelTimer() + // Check if feedback prompt should be shown + self.checkFeedbackPrompt() + // Force UI update by triggering objectWillChange self.objectWillChange.send() logger.info("UI update triggered, lastUpdated: \(self.lastUpdated?.description ?? "nil")") @@ -704,7 +713,7 @@ class AppState: ObservableObject { // MARK: - Updates func checkForAppUpdates() { guard let currentVersion = Bundle.main.infoDictionary?["CFBundleShortVersionString"] as? String else { return } - + Task { if let release = await updateManager.checkForUpdates(currentVersion: currentVersion) { await MainActor.run { @@ -713,6 +722,73 @@ class AppState: ObservableObject { } } } + + // MARK: - Feedback Prompt + + func loadFeedbackState() { + if let data = UserDefaults.standard.data(forKey: "appFeedbackState"), + let state = try? JSONDecoder().decode(AppFeedbackState.self, from: data) { + feedbackState = state + } else { + feedbackState = AppFeedbackState( + firstLaunchDate: Date(), + hasResponded: false, + dismissCount: 0, + lastDismissedDate: nil + ) + saveFeedbackState() + } + } + + func saveFeedbackState() { + guard let state = feedbackState, + let data = try? JSONEncoder().encode(state) else { return } + UserDefaults.standard.set(data, forKey: "appFeedbackState") + } + + func checkFeedbackPrompt() { + guard let state = feedbackState else { return } + guard !state.hasResponded else { return } + guard state.dismissCount < Self.feedbackThresholds.count else { return } + + // 5-minute cooldown after last dismissal + if let lastDismissed = state.lastDismissedDate, + Date().timeIntervalSince(lastDismissed) < 300 { + return + } + + let elapsed = Date().timeIntervalSince(state.firstLaunchDate) + let requiredTime = Self.feedbackThresholds[state.dismissCount] + + if elapsed >= requiredTime { + showFeedbackPrompt = true + } + } + + func feedbackRespondedPositive() { + feedbackState?.hasResponded = true + showFeedbackPrompt = false + saveFeedbackState() + if let url = URL(string: "https://www.torn.com/forums.php#/p=threads&f=67&t=16532308") { + NSWorkspace.shared.open(url) + } + } + + func feedbackRespondedNegative() { + feedbackState?.hasResponded = true + showFeedbackPrompt = false + saveFeedbackState() + if let url = URL(string: "mailto:pawel@orzech.lol?subject=MacTorn%20Feedback") { + NSWorkspace.shared.open(url) + } + } + + func feedbackDismissed() { + feedbackState?.dismissCount += 1 + feedbackState?.lastDismissedDate = Date() + showFeedbackPrompt = false + saveFeedbackState() + } } // MARK: - Errors diff --git a/MacTorn/MacTorn/Views/Components/FeedbackPromptView.swift b/MacTorn/MacTorn/Views/Components/FeedbackPromptView.swift new file mode 100644 index 0000000..8192354 --- /dev/null +++ b/MacTorn/MacTorn/Views/Components/FeedbackPromptView.swift @@ -0,0 +1,74 @@ +import SwiftUI + +struct FeedbackPromptView: View { + @EnvironmentObject var appState: AppState + @Environment(\.reduceTransparency) private var reduceTransparency + + var body: some View { + VStack(spacing: 16) { + Image(systemName: "heart.fill") + .font(.system(size: 32)) + .foregroundStyle( + LinearGradient( + colors: [.pink, .red], + startPoint: .topLeading, + endPoint: .bottomTrailing + ) + ) + + Text("Enjoying MacTorn?") + .font(.headline) + + Text("Your feedback helps make the app better for everyone.") + .font(.caption) + .foregroundColor(.secondary) + .multilineTextAlignment(.center) + + VStack(spacing: 8) { + Button { + appState.feedbackRespondedPositive() + } label: { + HStack(spacing: 6) { + Image(systemName: "hand.thumbsup.fill") + Text("Yes! Leave a review") + } + .frame(maxWidth: .infinity) + .padding(.vertical, 8) + .background(Color.green.opacity(reduceTransparency ? 0.4 : 0.2)) + .cornerRadius(8) + } + .buttonStyle(.plain) + + Button { + appState.feedbackRespondedNegative() + } label: { + HStack(spacing: 6) { + Image(systemName: "envelope.fill") + Text("Not really — send feedback") + } + .frame(maxWidth: .infinity) + .padding(.vertical, 8) + .background(Color.orange.opacity(reduceTransparency ? 0.4 : 0.2)) + .cornerRadius(8) + } + .buttonStyle(.plain) + } + + Button { + appState.feedbackDismissed() + } label: { + Text("Not now") + .font(.caption) + .foregroundColor(.secondary) + } + .buttonStyle(.plain) + } + .padding(20) + .frame(width: 260) + .background( + RoundedRectangle(cornerRadius: 12) + .fill(reduceTransparency ? Color(.windowBackgroundColor) : Color(.windowBackgroundColor).opacity(0.95)) + .shadow(radius: 8) + ) + } +} diff --git a/MacTorn/MacTorn/Views/ContentView.swift b/MacTorn/MacTorn/Views/ContentView.swift index 89df6a3..50e0e61 100644 --- a/MacTorn/MacTorn/Views/ContentView.swift +++ b/MacTorn/MacTorn/Views/ContentView.swift @@ -58,7 +58,7 @@ struct ContentView: View { if appState.isLoading && appState.lastUpdated == nil { (reduceTransparency ? Color(.windowBackgroundColor) : Color.black.opacity(0.4)) .background(reduceTransparency ? AnyShapeStyle(Color(.windowBackgroundColor)) : AnyShapeStyle(.ultraThinMaterial)) - + VStack(spacing: 12) { ProgressView() .controlSize(.large) @@ -67,6 +67,15 @@ struct ContentView: View { .foregroundColor(.secondary) } } + + // Feedback Prompt Overlay + if appState.showFeedbackPrompt { + (reduceTransparency ? Color(.windowBackgroundColor) : Color.black.opacity(0.3)) + .background(reduceTransparency ? AnyShapeStyle(Color(.windowBackgroundColor)) : AnyShapeStyle(.ultraThinMaterial)) + + FeedbackPromptView() + .environmentObject(appState) + } } .frame(width: 320) .onAppear { diff --git a/MacTorn/MacTornTests/ViewModels/AppStateFeedbackTests.swift b/MacTorn/MacTornTests/ViewModels/AppStateFeedbackTests.swift new file mode 100644 index 0000000..b337db4 --- /dev/null +++ b/MacTorn/MacTornTests/ViewModels/AppStateFeedbackTests.swift @@ -0,0 +1,186 @@ +import XCTest +@testable import MacTorn + +@MainActor +final class AppStateFeedbackTests: XCTestCase { + + var mockSession: MockNetworkSession! + var appState: AppState! + + override func setUp() async throws { + try await super.setUp() + mockSession = MockNetworkSession() + UserDefaults.standard.removeObject(forKey: "appFeedbackState") + appState = AppState(session: mockSession) + } + + override func tearDown() async throws { + appState.stopPolling() + appState = nil + mockSession = nil + UserDefaults.standard.removeObject(forKey: "appFeedbackState") + try await super.tearDown() + } + + // MARK: - First Launch + + func testFirstLaunch_createsFeedbackState() { + XCTAssertNotNil(appState.feedbackState) + XCTAssertFalse(appState.feedbackState!.hasResponded) + XCTAssertEqual(appState.feedbackState!.dismissCount, 0) + XCTAssertNil(appState.feedbackState!.lastDismissedDate) + } + + // MARK: - Threshold Logic + + func testBeforeOneHour_promptDoesNotShow() { + // firstLaunchDate is just now, so less than 1 hour has elapsed + appState.checkFeedbackPrompt() + XCTAssertFalse(appState.showFeedbackPrompt) + } + + func testAfterOneHour_promptShows() { + appState.feedbackState?.firstLaunchDate = Date().addingTimeInterval(-3601) + appState.saveFeedbackState() + + appState.checkFeedbackPrompt() + XCTAssertTrue(appState.showFeedbackPrompt) + } + + func testAfterDismissOnce_needsOneWeek() { + // Set first launch to 2 hours ago, dismiss once + appState.feedbackState?.firstLaunchDate = Date().addingTimeInterval(-2 * 3600) + appState.feedbackState?.dismissCount = 1 + appState.feedbackState?.lastDismissedDate = Date().addingTimeInterval(-600) // 10 min ago (past cooldown) + appState.saveFeedbackState() + + appState.checkFeedbackPrompt() + // 2 hours < 1 week, so should not show + XCTAssertFalse(appState.showFeedbackPrompt) + } + + func testAfterDismissOnce_afterOneWeek_promptShows() { + appState.feedbackState?.firstLaunchDate = Date().addingTimeInterval(-8 * 86400) // 8 days ago + appState.feedbackState?.dismissCount = 1 + appState.feedbackState?.lastDismissedDate = Date().addingTimeInterval(-600) + appState.saveFeedbackState() + + appState.checkFeedbackPrompt() + XCTAssertTrue(appState.showFeedbackPrompt) + } + + func testAfterDismissTwice_needsOneMonth() { + appState.feedbackState?.firstLaunchDate = Date().addingTimeInterval(-14 * 86400) // 14 days ago + appState.feedbackState?.dismissCount = 2 + appState.feedbackState?.lastDismissedDate = Date().addingTimeInterval(-600) + appState.saveFeedbackState() + + appState.checkFeedbackPrompt() + // 14 days < 30 days, so should not show + XCTAssertFalse(appState.showFeedbackPrompt) + } + + func testAfterDismissTwice_afterOneMonth_promptShows() { + appState.feedbackState?.firstLaunchDate = Date().addingTimeInterval(-31 * 86400) // 31 days ago + appState.feedbackState?.dismissCount = 2 + appState.feedbackState?.lastDismissedDate = Date().addingTimeInterval(-600) + appState.saveFeedbackState() + + appState.checkFeedbackPrompt() + XCTAssertTrue(appState.showFeedbackPrompt) + } + + func testAfterDismissThreeTimes_neverShows() { + appState.feedbackState?.firstLaunchDate = Date().addingTimeInterval(-365 * 86400) // 1 year ago + appState.feedbackState?.dismissCount = 3 + appState.saveFeedbackState() + + appState.checkFeedbackPrompt() + XCTAssertFalse(appState.showFeedbackPrompt) + } + + // MARK: - Responses + + func testPositiveResponse_setsHasRespondedAndHidesPrompt() { + appState.showFeedbackPrompt = true + appState.feedbackRespondedPositive() + + XCTAssertTrue(appState.feedbackState!.hasResponded) + XCTAssertFalse(appState.showFeedbackPrompt) + } + + func testNegativeResponse_setsHasRespondedAndHidesPrompt() { + appState.showFeedbackPrompt = true + appState.feedbackRespondedNegative() + + XCTAssertTrue(appState.feedbackState!.hasResponded) + XCTAssertFalse(appState.showFeedbackPrompt) + } + + // MARK: - Dismiss + + func testDismiss_incrementsDismissCount() { + XCTAssertEqual(appState.feedbackState!.dismissCount, 0) + + appState.feedbackDismissed() + + XCTAssertEqual(appState.feedbackState!.dismissCount, 1) + XCTAssertFalse(appState.showFeedbackPrompt) + XCTAssertNotNil(appState.feedbackState!.lastDismissedDate) + } + + // MARK: - After Responded + + func testAfterResponded_neverShowsAgain() { + appState.feedbackState?.hasResponded = true + appState.feedbackState?.firstLaunchDate = Date().addingTimeInterval(-365 * 86400) + appState.saveFeedbackState() + + appState.checkFeedbackPrompt() + XCTAssertFalse(appState.showFeedbackPrompt) + } + + // MARK: - Persistence + + func testStatePersistsAcrossAppStateInstances() { + // Set a specific first launch date and dismiss once + appState.feedbackState?.firstLaunchDate = Date().addingTimeInterval(-86400) + appState.feedbackState?.dismissCount = 1 + appState.saveFeedbackState() + + // Create a new AppState instance (simulates app restart) + let newAppState = AppState(session: mockSession) + + XCTAssertNotNil(newAppState.feedbackState) + XCTAssertEqual(newAppState.feedbackState!.dismissCount, 1) + // firstLaunchDate should be approximately 1 day ago + let elapsed = Date().timeIntervalSince(newAppState.feedbackState!.firstLaunchDate) + XCTAssertTrue(elapsed > 86300 && elapsed < 86500) + + newAppState.stopPolling() + } + + // MARK: - Cooldown + + func testFiveMinuteCooldown_preventsImmediateReshow() { + // Set eligible threshold (1 hour elapsed, dismissCount 0) + appState.feedbackState?.firstLaunchDate = Date().addingTimeInterval(-3601) + // But dismissed just 2 minutes ago + appState.feedbackState?.lastDismissedDate = Date().addingTimeInterval(-120) + // dismissCount is still 0 since we're simulating the state manually + appState.saveFeedbackState() + + appState.checkFeedbackPrompt() + XCTAssertFalse(appState.showFeedbackPrompt) + } + + func testAfterCooldown_promptCanShow() { + appState.feedbackState?.firstLaunchDate = Date().addingTimeInterval(-3601) + // Dismissed 6 minutes ago (past the 5-minute cooldown) + appState.feedbackState?.lastDismissedDate = Date().addingTimeInterval(-360) + appState.saveFeedbackState() + + appState.checkFeedbackPrompt() + XCTAssertTrue(appState.showFeedbackPrompt) + } +}