diff --git a/p2p_wallet/Common/Services/ApplicationUpdate/ApplicationUpdateManager.swift b/p2p_wallet/Common/Services/ApplicationUpdate/ApplicationUpdateManager.swift new file mode 100644 index 000000000..93821955c --- /dev/null +++ b/p2p_wallet/Common/Services/ApplicationUpdate/ApplicationUpdateManager.swift @@ -0,0 +1,62 @@ +import Combine +import Foundation + +class ApplicationUpdateManager { + enum State { + case updateAvailable(Version) + case noUpdate + } + + private let provider: ApplicationUpdateProvider + + init(provider: ApplicationUpdateProvider) { + self.provider = provider + } + + var currentInstalledVersion: Version? { + let appVersionKey = "CFBundleShortVersionString" + guard let appVersionValue = Bundle.main.object(forInfoDictionaryKey: appVersionKey) as? String else { + return nil + } + + return try? Version(from: appVersionValue) + } + + func isUpdateAvailable() async -> State { + guard + let appVersion = currentInstalledVersion, + let storeAppVersion = try? await provider.info() + else { + return .noUpdate + } + + print("[ApplicationUpdateManager]", appVersion, storeAppVersion) + + // Check + if storeAppVersion.major > appVersion.major { + return .updateAvailable(storeAppVersion) + } else if storeAppVersion.minor > appVersion.minor { + return .updateAvailable(storeAppVersion) + } else if storeAppVersion.patch > appVersion.patch { + return .updateAvailable(storeAppVersion) + } + + return .noUpdate + } + + func awareUser(version: Version) async { + UserDefaults.standard.set(version.string, forKey: "application_user_awareness") + } + + func isUserAwareAboutUpdate(version: Version) async -> Bool { + guard let userAwareness = UserDefaults.standard.object(forKey: "application_user_awareness") as? String else { + return false + } + + if version.string == userAwareness { + return true + } else { + return false + } + } +} diff --git a/p2p_wallet/Common/Services/ApplicationUpdate/ApplicationUpdateProvider.swift b/p2p_wallet/Common/Services/ApplicationUpdate/ApplicationUpdateProvider.swift new file mode 100644 index 000000000..3d147a005 --- /dev/null +++ b/p2p_wallet/Common/Services/ApplicationUpdate/ApplicationUpdateProvider.swift @@ -0,0 +1,19 @@ +import Firebase +import Foundation + +protocol ApplicationUpdateProvider { + func info() async throws -> Version +} + +class FirebaseApplicationUpdateProvider: ApplicationUpdateProvider { + func info() async throws -> Version { + let remoteConfig = RemoteConfig.remoteConfig() + let appVersion = remoteConfig.configValue(forKey: "app_version", source: .remote).stringValue + + guard let appVersion else { + throw Version.VersionError.invalidFormat + } + + return try Version(from: appVersion) + } +} diff --git a/p2p_wallet/Common/Services/ApplicationUpdate/Version.swift b/p2p_wallet/Common/Services/ApplicationUpdate/Version.swift new file mode 100644 index 000000000..69fd8e7f9 --- /dev/null +++ b/p2p_wallet/Common/Services/ApplicationUpdate/Version.swift @@ -0,0 +1,48 @@ +import Foundation + +struct Version: Decodable { + // MARK: - Enumerations + + enum VersionError: Error { + case invalidFormat + } + + // MARK: - Public properties + + let major: Int + let minor: Int + let patch: Int + + // MARK: - Init + + init(from decoder: Decoder) throws { + do { + let container = try decoder.singleValueContainer() + let version = try container.decode(String.self) + try self.init(from: version) + } catch { + throw VersionError.invalidFormat + } + } + + init(from version: String) throws { + let versionComponents = version.components(separatedBy: ".").map { Int($0) } + guard versionComponents.count == 3 else { + throw VersionError.invalidFormat + } + + guard let major = versionComponents[0], let minor = versionComponents[1], + let patch = versionComponents[2] + else { + throw VersionError.invalidFormat + } + + self.major = major + self.minor = minor + self.patch = patch + } + + var string: String { + "\(major).\(minor).\(patch)" + } +} diff --git a/p2p_wallet/Injection/Resolver+registerAllServices.swift b/p2p_wallet/Injection/Resolver+registerAllServices.swift index 2da98573d..20206bec9 100644 --- a/p2p_wallet/Injection/Resolver+registerAllServices.swift +++ b/p2p_wallet/Injection/Resolver+registerAllServices.swift @@ -81,6 +81,11 @@ extension Resolver: ResolverRegistering { .implements(KeyAppTokenProvider.self) .scope(.application) + register { + ApplicationUpdateManager(provider: FirebaseApplicationUpdateProvider()) + } + .scope(.application) + register { DeviceShareMigrationService( isWeb3AuthUser: resolve(UserWalletManager.self) diff --git a/p2p_wallet/Resources/Base.lproj/Localizable.strings b/p2p_wallet/Resources/Base.lproj/Localizable.strings index ff467ad19..cfc778074 100644 --- a/p2p_wallet/Resources/Base.lproj/Localizable.strings +++ b/p2p_wallet/Resources/Base.lproj/Localizable.strings @@ -599,6 +599,7 @@ "Token 2022 transfer fee" = "Token 2022 transfer fee"; "Calculated by subtracting the token 2022 transfer fee from your balance" = "Calculated by subtracting the token 2022 transfer fee from your balance"; "Calculated by subtracting the token 2022 transfer fee and account creation fee from your balance" = "Calculated by subtracting the token 2022 transfer fee and account creation fee from your balance"; +"Update available" = "Update available"; "Referral program" = "Referral program"; "%@ all the time" = "%@ all the time"; "Here’s how do we count your profits for total balance and every single token" = "Here’s how do we count your profits for total balance and every single token"; diff --git a/p2p_wallet/Resources/en.lproj/Localizable.strings b/p2p_wallet/Resources/en.lproj/Localizable.strings index 06703bbd5..b0936a0d6 100644 --- a/p2p_wallet/Resources/en.lproj/Localizable.strings +++ b/p2p_wallet/Resources/en.lproj/Localizable.strings @@ -587,6 +587,7 @@ "Token 2022 transfer fee" = "Token 2022 transfer fee"; "Calculated by subtracting the token 2022 transfer fee from your balance" = "Calculated by subtracting the token 2022 transfer fee from your balance"; "Calculated by subtracting the token 2022 transfer fee and account creation fee from your balance" = "Calculated by subtracting the token 2022 transfer fee and account creation fee from your balance"; +"Update available" = "Update available"; "Referral program" = "Referral program"; "%@ all the time" = "%@ all the time"; "Here’s how do we count your profits for total balance and every single token" = "Here’s how do we count your profits for total balance and every single token"; diff --git a/p2p_wallet/Scenes/DebugMenu/View/DebugMenuView.swift b/p2p_wallet/Scenes/DebugMenu/View/DebugMenuView.swift index 669f3beaa..f7793b05b 100644 --- a/p2p_wallet/Scenes/DebugMenu/View/DebugMenuView.swift +++ b/p2p_wallet/Scenes/DebugMenu/View/DebugMenuView.swift @@ -82,6 +82,13 @@ struct DebugMenuView: View { DebugTextField(title: "Bridge:", content: $globalAppState.bridgeEndpoint) DebugTextField(title: "Token:", content: $globalAppState.tokenEndpoint) Toggle("Prefer direct swap", isOn: $globalAppState.preferDirectSwap) + + Button { + Task { + UserDefaults.standard.set(nil, forKey: "application_user_awareness") + } + } label: { Text("Clean version alert") } + Button { Task { ResolverScope.session.reset() diff --git a/p2p_wallet/Scenes/Main/Crypto/Container/CryptoView.swift b/p2p_wallet/Scenes/Main/Crypto/Container/CryptoView.swift index bb874c9ca..fc974a650 100644 --- a/p2p_wallet/Scenes/Main/Crypto/Container/CryptoView.swift +++ b/p2p_wallet/Scenes/Main/Crypto/Container/CryptoView.swift @@ -92,5 +92,14 @@ struct CryptoView: View { .onReceive(NotificationCenter.default.publisher(for: UIApplication.didBecomeActiveNotification)) { _ in viewModel.viewAppeared() } + .alert(L10n.updateAvailable, isPresented: $viewModel.updateAlert) { + Button(L10n.update, action: { + viewModel.openAppstore() + viewModel.userIsAwareAboutUpdate() + }) + Button(L10n.cancel, role: .cancel, action: { + viewModel.userIsAwareAboutUpdate() + }) + } } } diff --git a/p2p_wallet/Scenes/Main/Crypto/Container/CryptoViewModel.swift b/p2p_wallet/Scenes/Main/Crypto/Container/CryptoViewModel.swift index 3c9885aef..66520a98c 100644 --- a/p2p_wallet/Scenes/Main/Crypto/Container/CryptoViewModel.swift +++ b/p2p_wallet/Scenes/Main/Crypto/Container/CryptoViewModel.swift @@ -7,12 +7,14 @@ import Resolver import Sell import Send import SolanaSwift +import UIKit import Wormhole /// ViewModel of `Crypto` scene final class CryptoViewModel: BaseViewModel, ObservableObject { // MARK: - Dependencies + @Injected private var authenticationHandler: AuthenticationHandlerType @Injected private var solanaAccountsService: SolanaAccountsService @Injected private var ethereumAccountsService: EthereumAccountsService @Injected private var analyticsManager: AnalyticsManager @@ -23,6 +25,7 @@ final class CryptoViewModel: BaseViewModel, ObservableObject { @Injected private var nameStorage: NameStorageType @Injected private var sellDataService: any SellDataService @Injected private var createNameService: CreateNameService + @Injected private var applicationUpdateManager: ApplicationUpdateManager @Injected private var referralService: ReferralProgramService let navigation: PassthroughSubject @@ -35,6 +38,9 @@ final class CryptoViewModel: BaseViewModel, ObservableObject { @Published var state = State.pending @Published var address = "" + @Published var updateAlert: Bool = false + @Published var newVersion: Version? + // MARK: - Initializers init(navigation: PassthroughSubject) { @@ -56,6 +62,19 @@ final class CryptoViewModel: BaseViewModel, ObservableObject { await CryptoAccountsSynchronizationService().refresh() } + func userIsAwareAboutUpdate() { + guard let version = newVersion else { return } + Task { await applicationUpdateManager.awareUser(version: version) } + } + + func openAppstore() { + UIApplication.shared.open( + URL(string: "itms-apps://itunes.apple.com/app/id1605603333")!, + options: [:], + completionHandler: nil + ) + } + func viewAppeared() { if available(.solanaNegativeStatus) { solanaTracker.startTracking() @@ -182,6 +201,30 @@ private extension CryptoViewModel { } .store(in: &subscriptions) + // Update + authenticationHandler.authenticationStatusPublisher + .filter { $0 == nil } + .delay(for: 3, scheduler: RunLoop.main) + .sink { [weak self] _ in + Task { + guard let self else { return } + let result = await self.applicationUpdateManager.isUpdateAvailable() + switch result { + case .noUpdate: + return + case let .updateAvailable(version): + if await self.applicationUpdateManager.isUserAwareAboutUpdate(version: version) { + return + } else { + await MainActor.run { + self.updateAlert = true + self.newVersion = version + } + } + } + } + }.store(in: &subscriptions) + openReferralProgramDetails .map { CryptoNavigation.referral } .sink { [weak self] navigation in diff --git a/p2p_wallet/Scenes/Main/NewHome/HomeView.swift b/p2p_wallet/Scenes/Main/NewHome/HomeView.swift index d9bdb60cb..383303907 100644 --- a/p2p_wallet/Scenes/Main/NewHome/HomeView.swift +++ b/p2p_wallet/Scenes/Main/NewHome/HomeView.swift @@ -38,7 +38,7 @@ struct HomeView: View { } } - func navigation(@ViewBuilder content: @escaping () -> Content) -> some View { + func navigation(@ViewBuilder content: @escaping () -> some View) -> some View { NavigationView { ZStack { Color(.smoke)