diff --git a/androidClient/src/androidMain/AndroidManifest.xml b/androidClient/src/androidMain/AndroidManifest.xml
index 7696d689..1099348c 100644
--- a/androidClient/src/androidMain/AndroidManifest.xml
+++ b/androidClient/src/androidMain/AndroidManifest.xml
@@ -2,6 +2,9 @@
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/androidClient/src/androidMain/kotlin/network/bisq/mobile/client/MainActivity.kt b/androidClient/src/androidMain/kotlin/network/bisq/mobile/client/MainActivity.kt
index c34513bf..2de76992 100644
--- a/androidClient/src/androidMain/kotlin/network/bisq/mobile/client/MainActivity.kt
+++ b/androidClient/src/androidMain/kotlin/network/bisq/mobile/client/MainActivity.kt
@@ -1,19 +1,29 @@
package network.bisq.mobile.client
+import android.content.pm.PackageManager
import android.graphics.Color
+import android.os.Build
import android.os.Bundle
+import android.widget.Toast
import androidx.activity.ComponentActivity
import androidx.activity.SystemBarStyle
import androidx.activity.compose.setContent
import androidx.activity.enableEdgeToEdge
+import androidx.activity.result.ActivityResultLauncher
+import androidx.activity.result.contract.ActivityResultContracts
+import androidx.annotation.RequiresApi
import androidx.compose.runtime.Composable
import androidx.compose.ui.tooling.preview.Preview
+import androidx.core.content.ContextCompat
import network.bisq.mobile.presentation.MainPresenter
import network.bisq.mobile.presentation.ui.App
import org.koin.android.ext.android.inject
class MainActivity : ComponentActivity() {
private val presenter: MainPresenter by inject()
+
+ // TODO probably better to handle from presenter once the user reach home
+ private lateinit var requestPermissionLauncher: ActivityResultLauncher
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
@@ -23,6 +33,8 @@ class MainActivity : ComponentActivity() {
setContent {
App()
}
+
+ handleDynamicPermissions()
}
override fun onStart() {
@@ -50,6 +62,38 @@ class MainActivity : ComponentActivity() {
presenter.onDestroy()
super.onDestroy()
}
+
+ private fun handleDynamicPermissions() {
+ requestPermissionLauncher = registerForActivityResult(
+ ActivityResultContracts.RequestPermission()
+ ) { isGranted: Boolean ->
+ if (isGranted) {
+ // Permission granted, proceed with posting notifications
+ } else {
+ // Permission denied, show a message to the user
+ Toast.makeText(this, "Permission denied. Notifications won't be sent.", Toast.LENGTH_SHORT).show()
+ }
+ }
+
+ // Call the method to check and request permission in APIs where its mandatory
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
+ checkAndRequestNotificationPermission()
+ }
+ }
+
+ @RequiresApi(Build.VERSION_CODES.TIRAMISU)
+ private fun checkAndRequestNotificationPermission() {
+ // Check if the permission is granted
+ if (ContextCompat.checkSelfPermission(
+ this,
+ android.Manifest.permission.POST_NOTIFICATIONS
+ ) == PackageManager.PERMISSION_GRANTED) {
+ // Permission already granted, proceed with posting notifications
+ } else {
+ // Request permission if not granted
+ requestPermissionLauncher.launch(android.Manifest.permission.POST_NOTIFICATIONS)
+ }
+ }
}
@Preview
diff --git a/androidClient/src/androidMain/kotlin/network/bisq/mobile/client/MainApplication.kt b/androidClient/src/androidMain/kotlin/network/bisq/mobile/client/MainApplication.kt
index 6e3599a2..0da93378 100644
--- a/androidClient/src/androidMain/kotlin/network/bisq/mobile/client/MainApplication.kt
+++ b/androidClient/src/androidMain/kotlin/network/bisq/mobile/client/MainApplication.kt
@@ -4,6 +4,7 @@ import android.app.Application
import network.bisq.mobile.client.di.androidClientModule
import network.bisq.mobile.client.di.clientModule
import network.bisq.mobile.domain.di.domainModule
+import network.bisq.mobile.domain.di.serviceModule
import network.bisq.mobile.presentation.di.presentationModule
import org.koin.android.ext.koin.androidContext
@@ -15,7 +16,7 @@ class MainApplication: Application() {
startKoin {
androidContext(this@MainApplication)
- modules(listOf(domainModule, presentationModule, clientModule, androidClientModule))
+ modules(listOf(domainModule, serviceModule, presentationModule, clientModule, androidClientModule))
}
}
}
diff --git a/androidNode/src/androidMain/AndroidManifest.xml b/androidNode/src/androidMain/AndroidManifest.xml
index 8337a91c..df6fe268 100644
--- a/androidNode/src/androidMain/AndroidManifest.xml
+++ b/androidNode/src/androidMain/AndroidManifest.xml
@@ -2,6 +2,9 @@
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/androidNode/src/androidMain/kotlin/network/bisq/mobile/android/node/MainActivity.kt b/androidNode/src/androidMain/kotlin/network/bisq/mobile/android/node/MainActivity.kt
index 95877343..4e7f4f60 100644
--- a/androidNode/src/androidMain/kotlin/network/bisq/mobile/android/node/MainActivity.kt
+++ b/androidNode/src/androidMain/kotlin/network/bisq/mobile/android/node/MainActivity.kt
@@ -1,10 +1,17 @@
package network.bisq.mobile.android.node
+import android.content.pm.PackageManager
+import android.os.Build
import android.os.Bundle
+import android.widget.Toast
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
+import androidx.activity.result.ActivityResultLauncher
+import androidx.activity.result.contract.ActivityResultContracts
+import androidx.annotation.RequiresApi
import androidx.compose.runtime.Composable
import androidx.compose.ui.tooling.preview.Preview
+import androidx.core.content.ContextCompat
import network.bisq.mobile.presentation.MainPresenter
import network.bisq.mobile.presentation.ui.App
import org.koin.android.ext.android.inject
@@ -12,6 +19,9 @@ import org.koin.android.ext.android.inject
class MainActivity : ComponentActivity() {
private val presenter : MainPresenter by inject()
+ // TODO probably better to handle from presenter once the user reach home
+ private lateinit var requestPermissionLauncher: ActivityResultLauncher
+
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
presenter.attachView(this)
@@ -19,8 +29,9 @@ class MainActivity : ComponentActivity() {
setContent {
App()
}
- }
+ handleDynamicPermissions()
+ }
override fun onStart() {
super.onStart()
presenter.onStart()
@@ -46,6 +57,39 @@ class MainActivity : ComponentActivity() {
presenter.onDestroy()
super.onDestroy()
}
+
+ private fun handleDynamicPermissions() {
+ requestPermissionLauncher = registerForActivityResult(
+ ActivityResultContracts.RequestPermission()
+ ) { isGranted: Boolean ->
+ if (isGranted) {
+ // Permission granted, proceed with posting notifications
+ } else {
+ // Permission denied, show a message to the user
+ Toast.makeText(this, "Permission denied. Notifications won't be sent.", Toast.LENGTH_SHORT).show()
+ }
+ }
+
+ // Call the method to check and request permission in APIs where its mandatory
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
+ checkAndRequestNotificationPermission()
+ }
+ }
+
+ @RequiresApi(Build.VERSION_CODES.TIRAMISU)
+ private fun checkAndRequestNotificationPermission() {
+ // Check if the permission is granted
+ if (ContextCompat.checkSelfPermission(
+ this,
+ android.Manifest.permission.POST_NOTIFICATIONS
+ ) == PackageManager.PERMISSION_GRANTED) {
+ // Permission already granted, proceed with posting notifications
+ } else {
+ // Request permission if not granted
+ requestPermissionLauncher.launch(android.Manifest.permission.POST_NOTIFICATIONS)
+ }
+ }
+
}
@Preview
diff --git a/androidNode/src/androidMain/kotlin/network/bisq/mobile/android/node/MainApplication.kt b/androidNode/src/androidMain/kotlin/network/bisq/mobile/android/node/MainApplication.kt
index 76262685..29c3eb70 100644
--- a/androidNode/src/androidMain/kotlin/network/bisq/mobile/android/node/MainApplication.kt
+++ b/androidNode/src/androidMain/kotlin/network/bisq/mobile/android/node/MainApplication.kt
@@ -8,6 +8,7 @@ import bisq.common.facades.android.AndroidJdkFacade
import bisq.common.network.AndroidEmulatorLocalhostFacade
import network.bisq.mobile.android.node.di.androidNodeModule
import network.bisq.mobile.domain.di.domainModule
+import network.bisq.mobile.domain.di.serviceModule
import network.bisq.mobile.presentation.di.presentationModule
import org.bouncycastle.jce.provider.BouncyCastleProvider
import org.koin.android.ext.koin.androidContext
@@ -40,7 +41,7 @@ class MainApplication : Application() {
startKoin {
androidContext(this@MainApplication)
// order is important, last one is picked for each interface/class key
- modules(listOf(domainModule, presentationModule, androidNodeModule))
+ modules(listOf(domainModule, serviceModule, presentationModule, androidNodeModule))
}
}
}
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 659ed74b..ecaa720c 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
@@ -41,7 +41,7 @@ val androidNodeModule = module {
single { NodeOfferbookServiceFacade(get(), get()) }
// this line showcases both, the possibility to change behaviour of the app by changing one definition
// and binding the same obj to 2 different abstractions
- single { NodeMainPresenter(get(), get(), get(), get(), get()) } bind AppPresenter::class
+ single { NodeMainPresenter(get(), get(), get(), get(), get(), get()) } bind AppPresenter::class
single { OnBoardingNodePresenter(get()) } bind IOnboardingPresenter::class
}
\ No newline at end of file
diff --git a/androidNode/src/androidMain/kotlin/network/bisq/mobile/android/node/domain/market_price/NodeMarketPriceServiceFacade.kt b/androidNode/src/androidMain/kotlin/network/bisq/mobile/android/node/domain/market_price/NodeMarketPriceServiceFacade.kt
index 6e835dd7..ea1ae5a6 100644
--- a/androidNode/src/androidMain/kotlin/network/bisq/mobile/android/node/domain/market_price/NodeMarketPriceServiceFacade.kt
+++ b/androidNode/src/androidMain/kotlin/network/bisq/mobile/android/node/domain/market_price/NodeMarketPriceServiceFacade.kt
@@ -55,8 +55,12 @@ class NodeMarketPriceServiceFacade(private val applicationService: AndroidApplic
private fun observeSelectedMarket() {
selectedMarketPin?.unbind()
selectedMarketPin = marketPriceService.selectedMarket.addObserver { market ->
- _marketPriceItem.value = MarketPriceItem(toReplicatedMarket(market))
- updatePrice()
+ try {
+ _marketPriceItem.value = MarketPriceItem(toReplicatedMarket(market))
+ updatePrice()
+ } catch (e: Exception) {
+ log.e("Failed to update market item", e)
+ }
}
}
diff --git a/androidNode/src/androidMain/kotlin/network/bisq/mobile/android/node/domain/offerbook/market/NodeSelectedOfferbookMarketService.kt b/androidNode/src/androidMain/kotlin/network/bisq/mobile/android/node/domain/offerbook/market/NodeSelectedOfferbookMarketService.kt
index f209ee0e..8083b209 100644
--- a/androidNode/src/androidMain/kotlin/network/bisq/mobile/android/node/domain/offerbook/market/NodeSelectedOfferbookMarketService.kt
+++ b/androidNode/src/androidMain/kotlin/network/bisq/mobile/android/node/domain/offerbook/market/NodeSelectedOfferbookMarketService.kt
@@ -73,8 +73,8 @@ class NodeSelectedOfferbookMarketService(
private fun observeSelectedChannel() {
selectedChannelPin =
bisqEasyOfferbookChannelSelectionService.selectedChannel.addObserver { selectedChannel ->
- this.selectedChannel = selectedChannel as BisqEasyOfferbookChannel
- marketPriceService.setSelectedMarket(selectedChannel.market)
+ this.selectedChannel = selectedChannel as BisqEasyOfferbookChannel?
+ marketPriceService.setSelectedMarket(selectedChannel?.market)
applySelectedOfferbookMarket()
}
}
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 6e533f61..7a02b699 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
@@ -4,17 +4,19 @@ 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.controller.NotificationServiceController
import network.bisq.mobile.domain.service.market_price.MarketPriceServiceFacade
import network.bisq.mobile.domain.service.offerbook.OfferbookServiceFacade
import network.bisq.mobile.presentation.MainPresenter
class NodeMainPresenter(
+ notificationServiceController: NotificationServiceController,
private val provider: AndroidApplicationService.Provider,
private val androidMemoryReportService: AndroidMemoryReportService,
private val applicationBootstrapFacade: ApplicationBootstrapFacade,
private val offerbookServiceFacade: OfferbookServiceFacade,
private val marketPriceServiceFacade: MarketPriceServiceFacade
-) : MainPresenter() {
+) : MainPresenter(notificationServiceController) {
private var applicationServiceCreated = false
override fun onViewAttached() {
diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml
index bee6b3ed..00a10d62 100644
--- a/gradle/libs.versions.toml
+++ b/gradle/libs.versions.toml
@@ -8,6 +8,7 @@ androidx-activityCompose = "1.9.2"
androidx-appcompat = "1.7.0"
androidx-constraintlayout = "2.1.4"
+androidx-core = "1.13.1"
androidx-core-ktx = "1.13.1"
androidx-espresso-core = "3.6.1"
androidx-material = "1.12.0"
@@ -92,6 +93,7 @@ androidx-test-compose = { group = "androidx.compose.ui", name = "ui-test-junit4-
androidx-test-manifest = { group = "androidx.compose.ui", name = "ui-test-manifest", version.ref = "androidx-test-compose-ver" }
#kotlin-test-junit = { module = "org.jetbrains.kotlin:kotlin-test-junit", version.ref = "kotlin" }
+androidx-core = { group = "androidx.core", name = "core", version.ref = "androidx-core" }
#androidx-core-ktx = { group = "androidx.core", name = "core-ktx", version.ref = "androidx-core-ktx" }
roboelectric = { group = "org.robolectric", name = "robolectric", version.ref = "roboelectric" }
androidx-test = { group = "androidx.test", name = "core", version.ref = "androidx-test" }
diff --git a/iosClient/iosClient/Info.plist b/iosClient/iosClient/Info.plist
index 412e3781..cc9f0365 100644
--- a/iosClient/iosClient/Info.plist
+++ b/iosClient/iosClient/Info.plist
@@ -2,6 +2,12 @@
+ BGTaskSchedulerPermittedIdentifiers
+
+ network.bisq.mobile.iosUC4273Y485
+
+ CADisableMinimumFrameDurationOnPhone
+
CFBundleDevelopmentRegion
$(DEVELOPMENT_LANGUAGE)
CFBundleExecutable
@@ -20,13 +26,16 @@
1
LSRequiresIPhoneOS
- CADisableMinimumFrameDurationOnPhone
-
UIApplicationSceneManifest
UIApplicationSupportsMultipleScenes
+ UIBackgroundModes
+
+ fetch
+ processing
+
UILaunchScreen
UIRequiredDeviceCapabilities
diff --git a/iosClient/iosClient/LifecycleAwareComposeViewController.swift b/iosClient/iosClient/LifecycleAwareComposeViewController.swift
index 1b3221a1..6137ec30 100644
--- a/iosClient/iosClient/LifecycleAwareComposeViewController.swift
+++ b/iosClient/iosClient/LifecycleAwareComposeViewController.swift
@@ -10,7 +10,6 @@ class LifecycleAwareComposeViewController: UIViewController {
init(presenter: MainPresenter) {
self.presenter = presenter
super.init(nibName: nil, bundle: nil)
- presenter.attachView(view: self)
}
required init?(coder: NSCoder) {
@@ -64,6 +63,7 @@ class LifecycleAwareComposeViewController: UIViewController {
// Notify the child view controller that it was moved to a parent
mainViewController.didMove(toParent: self)
+ presenter.attachView(view: self)
}
// Equivalent to `onDestroy` in Android for final cleanup
diff --git a/iosClient/iosClient/iosClient.swift b/iosClient/iosClient/iosClient.swift
index f105b707..f7888fc7 100644
--- a/iosClient/iosClient/iosClient.swift
+++ b/iosClient/iosClient/iosClient.swift
@@ -3,6 +3,7 @@ import presentation
@main
struct iosClient: App {
+
init() {
DependenciesProviderHelper().doInitKoin()
}
@@ -12,4 +13,5 @@ struct iosClient: App {
ContentView()
}
}
-}
+
+}
\ No newline at end of file
diff --git a/shared/domain/build.gradle.kts b/shared/domain/build.gradle.kts
index f7ebdcf6..24f63cfe 100644
--- a/shared/domain/build.gradle.kts
+++ b/shared/domain/build.gradle.kts
@@ -65,6 +65,12 @@ kotlin {
implementation(libs.kotlinx.coroutines.test)
implementation(libs.koin.test)
}
+ androidMain.dependencies {
+ implementation(libs.androidx.core)
+
+ implementation(libs.koin.core)
+ implementation(libs.koin.android)
+ }
androidUnitTest.dependencies {
implementation(libs.mock.io)
implementation(libs.kotlin.test.junit.v180)
diff --git a/shared/domain/src/androidMain/kotlin/network/bisq/mobile/domain/di/DomainModule.android.kt b/shared/domain/src/androidMain/kotlin/network/bisq/mobile/domain/di/DomainModule.android.kt
new file mode 100644
index 00000000..e70224a9
--- /dev/null
+++ b/shared/domain/src/androidMain/kotlin/network/bisq/mobile/domain/di/DomainModule.android.kt
@@ -0,0 +1,9 @@
+package network.bisq.mobile.domain.di
+
+import network.bisq.mobile.domain.service.controller.NotificationServiceController
+import org.koin.dsl.module
+import org.koin.android.ext.koin.androidContext
+
+val serviceModule = module {
+ single { NotificationServiceController(androidContext()) }
+}
\ No newline at end of file
diff --git a/shared/domain/src/androidMain/kotlin/network/bisq/mobile/domain/service/BisqForegroundService.kt b/shared/domain/src/androidMain/kotlin/network/bisq/mobile/domain/service/BisqForegroundService.kt
new file mode 100644
index 00000000..fee110c9
--- /dev/null
+++ b/shared/domain/src/androidMain/kotlin/network/bisq/mobile/domain/service/BisqForegroundService.kt
@@ -0,0 +1,126 @@
+package network.bisq.mobile.domain.service
+
+import android.app.Notification
+import android.app.NotificationManager
+import android.app.PendingIntent
+import android.app.Service
+import android.content.Context
+import android.content.Intent
+import android.content.pm.PackageManager
+import android.content.pm.ServiceInfo
+import android.os.Build
+import android.os.IBinder
+import androidx.core.app.NotificationCompat
+import androidx.core.app.NotificationManagerCompat
+import androidx.core.app.ServiceCompat
+import androidx.core.content.ContextCompat
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.delay
+import kotlinx.coroutines.launch
+import network.bisq.mobile.utils.Logging
+
+/**
+ * Implements foreground service (api >= 26) or background service accordingly
+ *
+ * This class is open for extension (for example, for the androidNode)
+ *
+ * android docs: https://developer.android.com/develop/background-work/services/foreground-services
+ */
+open class BisqForegroundService : Service(), Logging {
+ companion object {
+ const val CHANNEL_ID = "BISQ_SERVICE_CHANNEL"
+ const val SERVICE_ID = 21000000
+ const val PUSH_NOTIFICATION_ID = 1
+ const val SERVICE_NAME = "Bisq Foreground Service"
+ const val PUSH_NOTIFICATION_ACTION_KEY = "network.bisq.bisqapps.ACTION_REQUEST_PERMISSION"
+ }
+
+ private lateinit var silentNotification: Notification
+
+ private lateinit var defaultNotification: Notification
+
+ override fun onCreate() {
+ super.onCreate()
+ initDefaultNotifications()
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE) {
+ ServiceCompat.startForeground(this, SERVICE_ID, silentNotification, ServiceInfo.FOREGROUND_SERVICE_TYPE_REMOTE_MESSAGING)
+ log.i { "Started as foreground service compat"}
+ } else {
+ startForeground(SERVICE_ID, silentNotification)
+ log.i { "Started foreground"}
+ }
+ log.i { "Service ready" }
+
+ CoroutineScope(Dispatchers.Main).launch {
+ delay(10000) // Wait for 10 seconds
+ // Create an Intent to open the MainActivity when the notification is tapped
+ val pendingIntent = null
+// val intent = Intent(this, MainActivity::class.java).apply {
+// flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK // Clears any existing task and starts a new one
+// }
+//
+// // Create a PendingIntent to wrap the Intent
+// val pendingIntent: PendingIntent = PendingIntent.getActivity(
+// this@BisqForegroundService,
+// 0,
+// intent,
+// PendingIntent.FLAG_UPDATE_CURRENT // This flag updates the existing PendingIntent if it's already created
+// )
+ val updatedNotification: Notification = NotificationCompat.Builder(this@BisqForegroundService, CHANNEL_ID)
+ .setContentTitle("New Update!")
+ .setContentText("Tap to open the app")
+ .setSmallIcon(android.R.drawable.ic_notification_overlay)
+ .setPriority(NotificationCompat.PRIORITY_DEFAULT)
+ .setOngoing(true) // Keeps the notification active
+ .setContentIntent(pendingIntent) // Set the pending intent to launch the app
+ .build()
+
+ // Update the notification
+// NotificationManagerCompat.from(this@BisqForegroundService).notify(SERVICE_ID, updatedNotification)
+
+ val notificationManager = this@BisqForegroundService.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
+ notificationManager.notify(PUSH_NOTIFICATION_ID, updatedNotification)
+
+ // Log the update
+ log.i { "Notification updated after 10 seconds" }
+ }
+ }
+
+ override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
+ // Check if notification permission is granted
+ if (ContextCompat.checkSelfPermission(
+ this,
+ android.Manifest.permission.POST_NOTIFICATIONS
+ ) != PackageManager.PERMISSION_GRANTED) {
+ // Send a broadcast to the activity to request permission
+ val broadcastIntent = Intent(PUSH_NOTIFICATION_ACTION_KEY)
+ sendBroadcast(broadcastIntent)
+ }
+ log.i { "Service starting sticky" }
+ return START_STICKY
+ }
+
+ override fun onDestroy() {
+ log.i { "Service is being destroyed" }
+ super.onDestroy()
+ // Cleanup tasks
+ }
+
+ override fun onBind(intent: Intent?): IBinder? = null
+
+ private fun initDefaultNotifications() {
+ silentNotification = NotificationCompat.Builder(this, CHANNEL_ID)
+ .setContentTitle("") // No title
+ .setContentText("") // No content
+ .setSmallIcon(android.R.drawable.ic_notification_overlay)
+ .setPriority(NotificationCompat.PRIORITY_MIN) // Silent notification
+ .setOngoing(true) // Keeps the notification active
+ .build()
+ defaultNotification = NotificationCompat.Builder(this, CHANNEL_ID)
+ .setContentTitle(SERVICE_NAME)
+ .setSmallIcon(android.R.drawable.ic_notification_overlay)
+ .setPriority(NotificationCompat.PRIORITY_DEFAULT) // For android previous to O
+ .build()
+ }
+}
\ No newline at end of file
diff --git a/shared/domain/src/androidMain/kotlin/network/bisq/mobile/domain/service/controller/NotificationServiceController.android.kt b/shared/domain/src/androidMain/kotlin/network/bisq/mobile/domain/service/controller/NotificationServiceController.android.kt
new file mode 100644
index 00000000..17e65b3d
--- /dev/null
+++ b/shared/domain/src/androidMain/kotlin/network/bisq/mobile/domain/service/controller/NotificationServiceController.android.kt
@@ -0,0 +1,83 @@
+package network.bisq.mobile.domain.service.controller
+
+import android.app.NotificationChannel
+import android.app.NotificationManager
+import android.content.Context
+import android.content.Intent
+import android.os.Build
+import androidx.core.app.NotificationCompat
+import network.bisq.mobile.domain.service.BisqForegroundService
+import network.bisq.mobile.utils.Logging
+
+/**
+ * Controller interacting with the bisq service
+ */
+@Suppress("EXPECT_ACTUAL_CLASSIFIERS_ARE_IN_BETA_WARNING")
+actual class NotificationServiceController (private val context: Context): ServiceController, Logging {
+
+ companion object {
+ const val SERVICE_NAME = "Bisq Service"
+ }
+ private var isRunning = false
+
+ /**
+ * Starts the service in the appropiate mode based on the current device running Android API
+ */
+ actual override fun startService() {
+ if (!isRunning) {
+ log.i { "Starting Bisq Service.."}
+ createNotificationChannel()
+ val intent = Intent(context, BisqForegroundService::class.java)
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
+ log.i { "OS supports foreground service" }
+ context.startForegroundService(intent)
+ } else {
+ // if the phone does not support foreground service
+ context.startService(intent)
+ }
+ isRunning = true
+ log.i { "Started Bisq Service"}
+ }
+ }
+
+ // TODO provide an access for this
+ actual override fun stopService() {
+ // TODO we need to leave the service running if the user is ok with it
+ if (isRunning) {
+ deleteNotificationChannel()
+ val intent = Intent(context, BisqForegroundService::class.java)
+ context.stopService(intent)
+ isRunning = false
+ }
+ }
+
+ // TODO support for on click
+ actual fun pushNotification(title: String, message: String) {
+ val notificationManager = context.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
+ val notification = NotificationCompat.Builder(context, BisqForegroundService.CHANNEL_ID)
+ .setContentTitle(title)
+ .setContentText(message)
+ .setSmallIcon(android.R.drawable.ic_notification_overlay)
+ .setPriority(NotificationCompat.PRIORITY_DEFAULT) // For android previous to O
+ .setOngoing(true)
+ .build()
+ notificationManager.notify(BisqForegroundService.PUSH_NOTIFICATION_ID, notification)
+ }
+
+ actual override fun isServiceRunning() = isRunning
+
+ private fun createNotificationChannel() {
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
+ val channel = NotificationChannel(BisqForegroundService.CHANNEL_ID, SERVICE_NAME, NotificationManager.IMPORTANCE_LOW)
+ val manager = context.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
+ manager.createNotificationChannel(channel)
+ }
+ }
+
+ private fun deleteNotificationChannel() {
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
+ val manager = context.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
+ manager.deleteNotificationChannel(BisqForegroundService.CHANNEL_ID)
+ }
+ }
+}
\ No newline at end of file
diff --git a/shared/domain/src/commonMain/kotlin/network/bisq/mobile/client/market/ClientMarketPriceServiceFacade.kt b/shared/domain/src/commonMain/kotlin/network/bisq/mobile/client/market/ClientMarketPriceServiceFacade.kt
index 529933bc..39404041 100644
--- a/shared/domain/src/commonMain/kotlin/network/bisq/mobile/client/market/ClientMarketPriceServiceFacade.kt
+++ b/shared/domain/src/commonMain/kotlin/network/bisq/mobile/client/market/ClientMarketPriceServiceFacade.kt
@@ -58,7 +58,7 @@ class ClientMarketPriceServiceFacade(
applyQuote()
} catch (e: Exception) {
- log.e("Error at API request", e)
+ log.e("Error at getQuotes API request", e)
}
}
}
diff --git a/shared/domain/src/commonMain/kotlin/network/bisq/mobile/client/offerbook/market/ClientMarketListItemService.kt b/shared/domain/src/commonMain/kotlin/network/bisq/mobile/client/offerbook/market/ClientMarketListItemService.kt
index f1d6c9b2..42fb9687 100644
--- a/shared/domain/src/commonMain/kotlin/network/bisq/mobile/client/offerbook/market/ClientMarketListItemService.kt
+++ b/shared/domain/src/commonMain/kotlin/network/bisq/mobile/client/offerbook/market/ClientMarketListItemService.kt
@@ -24,7 +24,7 @@ class ClientMarketListItemService(private val apiGateway: OfferbookApiGateway) :
// Misc
private val coroutineScope = CoroutineScope(BackgroundDispatcher)
private var job: Job? = null
- private var polling = Polling(1000) { updateNumOffers() }
+ private var polling = Polling(10000) { updateNumOffers() }
private var marketListItemsRequested = false
// Life cycle
@@ -45,7 +45,7 @@ class ClientMarketListItemService(private val apiGateway: OfferbookApiGateway) :
requestAndApplyNumOffers()
marketListItemsRequested = true
} catch (e: Exception) {
- log.e("Error at API request", e)
+ log.e("Error at Fill Market List Items API request", e)
}
}
}
@@ -87,7 +87,7 @@ class ClientMarketListItemService(private val apiGateway: OfferbookApiGateway) :
marketListItem
}
} catch (e: Exception) {
- log.e("Error at API request", e)
+ log.e("Error at apply num offers for markets API request", e)
}
}
diff --git a/shared/domain/src/commonMain/kotlin/network/bisq/mobile/client/offerbook/offer/ClientOfferbookListItemService.kt b/shared/domain/src/commonMain/kotlin/network/bisq/mobile/client/offerbook/offer/ClientOfferbookListItemService.kt
index c6a0aa8a..0b839267 100644
--- a/shared/domain/src/commonMain/kotlin/network/bisq/mobile/client/offerbook/offer/ClientOfferbookListItemService.kt
+++ b/shared/domain/src/commonMain/kotlin/network/bisq/mobile/client/offerbook/offer/ClientOfferbookListItemService.kt
@@ -24,7 +24,7 @@ class ClientOfferbookListItemService(private val apiGateway: OfferbookApiGateway
// Misc
private var job: Job? = null
- private var polling = Polling(1000) { updateOffers() }
+ private var polling = Polling(10000) { updateOffers() }
private var selectedMarket: MarketListItem? = null
private val coroutineScope = CoroutineScope(BackgroundDispatcher)
diff --git a/shared/domain/src/commonMain/kotlin/network/bisq/mobile/domain/di/DomainModule.kt b/shared/domain/src/commonMain/kotlin/network/bisq/mobile/domain/di/DomainModule.kt
index c62ad71d..18de796e 100644
--- a/shared/domain/src/commonMain/kotlin/network/bisq/mobile/domain/di/DomainModule.kt
+++ b/shared/domain/src/commonMain/kotlin/network/bisq/mobile/domain/di/DomainModule.kt
@@ -7,6 +7,10 @@ import network.bisq.mobile.domain.data.model.Greeting
import network.bisq.mobile.domain.data.persistance.KeyValueStorage
import network.bisq.mobile.domain.data.repository.*
import network.bisq.mobile.domain.getPlatformSettings
+import network.bisq.mobile.domain.data.repository.BisqStatsRepository
+import network.bisq.mobile.domain.data.repository.BtcPriceRepository
+import network.bisq.mobile.domain.data.repository.GreetingRepository
+import network.bisq.mobile.domain.data.repository.SettingsRepository
import org.koin.dsl.module
val domainModule = module {
diff --git a/shared/domain/src/commonMain/kotlin/network/bisq/mobile/domain/service/controller/NotificationServiceController.kt b/shared/domain/src/commonMain/kotlin/network/bisq/mobile/domain/service/controller/NotificationServiceController.kt
new file mode 100644
index 00000000..a1173823
--- /dev/null
+++ b/shared/domain/src/commonMain/kotlin/network/bisq/mobile/domain/service/controller/NotificationServiceController.kt
@@ -0,0 +1,12 @@
+package network.bisq.mobile.domain.service.controller
+
+/**
+ * And interface for a controller of a notification service
+ */
+@Suppress("EXPECT_ACTUAL_CLASSIFIERS_ARE_IN_BETA_WARNING")
+expect class NotificationServiceController: ServiceController {
+ fun pushNotification(title: String, message: String)
+ override fun startService()
+ override fun stopService()
+ override fun isServiceRunning(): Boolean
+}
\ No newline at end of file
diff --git a/shared/domain/src/commonMain/kotlin/network/bisq/mobile/domain/service/controller/ServiceController.kt b/shared/domain/src/commonMain/kotlin/network/bisq/mobile/domain/service/controller/ServiceController.kt
new file mode 100644
index 00000000..1ab1da4b
--- /dev/null
+++ b/shared/domain/src/commonMain/kotlin/network/bisq/mobile/domain/service/controller/ServiceController.kt
@@ -0,0 +1,10 @@
+package network.bisq.mobile.domain.service.controller
+
+/**
+ * Service controller behaviour definitions
+ */
+interface ServiceController {
+ fun startService()
+ fun stopService()
+ fun isServiceRunning(): Boolean
+}
\ No newline at end of file
diff --git a/shared/domain/src/iosMain/kotlin/network/bisq/mobile/domain/di/DomainModule.ios.kt b/shared/domain/src/iosMain/kotlin/network/bisq/mobile/domain/di/DomainModule.ios.kt
index bf9d5b98..3cc9310b 100644
--- a/shared/domain/src/iosMain/kotlin/network/bisq/mobile/domain/di/DomainModule.ios.kt
+++ b/shared/domain/src/iosMain/kotlin/network/bisq/mobile/domain/di/DomainModule.ios.kt
@@ -1,10 +1,12 @@
package network.bisq.mobile.domain.di
+import network.bisq.mobile.domain.service.controller.NotificationServiceController
import org.koin.core.qualifier.named
import org.koin.dsl.module
val iosDomainModule = module {
single(named("ApiBaseUrl")) { provideApiBaseUrl() }
+ single { NotificationServiceController() }
}
fun provideApiBaseUrl(): String {
diff --git a/shared/domain/src/iosMain/kotlin/network/bisq/mobile/domain/service/controller/NotificationServiceController.ios.kt b/shared/domain/src/iosMain/kotlin/network/bisq/mobile/domain/service/controller/NotificationServiceController.ios.kt
new file mode 100644
index 00000000..d8eddada
--- /dev/null
+++ b/shared/domain/src/iosMain/kotlin/network/bisq/mobile/domain/service/controller/NotificationServiceController.ios.kt
@@ -0,0 +1,169 @@
+package network.bisq.mobile.domain.service.controller
+
+import kotlinx.cinterop.ExperimentalForeignApi
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.delay
+import kotlinx.coroutines.launch
+import network.bisq.mobile.utils.Logging
+import platform.BackgroundTasks.*
+import platform.Foundation.NSDate
+import platform.Foundation.NSUUID
+import platform.Foundation.setValue
+import platform.UserNotifications.*
+import platform.darwin.NSObject
+
+
+@Suppress("EXPECT_ACTUAL_CLASSIFIERS_ARE_IN_BETA_WARNING")
+actual class NotificationServiceController: ServiceController, Logging {
+
+ companion object {
+ const val BACKGROUND_TASK_ID = "network.bisq.mobile.iosUC4273Y485"
+ const val CHECK_NOTIFICATIONS_DELAY = 15 * 10000L
+ }
+
+ private var isRunning = false
+ private var isBackgroundTaskRegistered = false
+ private val logScope = CoroutineScope(Dispatchers.Main)
+
+ private fun setupDelegate() {
+ val delegate = object : NSObject(), UNUserNotificationCenterDelegateProtocol {
+ override fun userNotificationCenter(
+ center: UNUserNotificationCenter,
+ willPresentNotification: UNNotification,
+ withCompletionHandler: (UNNotificationPresentationOptions) -> Unit
+ ) {
+ // Display alert, sound, or badge when the app is in the foreground
+ withCompletionHandler(
+ UNNotificationPresentationOptionAlert or UNNotificationPresentationOptionSound or UNNotificationPresentationOptionBadge
+ )
+ }
+
+ // Handle user actions on the notification
+ override fun userNotificationCenter(
+ center: UNUserNotificationCenter,
+ didReceiveNotificationResponse: UNNotificationResponse,
+ withCompletionHandler: () -> Unit
+ ) {
+ // Handle the response when the user taps the notification
+ withCompletionHandler()
+ }
+ }
+
+ UNUserNotificationCenter.currentNotificationCenter().delegate = delegate
+ logDebug("Notification center delegate applied")
+ }
+
+
+ actual override fun startService() {
+ if (isRunning) {
+ return
+ }
+ logDebug("Starting background service")
+ UNUserNotificationCenter.currentNotificationCenter().requestAuthorizationWithOptions(
+ UNAuthorizationOptionAlert or UNAuthorizationOptionSound or UNAuthorizationOptionBadge
+ ) { granted, error ->
+ if (granted) {
+ logDebug("Notification permission granted.")
+ setupDelegate()
+ registerBackgroundTask()
+ // Once permission is granted, you can start scheduling background tasks
+ startBackgroundTaskLoop()
+ logDebug("Background service started")
+ isRunning = true
+ } else {
+ logDebug("Notification permission denied: ${error?.localizedDescription}")
+ }
+ }
+ }
+
+ actual override fun stopService() {
+ BGTaskScheduler.sharedScheduler.cancelAllTaskRequests()
+ logDebug("Background service stopped")
+ isRunning = false
+ }
+
+ actual fun pushNotification(title: String, message: String) {
+ val content = UNMutableNotificationContent().apply {
+ setValue(title, forKey = "title")
+ setValue(message, forKey = "body")
+ setSound(UNNotificationSound.defaultSound())
+ }
+
+ val trigger = UNTimeIntervalNotificationTrigger.triggerWithTimeInterval(5.0, repeats = false)
+
+ val requestId = NSUUID().UUIDString
+ val request = UNNotificationRequest.requestWithIdentifier(
+ requestId,
+ content,
+ trigger
+ )
+ UNUserNotificationCenter.currentNotificationCenter().addNotificationRequest(request) { error ->
+ if (error != null) {
+ logDebug("Error adding notification request: ${error.localizedDescription}")
+ } else {
+ logDebug("Notification $requestId added successfully")
+ }
+ }
+ }
+
+ actual override fun isServiceRunning(): Boolean {
+ // iOS doesn't allow querying background task state directly
+ return isRunning
+ }
+
+ private fun handleBackgroundTask(task: BGProcessingTask) {
+ task.setTaskCompletedWithSuccess(true) // Mark the task as completed
+ logDebug("Background task completed successfully")
+ scheduleBackgroundTask() // Re-schedule the next task
+ }
+
+ private fun startBackgroundTaskLoop() {
+ CoroutineScope(Dispatchers.Default).launch {
+ while (isRunning) {
+ scheduleBackgroundTask()
+ delay(CHECK_NOTIFICATIONS_DELAY) // Check notifications every min
+ }
+ }
+ }
+
+ @OptIn(ExperimentalForeignApi::class)
+ private fun scheduleBackgroundTask() {
+ val request = BGProcessingTaskRequest(BACKGROUND_TASK_ID).apply {
+ requiresNetworkConnectivity = true
+ earliestBeginDate = NSDate(timeIntervalSinceReferenceDate = 10.0)
+ }
+ BGTaskScheduler.sharedScheduler.submitTaskRequest(request, null)
+ logDebug("Background task scheduled")
+ }
+
+// fun setupTaskHandlers() {
+// BGTaskScheduler.sharedScheduler.registerForTaskWithIdentifier(BACKGROUND_TASK_ID, BGProcessingTask.class, ::handleBackgroundTask)
+// }
+
+
+ private fun logDebug(message: String) {
+ logScope.launch {
+ log.d { message }
+ }
+ }
+
+
+ private fun registerBackgroundTask() {
+ if (isBackgroundTaskRegistered) {
+ logDebug("Background task is already registered.")
+ return
+ }
+
+ // Register for background task handler
+ BGTaskScheduler.sharedScheduler.registerForTaskWithIdentifier(
+ identifier = BACKGROUND_TASK_ID,
+ usingQueue = null
+ ) { task ->
+ handleBackgroundTask(task as BGProcessingTask)
+ }
+
+ isBackgroundTaskRegistered = true
+ logDebug("Background task handler registered.")
+ }
+}
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 897a443c..c13f2ea2 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
@@ -1,15 +1,17 @@
package network.bisq.mobile.client
import network.bisq.mobile.domain.data.repository.main.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
import network.bisq.mobile.presentation.MainPresenter
class ClientMainPresenter(
+ notificationServiceController: NotificationServiceController,
private val applicationBootstrapFacade: ApplicationBootstrapFacade,
private val offerbookServiceFacade: OfferbookServiceFacade,
private val marketPriceServiceFacade: MarketPriceServiceFacade
-) : MainPresenter() {
+) : MainPresenter(notificationServiceController) {
override fun onViewAttached() {
super.onViewAttached()
diff --git a/shared/presentation/src/commonMain/kotlin/network/bisq/mobile/presentation/MainPresenter.kt b/shared/presentation/src/commonMain/kotlin/network/bisq/mobile/presentation/MainPresenter.kt
index 7df17d48..a91d01a7 100644
--- a/shared/presentation/src/commonMain/kotlin/network/bisq/mobile/presentation/MainPresenter.kt
+++ b/shared/presentation/src/commonMain/kotlin/network/bisq/mobile/presentation/MainPresenter.kt
@@ -1,19 +1,33 @@
package network.bisq.mobile.presentation
+import androidx.annotation.CallSuper
import androidx.navigation.NavHostController
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
+import kotlinx.coroutines.launch
import network.bisq.mobile.android.node.BuildNodeConfig
import network.bisq.mobile.client.shared.BuildConfig
+import network.bisq.mobile.domain.data.BackgroundDispatcher
import network.bisq.mobile.domain.getPlatformInfo
+import network.bisq.mobile.domain.service.controller.NotificationServiceController
import network.bisq.mobile.presentation.ui.AppPresenter
+import kotlin.random.Random
/**
* Main Presenter as an example of implementation for now.
*/
-open class MainPresenter() :
+open class MainPresenter(private val notificationServiceController: NotificationServiceController) :
BasePresenter(null), AppPresenter {
+ companion object {
+ // FIXME this will be erased eventually, for now you can turn on to see the notifications working
+ // it will push a notification every 60 sec
+ const val testNotifications = false
+ const val PUSH_DELAY = 60000L
+ }
+
lateinit var navController: NavHostController
private set
@@ -39,9 +53,24 @@ open class MainPresenter() :
log.i { "iOS Client Version: ${BuildConfig.IOS_APP_VERSION}" }
log.i { "Android Client Version: ${BuildConfig.IOS_APP_VERSION}" }
log.i { "Android Node Version: ${BuildNodeConfig.APP_VERSION}" }
- // CoroutineScope(BackgroundDispatcher).launch {
- // greetingRepository.create(Greeting())
- // }
+ }
+
+ @CallSuper
+ override fun onViewAttached() {
+ super.onViewAttached()
+ notificationServiceController.startService()
+ // sample code for push notifications sends a random message every 10 secs
+ if (testNotifications) {
+ backgroundScope.launch {
+ while (notificationServiceController.isServiceRunning()) {
+ val randomTitle = "Title ${Random.nextInt(1, 100)}"
+ val randomMessage = "Message ${Random.nextInt(1, 100)}"
+ notificationServiceController.pushNotification(randomTitle, randomMessage)
+ log.d {"Pushed: $randomTitle - $randomMessage" }
+ delay(PUSH_DELAY) // 1 min
+ }
+ }
+ }
}
// Toggle action
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 66662bd6..475f46a8 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
@@ -31,7 +31,7 @@ val presentationModule = module {
single(named("RootNavController")) { getKoin().getProperty("RootNavController") }
single(named("TabNavController")) { getKoin().getProperty("TabNavController") }
- single { ClientMainPresenter(get(), get(), get()) } bind AppPresenter::class
+ single { ClientMainPresenter(get(), get(), get(), get()) } bind AppPresenter::class
single { TopBarPresenter(get(), get()) } bind ITopBarPresenter::class
diff --git a/shared/presentation/src/commonMain/kotlin/network/bisq/mobile/presentation/ui/App.kt b/shared/presentation/src/commonMain/kotlin/network/bisq/mobile/presentation/ui/App.kt
index 202cff8e..c37704ba 100644
--- a/shared/presentation/src/commonMain/kotlin/network/bisq/mobile/presentation/ui/App.kt
+++ b/shared/presentation/src/commonMain/kotlin/network/bisq/mobile/presentation/ui/App.kt
@@ -10,6 +10,7 @@ import org.jetbrains.compose.ui.tooling.preview.Preview
import kotlinx.coroutines.flow.StateFlow
import network.bisq.mobile.presentation.ViewPresenter
import network.bisq.mobile.presentation.ui.components.SwipeBackIOSNavigationHandler
+import network.bisq.mobile.presentation.ui.helpers.RememberPresenterLifecycle
import org.koin.compose.koinInject
import network.bisq.mobile.presentation.ui.navigation.Routes
@@ -41,20 +42,12 @@ fun App() {
var isNavControllerSet by remember { mutableStateOf(false) }
val presenter: AppPresenter = koinInject()
- DisposableEffect(Unit) {
-// For the main presenter use case we leave this for the moment the activity/viewcontroller respectively gets attached
-// presenter.onViewAttached()
+ RememberPresenterLifecycle(presenter, {
getKoin().setProperty("RootNavController", rootNavController)
getKoin().setProperty("TabNavController", tabNavController)
presenter.setNavController(rootNavController)
isNavControllerSet = true
-
- onDispose {
- // Optional cleanup logic
-// getKoin().setProperty("RootNavController", null)
-// getKoin().setProperty("TabNavController", null)
- }
- }
+ })
val lyricist = rememberStrings()
diff --git a/shared/presentation/src/commonMain/kotlin/network/bisq/mobile/presentation/ui/helpers/LifecycleHelper.kt b/shared/presentation/src/commonMain/kotlin/network/bisq/mobile/presentation/ui/helpers/LifecycleHelper.kt
index d7814c04..2200419c 100644
--- a/shared/presentation/src/commonMain/kotlin/network/bisq/mobile/presentation/ui/helpers/LifecycleHelper.kt
+++ b/shared/presentation/src/commonMain/kotlin/network/bisq/mobile/presentation/ui/helpers/LifecycleHelper.kt
@@ -4,13 +4,24 @@ import androidx.compose.runtime.Composable
import androidx.compose.runtime.DisposableEffect
import network.bisq.mobile.presentation.ViewPresenter
+/**
+ * @param presenter
+ * @param onExecute callback after view attached
+ * @param onDispose callback before on view unnattaching
+ */
@Composable
-fun RememberPresenterLifecycle(presenter: ViewPresenter) {
+fun RememberPresenterLifecycle(presenter: ViewPresenter, onExecute: (() -> Unit)? = null, onDispose: (() -> Unit)? = null) {
DisposableEffect(presenter) {
presenter.onViewAttached() // Called when the view is attached
+ onExecute?.let {
+ onExecute()
+ }
onDispose {
presenter.onViewUnattaching() // Called when the view is detached
+ onDispose?.let {
+ onDispose()
+ }
}
}
}
\ No newline at end of file
diff --git a/shared/presentation/src/commonMain/kotlin/network/bisq/mobile/presentation/ui/uicases/GettingStartedPresenter.kt b/shared/presentation/src/commonMain/kotlin/network/bisq/mobile/presentation/ui/uicases/GettingStartedPresenter.kt
index ebc6b832..a2b2b583 100644
--- a/shared/presentation/src/commonMain/kotlin/network/bisq/mobile/presentation/ui/uicases/GettingStartedPresenter.kt
+++ b/shared/presentation/src/commonMain/kotlin/network/bisq/mobile/presentation/ui/uicases/GettingStartedPresenter.kt
@@ -12,7 +12,6 @@ import network.bisq.mobile.domain.data.repository.BisqStatsRepository
import network.bisq.mobile.domain.data.repository.BtcPriceRepository
import network.bisq.mobile.presentation.BasePresenter
import network.bisq.mobile.presentation.MainPresenter
-import network.bisq.mobile.presentation.ui.navigation.Routes
class GettingStartedPresenter(
mainPresenter: MainPresenter,
diff --git a/shared/presentation/src/iosMain/kotlin/network/bisq/mobile/presentation/di/DependenciesProviderHelper.kt b/shared/presentation/src/iosMain/kotlin/network/bisq/mobile/presentation/di/DependenciesProviderHelper.kt
index 66bd3a59..3b8a2427 100644
--- a/shared/presentation/src/iosMain/kotlin/network/bisq/mobile/presentation/di/DependenciesProviderHelper.kt
+++ b/shared/presentation/src/iosMain/kotlin/network/bisq/mobile/presentation/di/DependenciesProviderHelper.kt
@@ -11,6 +11,9 @@ import org.koin.core.context.startKoin
import org.koin.core.parameter.parametersOf
import org.koin.core.qualifier.Qualifier
+/**
+ * Helper for iOS koin injection
+ */
class DependenciesProviderHelper {
fun initKoin() {