diff --git a/Shared/Common/Resources/BaseConstants.swift b/Shared/Common/Resources/BaseConstants.swift index 6fec3d7a5..313e9d483 100644 --- a/Shared/Common/Resources/BaseConstants.swift +++ b/Shared/Common/Resources/BaseConstants.swift @@ -10,6 +10,7 @@ public let isRunningTest = NSClassFromString("XCTestCase") != nil class Constant { class app { static let group = "group.org.mozilla.ios.Lockbox" + static let syncTimeout: Double = 20 } struct fxa { @@ -72,6 +73,7 @@ class Constant { static let usernamePlaceholder = NSLocalizedString("username_placeholder", value: "(no username)", comment: "Placeholder text when there is no username. String should include appropriate open/close parenthetical or similar symbols to indicate this is a placeholder, not a real username.") static let searchYourEntries = NSLocalizedString("search.placeholder", value: "Search logins", comment: "Placeholder text for search field") static let emptyListPlaceholder = NSLocalizedString("list.empty", value: "%@ lets you access passwords you’ve already saved to Firefox. To view your logins here, you’ll need to sign in and sync with Firefox.", comment: "Label shown when there are no logins to list. %@ will be replaced with the application name") + static let syncTimedOut = NSLocalizedString("sync.timeout", value: "Sync timed out", comment: "This is the message displayed when syncing entries from the server times out") } } diff --git a/Shared/Store/BaseDataStore.swift b/Shared/Store/BaseDataStore.swift index 70b06003b..45381e454 100644 --- a/Shared/Store/BaseDataStore.swift +++ b/Shared/Store/BaseDataStore.swift @@ -24,7 +24,7 @@ enum SyncError: Error { } enum SyncState: Equatable { - case Syncing, Synced + case Syncing, Synced, TimedOut public static func ==(lhs: SyncState, rhs: SyncState) -> Bool { switch (lhs, rhs) { @@ -32,6 +32,8 @@ enum SyncState: Equatable { return true case (Synced, Synced): return true + case (TimedOut, TimedOut): + return true default: return false } @@ -71,7 +73,7 @@ class BaseDataStore { internal var disposeBag = DisposeBag() private var listSubject = BehaviorRelay<[LoginRecord]>(value: []) - private var syncSubject = ReplaySubject.create(bufferSize: 1) + private var syncSubject = BehaviorRelay(value: .Synced) private var storageStateSubject = ReplaySubject.create(bufferSize: 1) private let dispatcher: Dispatcher @@ -229,13 +231,21 @@ extension BaseDataStore { else { return } if (networkStore.isConnectedToNetwork) { - self.syncSubject.onNext(SyncState.Syncing) + self.syncSubject.accept(SyncState.Syncing) } else { - self.syncSubject.onNext(SyncState.Synced) + self.syncSubject.accept(SyncState.Synced) return } - + queue.async { + self.queue.asyncAfter(deadline: .now() + Constant.app.syncTimeout, execute: { + // this block serves to "cancel" the sync if the operation is running slowly + if (self.syncSubject.value != .Synced) { + self.syncSubject.accept(.TimedOut) + self.dispatcher.dispatch(action: SentryAction(title: "Sync timeout without error", error: nil, function: "", line: "")) + } + }) + do { try self.loginsStorage?.sync(unlockInfo: syncInfo) } catch let error as LoginStoreError { @@ -243,7 +253,7 @@ extension BaseDataStore { } catch let error { NSLog("Unknown error syncing: \(error)") } - self.syncSubject.onNext(SyncState.Synced) + self.syncSubject.accept(SyncState.Synced) } } diff --git a/lockbox-ios/Action/SentryAction.swift b/lockbox-ios/Action/SentryAction.swift index aeb3d021d..588ec4089 100644 --- a/lockbox-ios/Action/SentryAction.swift +++ b/lockbox-ios/Action/SentryAction.swift @@ -6,7 +6,7 @@ import Foundation struct SentryAction: Action { let title: String - let error: Error + let error: Error? let function: String let line: String } diff --git a/lockbox-ios/Presenter/ItemListPresenter.swift b/lockbox-ios/Presenter/ItemListPresenter.swift index 88fec4cb6..085626730 100644 --- a/lockbox-ios/Presenter/ItemListPresenter.swift +++ b/lockbox-ios/Presenter/ItemListPresenter.swift @@ -7,7 +7,7 @@ import RxSwift import RxCocoa import RxDataSources -protocol ItemListViewProtocol: AlertControllerView, SpinnerAlertView, BaseItemListViewProtocol { +protocol ItemListViewProtocol: AlertControllerView, StatusAlertView, SpinnerAlertView, BaseItemListViewProtocol { func bind(sortingButtonTitle: Driver) func bind(scrollAction: Driver) var sortingButtonEnabled: AnyObserver? { get } @@ -174,7 +174,7 @@ extension ItemListPresenter { fileprivate func setupSpinnerDisplay() { // when this observable emits an event, the spinner gets dismissed let hideSpinnerObservable = self.dataStore.syncState - .filter { $0 == SyncState.Synced } + .filter { $0 == SyncState.Synced || $0 == SyncState.TimedOut } .map { _ in return () } .asDriver(onErrorJustReturn: ()) @@ -194,6 +194,15 @@ extension ItemListPresenter { } }) .disposed(by: self.disposeBag) + + self.dataStore.syncState + .filter { $0 == SyncState.TimedOut } + .map { _ in () } + .asDriver(onErrorJustReturn: () ) + .drive(onNext: { _ in + self.view?.displayTemporaryAlert(Constant.string.syncTimedOut, timeout: 5) + }) + .disposed(by: self.disposeBag) } } diff --git a/lockbox-ios/Store/SentryStore.swift b/lockbox-ios/Store/SentryStore.swift index ceb384300..235d0cae6 100644 --- a/lockbox-ios/Store/SentryStore.swift +++ b/lockbox-ios/Store/SentryStore.swift @@ -18,7 +18,7 @@ class SentryStore { .subscribe(onNext: { (action) in Client.shared?.reportUserException( action.title, - reason: action.error.localizedDescription, + reason: action.error?.localizedDescription ?? "", language: NSLocale.preferredLanguages.first ?? "", lineOfCode: action.line, stackTrace: Thread.callStackSymbols, diff --git a/lockbox-iosTests/BaseDataStoreSpec.swift b/lockbox-iosTests/BaseDataStoreSpec.swift index bb1afdd91..adb6187d8 100644 --- a/lockbox-iosTests/BaseDataStoreSpec.swift +++ b/lockbox-iosTests/BaseDataStoreSpec.swift @@ -388,7 +388,7 @@ class BaseDataStoreSpec: QuickSpec { let syncStates: [SyncState] = self.syncObserver.events.map { $0.value.element! } - expect(syncStates).to(equal([SyncState.Synced, SyncState.Syncing, SyncState.Synced])) + expect(syncStates).to(equal([SyncState.Synced, SyncState.Synced, SyncState.Syncing, SyncState.Synced])) } } diff --git a/lockbox-iosTests/ItemListPresenterSpec.swift b/lockbox-iosTests/ItemListPresenterSpec.swift index c95a2da20..650cba0d7 100644 --- a/lockbox-iosTests/ItemListPresenterSpec.swift +++ b/lockbox-iosTests/ItemListPresenterSpec.swift @@ -31,10 +31,10 @@ class ItemListPresenterSpec: QuickSpec { var fakeOnSettingsPressed = PublishSubject() var fakeOnSortingButtonPressed = PublishSubject() var setFilterEnabledValue: Bool? - var displayOptionSheetButtons: [AlertActionButtonConfiguration]? - var displayOptionSheetTitle: String? + var temporaryAlertArgument: String? + var displayOptionSheetTitle: String? func bind(items: Driver<[ItemSectionModel]>) { items.drive(itemsObserver).disposed(by: self.disposeBag) } @@ -53,6 +53,10 @@ class ItemListPresenterSpec: QuickSpec { self.displayOptionSheetTitle = title } + func displayTemporaryAlert(_ message: String, timeout: TimeInterval) { + self.temporaryAlertArgument = message + } + func dismissKeyboard() { self.dismissKeyboardCalled = true } @@ -284,6 +288,16 @@ class ItemListPresenterSpec: QuickSpec { } } } + + describe("if the sync times out") { + beforeEach { + self.dataStore.syncStateStub.onNext(SyncState.TimedOut) + } + + it("displays a temporary alert for the user") { + expect(self.view.temporaryAlertArgument).to(equal(Constant.string.syncTimedOut)) + } + } } describe("manual sync") {