From 82cf9c12597acbe7523ec6d1a652ad804572a2cf Mon Sep 17 00:00:00 2001 From: rodvar <1040902+rodvar@users.noreply.github.com> Date: Thu, 12 Dec 2024 13:28:22 +1100 Subject: [PATCH] Feature/clients bootstrap (#113) * - add trusted node basic navigation logic customization for xClients, and override on node * - wrap api client in TrustedNodeService to handle it with different components and used it --- .../android/node/di/AndroidNodeModule.kt | 13 +++- .../NodeApplicationBootstrapFacade.kt | 2 +- .../node/presentation/NodeMainPresenter.kt | 2 +- .../node/presentation/NodeSplashPresenter.kt | 23 +++++++ .../ClientApplicationBootstrapFacade.kt | 54 +++++++++++----- .../bisq/mobile/client/di/ClientModule.kt | 11 ++-- .../client/websocket/WebSocketClient.kt | 2 +- .../domain/service/TrustedNodeService.kt | 36 +++++++++++ .../bootstrap/ApplicationBootstrapFacade.kt | 5 +- .../bisq/mobile/client/ClientMainPresenter.kt | 17 +----- .../presentation/di/PresentationModule.kt | 2 +- .../ui/uicases/startup/SplashPresenter.kt | 61 +++++++++++-------- 12 files changed, 162 insertions(+), 66 deletions(-) create mode 100644 androidNode/src/androidMain/kotlin/network/bisq/mobile/android/node/presentation/NodeSplashPresenter.kt create mode 100644 shared/domain/src/commonMain/kotlin/network/bisq/mobile/domain/service/TrustedNodeService.kt diff --git a/androidNode/src/androidMain/kotlin/network/bisq/mobile/android/node/di/AndroidNodeModule.kt b/androidNode/src/androidMain/kotlin/network/bisq/mobile/android/node/di/AndroidNodeModule.kt index cbf5c815..f156594d 100644 --- a/androidNode/src/androidMain/kotlin/network/bisq/mobile/android/node/di/AndroidNodeModule.kt +++ b/androidNode/src/androidMain/kotlin/network/bisq/mobile/android/node/di/AndroidNodeModule.kt @@ -6,16 +6,18 @@ import network.bisq.mobile.android.node.domain.market_price.NodeMarketPriceServi import network.bisq.mobile.android.node.domain.offerbook.NodeOfferbookServiceFacade import network.bisq.mobile.android.node.domain.user_profile.NodeUserProfileServiceFacade import network.bisq.mobile.android.node.presentation.NodeMainPresenter +import network.bisq.mobile.android.node.presentation.NodeSplashPresenter import network.bisq.mobile.android.node.presentation.OnBoardingNodePresenter import network.bisq.mobile.android.node.service.AndroidNodeCatHashService import network.bisq.mobile.android.node.service.AndroidMemoryReportService -import network.bisq.mobile.domain.data.repository.main.bootstrap.ApplicationBootstrapFacade +import network.bisq.mobile.domain.service.bootstrap.ApplicationBootstrapFacade import network.bisq.mobile.domain.service.market_price.MarketPriceServiceFacade import network.bisq.mobile.domain.service.offerbook.OfferbookServiceFacade import network.bisq.mobile.domain.service.user_profile.UserProfileServiceFacade import network.bisq.mobile.presentation.MainPresenter import network.bisq.mobile.presentation.ui.AppPresenter import network.bisq.mobile.presentation.ui.uicases.startup.IOnboardingPresenter +import network.bisq.mobile.presentation.ui.uicases.startup.SplashPresenter import org.koin.android.ext.koin.androidContext import org.koin.dsl.bind import org.koin.dsl.module @@ -43,5 +45,14 @@ val androidNodeModule = module { // and binding the same obj to 2 different abstractions single { NodeMainPresenter(get(), get(), get(), get(), get(), get()) } bind AppPresenter::class + single { + NodeSplashPresenter( + get(), + get(), + get(), + get(), + ) + } + single { OnBoardingNodePresenter(get(), get()) } bind IOnboardingPresenter::class } \ No newline at end of file diff --git a/androidNode/src/androidMain/kotlin/network/bisq/mobile/android/node/domain/bootstrap/NodeApplicationBootstrapFacade.kt b/androidNode/src/androidMain/kotlin/network/bisq/mobile/android/node/domain/bootstrap/NodeApplicationBootstrapFacade.kt index b054e958..ff9f10cf 100644 --- a/androidNode/src/androidMain/kotlin/network/bisq/mobile/android/node/domain/bootstrap/NodeApplicationBootstrapFacade.kt +++ b/androidNode/src/androidMain/kotlin/network/bisq/mobile/android/node/domain/bootstrap/NodeApplicationBootstrapFacade.kt @@ -4,7 +4,7 @@ import bisq.application.State import bisq.common.observable.Observable import bisq.common.observable.Pin import network.bisq.mobile.android.node.AndroidApplicationService -import network.bisq.mobile.domain.data.repository.main.bootstrap.ApplicationBootstrapFacade +import network.bisq.mobile.domain.service.bootstrap.ApplicationBootstrapFacade class NodeApplicationBootstrapFacade( private val applicationService: AndroidApplicationService.Provider diff --git a/androidNode/src/androidMain/kotlin/network/bisq/mobile/android/node/presentation/NodeMainPresenter.kt b/androidNode/src/androidMain/kotlin/network/bisq/mobile/android/node/presentation/NodeMainPresenter.kt index 763cf6c9..525dd2a8 100644 --- a/androidNode/src/androidMain/kotlin/network/bisq/mobile/android/node/presentation/NodeMainPresenter.kt +++ b/androidNode/src/androidMain/kotlin/network/bisq/mobile/android/node/presentation/NodeMainPresenter.kt @@ -3,7 +3,7 @@ package network.bisq.mobile.android.node.presentation import android.app.Activity import network.bisq.mobile.android.node.AndroidApplicationService import network.bisq.mobile.android.node.service.AndroidMemoryReportService -import network.bisq.mobile.domain.data.repository.main.bootstrap.ApplicationBootstrapFacade +import network.bisq.mobile.domain.service.bootstrap.ApplicationBootstrapFacade import network.bisq.mobile.domain.service.controller.NotificationServiceController import network.bisq.mobile.domain.service.market_price.MarketPriceServiceFacade import network.bisq.mobile.domain.service.offerbook.OfferbookServiceFacade diff --git a/androidNode/src/androidMain/kotlin/network/bisq/mobile/android/node/presentation/NodeSplashPresenter.kt b/androidNode/src/androidMain/kotlin/network/bisq/mobile/android/node/presentation/NodeSplashPresenter.kt new file mode 100644 index 00000000..4e6dafbf --- /dev/null +++ b/androidNode/src/androidMain/kotlin/network/bisq/mobile/android/node/presentation/NodeSplashPresenter.kt @@ -0,0 +1,23 @@ +package network.bisq.mobile.android.node.presentation + +import network.bisq.mobile.domain.data.model.Settings +import network.bisq.mobile.domain.data.repository.SettingsRepository +import network.bisq.mobile.domain.service.bootstrap.ApplicationBootstrapFacade +import network.bisq.mobile.domain.service.user_profile.UserProfileServiceFacade +import network.bisq.mobile.presentation.MainPresenter +import network.bisq.mobile.presentation.ui.uicases.startup.SplashPresenter + +class NodeSplashPresenter( + mainPresenter: MainPresenter, + applicationBootstrapFacade: ApplicationBootstrapFacade, + private val userProfileService: UserProfileServiceFacade, + private val settingsRepository: SettingsRepository +) : SplashPresenter(mainPresenter, applicationBootstrapFacade, userProfileService, settingsRepository) { + + /** + * Default implementation in shared is for xClients. Override on node to avoid this. + */ + override fun doCustomNavigationLogic(settings: Settings) { + // do nothin + } +} diff --git a/shared/domain/src/commonMain/kotlin/network/bisq/mobile/client/bootstrap/ClientApplicationBootstrapFacade.kt b/shared/domain/src/commonMain/kotlin/network/bisq/mobile/client/bootstrap/ClientApplicationBootstrapFacade.kt index 966a621c..82d3bde3 100644 --- a/shared/domain/src/commonMain/kotlin/network/bisq/mobile/client/bootstrap/ClientApplicationBootstrapFacade.kt +++ b/shared/domain/src/commonMain/kotlin/network/bisq/mobile/client/bootstrap/ClientApplicationBootstrapFacade.kt @@ -1,31 +1,53 @@ -package network.bisq.mobile.android.node.main.bootstrap +package network.bisq.mobile.client.bootstrap import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.delay import kotlinx.coroutines.launch import network.bisq.mobile.domain.data.BackgroundDispatcher -import network.bisq.mobile.domain.data.repository.main.bootstrap.ApplicationBootstrapFacade +import network.bisq.mobile.domain.data.repository.SettingsRepository +import network.bisq.mobile.domain.service.TrustedNodeService +import network.bisq.mobile.domain.service.bootstrap.ApplicationBootstrapFacade -class ClientApplicationBootstrapFacade() : +class ClientApplicationBootstrapFacade( + private val settingsRepository: SettingsRepository, + private val trustedNodeService: TrustedNodeService) : ApplicationBootstrapFacade() { + private val backgroundScope = CoroutineScope(BackgroundDispatcher) override fun activate() { - setState("Dummy state 1") + // TODO all texts here shoul use the translation module + setState("Bootstrapping..") setProgress(0f) // just dummy loading simulation, might be that there is no loading delay at the end... - CoroutineScope(BackgroundDispatcher).launch { - delay(50L) - setState("Dummy state 2") - setProgress(0.25f) + backgroundScope.launch { + settingsRepository.fetch() + val url = settingsRepository.data.value?.bisqApiUrl + log.d { "Settings url $url" } - delay(50L) - setState("Dummy state 3") - setProgress(0.5f) - - delay(50L) - setState("Dummy state 4") - setProgress(1f) +// TODO this is validated elsewhere, need to unify it or get +// rid of this facade otherwise +// Main issue is that the Trusted node setup screen is not +// if (url == null) { +// setState("Trusted node not configured") +// setProgress(0f) +// } else { + setProgress(0.5f) + setState("Connecting to Trusted Node..") + if (!trustedNodeService.isConnected()) { + try { + trustedNodeService.connect() + setState("Connected to Trusted Node") + setProgress(1.0f) + } catch (e: Exception) { + log.e(e) { "Failed to connect to trusted node" } + setState("No connectivity") + setProgress(1.0f) + } + } else { + setState("Connected to Trusted Node") + setProgress(1.0f) + } +// } } } diff --git a/shared/domain/src/commonMain/kotlin/network/bisq/mobile/client/di/ClientModule.kt b/shared/domain/src/commonMain/kotlin/network/bisq/mobile/client/di/ClientModule.kt index f6e860e4..3f0d1194 100644 --- a/shared/domain/src/commonMain/kotlin/network/bisq/mobile/client/di/ClientModule.kt +++ b/shared/domain/src/commonMain/kotlin/network/bisq/mobile/client/di/ClientModule.kt @@ -9,12 +9,11 @@ import kotlinx.serialization.json.Json import kotlinx.serialization.modules.SerializersModule import kotlinx.serialization.modules.contextual import kotlinx.serialization.modules.polymorphic -import network.bisq.mobile.android.node.main.bootstrap.ClientApplicationBootstrapFacade +import network.bisq.mobile.client.bootstrap.ClientApplicationBootstrapFacade import network.bisq.mobile.client.market.ClientMarketPriceServiceFacade import network.bisq.mobile.client.market.MarketPriceApiGateway import network.bisq.mobile.client.offerbook.ClientOfferbookServiceFacade import network.bisq.mobile.client.offerbook.offer.OfferbookApiGateway -import network.bisq.mobile.client.shared.BuildConfig import network.bisq.mobile.client.websocket.WebSocketClient import network.bisq.mobile.client.websocket.api_proxy.WebSocketApiClient import network.bisq.mobile.client.websocket.messages.SubscriptionRequest @@ -28,7 +27,8 @@ import network.bisq.mobile.client.websocket.messages.WebSocketRestApiResponse import network.bisq.mobile.client.user_profile.ClientUserProfileServiceFacade import network.bisq.mobile.client.user_profile.UserProfileApiGateway import network.bisq.mobile.domain.data.EnvironmentController -import network.bisq.mobile.domain.data.repository.main.bootstrap.ApplicationBootstrapFacade +import network.bisq.mobile.domain.service.TrustedNodeService +import network.bisq.mobile.domain.service.bootstrap.ApplicationBootstrapFacade import network.bisq.mobile.domain.service.market_price.MarketPriceServiceFacade import network.bisq.mobile.domain.service.offerbook.OfferbookServiceFacade import network.bisq.mobile.domain.service.user_profile.UserProfileServiceFacade @@ -68,7 +68,7 @@ val clientModule = module { } } - single { ClientApplicationBootstrapFacade() } + single { ClientApplicationBootstrapFacade(get(), get()) } single { EnvironmentController() } single(named("ApiHost")) { get().getApiHost() } @@ -84,6 +84,9 @@ val clientModule = module { get(named("WebsocketApiPort")) ) } + + single { TrustedNodeService(get()) } + // single { WebSocketHttpClient(get()) } single { println("Running on simulator: ${get().isSimulator()}") diff --git a/shared/domain/src/commonMain/kotlin/network/bisq/mobile/client/websocket/WebSocketClient.kt b/shared/domain/src/commonMain/kotlin/network/bisq/mobile/client/websocket/WebSocketClient.kt index a1e2392a..612877f2 100644 --- a/shared/domain/src/commonMain/kotlin/network/bisq/mobile/client/websocket/WebSocketClient.kt +++ b/shared/domain/src/commonMain/kotlin/network/bisq/mobile/client/websocket/WebSocketClient.kt @@ -41,7 +41,7 @@ class WebSocketClient( private val webSocketUrl: String = "ws://$host:$port/websocket" private var session: DefaultClientWebSocketSession? = null - private var isConnected = false + var isConnected = false private val webSocketEventObservers = ConcurrentMap() private val requestResponseHandlers = mutableMapOf() private var connectionReady = CompletableDeferred() diff --git a/shared/domain/src/commonMain/kotlin/network/bisq/mobile/domain/service/TrustedNodeService.kt b/shared/domain/src/commonMain/kotlin/network/bisq/mobile/domain/service/TrustedNodeService.kt new file mode 100644 index 00000000..4f175f85 --- /dev/null +++ b/shared/domain/src/commonMain/kotlin/network/bisq/mobile/domain/service/TrustedNodeService.kt @@ -0,0 +1,36 @@ +package network.bisq.mobile.domain.service + +import kotlinx.coroutines.CoroutineScope +import network.bisq.mobile.client.websocket.WebSocketClient +import network.bisq.mobile.domain.data.BackgroundDispatcher +import network.bisq.mobile.utils.Logging + +/** + * This service allows to interact with the underlaying connectivity system + * against the trusted node for the client. + */ +class TrustedNodeService(private val websocketClient: WebSocketClient): Logging { + private val backgroundScope = CoroutineScope(BackgroundDispatcher) + + // TODO websocketClient.isConnected should be observable so that we emit + // events when disconnected and UI can react + fun isConnected() = websocketClient.isConnected + + /** + * Connects to the trusted node, throws an exception if connection fails + */ + suspend fun connect() { + runCatching { + websocketClient.connect() + }.onSuccess { + log.d { "Connected to trusted node" } + }.onFailure { + log.e { "ERROR: FAILED to connect to trusted node - details above" } + throw it + } + } + + suspend fun disconnect() { + // TODO + } +} \ No newline at end of file diff --git a/shared/domain/src/commonMain/kotlin/network/bisq/mobile/domain/service/bootstrap/ApplicationBootstrapFacade.kt b/shared/domain/src/commonMain/kotlin/network/bisq/mobile/domain/service/bootstrap/ApplicationBootstrapFacade.kt index dcd31837..e51fe737 100644 --- a/shared/domain/src/commonMain/kotlin/network/bisq/mobile/domain/service/bootstrap/ApplicationBootstrapFacade.kt +++ b/shared/domain/src/commonMain/kotlin/network/bisq/mobile/domain/service/bootstrap/ApplicationBootstrapFacade.kt @@ -1,10 +1,11 @@ -package network.bisq.mobile.domain.data.repository.main.bootstrap +package network.bisq.mobile.domain.service.bootstrap import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow import network.bisq.mobile.domain.LifeCycleAware +import network.bisq.mobile.utils.Logging -abstract class ApplicationBootstrapFacade: LifeCycleAware { +abstract class ApplicationBootstrapFacade: LifeCycleAware, Logging { private val _state = MutableStateFlow("") val state: StateFlow = _state fun setState(value: String) { diff --git a/shared/presentation/src/commonMain/kotlin/network/bisq/mobile/client/ClientMainPresenter.kt b/shared/presentation/src/commonMain/kotlin/network/bisq/mobile/client/ClientMainPresenter.kt index 513b63a4..6756b29f 100644 --- a/shared/presentation/src/commonMain/kotlin/network/bisq/mobile/client/ClientMainPresenter.kt +++ b/shared/presentation/src/commonMain/kotlin/network/bisq/mobile/client/ClientMainPresenter.kt @@ -2,7 +2,8 @@ package network.bisq.mobile.client import kotlinx.coroutines.launch import network.bisq.mobile.client.websocket.WebSocketClient -import network.bisq.mobile.domain.data.repository.main.bootstrap.ApplicationBootstrapFacade +import network.bisq.mobile.domain.service.TrustedNodeService +import network.bisq.mobile.domain.service.bootstrap.ApplicationBootstrapFacade import network.bisq.mobile.domain.service.controller.NotificationServiceController import network.bisq.mobile.domain.service.market_price.MarketPriceServiceFacade import network.bisq.mobile.domain.service.offerbook.OfferbookServiceFacade @@ -11,7 +12,7 @@ import network.bisq.mobile.presentation.MainPresenter class ClientMainPresenter( notificationServiceController: NotificationServiceController, private val applicationBootstrapFacade: ApplicationBootstrapFacade, - private val webSocketClient: WebSocketClient, + private val trustedNodeService: TrustedNodeService, private val offerbookServiceFacade: OfferbookServiceFacade, private val marketPriceServiceFacade: MarketPriceServiceFacade ) : MainPresenter(notificationServiceController) { @@ -19,18 +20,6 @@ class ClientMainPresenter( override fun onViewAttached() { super.onViewAttached() runCatching { - backgroundScope.launch { - runCatching { - webSocketClient.connect() - }.onSuccess { - log.d { "Connected to trusted node" } - }.onFailure { - // TODO give user feedback (we could have a general error screen covering usual - // issues like connection issues and potential solutions) - log.e { "ERROR: FAILED to connect to trusted node - details above" } - } - } - applicationBootstrapFacade.activate() offerbookServiceFacade.activate() marketPriceServiceFacade.activate() diff --git a/shared/presentation/src/commonMain/kotlin/network/bisq/mobile/presentation/di/PresentationModule.kt b/shared/presentation/src/commonMain/kotlin/network/bisq/mobile/presentation/di/PresentationModule.kt index 371e463b..f604c3ef 100644 --- a/shared/presentation/src/commonMain/kotlin/network/bisq/mobile/presentation/di/PresentationModule.kt +++ b/shared/presentation/src/commonMain/kotlin/network/bisq/mobile/presentation/di/PresentationModule.kt @@ -42,7 +42,7 @@ val presentationModule = module { single { TopBarPresenter(get(), get()) } bind ITopBarPresenter::class - single { + single { SplashPresenter( get(), get(), diff --git a/shared/presentation/src/commonMain/kotlin/network/bisq/mobile/presentation/ui/uicases/startup/SplashPresenter.kt b/shared/presentation/src/commonMain/kotlin/network/bisq/mobile/presentation/ui/uicases/startup/SplashPresenter.kt index 0cc3f8e5..079f0f53 100644 --- a/shared/presentation/src/commonMain/kotlin/network/bisq/mobile/presentation/ui/uicases/startup/SplashPresenter.kt +++ b/shared/presentation/src/commonMain/kotlin/network/bisq/mobile/presentation/ui/uicases/startup/SplashPresenter.kt @@ -5,10 +5,9 @@ import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Job import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.launch -import network.bisq.mobile.domain.data.BackgroundDispatcher import network.bisq.mobile.domain.data.model.Settings import network.bisq.mobile.domain.data.repository.SettingsRepository -import network.bisq.mobile.domain.data.repository.main.bootstrap.ApplicationBootstrapFacade +import network.bisq.mobile.domain.service.bootstrap.ApplicationBootstrapFacade import network.bisq.mobile.domain.service.user_profile.UserProfileServiceFacade import network.bisq.mobile.presentation.BasePresenter import network.bisq.mobile.presentation.MainPresenter @@ -16,7 +15,7 @@ import network.bisq.mobile.presentation.ui.navigation.Routes open class SplashPresenter( mainPresenter: MainPresenter, - private val applicationBootstrapFacade: ApplicationBootstrapFacade, + applicationBootstrapFacade: ApplicationBootstrapFacade, private val userProfileService: UserProfileServiceFacade, private val settingsRepository: SettingsRepository ) : BasePresenter(mainPresenter) { @@ -44,41 +43,53 @@ open class SplashPresenter( CoroutineScope(Dispatchers.Main).launch { settingsRepository.fetch() - val settings: Settings = settingsRepository.data.value ?: Settings() + val settings: Settings? = settingsRepository.data.value - if (userProfileService.hasUserProfile()) { - // rootNavigator.navigate(Routes.TrustedNodeSetup.name) { - // [DONE] For androidNode, goto TabContainer - rootNavigator.navigate(Routes.TabContainer.name) { + if (settings == null) { + rootNavigator.navigate(Routes.TrustedNodeSetup.name) { popUpTo(Routes.Splash.name) { inclusive = true } } - - // TODO: This is only for xClient. - // How to handle between xClient and androidNode - if (settings.bisqApiUrl.isEmpty()) { - // Test if the Bisq remote instance is up and responding - // If yes, goto TabContainer screen. - // If no, goto TrustedNodeSetupScreen - } else { - // If no, goto TrustedNodeSetupScreen - } - } else { - // If firstTimeApp launch, goto Onboarding[clientMode] (androidNode / xClient) - // If not, goto CreateProfile - if (settings.firstLaunch) { - rootNavigator.navigate(Routes.Onboarding.name) { + if (userProfileService.hasUserProfile()) { + // rootNavigator.navigate(Routes.TrustedNodeSetup.name) { + // [DONE] For androidNode, goto TabContainer + rootNavigator.navigate(Routes.TabContainer.name) { popUpTo(Routes.Splash.name) { inclusive = true } } + + doCustomNavigationLogic(settings) } else { - rootNavigator.navigate(Routes.CreateProfile.name) { - popUpTo(Routes.Splash.name) { inclusive = true } + // If firstTimeApp launch, goto Onboarding[clientMode] (androidNode / xClient) + // If not, goto CreateProfile + if (settings.firstLaunch) { + rootNavigator.navigate(Routes.Onboarding.name) { + popUpTo(Routes.Splash.name) { inclusive = true } + } + } else { + rootNavigator.navigate(Routes.CreateProfile.name) { + popUpTo(Routes.Splash.name) { inclusive = true } + } } } } } } + /** + * Default implementation in shared is for xClients. Override on node to avoid this. + */ + open fun doCustomNavigationLogic(settings: Settings) { + if (settings.bisqApiUrl.isNotEmpty()) { + // Test if the Bisq remote instance is up and responding + // If yes, goto TabContainer screen. + // If no, goto TrustedNodeSetupScreen + } else { + rootNavigator.navigate(Routes.TrustedNodeSetup.name) { + popUpTo(Routes.Splash.name) { inclusive = true } + } + } + } + private fun cancelJob() { job?.cancel() job = null