From 1950a7a2a1ca5a73b4b22b7d74e9e70851f17a30 Mon Sep 17 00:00:00 2001 From: Hugo Pezziardi Date: Fri, 22 Nov 2024 18:21:36 +0100 Subject: [PATCH 1/3] =?UTF-8?q?=E2=9C=A8=20(UtilsKit):=20Add=20UpdateStatu?= =?UTF-8?q?sFetcher=20custom=20with=20bonus=20OS=20check?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Sources/UpdateStatusFetcher.swift | 122 ++++++++++++++++++ 1 file changed, 122 insertions(+) create mode 100644 Modules/UtilsKit/Sources/UpdateStatusFetcher.swift diff --git a/Modules/UtilsKit/Sources/UpdateStatusFetcher.swift b/Modules/UtilsKit/Sources/UpdateStatusFetcher.swift new file mode 100644 index 0000000000..18cfd1a674 --- /dev/null +++ b/Modules/UtilsKit/Sources/UpdateStatusFetcher.swift @@ -0,0 +1,122 @@ +// Leka - iOS Monorepo +// Copyright APF France handicap +// SPDX-License-Identifier: Apache-2.0 + +// AppUpdate folder is heavily inspired by: +// https://github.com/AvdLee/AppUpdately +// AvdLee/AppUpdately License: MIT + +import Combine +import Foundation + +// MARK: - AppMetadata + +struct AppMetadata: Codable { + let trackViewURL: URL + let version: String + let minimumOsVersion: String +} + +// MARK: - AppMetadataResults + +struct AppMetadataResults: Codable { + let results: [AppMetadata] +} + +// MARK: - UpdateStatusFetcher + +public struct UpdateStatusFetcher { + // MARK: Lifecycle + + public init(bundleIdentifier: String = Bundle.main.bundleIdentifier!, urlSession: URLSession = .shared) { + self.url = URL(string: self.prefixURL + "\(bundleIdentifier)")! + self.bundleIdentifier = bundleIdentifier + self.urlSession = urlSession + } + + // MARK: Public + + public enum Status: Equatable { + case newerVersion + case upToDate + case updateAvailable(version: String, storeURL: URL) + case underMinimumOsVersion + } + + public enum FetchError: LocalizedError { + case metadata + case bundleShortVersion + + // MARK: Public + + public var errorDescription: String? { + switch self { + case .metadata: + "Metadata could not be found" + case .bundleShortVersion: + "Bundle short version could not be found" + } + } + } + + public func fetch(_ completion: @escaping (Swift.Result) -> Void) -> AnyCancellable { + self.urlSession + .dataTaskPublisher(for: self.url) + .map(\.data) + .decode(type: AppMetadataResults.self, decoder: self.decoder) + .tryMap { metadataResults -> Status in + guard let appMetadata = metadataResults.results.first else { + throw FetchError.metadata + } + return try self.updateStatus(for: appMetadata) + } + .sink { completionStatus in + switch completionStatus { + case let .failure(error): + completion(.failure(error)) + case .finished: + break + } + } receiveValue: { status in + completion(.success(status)) + } + } + + // MARK: Internal + + let url: URL + var currentVersionProvider: () -> String? = { + Bundle.main.object(forInfoDictionaryKey: "CFBundleShortVersionString") as? String + } + + // MARK: Private + + private let prefixURL = "https://itunes.apple.com/lookup?bundleId=" + private let bundleIdentifier: String + private let decoder: JSONDecoder = .init() + private let urlSession: URLSession + + private func updateStatus(for appMetadata: AppMetadata) throws -> Status { + guard let currentVersion = currentVersionProvider() else { + throw UpdateStatusFetcher.FetchError.bundleShortVersion + } + + // Get the device's current iOS version + let deviceOSVersion = ProcessInfo.processInfo.operatingSystemVersion + let deviceOSString = "\(deviceOSVersion.majorVersion).\(deviceOSVersion.minorVersion).\(deviceOSVersion.patchVersion)" + + // Compare the required iOS version with the device's current version + if deviceOSString.compare(appMetadata.minimumOsVersion, options: .numeric) == .orderedAscending { + return UpdateStatusFetcher.Status.underMinimumOsVersion + } + + switch currentVersion.compare(appMetadata.version) { + case .orderedDescending: + return UpdateStatusFetcher.Status.newerVersion + case .orderedSame: + return UpdateStatusFetcher.Status.upToDate + case .orderedAscending: + return UpdateStatusFetcher.Status.updateAvailable(version: appMetadata.version, storeURL: appMetadata.trackViewURL) + } + } +} From b3de668289c91e9779b69ac54e5fca169b1bbf49 Mon Sep 17 00:00:00 2001 From: Hugo Pezziardi Date: Fri, 22 Nov 2024 18:22:54 +0100 Subject: [PATCH 2/3] =?UTF-8?q?=E2=99=BB=EF=B8=8F=20(LekaApp):=20Use=20cus?= =?UTF-8?q?tom=20UpdateStatusFetcher?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Resources/l10n/Localizable.xcstrings | 108 +++++++++--------- Apps/LekaApp/Sources/MainApp.swift | 21 +--- .../Views/MainView/MainView+l10n.swift | 8 +- .../Sources/Views/MainView/MainView.swift | 17 +-- .../Views/MainView/SettingsLabel.swift | 7 +- .../Sources/Views/Settings/SettingsView.swift | 2 +- 6 files changed, 81 insertions(+), 82 deletions(-) diff --git a/Apps/LekaApp/Resources/l10n/Localizable.xcstrings b/Apps/LekaApp/Resources/l10n/Localizable.xcstrings index 3288aa39ed..ab7d70e16b 100644 --- a/Apps/LekaApp/Resources/l10n/Localizable.xcstrings +++ b/Apps/LekaApp/Resources/l10n/Localizable.xcstrings @@ -1748,6 +1748,60 @@ } } }, + "lekaapp.main_view.app_update_alert.action": { + "comment": "The action button of the alert to inform the user that an update is available", + "extractionState": "extracted_with_value", + "localizations": { + "en": { + "stringUnit": { + "state": "new", + "value": "Update now" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Mettre \u00e0 jour maintenant" + } + } + } + }, + "lekaapp.main_view.app_update_alert.message": { + "comment": "The message of the alert to inform the user that an update is available", + "extractionState": "extracted_with_value", + "localizations": { + "en": { + "stringUnit": { + "state": "new", + "value": "Enjoy new features by updating to the latest version of Leka!" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Profitez des nouvelles fonctionnalit\u00e9s en t\u00e9l\u00e9chargeant la nouvelle mise \u00e0 jour de l\u2019app Leka !" + } + } + } + }, + "lekaapp.main_view.app_update_alert.title": { + "comment": "The title of the alert to inform the user that an update is available", + "extractionState": "extracted_with_value", + "localizations": { + "en": { + "stringUnit": { + "state": "new", + "value": "New update available" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Nouvelle mise \u00e0 jour disponible" + } + } + } + }, "lekaapp.main_view.detailView.disconnected_library_message": { "comment": "The message to invite users to connect to display the Library", "extractionState": "extracted_with_value", @@ -2162,60 +2216,6 @@ } } }, - "lekaapp.main_view.update_alert.action": { - "comment": "The action button of the alert to inform the user that an update is available", - "extractionState": "extracted_with_value", - "localizations": { - "en": { - "stringUnit": { - "state": "new", - "value": "Update now" - } - }, - "fr": { - "stringUnit": { - "state": "translated", - "value": "Mettre \u00e0 jour maintenant" - } - } - } - }, - "lekaapp.main_view.update_alert.message": { - "comment": "The message of the alert to inform the user that an update is available", - "extractionState": "extracted_with_value", - "localizations": { - "en": { - "stringUnit": { - "state": "new", - "value": "Enjoy new features by updating to the latest version of Leka!" - } - }, - "fr": { - "stringUnit": { - "state": "translated", - "value": "Profitez des nouvelles fonctionnalit\u00e9s en t\u00e9l\u00e9chargeant la nouvelle mise \u00e0 jour de l\u2019app Leka !" - } - } - } - }, - "lekaapp.main_view.update_alert.title": { - "comment": "The title of the alert to inform the user that an update is available", - "extractionState": "extracted_with_value", - "localizations": { - "en": { - "stringUnit": { - "state": "new", - "value": "New update available" - } - }, - "fr": { - "stringUnit": { - "state": "translated", - "value": "Nouvelle mise \u00e0 jour disponible" - } - } - } - }, "lekaapp.news_view.description": { "comment": "News description", "extractionState": "extracted_with_value", diff --git a/Apps/LekaApp/Sources/MainApp.swift b/Apps/LekaApp/Sources/MainApp.swift index 1788abf92f..518247279d 100644 --- a/Apps/LekaApp/Sources/MainApp.swift +++ b/Apps/LekaApp/Sources/MainApp.swift @@ -4,7 +4,6 @@ import AccountKit import AnalyticsKit -import AppUpdately import Combine import ContentKit import DesignKit @@ -12,6 +11,7 @@ import FirebaseKit import LocalizationKit import LogKit import SwiftUI +import UtilsKit let log = LogKit.createLoggerFor(app: "LekaApp") @@ -48,7 +48,7 @@ struct LekaApp: App { class UpdateStatus: ObservableObject { static let shared = UpdateStatus() - @Published var isUpdateAvailable: Bool = false + @Published var status: UpdateStatusFetcher.Status = .upToDate } @UIApplicationDelegateAdaptor(AppDelegate.self) var delegate @@ -87,23 +87,14 @@ struct LekaApp: App { guard let status = try? result.get() else { DispatchQueue.main.asyncAfter(deadline: .now() + 2.0) { self.showMainView = true - self.appUpdateStatus.isUpdateAvailable = false + self.appUpdateStatus.status = .upToDate } return } - switch status { - case .upToDate, - .newerVersion: - DispatchQueue.main.asyncAfter(deadline: .now() + 2.0) { - self.showMainView = true - self.appUpdateStatus.isUpdateAvailable = false - } - case .updateAvailable: - DispatchQueue.main.asyncAfter(deadline: .now() + 2.0) { - self.showMainView = true - self.appUpdateStatus.isUpdateAvailable = true - } + DispatchQueue.main.asyncAfter(deadline: .now() + 2.0) { + self.showMainView = true + self.appUpdateStatus.status = status } } } diff --git a/Apps/LekaApp/Sources/Views/MainView/MainView+l10n.swift b/Apps/LekaApp/Sources/Views/MainView/MainView+l10n.swift index 6945b6b533..e3a4a6ab97 100644 --- a/Apps/LekaApp/Sources/Views/MainView/MainView+l10n.swift +++ b/Apps/LekaApp/Sources/Views/MainView/MainView+l10n.swift @@ -45,10 +45,10 @@ extension l10n { static let disconnectedLibraryMessage = LocalizedString("lekaapp.main_view.detailView.disconnected_library_message", value: "Log in to your account to access your personal library.", comment: "The message to invite users to connect to display the Library") } - enum UpdateAlert { - static let title = LocalizedString("lekaapp.main_view.update_alert.title", value: "New update available", comment: "The title of the alert to inform the user that an update is available") - static let message = LocalizedString("lekaapp.main_view.update_alert.message", value: "Enjoy new features by updating to the latest version of Leka!", comment: "The message of the alert to inform the user that an update is available") - static let action = LocalizedString("lekaapp.main_view.update_alert.action", value: "Update now", comment: "The action button of the alert to inform the user that an update is available") + enum AppUpdateAlert { + static let title = LocalizedString("lekaapp.main_view.app_update_alert.title", value: "New update available", comment: "The title of the alert to inform the user that an update is available") + static let message = LocalizedString("lekaapp.main_view.app_update_alert.message", value: "Enjoy new features by updating to the latest version of Leka!", comment: "The message of the alert to inform the user that an update is available") + static let action = LocalizedString("lekaapp.main_view.app_update_alert.action", value: "Update now", comment: "The action button of the alert to inform the user that an update is available") } } } diff --git a/Apps/LekaApp/Sources/Views/MainView/MainView.swift b/Apps/LekaApp/Sources/Views/MainView/MainView.swift index 54b18b4012..c7cff34d20 100644 --- a/Apps/LekaApp/Sources/Views/MainView/MainView.swift +++ b/Apps/LekaApp/Sources/Views/MainView/MainView.swift @@ -122,11 +122,11 @@ struct MainView: View { } .listStyle(.sidebar) } - .alert(isPresented: self.$showingUpdateAlert) { + .alert(isPresented: self.$showingAppUpdateAlert) { Alert( - title: Text(l10n.MainView.UpdateAlert.title), - message: Text(l10n.MainView.UpdateAlert.message), - primaryButton: .default(Text(l10n.MainView.UpdateAlert.action), action: { + title: Text(l10n.MainView.AppUpdateAlert.title), + message: Text(l10n.MainView.AppUpdateAlert.message), + primaryButton: .default(Text(l10n.MainView.AppUpdateAlert.action), action: { AnalyticsManager.logEventAppUpdateOpenAppStore() if let url = URL(string: "https://apps.apple.com/app/leka/id6446940339") { UIApplication.shared.open(url) @@ -317,8 +317,11 @@ struct MainView: View { } .onDisappear { DispatchQueue.main.asyncAfter(deadline: .now() + 2) { - if self.appUpdateStatus.isUpdateAvailable { - self.showingUpdateAlert = true + switch self.appUpdateStatus.status { + case .updateAvailable: + self.showingAppUpdateAlert = true + default: + break } } } @@ -417,7 +420,7 @@ struct MainView: View { @StateObject private var rootAccountViewModel = RootAccountManagerViewModel() @StateObject var appUpdateStatus: LekaApp.UpdateStatus = .shared - @State private var showingUpdateAlert: Bool = false + @State private var showingAppUpdateAlert: Bool = false private var persistentDataManager: PersistentDataManager = .shared private var caregiverManager: CaregiverManager = .shared diff --git a/Apps/LekaApp/Sources/Views/MainView/SettingsLabel.swift b/Apps/LekaApp/Sources/Views/MainView/SettingsLabel.swift index d3368ca885..a2be9305a1 100644 --- a/Apps/LekaApp/Sources/Views/MainView/SettingsLabel.swift +++ b/Apps/LekaApp/Sources/Views/MainView/SettingsLabel.swift @@ -28,7 +28,12 @@ struct SettingsLabel: View { .background(.red) .clipShape(.circle) .offset(x: 95, y: -20) - .opacity(self.appUpdateStatus.isUpdateAvailable ? 1 : 0) + .opacity({ + if case .updateAvailable = self.appUpdateStatus.status { + return 1.0 + } + return 0.0 + }()) ) } diff --git a/Apps/LekaApp/Sources/Views/Settings/SettingsView.swift b/Apps/LekaApp/Sources/Views/Settings/SettingsView.swift index 8a1865060e..341e782aff 100644 --- a/Apps/LekaApp/Sources/Views/Settings/SettingsView.swift +++ b/Apps/LekaApp/Sources/Views/Settings/SettingsView.swift @@ -23,7 +23,7 @@ struct SettingsView: View { var body: some View { Form { - if self.appUpdateStatus.isUpdateAvailable { + if case .updateAvailable = self.appUpdateStatus.status { Section { VStack(alignment: .center) { HStack(spacing: 20) { From e495e4fc05c2d70f214d8c12a9ea1ae5a7a90212 Mon Sep 17 00:00:00 2001 From: Hugo Pezziardi Date: Fri, 22 Nov 2024 18:20:04 +0100 Subject: [PATCH 3/3] =?UTF-8?q?=E2=9E=96=20(LekaApp):=20Remove=20AppUpdate?= =?UTF-8?q?ly=20package?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Apps/LekaApp/Project.swift | 2 -- Tuist/Package.resolved | 9 --------- Tuist/Package.swift | 4 ---- 3 files changed, 15 deletions(-) diff --git a/Apps/LekaApp/Project.swift b/Apps/LekaApp/Project.swift index 24c6060524..afe1d1f77a 100644 --- a/Apps/LekaApp/Project.swift +++ b/Apps/LekaApp/Project.swift @@ -75,8 +75,6 @@ let project = Project.app( .project(target: "LogKit", path: Path("../../Modules/LogKit")), .project(target: "RobotKit", path: Path("../../Modules/RobotKit")), .project(target: "UtilsKit", path: Path("../../Modules/UtilsKit")), - - .external(name: "AppUpdately"), .external(name: "DeviceKit"), .external(name: "Fit"), .external(name: "MarkdownUI"), diff --git a/Tuist/Package.resolved b/Tuist/Package.resolved index 3e2f2c0f7c..b9631734c6 100644 --- a/Tuist/Package.resolved +++ b/Tuist/Package.resolved @@ -18,15 +18,6 @@ "version" : "11.0.1" } }, - { - "identity" : "appupdately", - "kind" : "remoteSourceControl", - "location" : "https://github.com/AvdLee/AppUpdately", - "state" : { - "branch" : "main", - "revision" : "697c19e356241dcd097adde7f51398bdf18bf1ca" - } - }, { "identity" : "audiokit", "kind" : "remoteSourceControl", diff --git a/Tuist/Package.swift b/Tuist/Package.swift index 4c0071f20a..5a68ee64aa 100644 --- a/Tuist/Package.swift +++ b/Tuist/Package.swift @@ -70,10 +70,6 @@ let package = Package( url: "https://github.com/lukepistrol/SFSymbolsMacro", exact: "0.5.3" ), - .package( - url: "https://github.com/AvdLee/AppUpdately", - branch: "main" - ), ] )