diff --git a/Sources/App/DependencyRegistrator.swift b/Sources/App/DependencyRegistrator.swift index 211cc7c4d9a65ba097a7e4e0f445d160555d126a..1217667eec9b64be80a4d8fd39e9c28b17c19651 100644 --- a/Sources/App/DependencyRegistrator.swift +++ b/Sources/App/DependencyRegistrator.swift @@ -29,6 +29,7 @@ import ChatFeature import MenuFeature import TermsFeature import BackupFeature +import DrawerFeature import SearchFeature import LaunchFeature import RestoreFeature @@ -62,8 +63,8 @@ struct DependencyRegistrator { static func registerForMock() { container.register(XXLogger.noop) + container.register(VersionCheck.mock) container.register(CrashReporter.noop) - container.register(VersionChecker.mock) container.register(ReportingStatus.mock()) container.register(SendReport.mock()) container.register(MockNetworkMonitor() as NetworkMonitoring) @@ -86,8 +87,8 @@ struct DependencyRegistrator { container.register(KeyObjectStore.userDefaults) container.register(XXLogger.live()) + container.register(VersionCheck.live) container.register(CrashReporter.live) - container.register(VersionChecker.live()) container.register(ReportingStatus.live()) container.register(SendReport.live) @@ -110,6 +111,8 @@ struct DependencyRegistrator { PopToNavigator(), SetStackNavigator(), + OpenUpNavigator(), + PresentOnboardingStartNavigator( screen: OnboardingStartController.init, navigationController: { navController } @@ -161,6 +164,10 @@ struct DependencyRegistrator { PresentOnboardingCodeNavigator( screen: OnboardingCodeController.init(_:_:_:), navigationController: { navController } + ), + PresentDrawerNavigator( + screen: DrawerController.init(_:), + navigationController: { navController } ) // searchFactory: SearchContainerController.init, // restoreListFactory: RestoreListController.init, diff --git a/Sources/BackupFeature/Controllers/BackupConfigController.swift b/Sources/BackupFeature/Controllers/BackupConfigController.swift index 18848479c0193b3f62b2a00e5fc10dead562c015..9fd0b2735bab16831a77ee46d13c186b07c65b0a 100644 --- a/Sources/BackupFeature/Controllers/BackupConfigController.swift +++ b/Sources/BackupFeature/Controllers/BackupConfigController.swift @@ -231,7 +231,7 @@ final class BackupConfigController: UIViewController { spacingAfter: 40 ) - let drawer = DrawerController(with: [ + let drawer = DrawerController([ DrawerText( font: Fonts.Mulish.extraBold.font(size: 28.0), text: Localized.Backup.Config.infrastructure, @@ -290,7 +290,7 @@ final class BackupConfigController: UIViewController { spacingAfter: 40 ) - let drawer = DrawerController(with: [ + let drawer = DrawerController([ DrawerText( font: Fonts.Mulish.extraBold.font(size: 28.0), text: Localized.Backup.Config.frequency(serviceName), diff --git a/Sources/ChatFeature/Controllers/GroupChatController.swift b/Sources/ChatFeature/Controllers/GroupChatController.swift index 31a65eaffd081f19e397da2ace17604ec8c3a9a2..e101c03535310fcdbfab685901633919921e6bb6 100644 --- a/Sources/ChatFeature/Controllers/GroupChatController.swift +++ b/Sources/ChatFeature/Controllers/GroupChatController.swift @@ -264,7 +264,7 @@ public final class GroupChatController: UIViewController { style: .brandColored )) - let drawer = DrawerController(with: [text, button]) + let drawer = DrawerController([text, button]) button.action .receive(on: DispatchQueue.main) diff --git a/Sources/ChatFeature/Controllers/SingleChatController.swift b/Sources/ChatFeature/Controllers/SingleChatController.swift index 708b9d5043d22ad2df980e19a346c42ba17a0ac2..9f2f7f8227bbf3696cedc3916b1ea1c6f582ab12 100644 --- a/Sources/ChatFeature/Controllers/SingleChatController.swift +++ b/Sources/ChatFeature/Controllers/SingleChatController.swift @@ -373,7 +373,7 @@ public final class SingleChatController: UIViewController { style: .brandColored )) - let drawer = DrawerController(with: [text, button]) + let drawer = DrawerController([text, button]) button.action .receive(on: DispatchQueue.main) @@ -406,7 +406,7 @@ public final class SingleChatController: UIViewController { cancelButton.setStyle(.seeThrough) cancelButton.setTitle(Localized.Chat.Clear.cancel, for: .normal) - let drawer = DrawerController(with: [ + let drawer = DrawerController([ DrawerImage( image: Asset.drawerNegative.image ), diff --git a/Sources/ChatListFeature/Controller/ChatListTableController.swift b/Sources/ChatListFeature/Controller/ChatListTableController.swift index b67c29da977c59eeb16428f251481ef8c0b2674b..ed8bd0e9bcf323ee00ba46155ad2e5fc417ac626 100644 --- a/Sources/ChatListFeature/Controller/ChatListTableController.swift +++ b/Sources/ChatListFeature/Controller/ChatListTableController.swift @@ -174,7 +174,7 @@ extension ChatListTableController { let actionButton = DrawerCapsuleButton(model: .init(title: actionTitle, style: .red)) - let drawer = DrawerController(with: [ + let drawer = DrawerController([ DrawerText( font: Fonts.Mulish.bold.font(size: 26.0), text: title, diff --git a/Sources/ContactFeature/Controllers/ContactController.swift b/Sources/ContactFeature/Controllers/ContactController.swift index 0fbb22e4b0620b4f99c4ecd656d1e2fc09d10977..87a3f073ad467c00a5e3797cdcc190cb5cd52443 100644 --- a/Sources/ContactFeature/Controllers/ContactController.swift +++ b/Sources/ContactFeature/Controllers/ContactController.swift @@ -277,7 +277,7 @@ public final class ContactController: UIViewController { cancelButton.setStyle(.seeThrough) cancelButton.setTitle(Localized.Contact.Clear.cancel, for: .normal) - let drawer = DrawerController(with: [ + let drawer = DrawerController([ DrawerImage( image: Asset.drawerNegative.image ), @@ -360,7 +360,7 @@ extension ContactController { title: Localized.Settings.InfoDrawer.action ) - let drawer = DrawerController(with: [ + let drawer = DrawerController([ DrawerText( font: Fonts.Mulish.bold.font(size: 26.0), text: title, @@ -397,7 +397,7 @@ extension ContactController { style: .red )) - let drawer = DrawerController(with: [ + let drawer = DrawerController([ DrawerText( font: Fonts.Mulish.bold.font(size: 26.0), text: Localized.Contact.Delete.Drawer.title, diff --git a/Sources/DrawerFeature/DrawerController.swift b/Sources/DrawerFeature/DrawerController.swift index de613530cd8fc92b150c1d56af1a110ad767c0eb..a51fe87a53aa1f385e85460234dfa7ae92f913c3 100644 --- a/Sources/DrawerFeature/DrawerController.swift +++ b/Sources/DrawerFeature/DrawerController.swift @@ -6,8 +6,8 @@ public final class DrawerController: UIViewController { private let content: [DrawerItem] public var cancellables = Set<AnyCancellable>() - public init(with content: [DrawerItem]) { - self.content = content + public init(_ items: [DrawerItem]) { + self.content = items super.init(nibName: nil, bundle: nil) let views = content.map { $0.makeView() } diff --git a/Sources/LaunchFeature/LaunchController.swift b/Sources/LaunchFeature/LaunchController.swift index fbcd6044ff3888c8d3ceb1109f6b3f13ab3aa756..2eb224ca240c4dbb8e1f55c0ba4003c8d28eeb1b 100644 --- a/Sources/LaunchFeature/LaunchController.swift +++ b/Sources/LaunchFeature/LaunchController.swift @@ -3,17 +3,18 @@ import Shared import Combine import Navigation import PushFeature +import XXNavigation +import DrawerFeature import DependencyInjection public final class LaunchController: UIViewController { @Dependency var navigator: Navigator - // TO REMOVE: - public var pendingPushRoute: PushRouter.Route? - private let viewModel = LaunchViewModel() private lazy var screenView = LaunchView() + public var pendingPushRoute: PushRouter.Route? private var cancellables = Set<AnyCancellable>() + private var drawerCancellables = Set<AnyCancellable>() public override func viewDidAppear(_ animated: Bool) { super.viewDidAppear(animated) @@ -38,75 +39,118 @@ public final class LaunchController: UIViewController { UIColor(red: 63/255, green: 186/255, blue: 253/255, alpha: 1).cgColor, UIColor(red: 98/255, green: 163/255, blue: 255/255, alpha: 1).cgColor ] - gradient.frame = screenView.bounds gradient.startPoint = CGPoint(x: 1, y: 0) gradient.endPoint = CGPoint(x: 0, y: 1) screenView.layer.insertSublayer(gradient, at: 0) } - private func offerUpdate(model: Update) { - let drawerView = UIView() - drawerView.backgroundColor = Asset.neutralSecondary.color - drawerView.layer.cornerRadius = 5 - - let vStack = UIStackView() - vStack.axis = .vertical - vStack.spacing = 10 - drawerView.addSubview(vStack) + public override func viewDidLoad() { + super.viewDidLoad() + + viewModel + .statePublisher + .receive(on: DispatchQueue.main) + .sink { [unowned self] in + guard $0.shouldPushChats == false else { + guard $0.shouldShowTerms == false else { + navigator.perform(PresentTermsAndConditions(popAllowed: false)) + return + } + if let route = pendingPushRoute { + hasPendingPushRoute(route) + return + } + navigator.perform(PresentChatList()) + return + } + guard $0.shouldPushOnboarding == false else { + navigator.perform(PresentOnboardingStart()) + return + } + if let update = $0.shouldOfferUpdate { + offerUpdate(model: update) + } + }.store(in: &cancellables) + } - vStack.snp.makeConstraints { - $0.top.equalToSuperview().offset(18) - $0.left.equalToSuperview().offset(18) - $0.right.equalToSuperview().offset(-18) - $0.bottom.equalToSuperview().offset(-18) + private func hasPendingPushRoute(_ route: PushRouter.Route) { + switch route { + case .requests: + navigator.perform(PresentRequests()) + case .search(username: let username): + navigator.perform(PresentSearch(searching: username)) + case .groupChat(id: let groupId): + if let info = viewModel.getGroupInfoWith(groupId: groupId) { + navigator.perform(PresentGroupChat(model: info)) + return + } + navigator.perform(PresentChatList()) + case .contactChat(id: let userId): + if let model = viewModel.getContactWith(userId: userId) { + navigator.perform(PresentChat(contact: model)) + return + } + navigator.perform(PresentChatList()) } + } - let title = UILabel() - title.text = "App Update" - title.textAlignment = .center - title.textColor = Asset.neutralDark.color - - let body = UILabel() - body.numberOfLines = 0 - body.textAlignment = .center - body.textColor = Asset.neutralDark.color - - let update = CapsuleButton() - update.publisher(for: .touchUpInside) - .sink { UIApplication.shared.open(.init(string: model.urlString)!, options: [:]) } - .store(in: &cancellables) - - vStack.addArrangedSubview(title) - vStack.addArrangedSubview(body) - vStack.addArrangedSubview(update) - - body.text = model.content - update.set( - style: model.actionStyle, + private func offerUpdate(model: LaunchViewModel.UpdateModel) { + let updateButton = CapsuleButton() + updateButton.set( + style: .brandColored, title: model.positiveActionTitle ) + let notNowButton = CapsuleButton() + if let negativeTitle = model.negativeActionTitle { + notNowButton.set( + style: .red, + title: negativeTitle + ) + } + updateButton + .publisher(for: .touchUpInside) + .receive(on: DispatchQueue.main) + .sink { [unowned self] in + navigator.perform(DismissModal(from: self)) { + self.drawerCancellables.removeAll() + UIApplication.shared.open(.init(string: model.urlString)!) + } + }.store(in: &drawerCancellables) + + notNowButton + .publisher(for: .touchUpInside) + .receive(on: DispatchQueue.main) + .sink { [unowned self] in + navigator.perform(DismissModal(from: self)) { + self.viewModel.didRefuseUpdating() + } + }.store(in: &drawerCancellables) + + var actions: [UIView] = [updateButton] + if model.negativeActionTitle != nil { + actions.append(notNowButton) + } - // if let negativeTitle = model.negativeActionTitle { - // let negativeButton = CapsuleButton() - // negativeButton.set(style: .simplestColoredRed, title: negativeTitle) - // - // negativeButton.publisher(for: .touchUpInside) - // .sink { [unowned self] in - // blocker.hideWindow() - // viewModel.continueWithInitialization() - // }.store(in: &cancellables) - // - // vStack.addArrangedSubview(negativeButton) - // } - // - // blocker.window?.addSubview(drawerView) - // drawerView.snp.makeConstraints { - // $0.left.equalToSuperview().offset(18) - // $0.center.equalToSuperview() - // $0.right.equalToSuperview().offset(-18) - // } - // - // blocker.showWindow() + navigator.perform(PresentDrawer(items: [ + DrawerText( + font: Fonts.Mulish.bold.font(size: 26.0), + text: "App Update", + color: Asset.neutralActive.color, + alignment: .center, + spacingAfter: 19 + ), + DrawerText( + font: Fonts.Mulish.regular.font(size: 16.0), + text: model.content, + color: Asset.neutralBody.color, + alignment: .center, + spacingAfter: 19 + ), + DrawerStack( + axis: .vertical, + views: actions + ) + ], dismissable: false)) } } diff --git a/Sources/LaunchFeature/LaunchViewModel+Banned.swift b/Sources/LaunchFeature/LaunchViewModel+Banned.swift index d62c2c3d70fc34379fe00cca21dbf84e11cc79d4..1449fb675e7e190cc257785b55f43517f39ac518 100644 --- a/Sources/LaunchFeature/LaunchViewModel+Banned.swift +++ b/Sources/LaunchFeature/LaunchViewModel+Banned.swift @@ -43,7 +43,6 @@ extension LaunchViewModel { DispatchQueue.main.asyncAfter(deadline: .now() + 2) { self.updateBannedList(completion: completion) } - case .success(_): completion() } diff --git a/Sources/LaunchFeature/LaunchViewModel+Database.swift b/Sources/LaunchFeature/LaunchViewModel+Database.swift index 62fa45aafb2c7d73cc7efa2a0fb834938e889f8d..15a88e815fe9df6bc52f5adc15622e1a90ea4cc0 100644 --- a/Sources/LaunchFeature/LaunchViewModel+Database.swift +++ b/Sources/LaunchFeature/LaunchViewModel+Database.swift @@ -47,22 +47,20 @@ extension LaunchViewModel { _ = try? database.bulkUpdateContacts(.init(authStatus: [.verificationInProgress]), .init(authStatus: .verificationFailed)) } + func getContactWith(userId: Data) -> XXModels.Contact? { + let query = Contact.Query( + id: [userId], + isBlocked: reportingStatus.isEnabled() ? false : nil, + isBanned: reportingStatus.isEnabled() ? false : nil + ) - // func getContactWith(userId: Data) -> XXModels.Contact? { - // let query = Contact.Query( - // id: [userId], - // isBlocked: reportingStatus.isEnabled() ? false : nil, - // isBanned: reportingStatus.isEnabled() ? false : nil - // ) - // - // guard let database: Database = try? DependencyInjection.Container.shared.resolve(), - // let contact = try? database.fetchContacts(query).first else { - // return nil - // } - // - // return contact - // } + guard let database: Database = try? DependencyInjection.Container.shared.resolve(), + let contact = try? database.fetchContacts(query).first else { + return nil + } + return contact + } func getGroupInfoWith(groupId: Data) -> GroupInfo? { let query = GroupInfo.Query(groupId: groupId) diff --git a/Sources/LaunchFeature/LaunchViewModel+Errors.swift b/Sources/LaunchFeature/LaunchViewModel+Errors.swift new file mode 100644 index 0000000000000000000000000000000000000000..399667657243523475a2857e9838451a0198d587 --- /dev/null +++ b/Sources/LaunchFeature/LaunchViewModel+Errors.swift @@ -0,0 +1,42 @@ +import XXClient +import Foundation + +extension LaunchViewModel { + func updateErrors( + completion: @escaping (Result<Void, Error>) -> Void + ) { + let url = "https://git.xx.network/elixxir/client-error-database/-/raw/main/clientErrors.json" + downloadErrors(from: url) { + switch $0 { + case .success(let string): + do { + try UpdateCommonErrors.live(jsonFile: string) + completion(.success(())) + } catch { + completion(.failure(error)) + } + case .failure(let error): + completion(.failure(error)) + } + } + } + + func downloadErrors( + from urlString: String, + completion: @escaping (Result<String, Error>) -> Void + ) { + URLSession.shared.dataTask(with: URL(string: urlString)!) { data, _, error in + if let error { + completion(.failure(error)) + return + } + guard let data else { + fatalError("No errors or data when downloading \(urlString)") + } + guard let string = String(data: data, encoding: .utf8) else { + fatalError("Impossible to decode error json") + } + completion(.success(string)) + }.resume() + } +} diff --git a/Sources/LaunchFeature/LaunchViewModel+Messenger.swift b/Sources/LaunchFeature/LaunchViewModel+Messenger.swift index f5ed8ee1af24b5fe4793f2b337fe979da126debe..2971fe40e65127804228af2c8b0a06df90ea3311 100644 --- a/Sources/LaunchFeature/LaunchViewModel+Messenger.swift +++ b/Sources/LaunchFeature/LaunchViewModel+Messenger.swift @@ -1,3 +1,4 @@ +import Shared import XXClient import XXModels import XXLogger @@ -393,4 +394,52 @@ extension LaunchViewModel { DependencyInjection.Container.shared.register(manager) try! manager.setStatus(dummyTrafficOn) } + + func setupMessenger() throws { + setupLogWriter() + setupAuthCallback() + setupBackupCallback() + setupMessageCallback() + + if messenger.isLoaded() == false { + if messenger.isCreated() == false { + try messenger.create() + } + + try messenger.load() + } + + try messenger.start() + + if messenger.isConnected() == false { + try messenger.connect() + try messenger.listenForMessages() + } + + try generateGroupManager() + try generateTrafficManager() + try generateTransferManager() + listenToNetworkUpdates() + + if messenger.isLoggedIn() == false { + if try messenger.isRegistered() { + try messenger.logIn() + hudController.dismiss() + stateSubject.value.shouldPushChats = true + } else { + try? sftpManager.unlink() + try? dropboxManager.unlink() + hudController.dismiss() + stateSubject.value.shouldPushOnboarding = true + } + } else { + hudController.dismiss() + stateSubject.value.shouldPushChats = true + } + if !messenger.isBackupRunning() { + try? messenger.resumeBackup() + } + // TODO: Biometric auth + + } } diff --git a/Sources/LaunchFeature/LaunchViewModel+VersionCheck.swift b/Sources/LaunchFeature/LaunchViewModel+VersionCheck.swift deleted file mode 100644 index 4496c75f3f4198ce391e1a5621d0de54127c6184..0000000000000000000000000000000000000000 --- a/Sources/LaunchFeature/LaunchViewModel+VersionCheck.swift +++ /dev/null @@ -1,33 +0,0 @@ -import Shared -import VersionChecking - -extension LaunchViewModel { - func versionFailed(error: Error) { - hudController.show(.init( - title: Localized.Launch.Version.failed, - content: error.localizedDescription - )) - } - - func versionUpdateRequired(_ info: DappVersionInformation) { - hudController.dismiss() - routeSubject.send(.update(Update( - content: info.minimumMessage, - urlString: info.appUrl, - positiveActionTitle: Localized.Launch.Version.Required.positive, - negativeActionTitle: nil, - actionStyle: .brandColored - ))) - } - - func versionUpdateRecommended(_ info: DappVersionInformation) { - hudController.dismiss() - routeSubject.send(.update(Update( - content: Localized.Launch.Version.Recommended.title, - urlString: info.appUrl, - positiveActionTitle: Localized.Launch.Version.Recommended.positive, - negativeActionTitle: Localized.Launch.Version.Recommended.negative, - actionStyle: .simplestColoredRed - ))) - } -} diff --git a/Sources/LaunchFeature/LaunchViewModel.swift b/Sources/LaunchFeature/LaunchViewModel.swift index 3639e1d1d8a7a31c755f20350dc83469446a40f6..74d77143687839378a29cc4677c7666dddced9d5 100644 --- a/Sources/LaunchFeature/LaunchViewModel.swift +++ b/Sources/LaunchFeature/LaunchViewModel.swift @@ -2,49 +2,43 @@ import Shared import Combine import Defaults import XXModels -import XXLogger import Keychain +import XXClient +import CloudFiles import Foundation import Permissions import BackupFeature +import NetworkMonitor import VersionChecking import ReportingFeature import CombineSchedulers +import CloudFilesDropbox +import XXMessengerClient import DependencyInjection -import XXClient -import struct XXClient.FileTransfer import class XXClient.Cancellable -import XXDatabase -import XXLegacyDatabaseMigrator -import XXMessengerClient -import NetworkMonitor - -import CloudFiles -import CloudFilesSFTP -import CloudFilesDropbox - -struct Update { - let content: String - let urlString: String - let positiveActionTitle: String - let negativeActionTitle: String? - let actionStyle: CapsuleButtonStyle -} +final class LaunchViewModel { + struct UpdateModel { + let content: String + let urlString: String + let positiveActionTitle: String + let negativeActionTitle: String? + let actionStyle: CapsuleButtonStyle + } -enum LaunchRoute { - case chats - case update(Update) - case onboarding -} + struct ViewState { + var shouldShowTerms = false + var shouldPushChats = false + var shouldOfferUpdate: UpdateModel? + var shouldPushOnboarding = false + } -final class LaunchViewModel { @Dependency var database: Database @Dependency var messenger: Messenger + @Dependency var versionCheck: VersionCheck @Dependency var hudController: HUDController @Dependency var backupService: BackupService - @Dependency var versionChecker: VersionChecker @Dependency var fetchBannedList: FetchBannedList @Dependency var reportingStatus: ReportingStatus @Dependency var toastController: ToastController @@ -63,125 +57,108 @@ final class LaunchViewModel { var networkCallbacksCancellable: Cancellable? var messageListenerCallbacksCancellable: Cancellable? - var routePublisher: AnyPublisher<LaunchRoute, Never> { - routeSubject.eraseToAnyPublisher() + var statePublisher: AnyPublisher<ViewState, Never> { + stateSubject.eraseToAnyPublisher() } private var scheduler: AnySchedulerOf<DispatchQueue> = { DispatchQueue.global().eraseToAnyScheduler() }() - private let dropboxManager = CloudFilesManager.dropbox( + let dropboxManager = CloudFilesManager.dropbox( appKey: "ppx0de5f16p9aq2", path: "/backup/backup.xxm" ) - private let sftpManager = CloudFilesManager.sftp( + let sftpManager = CloudFilesManager.sftp( host: "", username: "", password: "", fileName: "" ) - var cancellables = Set<AnyCancellable>() - let routeSubject = PassthroughSubject<LaunchRoute, Never>() + let stateSubject = CurrentValueSubject <ViewState, Never>(.init()) func viewDidAppear() { scheduler.schedule(after: .init(.now() + 1)) { [weak self] in guard let self else { return } - self.hudController.show() - self.versionChecker() - .sink { [unowned self] in - switch $0 { - case .upToDate: - self.updateBannedList { - self.updateErrors { - self.continueWithInitialization() - } - } - case .failure(let error): - self.versionFailed(error: error) - case .updateRequired(let info): - self.versionUpdateRequired(info) - case .updateRecommended(let info): - self.versionUpdateRecommended(info) - } - }.store(in: &self.cancellables) + self.startLaunch() } } - func continueWithInitialization() { - do { - try self.setupDatabase() - - setupLogWriter() - setupAuthCallback() - setupBackupCallback() - setupMessageCallback() - - if messenger.isLoaded() == false { - if messenger.isCreated() == false { - try messenger.create() - } - - try messenger.load() - } - - try messenger.start() - - if messenger.isConnected() == false { - try messenger.connect() - try messenger.listenForMessages() + private func startLaunch() { + if !didAcceptTerms { + stateSubject.value.shouldShowTerms = true + } + hudController.show() + versionCheck.verify { [weak self] in + guard let self else { return } + switch $0 { + case .upToDate: + self.didVerifyVersion() + case .failure(let error): + self.hudController.show(.init( + title: Localized.Launch.Version.failed, + content: error.localizedDescription + )) + case .outdated(let info): + self.hudController.dismiss() + let isRequired = info.isRequired ?? false + + let content = isRequired ? + info.minimumMessage : + Localized.Launch.Version.Recommended.title + + let positiveActionTitle = isRequired ? + Localized.Launch.Version.Required.positive : + Localized.Launch.Version.Recommended.positive + + self.stateSubject.value.shouldOfferUpdate = .init( + content: content, + urlString: info.appUrl, + positiveActionTitle: positiveActionTitle, + negativeActionTitle: isRequired ? nil : Localized.Launch.Version.Recommended.negative, + actionStyle: isRequired ? .brandColored : .simplestColoredRed + ) } + } + } - try generateGroupManager() - try generateTrafficManager() - try generateTransferManager() - listenToNetworkUpdates() + func didRefuseUpdating() { + hudController.show() + didVerifyVersion() + } - if messenger.isLoggedIn() == false { - if try messenger.isRegistered() { - try messenger.logIn() - hudController.dismiss() - routeSubject.send(.chats) - } else { - try? sftpManager.unlink() - try? dropboxManager.unlink() - hudController.dismiss() - routeSubject.send(.onboarding) + private func didVerifyVersion() { + updateBannedList { [weak self] in + guard let self else { return } + self.updateErrors { + switch $0 { + case .success: + self.didFinishAsyncWork() + case .failure(let error): + self.hudController.show(.init(error: error)) } - } else { - hudController.dismiss() - routeSubject.send(.chats) } - if !messenger.isBackupRunning() { - try? messenger.resumeBackup() - } - // TODO: Biometric auth + } + } + private func didFinishAsyncWork() { + do { + try setupDatabase() + try setupMessenger() } catch { let xxError = CreateUserFriendlyErrorMessage.live(error.localizedDescription) hudController.show(.init(content: xxError)) } } - private func cleanUp() { - // try? cMixManager.remove() - // try? keychainHandler.clear() - } - - private func presentOnboardingFlow() { - hudController.dismiss() - routeSubject.send(.onboarding) - } - private func checkBiometrics(completion: @escaping (Result<Bool, Error>) -> Void) { if permissionHandler.isBiometricsAvailable && isBiometricsOn { permissionHandler.requestBiometrics { switch $0 { case .success(let granted): completion(.success(granted)) - case .failure(let error): completion(.failure(error)) } @@ -190,76 +167,4 @@ final class LaunchViewModel { completion(.success(true)) } } - - private func updateErrors(completion: @escaping () -> Void) { - let errorsURLString = "https://git.xx.network/elixxir/client-error-database/-/raw/main/clientErrors.json" - - URLSession.shared.dataTask(with: URL(string: errorsURLString)!) { [weak self] data, _, error in - guard let self else { return } - - guard error == nil else { - print(">>> Issue when trying to download errors json: \(error!.localizedDescription)") - self.updateErrors(completion: completion) - return - } - - guard let data = data, let json = String(data: data, encoding: .utf8) else { - print(">>> Issue when trying to unwrap errors json") - return - } - - do { - try UpdateCommonErrors.live(jsonFile: json) - completion() - } catch { - print(">>> Issue when trying to update common errors: \(error.localizedDescription)") - } - }.resume() - } } - -// viewModel.routePublisher -// .receive(on: DispatchQueue.main) -// .sink { [unowned self] in -// switch $0 { -// case .chats: -// guard didAcceptTerms == true else { -// navigator.perform(PresentTermsAndConditions(popAllowed: false)) -// return -// } -// -// if let pushRoute = pendingPushRoute { -// switch pushRoute { -// case .requests: -// navigator.perform(PresentRequests()) -// -// case .search(username: let username): -// navigator.perform(PresentSearch(searching: username)) -// -// case .groupChat(id: let groupId): -// if let info = viewModel.getGroupInfoWith(groupId: groupId) { -// navigator.perform(PresentGroupChat(model: info)) -// return -// } -// navigator.perform(PresentChatList()) -// -// case .contactChat(id: let userId): -// if let model = viewModel.getContactWith(userId: userId) { -// navigator.perform(PresentChat(contact: model)) -// return -// } -// navigator.perform(PresentChatList()) -// } -// -// return -// } -// -// navigator.perform(PresentChatList()) -// -// case .onboarding: -// navigator.perform(PresentOnboardingStart()) -// -// case .update(let model): -// offerUpdate(model: model) -// } -// }.store(in: &cancellables) diff --git a/Sources/MenuFeature/Controllers/MenuController.swift b/Sources/MenuFeature/Controllers/MenuController.swift index 22cee48831683ba0ba47619f640dfd6990b2310c..dbd587248752db7580356c9f40a79541110867f3 100644 --- a/Sources/MenuFeature/Controllers/MenuController.swift +++ b/Sources/MenuFeature/Controllers/MenuController.swift @@ -201,7 +201,7 @@ public final class MenuController: UIViewController { style: .red )) - let drawer = DrawerController(with: [ + let drawer = DrawerController([ DrawerText( font: Fonts.Mulish.bold.font(size: 26.0), text: title, diff --git a/Sources/OnboardingFeature/Controllers/OnboardingCodeController.swift b/Sources/OnboardingFeature/Controllers/OnboardingCodeController.swift index b8fe915aee1e3df24665104a1c99db4e745e71a1..82044ac6f26b271bc9977519260ed1370b3a8d6e 100644 --- a/Sources/OnboardingFeature/Controllers/OnboardingCodeController.swift +++ b/Sources/OnboardingFeature/Controllers/OnboardingCodeController.swift @@ -144,7 +144,7 @@ public final class OnboardingCodeController: UIViewController { let actionButton = CapsuleButton() actionButton.set(style: .seeThrough, title: Localized.Settings.InfoDrawer.action) - let drawer = DrawerController(with: [ + let drawer = DrawerController([ DrawerText( font: Fonts.Mulish.bold.font(size: 26.0), text: title, @@ -175,6 +175,6 @@ public final class OnboardingCodeController: UIViewController { } }.store(in: &drawerCancellables) - navigator.perform(PresentDrawer()) +// navigator.perform(PresentDrawer()) } } diff --git a/Sources/OnboardingFeature/Controllers/OnboardingEmailController.swift b/Sources/OnboardingFeature/Controllers/OnboardingEmailController.swift index b6554d3dad3252d2e22fda105c0fc614fd45c9e3..3844b24cd957422f2736e76db95e529e13fbbc07 100644 --- a/Sources/OnboardingFeature/Controllers/OnboardingEmailController.swift +++ b/Sources/OnboardingFeature/Controllers/OnboardingEmailController.swift @@ -115,44 +115,42 @@ public final class OnboardingEmailController: UIViewController { title: Localized.Settings.InfoDrawer.action ) - let drawer = DrawerController(with: [ - DrawerText( - font: Fonts.Mulish.bold.font(size: 26.0), - text: title, - color: Asset.neutralActive.color, - alignment: .left, - spacingAfter: 19 - ), - DrawerLinkText( - text: subtitle, - urlString: urlString, - spacingAfter: 37 - ), - DrawerStack(views: [ - actionButton, - FlexibleSpace() - ]) - ]) +// navigator.perform(PresentDrawer([ +// DrawerText( +// font: Fonts.Mulish.bold.font(size: 26.0), +// text: title, +// color: Asset.neutralActive.color, +// alignment: .left, +// spacingAfter: 19 +// ), +// DrawerLinkText( +// text: subtitle, +// urlString: urlString, +// spacingAfter: 37 +// ), +// DrawerStack(views: [ +// actionButton, +// FlexibleSpace() +// ]) +// ])) actionButton.publisher(for: .touchUpInside) .receive(on: DispatchQueue.main) .sink { - drawer.dismiss(animated: true) { [weak self] in - guard let self = self else { return } - self.drawerCancellables.removeAll() - } + // drawer.dismiss(animated: true) { [weak self] in + // guard let self = self else { return } + // self.drawerCancellables.removeAll() + // } }.store(in: &drawerCancellables) - - navigator.perform(PresentDrawer()) } } -// coordinator.toEmailConfirmation(with: $0, from: self) { controller in -// let successModel = OnboardingSuccessModel( -// title: Localized.Onboarding.Success.Email.title, -// subtitle: nil, -// nextController: self.coordinator.toPhone(from:) -// ) -// -// self.coordinator.toSuccess(with: successModel, from: controller) -// } + // coordinator.toEmailConfirmation(with: $0, from: self) { controller in + // let successModel = OnboardingSuccessModel( + // title: Localized.Onboarding.Success.Email.title, + // subtitle: nil, + // nextController: self.coordinator.toPhone(from:) + // ) + // + // self.coordinator.toSuccess(with: successModel, from: controller) + // } diff --git a/Sources/OnboardingFeature/Controllers/OnboardingPhoneController.swift b/Sources/OnboardingFeature/Controllers/OnboardingPhoneController.swift index dc9df4ea436f0e6cfa10f24449f4daa060b5d1b8..1e64773787bb8bb46eaebb12ae86a5134a6c8166 100644 --- a/Sources/OnboardingFeature/Controllers/OnboardingPhoneController.swift +++ b/Sources/OnboardingFeature/Controllers/OnboardingPhoneController.swift @@ -133,7 +133,7 @@ public final class OnboardingPhoneController: UIViewController { title: Localized.Settings.InfoDrawer.action ) - let drawer = DrawerController(with: [ + let drawer = DrawerController([ DrawerText( font: Fonts.Mulish.bold.font(size: 26.0), text: title, @@ -161,7 +161,7 @@ public final class OnboardingPhoneController: UIViewController { } }.store(in: &drawerCancellables) - navigator.perform(PresentDrawer()) +// navigator.perform(PresentDrawer()) } } diff --git a/Sources/OnboardingFeature/Controllers/OnboardingUsernameController.swift b/Sources/OnboardingFeature/Controllers/OnboardingUsernameController.swift index 5939dab891e93f9031990eaca31da3ee8a86c7f9..7edb740b5feebeb134d655e4fdfc76d90d9739b5 100644 --- a/Sources/OnboardingFeature/Controllers/OnboardingUsernameController.swift +++ b/Sources/OnboardingFeature/Controllers/OnboardingUsernameController.swift @@ -116,7 +116,7 @@ public final class OnboardingUsernameController: UIViewController { title: Localized.Settings.InfoDrawer.action ) - let drawer = DrawerController(with: [ + let drawer = DrawerController([ DrawerText( font: Fonts.Mulish.bold.font(size: 26.0), text: title, @@ -144,6 +144,6 @@ public final class OnboardingUsernameController: UIViewController { } }.store(in: &drawerCancellables) - navigator.perform(PresentDrawer()) +// navigator.perform(PresentDrawer()) } } diff --git a/Sources/OnboardingFeature/Controllers/OnboardingWelcomeController.swift b/Sources/OnboardingFeature/Controllers/OnboardingWelcomeController.swift index c979f8121e6f02758d1fbc8ec37f7a64b96fc5d3..69033a19f3555a189f36d0094fceebdcc34ab600 100644 --- a/Sources/OnboardingFeature/Controllers/OnboardingWelcomeController.swift +++ b/Sources/OnboardingFeature/Controllers/OnboardingWelcomeController.swift @@ -59,40 +59,40 @@ public final class OnboardingWelcomeController: UIViewController { subtitle: String, urlString: String = "" ) { - let actionButton = CapsuleButton() - actionButton.set( - style: .seeThrough, - title: Localized.Settings.InfoDrawer.action - ) - - let drawer = DrawerController(with: [ - DrawerText( - font: Fonts.Mulish.bold.font(size: 26.0), - text: title, - color: Asset.neutralActive.color, - alignment: .left, - spacingAfter: 19 - ), - DrawerLinkText( - text: subtitle, - urlString: urlString, - spacingAfter: 37 - ), - DrawerStack(views: [ - actionButton, - FlexibleSpace() - ]) - ]) - - actionButton.publisher(for: .touchUpInside) - .receive(on: DispatchQueue.main) - .sink { - drawer.dismiss(animated: true) { [weak self] in - guard let self = self else { return } - self.drawerCancellables.removeAll() - } - }.store(in: &drawerCancellables) - - navigator.perform(PresentDrawer()) +// let actionButton = CapsuleButton() +// actionButton.set( +// style: .seeThrough, +// title: Localized.Settings.InfoDrawer.action +// ) +// +// let drawer = DrawerController([ +// DrawerText( +// font: Fonts.Mulish.bold.font(size: 26.0), +// text: title, +// color: Asset.neutralActive.color, +// alignment: .left, +// spacingAfter: 19 +// ), +// DrawerLinkText( +// text: subtitle, +// urlString: urlString, +// spacingAfter: 37 +// ), +// DrawerStack(views: [ +// actionButton, +// FlexibleSpace() +// ]) +// ]) +// +// actionButton.publisher(for: .touchUpInside) +// .receive(on: DispatchQueue.main) +// .sink { +// drawer.dismiss(animated: true) { [weak self] in +// guard let self = self else { return } +// self.drawerCancellables.removeAll() +// } +// }.store(in: &drawerCancellables) +// +// navigator.perform(PresentDrawer()) } } diff --git a/Sources/ProfileFeature/Controllers/ProfileController.swift b/Sources/ProfileFeature/Controllers/ProfileController.swift index 787f83f6e0d429262780485b11cef70dae17a917..58a74cd0a6cca98ed8c29fa4520abc223e7466a0 100644 --- a/Sources/ProfileFeature/Controllers/ProfileController.swift +++ b/Sources/ProfileFeature/Controllers/ProfileController.swift @@ -151,7 +151,7 @@ public final class ProfileController: UIViewController { style: .red )) - let drawer = DrawerController(with: [ + let drawer = DrawerController([ DrawerText( font: Fonts.Mulish.bold.font(size: 26.0), text: title, diff --git a/Sources/ReportingFeature/MakeReportDrawer.swift b/Sources/ReportingFeature/MakeReportDrawer.swift index b8b4aa394481d3e292ef236711b1f31f82704d3e..bd102a6ee2e046b03c787d250f8a0573d25af7c3 100644 --- a/Sources/ReportingFeature/MakeReportDrawer.swift +++ b/Sources/ReportingFeature/MakeReportDrawer.swift @@ -34,7 +34,7 @@ extension MakeReportDrawer { reportButton.setStyle(.red) reportButton.setTitle(Localized.Chat.Report.action, for: .normal) - let drawer = DrawerController(with: [ + let drawer = DrawerController([ DrawerImage( image: Asset.drawerNegative.image ), diff --git a/Sources/RequestsFeature/Controllers/RequestsReceivedController.swift b/Sources/RequestsFeature/Controllers/RequestsReceivedController.swift index feec2f2873d90a2fbbbaa492e03e4c22a4ac5567..279f29b627f8a072609868fd3686c6138d435e68 100644 --- a/Sources/RequestsFeature/Controllers/RequestsReceivedController.swift +++ b/Sources/RequestsFeature/Controllers/RequestsReceivedController.swift @@ -166,7 +166,7 @@ extension RequestsReceivedController { drawerLaterButton ]) - let drawer = DrawerController(with: items) + let drawer = DrawerController(items) drawerSendButton.action .receive(on: DispatchQueue.main) @@ -240,7 +240,7 @@ extension RequestsReceivedController { drawerLaterButton ]) - let drawer = DrawerController(with: items) + let drawer = DrawerController(items) drawerSendButton.action .receive(on: DispatchQueue.main) @@ -344,7 +344,7 @@ extension RequestsReceivedController { items.append(contentsOf: [drawerAcceptButton, drawerHideButton]) - let drawer = DrawerController(with: items) + let drawer = DrawerController(items) drawerAcceptButton.action .receive(on: DispatchQueue.main) @@ -471,7 +471,7 @@ extension RequestsReceivedController { items.append(contentsOf: [drawerAcceptButton, drawerHideButton]) - let drawer = DrawerController(with: items) + let drawer = DrawerController(items) var nickname: String? var allowsSave = true @@ -551,7 +551,7 @@ extension RequestsReceivedController { items.append(drawerDoneButton) - let drawer = DrawerController(with: items) + let drawer = DrawerController(items) drawerDoneButton.action .receive(on: DispatchQueue.main) diff --git a/Sources/RestoreFeature/Controllers/RestoreController.swift b/Sources/RestoreFeature/Controllers/RestoreController.swift index 12bc3a7d244e147e3fc7433a1eaa036db828db01..f88ead1da0327ede349a9e1fdf3a4b0d5eb0b13a 100644 --- a/Sources/RestoreFeature/Controllers/RestoreController.swift +++ b/Sources/RestoreFeature/Controllers/RestoreController.swift @@ -105,7 +105,7 @@ extension RestoreController { style: .brandColored )) - let drawer = DrawerController(with: [ + let drawer = DrawerController([ DrawerText( font: Fonts.Mulish.bold.font(size: 26.0), text: Localized.AccountRestore.Warning.title, diff --git a/Sources/RestoreFeature/Controllers/RestoreListController.swift b/Sources/RestoreFeature/Controllers/RestoreListController.swift index 026bdbd0fe809898df7ead2282729f44732f1e47..6e85c99fb155a90394836d095964fa2dbc1e49b4 100644 --- a/Sources/RestoreFeature/Controllers/RestoreListController.swift +++ b/Sources/RestoreFeature/Controllers/RestoreListController.swift @@ -97,7 +97,7 @@ extension RestoreListController { style: .brandColored )) - let drawer = DrawerController(with: [ + let drawer = DrawerController([ DrawerText( font: Fonts.Mulish.bold.font(size: 26.0), text: Localized.AccountRestore.Warning.title, diff --git a/Sources/ScanFeature/Controllers/ScanContainerController.swift b/Sources/ScanFeature/Controllers/ScanContainerController.swift index 883f95fdb92a27d2b4f860ec2356f450a7ceeaa5..a85c06a797e1013275d261d2cbc51bdbf298db4f 100644 --- a/Sources/ScanFeature/Controllers/ScanContainerController.swift +++ b/Sources/ScanFeature/Controllers/ScanContainerController.swift @@ -115,7 +115,7 @@ public final class ScanContainerController: UIViewController { title: Localized.Settings.InfoDrawer.action ) - let drawer = DrawerController(with: [ + let drawer = DrawerController([ DrawerText( font: Fonts.Mulish.bold.font(size: 26.0), text: title, diff --git a/Sources/SearchFeature/Controllers/SearchContainerController.swift b/Sources/SearchFeature/Controllers/SearchContainerController.swift index 77f2a535feca6a5dacafca6e9632e3f0396d26e4..a78f5993fb3a7381f0f337884a8e03a878324bac 100644 --- a/Sources/SearchFeature/Controllers/SearchContainerController.swift +++ b/Sources/SearchFeature/Controllers/SearchContainerController.swift @@ -137,7 +137,7 @@ extension SearchContainerController { title: Localized.ChatList.Traffic.negative ) - let drawer = DrawerController(with: [ + let drawer = DrawerController([ DrawerText( font: Fonts.Mulish.bold.font(size: 26.0), text: Localized.ChatList.Traffic.title, diff --git a/Sources/SearchFeature/Controllers/SearchLeftController.swift b/Sources/SearchFeature/Controllers/SearchLeftController.swift index 7eb72535120e8f30f86258365e4ca86d422572e4..bc6b1fbd5f78c9fc6a62b705f11bdf929a6e1faf 100644 --- a/Sources/SearchFeature/Controllers/SearchLeftController.swift +++ b/Sources/SearchFeature/Controllers/SearchLeftController.swift @@ -208,7 +208,7 @@ final class SearchLeftController: UIViewController { title: Localized.Ud.Placeholder.Drawer.action ) - let drawer = DrawerController(with: [ + let drawer = DrawerController([ DrawerText( font: Fonts.Mulish.bold.font(size: 26.0), text: Localized.Ud.Placeholder.Drawer.title, @@ -282,7 +282,7 @@ final class SearchLeftController: UIViewController { items.append(drawerSaveButton) - let drawer = DrawerController(with: items) + let drawer = DrawerController(items) var nickname: String? var allowsSave = true @@ -402,7 +402,7 @@ final class SearchLeftController: UIViewController { ) items.append(contentsOf: [drawerSendButton, drawerCancelButton]) - let drawer = DrawerController(with: items) + let drawer = DrawerController(items) drawerSendButton.action .receive(on: DispatchQueue.main) diff --git a/Sources/SettingsFeature/Controllers/AccountDeleteController.swift b/Sources/SettingsFeature/Controllers/AccountDeleteController.swift index 01ad5ad59576f8654f8ed66cf9250e42dbfc4f5d..31674910b97735d82456859454c56c4325c9c18d 100644 --- a/Sources/SettingsFeature/Controllers/AccountDeleteController.swift +++ b/Sources/SettingsFeature/Controllers/AccountDeleteController.swift @@ -77,7 +77,7 @@ public final class AccountDeleteController: UIViewController { title: Localized.Settings.InfoDrawer.action ) - let drawer = DrawerController(with: [ + let drawer = DrawerController([ DrawerText( font: Fonts.Mulish.bold.font(size: 26.0), text: title, diff --git a/Sources/SettingsFeature/Controllers/SettingsController.swift b/Sources/SettingsFeature/Controllers/SettingsController.swift index 74ca0b94de5d3d367874e3474d3260b331608875..5cce068e5dfceae71439bc1c7f9e4a3047548275 100644 --- a/Sources/SettingsFeature/Controllers/SettingsController.swift +++ b/Sources/SettingsFeature/Controllers/SettingsController.swift @@ -198,7 +198,7 @@ public final class SettingsController: UIViewController { cancelButton.setStyle(.seeThrough) cancelButton.setTitle(Localized.ChatList.Dashboard.cancel, for: .normal) - let drawer = DrawerController(with: [ + let drawer = DrawerController([ DrawerImage( image: Asset.drawerNegative.image ), @@ -259,7 +259,7 @@ extension SettingsController { title: Localized.Settings.InfoDrawer.action ) - let drawer = DrawerController(with: [ + let drawer = DrawerController([ DrawerText( font: Fonts.Mulish.bold.font(size: 26.0), text: title, diff --git a/Sources/VersionChecking/BackendVersionInformation.swift b/Sources/VersionChecking/BackendVersionInformation.swift new file mode 100644 index 0000000000000000000000000000000000000000..bb52e089d69a5bdc73672d70b936d0afe941c341 --- /dev/null +++ b/Sources/VersionChecking/BackendVersionInformation.swift @@ -0,0 +1,7 @@ +struct BackendVersionInformation: Codable { + var info: DappVersionInformation + + private enum CodingKeys: String, CodingKey { + case info = "dapp-id" + } +} diff --git a/Sources/VersionChecking/DappVersionInformation.swift b/Sources/VersionChecking/DappVersionInformation.swift new file mode 100644 index 0000000000000000000000000000000000000000..c6d49f32bf3f211ea669cf003a9cc663c86adc76 --- /dev/null +++ b/Sources/VersionChecking/DappVersionInformation.swift @@ -0,0 +1,14 @@ +public struct DappVersionInformation: Codable { + public var appUrl: String + public var minimum: String + public var isRequired: Bool? + public var recommended: String + public var minimumMessage: String + + private enum CodingKeys: String, CodingKey { + case appUrl = "new_ios_app_url" + case minimum = "new_ios_min_version" + case recommended = "new_ios_recommended_version" + case minimumMessage = "new_minimum_popup_msg" + } +} diff --git a/Sources/VersionChecking/VersionChecking.swift b/Sources/VersionChecking/VersionChecking.swift index bf50d90c877a65858940a4bdad70b1101e1dc1d0..820a2aac66a5db65577ab4e39109c33256e26e7a 100644 --- a/Sources/VersionChecking/VersionChecking.swift +++ b/Sources/VersionChecking/VersionChecking.swift @@ -1,109 +1,53 @@ import Combine import Foundation -public enum VersionInfo { +public typealias VersionCompletion = (VersionCheck.Requirement) -> Void + +public struct VersionCheck { + public enum Requirement { case upToDate case failure(Error) - case updateRequired(DappVersionInformation) - case updateRecommended(DappVersionInformation) -} - -public struct VersionDataFetcher { - var run: () -> AnyPublisher<DappVersionInformation, Error> - - public init(run: @escaping () -> AnyPublisher<DappVersionInformation, Error>) { - self.run = run - } - - public func callAsFunction() -> AnyPublisher<DappVersionInformation, Error> { run() } -} - -public struct VersionChecker { - var run: () -> AnyPublisher<VersionInfo, Never> - - public init(run: @escaping () -> AnyPublisher<VersionInfo, Never>) { - self.run = run - } - - public func callAsFunction() -> AnyPublisher<VersionInfo, Never> { run() } -} - -public extension VersionChecker { + case outdated(DappVersionInformation) + } - static let mock: Self = .init { Just(.upToDate).eraseToAnyPublisher() } - - static func live( - fetchVersion: VersionDataFetcher = .live(), - bundleVersion: @escaping () -> String = { Bundle.main.infoDictionary?["CFBundleShortVersionString"] as! String } - ) -> Self { - .init { - fetchVersion() - .map { dappInfo -> VersionInfo in - let version = bundleVersion() - if version >= dappInfo.recommended { - return .upToDate - } else if version >= dappInfo.minimum { - return .updateRecommended(dappInfo) - } else { - return .updateRequired(dappInfo) - } - } - .catch { Just(VersionInfo.failure($0)) } - .eraseToAnyPublisher() - } - } + public var verify: (@escaping VersionCompletion) -> Void } -public extension VersionDataFetcher { - static func mock() -> Self { - .init { - Just(DappVersionInformation( - appUrl: "https://testflight.apple.com/join/L1Rj0so3", - minimum: "1.0", - recommended: "1.0", - minimumMessage: "This app version is not supported anymore, please update to the latest version to keep enjoying our app" - )) - .setFailureType(to: Error.self) - .eraseToAnyPublisher() - } - } - - static func live() -> Self { - .init { - let request = URLRequest( - url: URL(string: "https://elixxir-bins.s3-us-west-1.amazonaws.com/client/dapps/appdb.json")!, - cachePolicy: .reloadIgnoringLocalAndRemoteCacheData, - timeoutInterval: 5 - ) - - return URLSession.shared - .dataTaskPublisher(for: request) - .map(\.data) - .decode(type: BackendVersionInformation.self, decoder: JSONDecoder()) - .map(\.info) - .eraseToAnyPublisher() +public extension VersionCheck { + static let mock: Self = .init { $0(.outdated(.init( + appUrl: "https://testflight.apple.com/join/L1Rj0so3", + minimum: "2.0", + isRequired: false, + recommended: "5.0", + minimumMessage: "This app version is not supported anymore, please update to the latest version to keep enjoying our app" + ))) } + + static let live: Self = .init { completion in + let request = URLRequest( + url: URL(string: "https://elixxir-bins.s3-us-west-1.amazonaws.com/client/dapps/appdb.json")!, + cachePolicy: .reloadIgnoringLocalAndRemoteCacheData, + timeoutInterval: 5 + ) + URLSession.shared.dataTask(with: request) { data, _, error in + if let error { + completion(.failure(error)) + return + } + guard let data else { + fatalError("No data for version checking") + } + guard var model = try? JSONDecoder().decode(BackendVersionInformation.self, from: data) else { + fatalError() + } + let bundleVersion = Bundle.main.infoDictionary?["CFBundleShortVersionString"] as! String + if bundleVersion >= model.info.recommended { + completion(.upToDate) + } else { + if bundleVersion < model.info.minimum { + model.info.isRequired = true } - } -} - -public struct DappVersionInformation: Codable { - public let appUrl: String - public let minimum: String - public let recommended: String - public let minimumMessage: String - - private enum CodingKeys: String, CodingKey { - case appUrl = "new_ios_app_url" - case minimum = "new_ios_min_version" - case recommended = "new_ios_recommended_version" - case minimumMessage = "new_minimum_popup_msg" - } -} - -private struct BackendVersionInformation: Codable { - let info: DappVersionInformation - - private enum CodingKeys: String, CodingKey { - case info = "dapp-id" - } + completion(.outdated(model.info)) + } + }.resume() + } } diff --git a/Sources/XXNavigation/Actions/PresentDrawer.swift b/Sources/XXNavigation/Actions/PresentDrawer.swift index 44e85b9584e45614c46225fb83c302d130e52e1a..01e878b3ec02bd211213f9e1fb2f73f95a0de0bc 100644 --- a/Sources/XXNavigation/Actions/PresentDrawer.swift +++ b/Sources/XXNavigation/Actions/PresentDrawer.swift @@ -1,9 +1,18 @@ import Navigation +import DrawerFeature public struct PresentDrawer: Navigation.Action { + public var items: [DrawerItem] public var animated: Bool = true + public var dismissable: Bool = true - public init(animated: Bool = true) { + public init( + items: [DrawerItem], + animated: Bool = true, + dismissable: Bool = true + ) { + self.items = items self.animated = animated + self.dismissable = dismissable } } diff --git a/Sources/XXNavigation/CustomActions/OpenUp.swift b/Sources/XXNavigation/CustomActions/OpenUp.swift new file mode 100644 index 0000000000000000000000000000000000000000..6aea3325084a82f7405f6e106087cbb3b8ff7f6c --- /dev/null +++ b/Sources/XXNavigation/CustomActions/OpenUp.swift @@ -0,0 +1,201 @@ +import UIKit +import Navigation + +/// Open up view controller on provided parent view controller +public struct OpenUp: Action { + /// - Parameters: + /// - viewController: View controller to present + /// - parent: Parent view controller from which presentation should happen + /// - animated: Animate the transition + /// - dismissable: Dismissable upon background touch flag + public init( + _ viewController: UIViewController, + from parent: UIViewController, + animated: Bool = true, + dismissable: Bool = true + ) { + self.viewController = viewController + self.parent = parent + self.animated = animated + self.dismissable = dismissable + } + + /// View controller to present + public var viewController: UIViewController + + /// Parent view controller from which presentation should happen + public var parent: UIViewController + + /// Animate the transition + public var animated: Bool + + /// Dismissable upon background touch flag + public var dismissable: Bool +} + +/// Performs `OpenUp` action +public struct OpenUpNavigator: TypedNavigator { + let transitioningDelegate = BottomPresenter() + + public init() {} + + public func perform(_ action: OpenUp, completion: @escaping () -> Void) { + transitioningDelegate.isDismissableOnBackgroundTouch = action.dismissable + action.viewController.transitioningDelegate = transitioningDelegate + action.viewController.modalPresentationStyle = .overFullScreen + + action.parent.present( + action.viewController, + animated: action.animated, + completion: completion + ) + } +} + +final class BottomPresenter: NSObject, UIViewControllerTransitioningDelegate { + var isDismissableOnBackgroundTouch: Bool = true + private var transition: BottomTransition? + + public func animationController( + forPresented presented: UIViewController, + presenting: UIViewController, + source: UIViewController + ) -> UIViewControllerAnimatedTransitioning? { + transition = BottomTransition(isDismissableOnBackgroundTouch) { [weak self] in + self?.transition = nil + } + + return transition + } + + public func animationController(forDismissed dismissed: UIViewController) -> UIViewControllerAnimatedTransitioning? { + transition?.direction = .dismiss + return transition + } +} + +import Combine +import SnapKit +final class BottomTransition: NSObject, UIViewControllerAnimatedTransitioning { + enum Direction { + case present + case dismiss + } + + let isDismissableOnBackground: Bool + var direction: Direction = .present + private let onDismissal: (() -> Void)? + private weak var darkOverlayView: UIControl? + private weak var topConstraint: Constraint? + private weak var bottomConstraint: Constraint? + private var cancellables = Set<AnyCancellable>() + + private var presentedConstraints: [NSLayoutConstraint] = [] + private var dismissedConstraints: [NSLayoutConstraint] = [] + + init( + _ isDismissableOnBackground: Bool = true, + onDismissal: (() -> Void)? + ) { + self.onDismissal = onDismissal + self.isDismissableOnBackground = isDismissableOnBackground + super.init() + } + + func transitionDuration(using context: UIViewControllerContextTransitioning?) -> TimeInterval { 0.5 } + + func animateTransition(using context: UIViewControllerContextTransitioning) { + switch direction { + case .present: + present(using: context) + case .dismiss: + dismiss(using: context) + } + } + + private func present(using context: UIViewControllerContextTransitioning) { + guard let presentingController = context.viewController(forKey: .from), + let presentedView = context.view(forKey: .to) else { + context.completeTransition(false) + return + } + + let darkOverlayView = UIControl() + self.darkOverlayView = darkOverlayView + + darkOverlayView.alpha = 0.0 + darkOverlayView.backgroundColor = UIColor.black.withAlphaComponent(0.5) + context.containerView.addSubview(darkOverlayView) + darkOverlayView.frame = context.containerView.bounds + + darkOverlayView + .publisher(for: .touchUpInside) + .sink { [weak presentingController] _ in + guard self.isDismissableOnBackground else { return } + presentingController?.dismiss(animated: true) + }.store(in: &cancellables) + + context.containerView.addSubview(presentedView) + presentedView.translatesAutoresizingMaskIntoConstraints = false + + presentedConstraints = [ + presentedView.leftAnchor.constraint(equalTo: context.containerView.leftAnchor), + presentedView.rightAnchor.constraint(equalTo: context.containerView.rightAnchor), + presentedView.bottomAnchor.constraint(equalTo: context.containerView.bottomAnchor), + presentedView.topAnchor.constraint( + greaterThanOrEqualTo: context.containerView.safeAreaLayoutGuide.topAnchor, + constant: 60 + ) + ] + + dismissedConstraints = [ + presentedView.leftAnchor.constraint(equalTo: context.containerView.leftAnchor), + presentedView.rightAnchor.constraint(equalTo: context.containerView.rightAnchor), + presentedView.topAnchor.constraint(equalTo: context.containerView.bottomAnchor) + ] + + NSLayoutConstraint.activate(dismissedConstraints) + + context.containerView.setNeedsLayout() + context.containerView.layoutIfNeeded() + + NSLayoutConstraint.deactivate(dismissedConstraints) + NSLayoutConstraint.activate(presentedConstraints) + + UIView.animate( + withDuration: transitionDuration(using: context), + delay: 0, + usingSpringWithDamping: 1, + initialSpringVelocity: 0, + options: .curveEaseInOut, + animations: { + darkOverlayView.alpha = 1.0 + context.containerView.setNeedsLayout() + context.containerView.layoutIfNeeded() + }, + completion: { _ in + context.completeTransition(true) + }) + } + + private func dismiss(using context: UIViewControllerContextTransitioning) { + NSLayoutConstraint.deactivate(presentedConstraints) + NSLayoutConstraint.activate(dismissedConstraints) + + UIView.animate( + withDuration: transitionDuration(using: context), + delay: 0, + usingSpringWithDamping: 1, + initialSpringVelocity: 0, + options: .curveEaseInOut, + animations: { [weak darkOverlayView] in + darkOverlayView?.alpha = 0.0 + context.containerView.setNeedsLayout() + context.containerView.layoutIfNeeded() + }, + completion: { [weak self] _ in + context.completeTransition(true) + self?.onDismissal?() + }) + } +} diff --git a/Sources/XXNavigation/Navigators/PresentDrawerNavigator.swift b/Sources/XXNavigation/Navigators/PresentDrawerNavigator.swift index 8b137891791fe96927ad78e64b0aad7bded08bdc..aec7278800720dbb2471ac9786f09b465d50adde 100644 --- a/Sources/XXNavigation/Navigators/PresentDrawerNavigator.swift +++ b/Sources/XXNavigation/Navigators/PresentDrawerNavigator.swift @@ -1 +1,30 @@ +import UIKit +import Navigation +import DrawerFeature +import DependencyInjection +public struct PresentDrawerNavigator: TypedNavigator { + @Dependency var navigator: Navigator + var screen: ([DrawerItem]) -> UIViewController + var navigationController: () -> UINavigationController + + public func perform(_ action: PresentDrawer, completion: @escaping () -> Void) { + if let topViewController = navigationController().topViewController { + let openUpAction = OpenUp( + screen(action.items), + from: topViewController, + animated: action.animated, + dismissable: action.dismissable + ) + navigator.perform(openUpAction, completion: completion) + } + } + + public init( + screen: @escaping ([DrawerItem]) -> UIViewController, + navigationController: @escaping () -> UINavigationController + ) { + self.screen = screen + self.navigationController = navigationController + } +}