diff --git a/URLShare/ShareBookmarkView.swift b/URLShare/ShareBookmarkView.swift index 933431e..49023e4 100644 --- a/URLShare/ShareBookmarkView.swift +++ b/URLShare/ShareBookmarkView.swift @@ -14,32 +14,43 @@ struct ShareBookmarkView: View { var body: some View { VStack(spacing: 0) { - ScrollViewReader { proxy in - ScrollView { - VStack(spacing: 0) { - logoSection - serverStatusSection - urlSection - tagManagementSection - .id(AddBookmarkFieldFocus.labels) - titleSection - .id(AddBookmarkFieldFocus.title) - statusSection - Spacer(minLength: 100) // Space for button - } + if !viewModel.isConfigured || viewModel.sessionExpired { + // Show setup required screen + VStack(spacing: 24) { + logoSection + setupRequiredSection + Spacer() } - .padding(.bottom, max(0, keyboardHeight - 120)) - .onChange(of: focusedField) { newField, _ in - guard let field = newField else { return } - DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) { - withAnimation(.easeInOut(duration: 0.25)) { - proxy.scrollTo(field, anchor: .center) + .padding() + } else { + // Normal UI + ScrollViewReader { proxy in + ScrollView { + VStack(spacing: 0) { + logoSection + serverStatusSection + urlSection + tagManagementSection + .id(AddBookmarkFieldFocus.labels) + titleSection + .id(AddBookmarkFieldFocus.title) + statusSection + Spacer(minLength: 100) // Space for button + } + } + .padding(.bottom, max(0, keyboardHeight - 120)) + .onChange(of: focusedField) { newField, _ in + guard let field = newField else { return } + DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) { + withAnimation(.easeInOut(duration: 0.25)) { + proxy.scrollTo(field, anchor: .center) + } } } } + + saveButtonSection } - - saveButtonSection } .background(Color(.systemGroupedBackground)) .ignoresSafeArea(.keyboard, edges: .bottom) @@ -72,6 +83,31 @@ struct ShareBookmarkView: View { .padding(.top, 24) .opacity(0.9) } + + @ViewBuilder + private var setupRequiredSection: some View { + VStack(spacing: 20) { + Image(systemName: viewModel.sessionExpired ? "person.crop.circle.badge.exclamationmark" : "exclamationmark.triangle") + .font(.system(size: 60)) + .foregroundColor(.orange) + .padding(.top, 40) + + VStack(spacing: 12) { + Text(viewModel.sessionExpired ? "Session Expired" : "Setup Required") + .font(.system(size: 24, weight: .bold)) + .foregroundColor(.primary) + + Text(viewModel.sessionExpired + ? "Please log in via the Readeck app to continue saving bookmarks." + : "Please complete the setup in the Readeck app before using the share extension.") + .font(.system(size: 16)) + .foregroundColor(.secondary) + .multilineTextAlignment(.center) + .padding(.horizontal, 32) + } + } + .frame(maxWidth: .infinity) + } @ViewBuilder private var serverStatusSection: some View { diff --git a/URLShare/ShareBookmarkViewModel.swift b/URLShare/ShareBookmarkViewModel.swift index 1d22ad5..b19948c 100644 --- a/URLShare/ShareBookmarkViewModel.swift +++ b/URLShare/ShareBookmarkViewModel.swift @@ -11,6 +11,8 @@ class ShareBookmarkViewModel: ObservableObject { @Published var isSaving: Bool = false @Published var searchText: String = "" @Published var isServerReachable: Bool = true + @Published var isConfigured: Bool = true + @Published var sessionExpired: Bool = false let tagSortOrder: TagSortOrder = .byCount // Share Extension always uses byCount let extensionContext: NSExtensionContext? @@ -21,6 +23,13 @@ class ShareBookmarkViewModel: ObservableObject { init(extensionContext: NSExtensionContext?) { self.extensionContext = extensionContext logger.info("ShareBookmarkViewModel initialized with extension context: \(extensionContext != nil)") + + // Check if app is configured by verifying token exists + checkConfiguration() + + // Setup notification observer for 401 errors + setupNotificationObservers() + extractSharedContent() } @@ -174,6 +183,30 @@ class ShareBookmarkViewModel: ObservableObject { searchText = "" } + private func checkConfiguration() { + let token = KeychainHelper.shared.loadToken() + let endpoint = KeychainHelper.shared.loadEndpoint() + + if token == nil || token?.isEmpty == true || endpoint == nil || endpoint?.isEmpty == true { + logger.warning("Share extension opened but app is not configured (missing token or endpoint)") + isConfigured = false + } else { + logger.info("Share extension opened with valid configuration") + isConfigured = true + } + } + + private func setupNotificationObservers() { + NotificationCenter.default.addObserver( + forName: .unauthorizedAPIResponse, + object: nil, + queue: .main + ) { [weak self] _ in + self?.logger.warning("Received 401 Unauthorized - session expired") + self?.sessionExpired = true + } + } + private func completeExtensionRequest() { logger.debug("Completing extension request") guard let context = extensionContext else { @@ -189,4 +222,8 @@ class ShareBookmarkViewModel: ObservableObject { } } } + + deinit { + NotificationCenter.default.removeObserver(self) + } } diff --git a/URLShare/SimpleAPI.swift b/URLShare/SimpleAPI.swift index 4d20450..20b9de8 100644 --- a/URLShare/SimpleAPI.swift +++ b/URLShare/SimpleAPI.swift @@ -76,6 +76,9 @@ class SimpleAPI { DispatchQueue.main.async { NotificationCenter.default.post(name: .unauthorizedAPIResponse, object: nil) } + logger.error("Authentication failed: 401 Unauthorized") + showStatus("Session expired. Please log in via the Readeck app.", true) + return } let msg = String(data: data, encoding: .utf8) ?? "Unknown error" logger.error("Server error \(httpResponse.statusCode): \(msg)") @@ -123,12 +126,15 @@ class SimpleAPI { } logger.logNetworkRequest(method: "GET", url: "/api/bookmarks/labels", statusCode: httpResponse.statusCode) - + guard 200...299 ~= httpResponse.statusCode else { if httpResponse.statusCode == 401 { DispatchQueue.main.async { NotificationCenter.default.post(name: .unauthorizedAPIResponse, object: nil) } + logger.error("Authentication failed: 401 Unauthorized") + showStatus("Session expired. Please log in via the Readeck app.", true) + return nil } let msg = String(data: data, encoding: .utf8) ?? "Unknown error" logger.error("Server error \(httpResponse.statusCode): \(msg)") diff --git a/readeck/UI/AppViewModel.swift b/readeck/UI/AppViewModel.swift index 2f8de10..4e85fe3 100644 --- a/readeck/UI/AppViewModel.swift +++ b/readeck/UI/AppViewModel.swift @@ -57,15 +57,15 @@ class AppViewModel { } private func handleUnauthorizedResponse() async { - print("AppViewModel: Handling 401 Unauthorized - logging out user") - + Logger.viewModel.info("Handling 401 Unauthorized - logging out user") + do { try await factory.makeLogoutUseCase().execute() loadSetupStatus() - - print("AppViewModel: User successfully logged out due to 401 error") + + Logger.viewModel.info("User successfully logged out due to 401 error") } catch { - print("AppViewModel: Error during logout: \(error)") + Logger.viewModel.error("Error during logout: \(error.localizedDescription)") } } @@ -106,22 +106,34 @@ class AppViewModel { } private func syncTagsOnAppStart() async { + // Don't sync if onboarding is not complete (no token/endpoint available) + guard settingsRepository.hasFinishedSetup else { + Logger.sync.debug("Skipping tag sync - onboarding not completed") + return + } + let now = Date() // Check if last sync was less than 2 minutes ago if let lastSync = lastAppStartTagSyncTime, now.timeIntervalSince(lastSync) < 120 { - print("AppViewModel: Skipping tag sync - last sync was less than 2 minutes ago") + Logger.sync.debug("Skipping tag sync - last sync was less than 2 minutes ago") return } // Sync tags from server to Core Data - print("AppViewModel: Syncing tags on app start") + Logger.sync.info("Syncing tags on app start") try? await syncTagsUseCase.execute() lastAppStartTagSyncTime = now } private func syncOfflineArticlesIfNeeded() { + // Don't sync if onboarding is not complete (no token/endpoint available) + guard settingsRepository.hasFinishedSetup else { + Logger.sync.debug("Skipping offline sync - onboarding not completed") + return + } + // Run offline sync in background without blocking app start Task.detached(priority: .background) { [weak self] in guard let self = self else { return } diff --git a/readeck/UI/BookmarkDetail/BookmarkDetailView2.swift b/readeck/UI/BookmarkDetail/BookmarkDetailView2.swift index c016ef4..1ea7b5a 100644 --- a/readeck/UI/BookmarkDetail/BookmarkDetailView2.swift +++ b/readeck/UI/BookmarkDetail/BookmarkDetailView2.swift @@ -248,18 +248,18 @@ struct BookmarkDetailView2: View { @ToolbarContentBuilder private var toolbarContent: some ToolbarContent { - - #if DEBUG - // Toggle button (left) - ToolbarItem(placement: .navigationBarLeading) { - Button(action: { - useNativeWebView.toggle() - }) { - Image(systemName: "sparkles") - .foregroundColor(.accentColor) + + if Bundle.main.isDebugBuild { + // Toggle button (left) + ToolbarItem(placement: .navigationBarLeading) { + Button(action: { + useNativeWebView.toggle() + }) { + Image(systemName: "sparkles") + .foregroundColor(.accentColor) + } } } - #endif // Top toolbar (right) ToolbarItem(placement: .navigationBarTrailing) { diff --git a/readeck/UI/Settings/OfflineReadingDetailView.swift b/readeck/UI/Settings/OfflineReadingDetailView.swift index b2a856d..e78b00d 100644 --- a/readeck/UI/Settings/OfflineReadingDetailView.swift +++ b/readeck/UI/Settings/OfflineReadingDetailView.swift @@ -136,38 +136,38 @@ struct OfflineReadingDetailView: View { .textCase(nil) } - #if DEBUG - Section { - // Debug: Toggle offline mode simulation - VStack(alignment: .leading, spacing: 4) { - Toggle(isOn: Binding( - get: { !appSettings.isNetworkConnected }, - set: { isOffline in - appSettings.isNetworkConnected = !isOffline - } - )) { - HStack { - Image(systemName: "airplane") - .foregroundColor(.orange) - - VStack(alignment: .leading, spacing: 2) { - Text("Simulate Offline Mode".localized) + if Bundle.main.isDebugBuild { + Section { + // Debug: Toggle offline mode simulation + VStack(alignment: .leading, spacing: 4) { + Toggle(isOn: Binding( + get: { !appSettings.isNetworkConnected }, + set: { isOffline in + appSettings.isNetworkConnected = !isOffline + } + )) { + HStack { + Image(systemName: "airplane") .foregroundColor(.orange) - Text("DEBUG: Toggle network status".localized) - .font(.caption) - .foregroundColor(.secondary) + + VStack(alignment: .leading, spacing: 2) { + Text("Simulate Offline Mode".localized) + .foregroundColor(.orange) + Text("DEBUG: Toggle network status".localized) + .font(.caption) + .foregroundColor(.secondary) + } } } } + } header: { + Text("Debug".localized) + .font(.subheadline) + .fontWeight(.semibold) + .foregroundColor(.primary) + .textCase(nil) } - } header: { - Text("Debug".localized) - .font(.subheadline) - .fontWeight(.semibold) - .foregroundColor(.primary) - .textCase(nil) } - #endif } } .listStyle(.insetGrouped) diff --git a/readeck/UI/Settings/SettingsContainerView.swift b/readeck/UI/Settings/SettingsContainerView.swift index 33d650e..7c7163c 100644 --- a/readeck/UI/Settings/SettingsContainerView.swift +++ b/readeck/UI/Settings/SettingsContainerView.swift @@ -86,12 +86,10 @@ struct SettingsContainerView: View { LegalPrivacySettingsView() - // Debug-only Logging Configuration - #if DEBUG - if Bundle.main.isDebugBuild { + // Debug-only Settings Section + if !Bundle.main.isProduction { debugSettingsSection } - #endif // App Info Section appInfoSection @@ -104,7 +102,6 @@ struct SettingsContainerView: View { } } - #if DEBUG @ViewBuilder private var debugSettingsSection: some View { Section { @@ -112,42 +109,16 @@ struct SettingsContainerView: View { icon: "wrench.and.screwdriver.fill", iconColor: .orange, title: "Debug Menu", - subtitle: "Network simulation, data management & more" + subtitle: "Network simulation, logging & more" ) { DebugMenuView() - } - - SettingsRowNavigationLink( - icon: "list.bullet.rectangle", - iconColor: .blue, - title: "Debug Logs", - subtitle: "View all debug messages" - ) { - DebugLogViewer() - } - - SettingsRowNavigationLink( - icon: "slider.horizontal.3", - iconColor: .purple, - title: "Logging Configuration", - subtitle: "Configure log levels and categories" - ) { - LoggingConfigurationView() - } - - SettingsRowNavigationLink( - icon: "textformat", - iconColor: .green, - title: "Font Debug", - subtitle: "View available fonts" - ) { - FontDebugView() + .environmentObject(AppSettings()) } } header: { HStack { Text("Debug Settings") Spacer() - Text("DEBUG BUILD") + Text(Bundle.main.isTestFlightBuild ? "TESTFLIGHT" : "DEBUG BUILD") .font(.caption2) .padding(.horizontal, 6) .padding(.vertical, 2) @@ -155,9 +126,10 @@ struct SettingsContainerView: View { .foregroundColor(.orange) .clipShape(Capsule()) } + } footer: { + Text("Debug menu is also accessible via shake gesture") } } - #endif @ViewBuilder private var appInfoSection: some View { diff --git a/readeck/UI/Settings/SettingsGeneralView.swift b/readeck/UI/Settings/SettingsGeneralView.swift index 75991ca..3d0cc07 100644 --- a/readeck/UI/Settings/SettingsGeneralView.swift +++ b/readeck/UI/Settings/SettingsGeneralView.swift @@ -45,35 +45,35 @@ struct SettingsGeneralView: View { Text("Activate the Read Aloud Feature to read aloud your articles. This is a really early preview and might not work perfectly.") } - #if DEBUG - Section { - Toggle("Safari Reader Mode", isOn: $viewModel.enableReaderMode) - Toggle("Automatically mark articles as read", isOn: $viewModel.autoMarkAsRead) - } header: { - Text("Reading Settings") - } + if Bundle.main.isDebugBuild { + Section { + Toggle("Safari Reader Mode", isOn: $viewModel.enableReaderMode) + Toggle("Automatically mark articles as read", isOn: $viewModel.autoMarkAsRead) + } header: { + Text("Reading Settings") + } - if let successMessage = viewModel.successMessage { - Section { - HStack { - Image(systemName: "checkmark.circle.fill") - .foregroundColor(.green) - Text(successMessage) - .foregroundColor(.green) + if let successMessage = viewModel.successMessage { + Section { + HStack { + Image(systemName: "checkmark.circle.fill") + .foregroundColor(.green) + Text(successMessage) + .foregroundColor(.green) + } + } + } + if let errorMessage = viewModel.errorMessage { + Section { + HStack { + Image(systemName: "exclamationmark.triangle.fill") + .foregroundColor(.red) + Text(errorMessage) + .foregroundColor(.red) + } } } } - if let errorMessage = viewModel.errorMessage { - Section { - HStack { - Image(systemName: "exclamationmark.triangle.fill") - .foregroundColor(.red) - Text(errorMessage) - .foregroundColor(.red) - } - } - } - #endif } .sheet(isPresented: $showReleaseNotes) { ReleaseNotesView() diff --git a/readeck/UI/Settings/SyncSettingsView.swift b/readeck/UI/Settings/SyncSettingsView.swift index f8a49df..ae6b15a 100644 --- a/readeck/UI/Settings/SyncSettingsView.swift +++ b/readeck/UI/Settings/SyncSettingsView.swift @@ -16,37 +16,37 @@ struct SyncSettingsView: View { var body: some View { Group { - #if DEBUG - Section { - Toggle("Automatic sync", isOn: $viewModel.autoSyncEnabled) - if viewModel.autoSyncEnabled { - Stepper("Sync interval: \(viewModel.syncInterval) minutes", value: $viewModel.syncInterval, in: 1...60) + if Bundle.main.isDebugBuild { + Section { + Toggle("Automatic sync", isOn: $viewModel.autoSyncEnabled) + if viewModel.autoSyncEnabled { + Stepper("Sync interval: \(viewModel.syncInterval) minutes", value: $viewModel.syncInterval, in: 1...60) + } + } header: { + Text("Sync Settings") } - } header: { - Text("Sync Settings") - } - if let successMessage = viewModel.successMessage { - Section { - HStack { - Image(systemName: "checkmark.circle.fill") - .foregroundColor(.green) - Text(successMessage) - .foregroundColor(.green) + if let successMessage = viewModel.successMessage { + Section { + HStack { + Image(systemName: "checkmark.circle.fill") + .foregroundColor(.green) + Text(successMessage) + .foregroundColor(.green) + } + } + } + if let errorMessage = viewModel.errorMessage { + Section { + HStack { + Image(systemName: "exclamationmark.triangle.fill") + .foregroundColor(.red) + Text(errorMessage) + .foregroundColor(.red) + } } } } - if let errorMessage = viewModel.errorMessage { - Section { - HStack { - Image(systemName: "exclamationmark.triangle.fill") - .foregroundColor(.red) - Text(errorMessage) - .foregroundColor(.red) - } - } - } - #endif } .task { await viewModel.loadGeneralSettings() diff --git a/readeck/UI/Utils/Logger.swift b/readeck/UI/Utils/Logger.swift index c6e9e60..cda8051 100644 --- a/readeck/UI/Utils/Logger.swift +++ b/readeck/UI/Utils/Logger.swift @@ -54,17 +54,17 @@ class LogConfiguration: ObservableObject { private init() { // First time setup: Enable logging in DEBUG builds with sensible defaults - #if DEBUG - if UserDefaults.standard.object(forKey: "LogConfigurationInitialized") == nil { - isLoggingEnabled = true - showPerformanceLogs = true - showTimestamps = true - includeSourceLocation = true - globalMinLevel = .debug - UserDefaults.standard.set(true, forKey: "LogConfigurationInitialized") - saveConfiguration() + if Bundle.main.isDebugBuild { + if UserDefaults.standard.object(forKey: "LogConfigurationInitialized") == nil { + isLoggingEnabled = true + showPerformanceLogs = true + showTimestamps = true + includeSourceLocation = true + globalMinLevel = .debug + UserDefaults.standard.set(true, forKey: "LogConfigurationInitialized") + saveConfiguration() + } } - #endif loadConfiguration() } @@ -184,7 +184,7 @@ struct Logger { // MARK: - Store Log private func storeLog(message: String, level: LogLevel, file: String, function: String, line: Int) { - #if DEBUG + guard Bundle.main.isDebugBuild else { return } guard config.isLoggingEnabled else { return } let entry = LogEntry( level: level, @@ -197,7 +197,6 @@ struct Logger { Task { await LogStore.shared.addEntry(entry) } - #endif } // MARK: - Convenience Methods