diff --git a/Nextcloud.xcodeproj/project.pbxproj b/Nextcloud.xcodeproj/project.pbxproj index 5bf775fb4a..7e7ae7e4ae 100644 --- a/Nextcloud.xcodeproj/project.pbxproj +++ b/Nextcloud.xcodeproj/project.pbxproj @@ -1,3 +1,4 @@ + // !$*UTF8*$! { archiveVersion = 1; @@ -113,6 +114,10 @@ C0DECA022F65000100C0D001 /* NCCameraRollTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = C0DECA012F65000100C0D001 /* NCCameraRollTests.swift */; }; CA1700013000000000000001 /* NCUploadedAssetsCleanupTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = CA1700013000000000000002 /* NCUploadedAssetsCleanupTests.swift */; }; CB3666201AF7550816B5CD6A /* NCContextMenuComment.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8932E90EC4278026D86CCCC9 /* NCContextMenuComment.swift */; }; + AFCE353927E5DE0500FEA6C2 /* NCShare+Helper.swift in Sources */ = {isa = PBXBuildFile; fileRef = AFCE353827E5DE0400FEA6C2 /* NCShare+Helper.swift */; }; + B5D45E732DA5172900773929 /* AppUpdater.swift in Sources */ = {isa = PBXBuildFile; fileRef = B5D45E712DA5172900773929 /* AppUpdater.swift */; }; + C04E2F232A17BB4D001BAD85 /* FilesIntegrationTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = C04E2F222A17BB4D001BAD85 /* FilesIntegrationTests.swift */; }; + D575039F27146F93008DC9DC /* String+Extension.swift in Sources */ = {isa = PBXBuildFile; fileRef = F7A0D1342591FBC5008F8A13 /* String+Extension.swift */; }; D5B6AA7827200C7200D49C24 /* NCActivityTableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = D5B6AA7727200C7200D49C24 /* NCActivityTableViewCell.swift */; }; F0A1B2C430B5000100D4E5F6 /* NCMediaViewerFloatingTitleViewTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = F0A1B2C330B5000100D4E5F6 /* NCMediaViewerFloatingTitleViewTests.swift */; }; F0A1B2C630B6000100D4E5F6 /* NCImageZoomViewTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = F0A1B2C530B6000100D4E5F6 /* NCImageZoomViewTests.swift */; }; @@ -1448,6 +1453,8 @@ AFCE353827E5DE0400FEA6C2 /* Shareable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Shareable.swift; sourceTree = ""; }; B601AA002F10000100C0D3E5 /* NCLogFilesView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NCLogFilesView.swift; sourceTree = ""; }; BB7697C94BA14450A0867940 /* NCContextMenuProfile.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NCContextMenuProfile.swift; sourceTree = ""; }; + AFCE353827E5DE0400FEA6C2 /* NCShare+Helper.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "NCShare+Helper.swift"; sourceTree = ""; }; + B5D45E712DA5172900773929 /* AppUpdater.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppUpdater.swift; sourceTree = ""; }; C0046CDA2A17B98400D87C9D /* NextcloudUITests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = NextcloudUITests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; C04E2F202A17BB4D001BAD85 /* NextcloudIntegrationTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = NextcloudIntegrationTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; C0DECA012F65000100C0D001 /* NCCameraRollTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NCCameraRollTests.swift; sourceTree = ""; }; @@ -2388,6 +2395,14 @@ path = Advanced; sourceTree = ""; }; + B5D45E722DA5172900773929 /* AppUpdate */ = { + isa = PBXGroup; + children = ( + B5D45E712DA5172900773929 /* AppUpdater.swift */, + ); + path = AppUpdate; + sourceTree = ""; + }; C0046CDB2A17B98400D87C9D /* NextcloudUITests */ = { isa = PBXGroup; children = ( @@ -3857,6 +3872,7 @@ AA517BB42D66149900F8D37C /* .tx */, F7BDC1DD300F4FF800C5D9FA /* Processor */, F7BDC1DE300F502C00C5D9FA /* Refresh */, + B5D45E722DA5172900773929 /* AppUpdate */, F702F2CC25EE5B4F008F8E80 /* AppDelegate.swift */, F7CAFE1A2F16AA8600DB35A5 /* main.swift */, F794E13E2BBC0F70003693D7 /* SceneDelegate.swift */, @@ -5271,6 +5287,7 @@ F70898692EDDB51700EF85BD /* NCSelectOpen+SelectDelegate.swift in Sources */, F7D4BF412CA2E8D800A5E746 /* TOPasscodeViewControllerAnimatedTransitioning.m in Sources */, F7D4BF422CA2E8D800A5E746 /* TOPasscodeSettingsViewController.m in Sources */, + B5D45E732DA5172900773929 /* AppUpdater.swift in Sources */, F7D4BF432CA2E8D800A5E746 /* TOPasscodeCircleImage.m in Sources */, F78026102E9CFA3700B63436 /* NCTransfersView.swift in Sources */, F7D4BF442CA2E8D800A5E746 /* TOPasscodeView.m in Sources */, diff --git a/iOSClient/AppUpdate/AppUpdater.swift b/iOSClient/AppUpdate/AppUpdater.swift new file mode 100644 index 0000000000..5f1b0e54eb --- /dev/null +++ b/iOSClient/AppUpdate/AppUpdater.swift @@ -0,0 +1,95 @@ +// +// AppUpdater.swift +// Nextcloud +// +// Created by Amrut Waghmare on 09/10/23. +// Copyright © 2023 Marino Faggiana. All rights reserved. +// + +import Foundation +import FirebaseRemoteConfig + +struct AppUpdaterKey { + static let lastUpdateCheckDate : String = "lastUpdateCheckDate" +} + +class AppUpdater { + func checkForUpdate() { + checkUpdate{ (version, isForceupdate) in + DispatchQueue.main.async { + if let version = version, let isForceupdate = isForceupdate { + if (isForceupdate) { + self.showUpdateAlert(version: version, isForceUpdate: isForceupdate) + } else { + if self.checkLastUpdate() { + self.showUpdateAlert(version: version, isForceUpdate: isForceupdate) + } + } + } + } + } + } + + func showUpdateAlert(version: String, isForceUpdate: Bool) { + guard let appDelegate = UIApplication.shared.delegate as? AppDelegate, let viewControlller = appDelegate.window?.rootViewController else { return } + let descriptionMsg = String(format: NSLocalizedString("update_description", comment: ""), version) + let alert = UIAlertController(title: NSLocalizedString("update_available", comment: ""), message: descriptionMsg, preferredStyle: .alert) + let updateAction = UIAlertAction(title: NSLocalizedString("update", comment: ""), style: .default, handler: { action in + guard let url = URL(string: NCBrandOptions.shared.appStoreUrl) else { return } + UIApplication.shared.open(url) + }) + alert.addAction(updateAction) + if !isForceUpdate { + alert.addAction(UIAlertAction(title: NSLocalizedString("not_now", comment: ""), style: .default, handler: { action in + self.saveAppUpdateCheckDate() + })) + } + alert.preferredAction = updateAction + viewControlller.present(alert, animated: true, completion: {}) + } + + func checkLastUpdate() -> Bool { + if let lastUpdateCheckDate = UserDefaults.standard.object(forKey: AppUpdaterKey.lastUpdateCheckDate) as? Date { + return daysBetweenDate(from: lastUpdateCheckDate) > 7 + } else { + return true + } + } + + func checkUpdate(completion: @escaping (String?, Bool?) -> Void) { + let remoteConfig = RemoteConfig.remoteConfig() + remoteConfig.fetch(withExpirationDuration: 1) { (status, error) in + if status == .success { + remoteConfig.activate { value, error in + // Remote config values fetched successfully + let iOSVersionString = remoteConfig["ios_app_version"].stringValue ?? "default_value" + let isForcheUpdate = remoteConfig["ios_force_update"].boolValue + if let currentVersionString = Bundle.main.infoDictionary?["CFBundleShortVersionString"] as? String, + let currentVersion = Int(currentVersionString.replacingOccurrences(of: ".", with: "")), + let iOSVersion = Int(iOSVersionString.replacingOccurrences(of: ".", with: "")) { +// if let currentVersion = Bundle.main.infoDictionary?["CFBundleShortVersionString"] as? String { + if iOSVersion != currentVersion { + // There is an update available + completion(iOSVersionString, isForcheUpdate) + } else { + completion(nil, nil) + } + } + + } + } else { + // Handle error + print("Error fetching remote config: \(error?.localizedDescription ?? "Unknown error")") + completion(nil, nil) + } + } + } + + func saveAppUpdateCheckDate() { + UserDefaults.standard.setValue(Date(), forKey: AppUpdaterKey.lastUpdateCheckDate) + } + + func daysBetweenDate(from date: Date) -> Int { + return Calendar.current.dateComponents([.day], from: date, to: Date()).day ?? 0 + } +} diff --git a/iOSClient/SceneDelegate.swift b/iOSClient/SceneDelegate.swift index 73b710d0c7..2ad4afbe5e 100644 --- a/iOSClient/SceneDelegate.swift +++ b/iOSClient/SceneDelegate.swift @@ -6,14 +6,12 @@ import Foundation import UIKit import NextcloudKit import WidgetKit +import SwiftEntryKit import SwiftUI import CoreLocation -import LucidBanner class SceneDelegate: UIResponder, UIWindowSceneDelegate { var window: UIWindow? - var lucidBanner: LucidBanner? - private let appDelegate = UIApplication.shared.delegate as? AppDelegate private var privacyProtectionWindow: UIWindow? private let global = NCGlobal.shared @@ -23,11 +21,12 @@ class SceneDelegate: UIResponder, UIWindowSceneDelegate { guard let windowScene = (scene as? UIWindowScene) else { return } + // Ensure MoEngage is initialized for multi-scene setups +// MoEngageAnalytics.setupIfNeeded() + let versionApp = NCUtility().getVersionMaintenance() var lastVersion: String? - lucidBanner = LucidBannerRegistry.shared.banner(for: windowScene) - if let groupDefaults = UserDefaults(suiteName: NCBrandOptions.shared.capabilitiesGroup) { lastVersion = groupDefaults.string(forKey: NCGlobal.shared.udLastVersion) groupDefaults.set(versionApp, forKey: global.udLastVersion) @@ -68,6 +67,13 @@ class SceneDelegate: UIResponder, UIWindowSceneDelegate { } else { self.startNextcloud(scene: scene, withActivateSceneForAccount: false) } + +// if let tabBarController = window?.rootViewController as? UITabBarController { +// if #available(iOS 18.0, *) { +// // Forces the "Compact" (iPhone) size class so the tab bar stays at the bottom +// tabBarController.traitOverrides.horizontalSizeClass = .compact +// } +// } } private func startNextcloud(scene: UIScene, withActivateSceneForAccount activateSceneForAccount: Bool) { @@ -114,7 +120,11 @@ class SceneDelegate: UIResponder, UIWindowSceneDelegate { NCPreferences().removeAll() if let bundleID = Bundle.main.bundleIdentifier { + let lastUpdateCheckDate = UserDefaults.standard.object(forKey: AppUpdaterKey.lastUpdateCheckDate) UserDefaults.standard.removePersistentDomain(forName: bundleID) + if lastUpdateCheckDate != nil { + UserDefaults.standard.setValue(lastUpdateCheckDate, forKey: AppUpdaterKey.lastUpdateCheckDate) + } } if NCBrandOptions.shared.disable_intro { @@ -124,7 +134,8 @@ class SceneDelegate: UIResponder, UIWindowSceneDelegate { window?.makeKeyAndVisible() } } else { - if let navigationController = UIStoryboard(name: "NCIntro", bundle: nil).instantiateInitialViewController() as? UINavigationController { + if let viewController = UIStoryboard(name: "NCIntro", bundle: nil).instantiateInitialViewController() as? NCIntroViewController { + let navigationController = UINavigationController(rootViewController: viewController) window?.rootViewController = navigationController window?.makeKeyAndVisible() } @@ -147,9 +158,8 @@ class SceneDelegate: UIResponder, UIWindowSceneDelegate { NotificationCenter.default.postOnMainThread(name: self.global.notificationCenterChangeTheming, userInfo: ["account": activeTblAccount.account]) } - // Start Networking Process + // Set up networking session await NCNetworkingProcess.shared.setCurrentAccount(activeTblAccount.account) - await NCNetworkingProcess.shared.startTimer(interval: NCNetworkingProcess.shared.maxInterval) } // Set up networking session for all configured accounts @@ -184,6 +194,10 @@ class SceneDelegate: UIResponder, UIWindowSceneDelegate { // window?.rootViewController = controller window?.makeKeyAndVisible() + // Re-evaluate in-app messages after main interface is visible +// Task { @MainActor in + MoEngageAnalytics.shared.displayInAppNotificationSafely(reason: "main interface launched") +// } // if activateSceneForAccount { self.activateSceneForAccount(scene, account: activeTblAccount.account, controller: controller) @@ -197,11 +211,6 @@ class SceneDelegate: UIResponder, UIWindowSceneDelegate { } func sceneDidDisconnect(_ scene: UIScene) { - guard let windowScene = scene as? UIWindowScene else { return } - - LucidBannerRegistry.shared.remove(for: windowScene) - lucidBanner = nil - print("[DEBUG] Scene did disconnect") } @@ -220,6 +229,30 @@ class SceneDelegate: UIResponder, UIWindowSceneDelegate { } func sceneDidBecomeActive(_ scene: UIScene) { + + let session = SceneManager.shared.getSession(scene: scene) + let controller = SceneManager.shared.getController(scene: scene) + nkLog(info: "Scene did become active") + + let oldVersion = UserDefaults.standard.value(forKey: NCSettingsBundleHelper.SettingsBundleKeys.BuildVersionKey) as? String + AppUpdater().checkForUpdate() + AnalyticsHelper.shared.trackAppVersion(oldVersion: oldVersion) + if let userAccount = NCManageDatabase.shared.getActiveTableAccount() { + AnalyticsHelper.shared.trackUsedStorageData(quotaUsed: userAccount.quotaUsed) + } + + NCSettingsBundleHelper.setVersionAndBuildNumber() + NCSettingsBundleHelper.checkAndExecuteSettings(delay: 0.5) + + // Re-evaluate in-app messages when scene becomes active +// Task { @MainActor in + MoEngageAnalytics.shared.displayInAppNotificationSafely(reason: "scene did become active") +// } + +// DispatchQueue.main.asyncAfter(deadline: .now() + 1.0) { +// MoEngageAnalytics.shared.requestAppStoreReview() +// } + hidePrivacyProtectionWindow() if !NextcloudKit.shared.isNetworkReachable(), @@ -243,7 +276,13 @@ class SceneDelegate: UIResponder, UIWindowSceneDelegate { } if NCPreferences().privacyScreenEnabled { - showPrivacyProtectionWindow() + if SwiftEntryKit.isCurrentlyDisplaying { + SwiftEntryKit.dismiss { + self.showPrivacyProtectionWindow() + } + } else { + showPrivacyProtectionWindow() + } } } @@ -294,7 +333,7 @@ class SceneDelegate: UIResponder, UIWindowSceneDelegate { return true } group.addTask { - try? await Task.sleep(for: .seconds(25)) + try? await Task.sleep(nanoseconds: 25 * 1_000_000_000) // ~25s return false } return await group.next() ?? false @@ -320,16 +359,15 @@ class SceneDelegate: UIResponder, UIWindowSceneDelegate { return } - func getMatchedAccount(user: String, url: String, account: String? = nil) async -> tableAccount? { + func getMatchedAccount(userId: String, url: String) async -> tableAccount? { let tblAccounts = await NCManageDatabase.shared.getAllTableAccountAsync() for tblAccount in tblAccounts { - let host = URL(string: tblAccount.urlBase)?.host() ?? "" - - if (account == tblAccount.account) || (url.contains(host) && user == tblAccount.userId) { + let urlBase = URL(string: tblAccount.urlBase) + if url.contains(urlBase?.host ?? "") && userId == tblAccount.userId { await NCAccount().changeAccount(tblAccount.account, userProfile: nil, controller: controller) // wait switch account - try? await Task.sleep(for: .seconds(1)) + try? await Task.sleep(nanoseconds: 1_000_000_000) return tblAccount } } @@ -359,7 +397,7 @@ class SceneDelegate: UIResponder, UIWindowSceneDelegate { subtitle: "_no_assistant_installed_", systemImage: "sparkles", imageAnimation: .none, - imageColor: .systemBlue + imageColor: NCBrandColor.shared.customer ) } } @@ -381,12 +419,8 @@ class SceneDelegate: UIResponder, UIWindowSceneDelegate { } Task { - if await getMatchedAccount(user: userScheme, url: urlScheme) == nil { - let message = String( - format: NSLocalizedString("account_does_not_exist", comment: ""), - userScheme, - ) - + if await getMatchedAccount(userId: userScheme, url: urlScheme) == nil { + let message = NSLocalizedString("_the_account_", comment: "") + " " + userScheme + NSLocalizedString("_of_", comment: "") + " " + urlScheme + " " + NSLocalizedString("_does_not_exist_", comment: "") let alertController = UIAlertController(title: NSLocalizedString("_info_", comment: ""), message: message, preferredStyle: .alert) alertController.addAction(UIAlertAction(title: NSLocalizedString("_ok_", comment: ""), style: .default, handler: { _ in })) @@ -443,42 +477,36 @@ class SceneDelegate: UIResponder, UIWindowSceneDelegate { else if scheme == self.global.appScheme && action == "open-file" { if let urlComponents = URLComponents(url: url, resolvingAgainstBaseURL: false) { + var serverUrl: String = "" + var fileName: String = "" let queryItems = urlComponents.queryItems guard let userScheme = queryItems?.filter({ $0.name == "user" }).first?.value, - // let pathScheme = queryItems?.filter({ $0.name == "path" }).first?.value, - let linkScheme = queryItems?.filter({ $0.name == "link" }).first?.value else { - return - } - let domain = URL(string: linkScheme)?.host ?? "" - let accountScheme = queryItems?.filter({ $0.name == "account" }).first?.value + let pathScheme = queryItems?.filter({ $0.name == "path" }).first?.value, + let linkScheme = queryItems?.filter({ $0.name == "link" }).first?.value else { return} Task { - guard let tblAccount = await getMatchedAccount(user: userScheme, url: linkScheme, account: accountScheme) else { + guard let tblAccount = await getMatchedAccount(userId: userScheme, url: linkScheme) else { + guard let domain = URL(string: linkScheme)?.host else { return } - let message = String(format: NSLocalizedString("_account_not_available_", comment: ""), userScheme, domain) + fileName = (pathScheme as NSString).lastPathComponent + let message = String(format: NSLocalizedString("_account_not_available_", comment: ""), userScheme, domain, fileName) let alertController = UIAlertController(title: NSLocalizedString("_info_", comment: ""), message: message, preferredStyle: .alert) alertController.addAction(UIAlertAction(title: NSLocalizedString("_ok_", comment: ""), style: .default, handler: { _ in })) controller.present(alertController, animated: true) return } + let davFiles = "remote.php/dav/files/" + tblAccount.userId - let results = await NextcloudKit.shared.getFileFromFileIdAsync(link: linkScheme, - account: tblAccount.account) - if results.error == .success, let file = results.file { - let metadata = await NCManageDatabaseCreateMetadata().convertFileToMetadataAsync(file) - await NCManageDatabase.shared.addMetadataAsync(metadata) - if metadata.hasPreview { - let results = await NextcloudKit.shared.downloadPreviewAsync(fileId: metadata.fileId, etag: metadata.etag, account: metadata.account) - if results.error == .success, - let data = results.responseData?.data { - NCUtility().createImageFileFrom(data: data, metadata: metadata) - } - } - await NCNetworking.shared.openFileView(serverUrl: metadata.serverUrl, - metadata: metadata, - sceneIdentifier: controller.sceneIdentifier) + if pathScheme.contains("/") { + fileName = (pathScheme as NSString).lastPathComponent + serverUrl = tblAccount.urlBase + "/" + davFiles + "/" + (pathScheme as NSString).deletingLastPathComponent + } else { + fileName = pathScheme + serverUrl = tblAccount.urlBase + "/" + davFiles } + + NCNetworking.shared.openFileViewInFolder(serverUrl: serverUrl, fileNameBlink: nil, fileNameOpen: fileName, sceneIdentifier: controller.sceneIdentifier) } } @@ -497,18 +525,29 @@ class SceneDelegate: UIResponder, UIWindowSceneDelegate { } Task { - _ = await getMatchedAccount(user: userScheme, url: urlScheme) + _ = await getMatchedAccount(userId: userScheme, url: urlScheme) } } else if let action { if DeepLink(rawValue: action) != nil { NCDeepLinkHandler().parseDeepLink(url, controller: controller) } } else { - scene.open(url, options: nil) + let applicationHandle = NCApplicationHandle() + let isHandled = applicationHandle.applicationOpenURL(url) + if isHandled { + return + } else { + scene.open(url, options: nil) + } } } private func showPrivacyProtectionWindow() { + guard privacyProtectionWindow == nil else { + privacyProtectionWindow?.isHidden = false + return + } + guard let windowScene = self.window?.windowScene else { return } @@ -543,16 +582,19 @@ class SceneDelegate: UIResponder, UIWindowSceneDelegate { } } + // Re-evaluate in-app messages after activating scene for account + MoEngageAnalytics.shared.displayInAppNotificationSafely(reason: "activated scene for account") + Task { - try? await Task.sleep(for: .seconds(1)) + try? await Task.sleep(nanoseconds: 1_000_000_000) let num = await NCAutoUpload.shared.initAutoUpload() nkLog(start: "Auto upload with \(num) photo") - try? await Task.sleep(for: .seconds(1.5)) + try? await Task.sleep(nanoseconds: 1_500_000_000) await NCService().startRequestServicesServer(account: account, controller: controller) - try? await Task.sleep(for: .seconds(2)) + try? await Task.sleep(nanoseconds: 2_000_000_000) await NCNetworking.shared.verifyZombie() } @@ -603,8 +645,7 @@ extension SceneDelegate: NCAccountRequestDelegate { // MARK: - Scene Manager -@MainActor -final class SceneManager { +final class SceneManager: @unchecked Sendable { static let shared = SceneManager() private var sceneController: [NCMainTabBarController: UIScene] = [:] @@ -648,9 +689,7 @@ final class SceneManager { } func getWindow(scene: UIScene?) -> UIWindow? { - guard let windowScene = scene as? UIWindowScene else { return nil } - - return windowScene.keyWindow + return (scene as? UIWindowScene)?.keyWindow } func getWindow(controller: UITabBarController?) -> UIWindow? { @@ -682,7 +721,7 @@ final class SceneManager { .compactMap { $0 as? UIWindowScene } .first } - + func getWindow(sceneIdentifier: String?) -> UIWindow? { // Try exact match via your registry if let sceneIdentifier, @@ -733,3 +772,4 @@ final class SceneManager { return NCSession.shared.getSession(controller: controller) } } +