diff --git a/Gem/App.swift b/Gem/App.swift index 19da7a3dd..dec23b71f 100644 --- a/Gem/App.swift +++ b/Gem/App.swift @@ -28,7 +28,6 @@ struct GemApp: App { RootScene( model: RootSceneViewModel( walletConnectorPresenter: resolver.services.walletConnectorManager.presenter, - onstartAsyncService: resolver.services.onstartAsyncService, onstartWalletService: resolver.services.onstartWalletService, transactionStateService: resolver.services.transactionStateService, connectionsService: resolver.services.connectionsService, @@ -38,8 +37,11 @@ struct GemApp: App { walletService: resolver.services.walletService, walletsService: resolver.services.walletsService, nameService: resolver.services.nameService, + releaseAlertService: resolver.services.releaseAlertService, + rateService: resolver.services.rateService, eventPresenterService: resolver.services.eventPresenterService, - avatarService: resolver.services.avatarService + avatarService: resolver.services.avatarService, + deviceService: resolver.services.deviceService ) ) .inject(resolver: resolver) @@ -53,6 +55,9 @@ class AppDelegate: NSObject, UIApplicationDelegate, UIWindowSceneDelegate { func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey : Any]? = nil) -> Bool { AppResolver.main.services.onstartService.configure() + Task { + await AppResolver.main.services.onstartAsyncService.run() + } return true } diff --git a/Gem/Services/AppResolver+Services.swift b/Gem/Services/AppResolver+Services.swift index 4f051f6a3..c0d04bfc9 100644 --- a/Gem/Services/AppResolver+Services.swift +++ b/Gem/Services/AppResolver+Services.swift @@ -54,6 +54,8 @@ extension AppResolver { let swapService: SwapService let subscriptionsService: SubscriptionService let appReleaseService: AppReleaseService + let releaseAlertService: ReleaseAlertService + let rateService: RateService let deviceObserverService: DeviceObserverService let onstartService: OnstartService let onstartAsyncService: OnstartAsyncService @@ -91,6 +93,8 @@ extension AppResolver { avatarService: AvatarService, swapService: SwapService, appReleaseService: AppReleaseService, + releaseAlertService: ReleaseAlertService, + rateService: RateService, subscriptionsService: SubscriptionService, deviceObserverService: DeviceObserverService, onstartService: OnstartService, @@ -128,6 +132,8 @@ extension AppResolver { self.avatarService = avatarService self.swapService = swapService self.appReleaseService = appReleaseService + self.releaseAlertService = releaseAlertService + self.rateService = rateService self.deviceObserverService = deviceObserverService self.subscriptionsService = subscriptionsService self.onstartService = onstartService diff --git a/Gem/Services/ServicesFactory.swift b/Gem/Services/ServicesFactory.swift index b179e513f..f55226a7e 100644 --- a/Gem/Services/ServicesFactory.swift +++ b/Gem/Services/ServicesFactory.swift @@ -154,6 +154,11 @@ struct ServicesFactory { let configService = GemAPIService() let releaseService = AppReleaseService(configService: configService) + let releaseAlertService = ReleaseAlertService( + appReleaseService: releaseService, + preferences: preferences + ) + let rateService = RateService(preferences: preferences) let onStartService = Self.makeOnstartService( assetStore: storeManager.assetStore, @@ -164,17 +169,14 @@ struct ServicesFactory { walletService: walletService ) let onstartAsyncService = Self.makeOnstartAsyncService( - assetStore: storeManager.assetStore, - nodeStore: storeManager.nodeStore, + nodeService: nodeService, preferences: preferences, assetsService: assetsService, - deviceService: deviceService, bannerSetupService: BannerSetupService( store: storeManager.bannerStore, preferences: preferences ), configService: configService, - releaseService: AppReleaseService(configService: configService), swappableChainsProvider: swapService ) let onstartWalletService = Self.makeOnstartWalletService( @@ -246,6 +248,8 @@ struct ServicesFactory { avatarService: avatarService, swapService: swapService, appReleaseService: releaseService, + releaseAlertService: releaseAlertService, + rateService: rateService, subscriptionsService: subscriptionService, deviceObserverService: deviceObserverService, onstartService: onStartService, @@ -461,26 +465,31 @@ extension ServicesFactory { } private static func makeOnstartAsyncService( - assetStore: AssetStore, - nodeStore: NodeStore, + nodeService: NodeService, preferences: Preferences, assetsService: AssetsService, - deviceService: DeviceService, bannerSetupService: BannerSetupService, configService: any GemAPIConfigService, - releaseService: AppReleaseService, swappableChainsProvider: any SwappableChainsProvider ) -> OnstartAsyncService { - OnstartAsyncService( - assetStore: assetStore, - nodeStore: nodeStore, - preferences: preferences, + let importAssetsService = ImportAssetsService( assetsService: assetsService, - deviceService: deviceService, - bannerSetupService: bannerSetupService, - configService: configService, - releaseService: releaseService, - swappableChainsProvider: swappableChainsProvider + assetStore: assetsService.assetStore, + preferences: preferences + ) + + return OnstartAsyncService( + runners: [ + BannerSetupRunner(bannerSetupService: bannerSetupService), + NodeImportRunner(nodeService: nodeService), + AssetsUpdateRunner( + configService: configService, + importAssetsService: importAssetsService, + assetsService: assetsService, + swappableChainsProvider: swappableChainsProvider, + preferences: preferences + ), + ] ) } diff --git a/Gem/ViewModels/RootSceneViewModel.swift b/Gem/ViewModels/RootSceneViewModel.swift index 3d2ae15cd..07e0d8376 100644 --- a/Gem/ViewModels/RootSceneViewModel.swift +++ b/Gem/ViewModels/RootSceneViewModel.swift @@ -5,9 +5,9 @@ import Components import DeviceService import EventPresenterService import Foundation -import GemstonePrimitives import LockManager import Localization +import NameService import Onboarding import Primitives import SwiftUI @@ -16,20 +16,21 @@ import TransactionsService import WalletConnector import WalletService import WalletsService -import NameService import AvatarService @Observable @MainActor final class RootSceneViewModel { - private let onstartAsyncService: OnstartAsyncService private let onstartWalletService: OnstartWalletService private let transactionStateService: TransactionStateService private let connectionsService: ConnectionsService private let deviceObserverService: DeviceObserverService private let notificationHandler: NotificationHandler private let walletsService: WalletsService + private let releaseAlertService: ReleaseAlertService + private let rateService: RateService private let eventPresenterService: EventPresenterService + private let deviceService: DeviceService let walletService: WalletService let nameService: NameService @@ -66,7 +67,6 @@ final class RootSceneViewModel { init( walletConnectorPresenter: WalletConnectorPresenter, - onstartAsyncService: OnstartAsyncService, onstartWalletService: OnstartWalletService, transactionStateService: TransactionStateService, connectionsService: ConnectionsService, @@ -76,11 +76,13 @@ final class RootSceneViewModel { walletService: WalletService, walletsService: WalletsService, nameService: NameService, + releaseAlertService: ReleaseAlertService, + rateService: RateService, eventPresenterService: EventPresenterService, - avatarService: AvatarService + avatarService: AvatarService, + deviceService: DeviceService ) { self.walletConnectorPresenter = walletConnectorPresenter - self.onstartAsyncService = onstartAsyncService self.onstartWalletService = onstartWalletService self.transactionStateService = transactionStateService self.connectionsService = connectionsService @@ -90,8 +92,11 @@ final class RootSceneViewModel { self.walletService = walletService self.walletsService = walletsService self.nameService = nameService + self.releaseAlertService = releaseAlertService + self.rateService = rateService self.eventPresenterService = eventPresenterService self.avatarService = avatarService + self.deviceService = deviceService } } @@ -99,17 +104,12 @@ final class RootSceneViewModel { extension RootSceneViewModel { func setup() { - onstartAsyncService.releaseAction = { [weak self] in - self?.setupUpdateReleaseAlert($0) - } - onstartAsyncService.setup() + rateService.perform() + Task { await checkForUpdate() } + Task { try await deviceService.update() } transactionStateService.setup() - Task { - try await connectionsService.setup() - } - Task { - try await deviceObserverService.startSubscriptionsObserver() - } + Task { try await connectionsService.setup() } + Task { try await deviceObserverService.startSubscriptionsObserver() } } } @@ -158,49 +158,48 @@ extension RootSceneViewModel { isPresentingConnectorError = error.localizedDescription } } - - private func setupUpdateReleaseAlert(_ release: Release) { +} + +// MARK: - Private + +extension RootSceneViewModel { + private func setup(wallet: Wallet) { + onstartWalletService.setup(wallet: wallet) + do { + try walletsService.setup(wallet: wallet) + } catch { + debugLog("RootSceneViewModel setupWallet error: \(error)") + } + } + + private func checkForUpdate() async { + guard let release = await releaseAlertService.checkForUpdate() else { return } + updateVersionAlertMessage = makeUpdateAlert(for: release) + } + + private func makeUpdateAlert(for release: Release) -> AlertMessage { let skipAction = AlertAction( title: Localized.Common.skip, role: .cancel, - action: { [weak self] in - Task { @MainActor in - self?.onstartAsyncService.skipRelease(release.version) - } + action: { [releaseAlertService] in + releaseAlertService.skipRelease(release) } ) let updateAction = AlertAction( title: Localized.UpdateApp.action, isDefaultAction: true, - action: { + action: { [releaseAlertService] in Task { @MainActor in - UIApplication.shared.open(PublicConstants.url(.appStore)) + releaseAlertService.openAppStore() } } ) - let actions = if release.upgradeRequired { - [updateAction] - } else { - [skipAction, updateAction] - } - - updateVersionAlertMessage = AlertMessage( + let actions = release.upgradeRequired ? [updateAction] : [skipAction, updateAction] + + return AlertMessage( title: Localized.UpdateApp.title, message: Localized.UpdateApp.description(release.version), actions: actions ) } } - -// MARK: - Private - -extension RootSceneViewModel { - private func setup(wallet: Wallet) { - onstartWalletService.setup(wallet: wallet) - do { - try walletsService.setup(wallet: wallet) - } catch { - debugLog("RootSceneViewModel setupWallet error: \(error)") - } - } -} diff --git a/Packages/ChainServices/NodeService/NodeService.swift b/Packages/ChainServices/NodeService/NodeService.swift index 82d2db793..43110fa74 100644 --- a/Packages/ChainServices/NodeService/NodeService.swift +++ b/Packages/ChainServices/NodeService/NodeService.swift @@ -54,6 +54,10 @@ public final class NodeService: Sendable { requestedChains.insert(chain) */ } + + public func importDefaultNodes() throws { + try AddNodeService(nodeStore: nodeStore).addNodes() + } } // MARK: - NodeURLFetchable diff --git a/Packages/FeatureServices/AppService/OnstartAsyncService.swift b/Packages/FeatureServices/AppService/OnstartAsyncService.swift index 4da8a2005..6967a5398 100644 --- a/Packages/FeatureServices/AppService/OnstartAsyncService.swift +++ b/Packages/FeatureServices/AppService/OnstartAsyncService.swift @@ -1,148 +1,23 @@ // Copyright (c). Gem Wallet. All rights reserved. import Foundation -import Store -import GemAPI import Primitives -import BannerService -import DeviceService -import Preferences -import AssetsService -import ChainService -import NodeService -public final class OnstartAsyncService: Sendable { +public struct OnstartAsyncService: Sendable { - private let assetStore: AssetStore - private let nodeStore: NodeStore - private let preferences: Preferences - private let configService: any GemAPIConfigService - private let importAssetsService: ImportAssetsService - private let deviceService: DeviceService - private let bannerSetupService: BannerSetupService - private let releaseService: AppReleaseService - private let swappableChainsProvider: any SwappableChainsProvider + private let runners: [any AsyncRunnable] - @MainActor - public var releaseAction: ((Release) -> Void)? - - public init( - assetStore: AssetStore, - nodeStore: NodeStore, - preferences: Preferences, - assetsService: AssetsService, - deviceService: DeviceService, - bannerSetupService: BannerSetupService, - configService: any GemAPIConfigService, - releaseService: AppReleaseService, - swappableChainsProvider: any SwappableChainsProvider - ) { - self.assetStore = assetStore - self.nodeStore = nodeStore - self.preferences = preferences - self.importAssetsService = ImportAssetsService( - assetsService: assetsService, - assetStore: assetStore, - preferences: preferences - ) - self.deviceService = deviceService - self.bannerSetupService = bannerSetupService - self.configService = configService - self.releaseService = releaseService - self.swappableChainsProvider = swappableChainsProvider - } - - public func setup() { - Task { - await migrations() - } - Task { - try bannerSetupService.setup() - } + public init(runners: [any AsyncRunnable]) { + self.runners = runners } - public func migrations() async { - do { - try importNodes() - - let config = try await configService.getConfig() - updateAssetsIfNeeded(config) - checkNewRelease(config) - } catch { - debugLog("Fetching config error: \(error)") - } - - performRate() - updateSwappableAssets() - updateDeviceService() - } - - public func skipRelease(_ version: String) { - preferences.skippedReleaseVersion = version - } - - // MARK: - Private methods - - private func importNodes() throws { - try AddNodeService(nodeStore: nodeStore).addNodes() - } - - private func updateAssetsIfNeeded(_ config: ConfigResponse) { - let versions = config.versions - if versions.fiatOnRampAssets > preferences.fiatOnRampAssetsVersion || versions.fiatOffRampAssets > preferences.fiatOffRampAssetsVersion { - Task { - do { - try await importAssetsService.updateFiatAssets() - debugLog("Update fiat assets version: on ramp: \(versions.fiatOnRampAssets), off ramp: \(versions.fiatOffRampAssets)") - } catch { - debugLog("Update fiat assets error: \(error)") - } + public func run() async { + for runner in runners { + do { + try await runner.run() + } catch { + debugLog("\(runner.id) failed: \(error)") } } - - if versions.swapAssets > preferences.swapAssetsVersion { - Task { - do { - try await importAssetsService.updateSwapAssets() - debugLog("Update swap assets version: \(versions.swapAssets)") - } catch { - debugLog("Update swap assets error: \(error)") - } - } - } - } - - private func checkNewRelease(_ config: ConfigResponse) { - guard let release = releaseService.release(config) else { - return - } - - if let skippedReleaseVersion = preferences.skippedReleaseVersion, - skippedReleaseVersion == release.version { - debugLog("Skipping newer version: \(release.version)") - return - } - - debugLog("Newer version available: \(release)") - Task { @MainActor [weak self] in - self?.releaseAction?(release) - } - } - - private func performRate() { - RateService(preferences: preferences).perform() - } - - private func updateSwappableAssets() { - Task { - let chains = swappableChainsProvider.supportedChains() - try assetStore.setAssetIsSwappable(for: chains.map { $0.id }, value: true) - } - } - - private func updateDeviceService() { - Task { - try await deviceService.update() - } } } diff --git a/Packages/FeatureServices/AppService/RateService.swift b/Packages/FeatureServices/AppService/RateService.swift index b1aaef1bc..2af2a057b 100644 --- a/Packages/FeatureServices/AppService/RateService.swift +++ b/Packages/FeatureServices/AppService/RateService.swift @@ -4,35 +4,34 @@ import Foundation import StoreKit import Preferences -struct RateService { +public struct RateService: Sendable { private let preferences: Preferences - init(preferences: Preferences) { + public init(preferences: Preferences) { self.preferences = preferences } - func perform() { + public func perform() { #if targetEnvironment(simulator) #else if preferences.launchesCount >= 5 && !preferences.rateApplicationShown { Task { @MainActor in - rate() - preferences.rateApplicationShown = true + if rate() { + preferences.rateApplicationShown = true + } } } #endif } -} - -// MARK: - Private -extension RateService { @MainActor - private func rate() { + @discardableResult + private func rate() -> Bool { guard let scene = UIApplication.shared .connectedScenes .first(where: { $0.activationState == .foregroundActive }) as? UIWindowScene - else { return } + else { return false } AppStore.requestReview(in: scene) + return true } } diff --git a/Packages/FeatureServices/AppService/ReleaseAlertService.swift b/Packages/FeatureServices/AppService/ReleaseAlertService.swift new file mode 100644 index 000000000..253d8f092 --- /dev/null +++ b/Packages/FeatureServices/AppService/ReleaseAlertService.swift @@ -0,0 +1,35 @@ +// Copyright (c). Gem Wallet. All rights reserved. + +import Foundation +import GemstonePrimitives +import Preferences +import Primitives +import UIKit + +public struct ReleaseAlertService: Sendable { + private let appReleaseService: AppReleaseService + private let preferences: Preferences + + public init( + appReleaseService: AppReleaseService, + preferences: Preferences + ) { + self.appReleaseService = appReleaseService + self.preferences = preferences + } + + public func checkForUpdate() async -> Release? { + guard let release = try? await appReleaseService.getNewestRelease() else { return nil } + guard preferences.skippedReleaseVersion != release.version else { return nil } + return release + } + + public func skipRelease(_ release: Release) { + preferences.skippedReleaseVersion = release.version + } + + @MainActor + public func openAppStore() { + UIApplication.shared.open(PublicConstants.url(.appStore)) + } +} diff --git a/Packages/FeatureServices/AppService/Runners/AssetsUpdateRunner.swift b/Packages/FeatureServices/AppService/Runners/AssetsUpdateRunner.swift new file mode 100644 index 000000000..0c7881640 --- /dev/null +++ b/Packages/FeatureServices/AppService/Runners/AssetsUpdateRunner.swift @@ -0,0 +1,66 @@ +// Copyright (c). Gem Wallet. All rights reserved. + +import Foundation +import GemAPI +import Primitives +import Preferences +import AssetsService + +public struct AssetsUpdateRunner: AsyncRunnable { + public let id = "assets_update" + + private let configService: any GemAPIConfigService + private let importAssetsService: ImportAssetsService + private let assetsService: AssetsService + private let swappableChainsProvider: any SwappableChainsProvider + private let preferences: Preferences + + public init( + configService: any GemAPIConfigService, + importAssetsService: ImportAssetsService, + assetsService: AssetsService, + swappableChainsProvider: any SwappableChainsProvider, + preferences: Preferences + ) { + self.configService = configService + self.importAssetsService = importAssetsService + self.assetsService = assetsService + self.swappableChainsProvider = swappableChainsProvider + self.preferences = preferences + } + + public func run() async throws { + let chains = swappableChainsProvider.supportedChains() + try assetsService.setSwappableAssets(for: chains) + + do { + let config = try await configService.getConfig() + async let fiat: () = updateFiatAssets(config: config) + async let swap: () = updateSwapAssets(config: config) + _ = try await (fiat, swap) + } catch { + debugLog("Config fetch failed: \(error)") + } + } + + private func updateFiatAssets(config: ConfigResponse) async throws { + guard shouldUpdateFiatAssets(versions: config.versions) else { return } + try await importAssetsService.updateFiatAssets() + debugLog("Updated fiat assets: on ramp \(config.versions.fiatOnRampAssets), off ramp \(config.versions.fiatOffRampAssets)") + } + + private func updateSwapAssets(config: ConfigResponse) async throws { + guard shouldUpdateSwapAssets(versions: config.versions) else { return } + try await importAssetsService.updateSwapAssets() + debugLog("Updated swap assets: \(config.versions.swapAssets)") + } + + private func shouldUpdateFiatAssets(versions: ConfigVersions) -> Bool { + versions.fiatOnRampAssets > preferences.fiatOnRampAssetsVersion || + versions.fiatOffRampAssets > preferences.fiatOffRampAssetsVersion + } + + private func shouldUpdateSwapAssets(versions: ConfigVersions) -> Bool { + versions.swapAssets > preferences.swapAssetsVersion + } +} diff --git a/Packages/FeatureServices/AppService/Runners/BannerSetupRunner.swift b/Packages/FeatureServices/AppService/Runners/BannerSetupRunner.swift new file mode 100644 index 000000000..46c3f1c25 --- /dev/null +++ b/Packages/FeatureServices/AppService/Runners/BannerSetupRunner.swift @@ -0,0 +1,19 @@ +// Copyright (c). Gem Wallet. All rights reserved. + +import Foundation +import BannerService +import Primitives + +public struct BannerSetupRunner: AsyncRunnable { + public let id = "banner_setup" + + private let bannerSetupService: BannerSetupService + + public init(bannerSetupService: BannerSetupService) { + self.bannerSetupService = bannerSetupService + } + + public func run() async throws { + try bannerSetupService.setup() + } +} diff --git a/Packages/FeatureServices/AppService/Runners/NodeImportRunner.swift b/Packages/FeatureServices/AppService/Runners/NodeImportRunner.swift new file mode 100644 index 000000000..412a20f9e --- /dev/null +++ b/Packages/FeatureServices/AppService/Runners/NodeImportRunner.swift @@ -0,0 +1,19 @@ +// Copyright (c). Gem Wallet. All rights reserved. + +import Foundation +import NodeService +import Primitives + +public struct NodeImportRunner: AsyncRunnable { + public let id = "node_import" + + private let nodeService: NodeService + + public init(nodeService: NodeService) { + self.nodeService = nodeService + } + + public func run() async throws { + try nodeService.importDefaultNodes() + } +} diff --git a/Packages/FeatureServices/AppService/TestKit/AssetsUpdateRunner+TestKit.swift b/Packages/FeatureServices/AppService/TestKit/AssetsUpdateRunner+TestKit.swift new file mode 100644 index 000000000..eeceddcf2 --- /dev/null +++ b/Packages/FeatureServices/AppService/TestKit/AssetsUpdateRunner+TestKit.swift @@ -0,0 +1,30 @@ +// Copyright (c). Gem Wallet. All rights reserved. + +import Foundation +import AppService +import GemAPI +import GemAPITestKit +import Primitives +import PrimitivesTestKit +import Preferences +import PreferencesTestKit +import AssetsService +import AssetsServiceTestKit + +public extension AssetsUpdateRunner { + static func mock( + configService: any GemAPIConfigService = GemAPIConfigServiceMock(config: .mock()), + importAssetsService: ImportAssetsService = .mock(), + assetsService: AssetsService = .mock(), + swappableChainsProvider: any SwappableChainsProvider = SwappableChainsProviderMock.mock(), + preferences: Preferences = .mock() + ) -> AssetsUpdateRunner { + AssetsUpdateRunner( + configService: configService, + importAssetsService: importAssetsService, + assetsService: assetsService, + swappableChainsProvider: swappableChainsProvider, + preferences: preferences + ) + } +} diff --git a/Packages/FeatureServices/AppService/TestKit/BannerSetupRunner+TestKit.swift b/Packages/FeatureServices/AppService/TestKit/BannerSetupRunner+TestKit.swift new file mode 100644 index 000000000..8dda689b6 --- /dev/null +++ b/Packages/FeatureServices/AppService/TestKit/BannerSetupRunner+TestKit.swift @@ -0,0 +1,14 @@ +// Copyright (c). Gem Wallet. All rights reserved. + +import Foundation +import AppService +import BannerService +import BannerServiceTestKit + +public extension BannerSetupRunner { + static func mock( + bannerSetupService: BannerSetupService = .mock() + ) -> BannerSetupRunner { + BannerSetupRunner(bannerSetupService: bannerSetupService) + } +} diff --git a/Packages/FeatureServices/AppService/TestKit/NodeImportRunner+TestKit.swift b/Packages/FeatureServices/AppService/TestKit/NodeImportRunner+TestKit.swift new file mode 100644 index 000000000..f9259ea22 --- /dev/null +++ b/Packages/FeatureServices/AppService/TestKit/NodeImportRunner+TestKit.swift @@ -0,0 +1,14 @@ +// Copyright (c). Gem Wallet. All rights reserved. + +import Foundation +import AppService +import NodeService +import NodeServiceTestKit + +public extension NodeImportRunner { + static func mock( + nodeService: NodeService = .mock() + ) -> NodeImportRunner { + NodeImportRunner(nodeService: nodeService) + } +} diff --git a/Packages/FeatureServices/AppService/TestKit/OnstartAsyncService+TestKit.swift b/Packages/FeatureServices/AppService/TestKit/OnstartAsyncService+TestKit.swift new file mode 100644 index 000000000..c3c116369 --- /dev/null +++ b/Packages/FeatureServices/AppService/TestKit/OnstartAsyncService+TestKit.swift @@ -0,0 +1,11 @@ +// Copyright (c). Gem Wallet. All rights reserved. + +import Foundation +import AppService +import Primitives + +public extension OnstartAsyncService { + static func mock(runners: [any AsyncRunnable] = []) -> OnstartAsyncService { + OnstartAsyncService(runners: runners) + } +} diff --git a/Packages/FeatureServices/AppService/Tests/OnstartAsyncServiceTests.swift b/Packages/FeatureServices/AppService/Tests/OnstartAsyncServiceTests.swift index abc0ad065..35cbbd53e 100644 --- a/Packages/FeatureServices/AppService/Tests/OnstartAsyncServiceTests.swift +++ b/Packages/FeatureServices/AppService/Tests/OnstartAsyncServiceTests.swift @@ -1,55 +1,71 @@ +// Copyright (c). Gem Wallet. All rights reserved. + +import Foundation import Testing -import StoreTestKit -import PreferencesTestKit -import DeviceServiceTestKit -import BannerServiceTestKit -import GemAPITestKit import AppServiceTestKit -import AssetsServiceTestKit +import Primitives @testable import AppService struct OnstartAsyncServiceTests { @Test - func testNewRelease() async throws { + func runEmpty() async { let service = OnstartAsyncService.mock() + await service.run() + } + + @Test + func runExecutesAllRunners() async { + let runner1 = TrackingRunner(id: "runner1") + let runner2 = TrackingRunner(id: "runner2") + let runner3 = TrackingRunner(id: "runner3") + let service = OnstartAsyncService.mock(runners: [runner1, runner2, runner3]) + + await service.run() - await confirmation(expectedCount: 1) { @MainActor confirmation in - service.releaseAction = { @MainActor release in - #expect(release.version == "16.1") - confirmation() - } - await service.migrations() - } + #expect(runner1.didRun) + #expect(runner2.didRun) + #expect(runner3.didRun) } @Test - func testSkipRelease() async throws { - let service = OnstartAsyncService.mock() + func failingRunnerDoesNotStopOthers() async { + let runner1 = TrackingRunner(id: "runner1") + let failingRunner = FailingRunner(id: "failing") + let runner2 = TrackingRunner(id: "runner2") + let service = OnstartAsyncService.mock(runners: [runner1, failingRunner, runner2]) + + await service.run() - await confirmation(expectedCount: 0) { @MainActor confirmation in - service.releaseAction = { @MainActor _ in - confirmation() - } - service.skipRelease("16.1") - await service.migrations() - } + #expect(runner1.didRun) + #expect(runner2.didRun) } } -extension OnstartAsyncService { - static func mock() -> OnstartAsyncService { - OnstartAsyncService( - assetStore: .mock(), - nodeStore: .mock(), - preferences: .mock(), - assetsService: .mock(), - deviceService: .mock(), - bannerSetupService: .mock(), - configService: GemAPIConfigServiceMock(config: .mock()), - releaseService: AppReleaseService(configService: GemAPIConfigServiceMock(config: .mock())), - swappableChainsProvider: SwappableChainsProviderMock() - ) +// MARK: - Test Helpers + +private final class TrackingRunner: AsyncRunnable, @unchecked Sendable { + let id: String + private(set) var didRun = false + + init(id: String) { + self.id = id } + + func run() async throws { + didRun = true + } +} + +private struct FailingRunner: AsyncRunnable { + let id: String + + func run() async throws { + throw TestError.intentional + } +} + +private enum TestError: Error { + case intentional } diff --git a/Packages/FeatureServices/AssetsService/AssetsService.swift b/Packages/FeatureServices/AssetsService/AssetsService.swift index 300d0022a..cc88ab3dd 100644 --- a/Packages/FeatureServices/AssetsService/AssetsService.swift +++ b/Packages/FeatureServices/AssetsService/AssetsService.swift @@ -171,4 +171,8 @@ public final class AssetsService: Sendable { return try await group.reduce(into: [AssetBasic]()) { if let asset = $1 { $0.append(asset) } } } } + + public func setSwappableAssets(for chains: [Chain]) throws { + try assetStore.setAssetIsSwappable(for: chains.map { $0.id }, value: true) + } } diff --git a/Packages/FeatureServices/AssetsService/TestKit/ImportAssetsService+TestKit.swift b/Packages/FeatureServices/AssetsService/TestKit/ImportAssetsService+TestKit.swift new file mode 100644 index 000000000..a984395f3 --- /dev/null +++ b/Packages/FeatureServices/AssetsService/TestKit/ImportAssetsService+TestKit.swift @@ -0,0 +1,22 @@ +// Copyright (c). Gem Wallet. All rights reserved. + +import Foundation +import AssetsService +import Preferences +import PreferencesTestKit +import Store +import StoreTestKit + +public extension ImportAssetsService { + static func mock( + assetsService: AssetsService = .mock(), + assetStore: AssetStore = .mock(), + preferences: Preferences = .mock() + ) -> ImportAssetsService { + ImportAssetsService( + assetsService: assetsService, + assetStore: assetStore, + preferences: preferences + ) + } +} diff --git a/Packages/FeatureServices/Package.swift b/Packages/FeatureServices/Package.swift index 9333d4c6e..003dfd5c9 100644 --- a/Packages/FeatureServices/Package.swift +++ b/Packages/FeatureServices/Package.swift @@ -402,6 +402,7 @@ let package = Package( "GemAPI", .product(name: "NodeService", package: "ChainServices"), .product(name: "ChainService", package: "ChainServices"), + "GemstonePrimitives", "Preferences", "BannerService", "DeviceService", @@ -415,7 +416,14 @@ let package = Package( .target( name: "AppServiceTestKit", dependencies: [ - "Primitives" + "AppService", + "Primitives", + .product(name: "GemAPITestKit", package: "GemAPI"), + "BannerServiceTestKit", + .product(name: "NodeServiceTestKit", package: "ChainServices"), + "DeviceServiceTestKit", + "AssetsServiceTestKit", + .product(name: "PreferencesTestKit", package: "Preferences"), ], path: "AppService/TestKit" ), @@ -603,12 +611,8 @@ let package = Package( dependencies: [ "AppService", "AppServiceTestKit", - "AssetsServiceTestKit", - "DeviceServiceTestKit", - "BannerServiceTestKit", - .product(name: "StoreTestKit", package: "Store"), - .product(name: "PreferencesTestKit", package: "Preferences"), .product(name: "GemAPITestKit", package: "GemAPI"), + .product(name: "PrimitivesTestKit", package: "Primitives"), ], path: "AppService/Tests" ), diff --git a/Packages/Primitives/Sources/AsyncRunnable.swift b/Packages/Primitives/Sources/AsyncRunnable.swift new file mode 100644 index 000000000..489a00cd5 --- /dev/null +++ b/Packages/Primitives/Sources/AsyncRunnable.swift @@ -0,0 +1,8 @@ +// Copyright (c). Gem Wallet. All rights reserved. + +import Foundation + +public protocol AsyncRunnable: Sendable { + var id: String { get } + func run() async throws +} diff --git a/core b/core index 031d64f76..d46e946b0 160000 --- a/core +++ b/core @@ -1 +1 @@ -Subproject commit 031d64f76e4a323fb545b28150b50bcb1929a70f +Subproject commit d46e946b037791199c9fc2307b40e1441d2d0bdb