Skip to content

Commit

Permalink
Feature/ios client ongoing trades notifications (#178)
Browse files Browse the repository at this point in the history
* iOS notifications implementation

 - background task that handles notifications only turned on whilst app is in the background
 - fix lifecycleaware not notifying onPause() correctly
 - ios notificationService implementation always return "in background" since the service only starts in background
* - cleanup code
  • Loading branch information
rodvar authored Jan 29, 2025
1 parent 88795ca commit 5e173e2
Show file tree
Hide file tree
Showing 6 changed files with 68 additions and 37 deletions.
10 changes: 7 additions & 3 deletions iosClient/iosClient/ContentView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -5,19 +5,23 @@ import presentation
import domain

struct ComposeView: UIViewControllerRepresentable {


@EnvironmentObject var notificationServiceWrapper: NotificationServiceWrapper
private let presenter: MainPresenter = get()

func makeUIViewController(context: Context) -> UIViewController {
return LifecycleAwareComposeViewController(presenter: presenter)
return LifecycleAwareComposeViewController(presenter: presenter, notificationServiceWrapper: notificationServiceWrapper)
}

func updateUIViewController(_ uiViewController: UIViewController, context: Context) {}
}

struct ContentView: View {
@EnvironmentObject var notificationServiceWrapper: NotificationServiceWrapper

var body: some View {
ComposeView()
.ignoresSafeArea(.keyboard) // Compose has own keyboard handler
.ignoresSafeArea(.keyboard) // Compose has own keyboard handler
.environmentObject(notificationServiceWrapper)
}
}
18 changes: 12 additions & 6 deletions iosClient/iosClient/LifecycleAwareComposeViewController.swift
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,11 @@ import domain

class LifecycleAwareComposeViewController: UIViewController {
private let presenter: MainPresenter
private let notificationServiceWrapper: NotificationServiceWrapper

init(presenter: MainPresenter) {
init(presenter: MainPresenter, notificationServiceWrapper: NotificationServiceWrapper) {
self.presenter = presenter
self.notificationServiceWrapper = notificationServiceWrapper
MainPresenter.companion.doInit()
super.init(nibName: nil, bundle: nil)
}
Expand All @@ -26,11 +28,15 @@ class LifecycleAwareComposeViewController: UIViewController {
override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
presenter.onResume()
NotificationCenter.default.addObserver(
self,
selector: #selector(handleDidBecomeActive),
name: UIApplication.didBecomeActiveNotification,
object: nil)
notificationServiceWrapper.notificationServiceController.stopService()
NotificationCenter.default.addObserver(forName: UIApplication.willResignActiveNotification, object: nil, queue: .main) { [self] _ in
notificationServiceWrapper.notificationServiceController.startService()
presenter.onPause()
}
NotificationCenter.default.addObserver(forName: UIApplication.didBecomeActiveNotification, object: nil, queue: .main) { [self] _ in
notificationServiceWrapper.notificationServiceController.stopService()
presenter.onResume()
}
}

@objc private func handleDidBecomeActive() {
Expand Down
13 changes: 11 additions & 2 deletions iosClient/iosClient/iosClient.swift
Original file line number Diff line number Diff line change
@@ -1,8 +1,17 @@
import SwiftUI
import presentation

class NotificationServiceWrapper: ObservableObject {
@Published var notificationServiceController: DomainNotificationServiceController

init() {
self.notificationServiceController = get()
}
}

@main
struct iosClient: App {
@StateObject var notificationServiceWrapper = NotificationServiceWrapper()

init() {
DependenciesProviderHelper().doInitKoin()
Expand All @@ -11,7 +20,7 @@ struct iosClient: App {
var body: some Scene {
WindowGroup {
ContentView()
.environmentObject(notificationServiceWrapper)
}
}

}
}
Original file line number Diff line number Diff line change
Expand Up @@ -135,8 +135,12 @@ actual class NotificationServiceController (private val appForegroundController:

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)
try {
val manager = context.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
manager.deleteNotificationChannel(BisqForegroundService.CHANNEL_ID)
} catch (e: Exception) {
log.e(e) { "Failed to delete bisq notification channel" }
}
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,6 @@ import platform.BackgroundTasks.*
import platform.Foundation.NSDate
import platform.Foundation.NSUUID
import platform.Foundation.setValue
import platform.UIKit.UIApplication
import platform.UIKit.UIApplicationState
import platform.UserNotifications.*
import platform.darwin.NSObject
import platform.darwin.dispatch_get_main_queue
Expand Down Expand Up @@ -66,6 +64,7 @@ actual class NotificationServiceController(private val appForegroundController:
logDebug("Notification Service already started, skipping launch")
return
}
stopService() // needed in iOS to clear the id registration and avoid duplicates
logDebug("Starting background service")
UNUserNotificationCenter.currentNotificationCenter().requestAuthorizationWithOptions(
UNAuthorizationOptionAlert or UNAuthorizationOptionSound or UNAuthorizationOptionBadge
Expand All @@ -84,6 +83,7 @@ actual class NotificationServiceController(private val appForegroundController:
}

actual override fun stopService() {
// unregisterAllObservers()
BGTaskScheduler.sharedScheduler.cancelAllTaskRequests()
logDebug("Background service stopped")
isRunning = false
Expand All @@ -103,12 +103,15 @@ actual class NotificationServiceController(private val appForegroundController:
observerJobs[stateFlow] = job
}

// TODO
actual override fun unregisterObserver(stateFlow: StateFlow<*>) {
observerJobs[stateFlow]?.cancel()
observerJobs.remove(stateFlow)
}

private fun unregisterAllObservers() {
observerJobs?.keys?.forEach { unregisterObserver(it) }
}

actual fun pushNotification(title: String, message: String) {
if (isAppInForeground()) {
log.w { "Skipping notification since app is in the foreground" }
Expand Down Expand Up @@ -144,9 +147,9 @@ actual class NotificationServiceController(private val appForegroundController:
}

private fun handleBackgroundTask(task: BGProcessingTask) {
task.setTaskCompletedWithSuccess(true) // Mark the task as completed
task.setTaskCompletedWithSuccess(true)
logDebug("Background task completed successfully")
scheduleBackgroundTask() // Re-schedule the next task
scheduleBackgroundTask()
}

private fun startBackgroundTaskLoop() {
Expand All @@ -169,11 +172,6 @@ actual class NotificationServiceController(private val appForegroundController:
logDebug("Background task scheduled")
}

// fun setupTaskHandlers() {
// BGTaskScheduler.sharedScheduler.registerForTaskWithIdentifier(BACKGROUND_TASK_ID, BGProcessingTask.class, ::handleBackgroundTask)
// }


private fun logDebug(message: String) {
logScope.launch { // (Dispatchers.Main)
log.d { message }
Expand All @@ -188,23 +186,24 @@ actual class NotificationServiceController(private val appForegroundController:
return
}

// Register for background task handler
BGTaskScheduler.sharedScheduler.registerForTaskWithIdentifier(
identifier = BACKGROUND_TASK_ID,
usingQueue = null
) { task ->
handleBackgroundTask(task as BGProcessingTask)
}
runCatching {
BGTaskScheduler.sharedScheduler.registerForTaskWithIdentifier(
identifier = BACKGROUND_TASK_ID,
usingQueue = null
) { task ->
handleBackgroundTask(task as BGProcessingTask)
}

isBackgroundTaskRegistered = true
logDebug("Background task handler registered")
isBackgroundTaskRegistered = true
logDebug("Background task handler registered")
}.onFailure {
log.e(it) { "Failed to register background task - notifications won't work" }
isBackgroundTaskRegistered = false
}
}

actual fun isAppInForeground(): Boolean {
var isForeground = false
dispatch_sync(dispatch_get_main_queue()) {
isForeground = (appForegroundController.isForeground.value)
}
return isForeground
// for iOS we handle this externally
return false
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,16 @@ open class MainPresenter(
@CallSuper
override fun onViewAttached() {
super.onViewAttached()
}

override fun onResume() {
super.onResume()
openTradesNotificationService.stopNotificationService()
}

override fun onPause() {
openTradesNotificationService.launchNotificationService()
super.onPause()
}

// Toggle action
Expand Down

0 comments on commit 5e173e2

Please sign in to comment.