fix: Prevent sync during onboarding and improve Share Extension UX
- Add guard checks in AppViewModel to prevent sync when hasFinishedSetup is false - Share Extension now shows friendly UI when app not configured or session expired - Check token existence via KeychainHelper instead of attempting API calls - Improve 401 error messages: "Session expired. Please log in via the Readeck app." - Replace #if DEBUG with BuildEnvironment.isDebugBuild for runtime checks - Enable Debug Menu in both DEBUG and TestFlight builds - Replace print() statements with Logger.sync/Logger.viewModel
This commit is contained in:
parent
997d740597
commit
03cd32dd4d
@ -14,6 +14,16 @@ struct ShareBookmarkView: View {
|
||||
|
||||
var body: some View {
|
||||
VStack(spacing: 0) {
|
||||
if !viewModel.isConfigured || viewModel.sessionExpired {
|
||||
// Show setup required screen
|
||||
VStack(spacing: 24) {
|
||||
logoSection
|
||||
setupRequiredSection
|
||||
Spacer()
|
||||
}
|
||||
.padding()
|
||||
} else {
|
||||
// Normal UI
|
||||
ScrollViewReader { proxy in
|
||||
ScrollView {
|
||||
VStack(spacing: 0) {
|
||||
@ -41,6 +51,7 @@ struct ShareBookmarkView: View {
|
||||
|
||||
saveButtonSection
|
||||
}
|
||||
}
|
||||
.background(Color(.systemGroupedBackground))
|
||||
.ignoresSafeArea(.keyboard, edges: .bottom)
|
||||
.contentShape(Rectangle())
|
||||
@ -73,6 +84,31 @@ struct ShareBookmarkView: View {
|
||||
.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 {
|
||||
if !viewModel.isServerReachable {
|
||||
|
||||
@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
@ -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)")
|
||||
@ -129,6 +132,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 nil
|
||||
}
|
||||
let msg = String(data: data, encoding: .utf8) ?? "Unknown error"
|
||||
logger.error("Server error \(httpResponse.statusCode): \(msg)")
|
||||
|
||||
@ -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 }
|
||||
|
||||
@ -249,7 +249,7 @@ struct BookmarkDetailView2: View {
|
||||
@ToolbarContentBuilder
|
||||
private var toolbarContent: some ToolbarContent {
|
||||
|
||||
#if DEBUG
|
||||
if Bundle.main.isDebugBuild {
|
||||
// Toggle button (left)
|
||||
ToolbarItem(placement: .navigationBarLeading) {
|
||||
Button(action: {
|
||||
@ -259,7 +259,7 @@ struct BookmarkDetailView2: View {
|
||||
.foregroundColor(.accentColor)
|
||||
}
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
// Top toolbar (right)
|
||||
ToolbarItem(placement: .navigationBarTrailing) {
|
||||
|
||||
@ -136,7 +136,7 @@ struct OfflineReadingDetailView: View {
|
||||
.textCase(nil)
|
||||
}
|
||||
|
||||
#if DEBUG
|
||||
if Bundle.main.isDebugBuild {
|
||||
Section {
|
||||
// Debug: Toggle offline mode simulation
|
||||
VStack(alignment: .leading, spacing: 4) {
|
||||
@ -167,7 +167,7 @@ struct OfflineReadingDetailView: View {
|
||||
.foregroundColor(.primary)
|
||||
.textCase(nil)
|
||||
}
|
||||
#endif
|
||||
}
|
||||
}
|
||||
}
|
||||
.listStyle(.insetGrouped)
|
||||
|
||||
@ -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 {
|
||||
|
||||
@ -45,7 +45,7 @@ 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
|
||||
if Bundle.main.isDebugBuild {
|
||||
Section {
|
||||
Toggle("Safari Reader Mode", isOn: $viewModel.enableReaderMode)
|
||||
Toggle("Automatically mark articles as read", isOn: $viewModel.autoMarkAsRead)
|
||||
@ -73,7 +73,7 @@ struct SettingsGeneralView: View {
|
||||
}
|
||||
}
|
||||
}
|
||||
#endif
|
||||
}
|
||||
}
|
||||
.sheet(isPresented: $showReleaseNotes) {
|
||||
ReleaseNotesView()
|
||||
|
||||
@ -16,7 +16,7 @@ struct SyncSettingsView: View {
|
||||
|
||||
var body: some View {
|
||||
Group {
|
||||
#if DEBUG
|
||||
if Bundle.main.isDebugBuild {
|
||||
Section {
|
||||
Toggle("Automatic sync", isOn: $viewModel.autoSyncEnabled)
|
||||
if viewModel.autoSyncEnabled {
|
||||
@ -46,7 +46,7 @@ struct SyncSettingsView: View {
|
||||
}
|
||||
}
|
||||
}
|
||||
#endif
|
||||
}
|
||||
}
|
||||
.task {
|
||||
await viewModel.loadGeneralSettings()
|
||||
|
||||
@ -54,7 +54,7 @@ class LogConfiguration: ObservableObject {
|
||||
|
||||
private init() {
|
||||
// First time setup: Enable logging in DEBUG builds with sensible defaults
|
||||
#if DEBUG
|
||||
if Bundle.main.isDebugBuild {
|
||||
if UserDefaults.standard.object(forKey: "LogConfigurationInitialized") == nil {
|
||||
isLoggingEnabled = true
|
||||
showPerformanceLogs = true
|
||||
@ -64,7 +64,7 @@ class LogConfiguration: ObservableObject {
|
||||
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
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user