diff --git a/composeApp/build.gradle.kts b/composeApp/build.gradle.kts
index bd98379a..cf834428 100644
--- a/composeApp/build.gradle.kts
+++ b/composeApp/build.gradle.kts
@@ -11,6 +11,7 @@ plugins {
alias(libs.plugins.composeHotReload)
alias(libs.plugins.kotlinPluginSerialization)
alias(libs.plugins.ksp)
+ alias(libs.plugins.sqldelight)
}
kotlin {
@@ -39,13 +40,14 @@ kotlin {
sourceSets {
androidMain.dependencies {
implementation(libs.androidx.activity.compose)
+ implementation(libs.androidx.work.runtime.ktx)
// implementation(libs.androidx.room.sqlite.wrapper)
implementation(libs.compose.uiToolingPreview)
implementation(libs.okhttp.coroutines)
+
+ implementation(libs.sqldelight.android.driver)
}
commonMain.dependencies {
- implementation(libs.lightning.kmp.core)
-
implementation(libs.androidx.datastore)
implementation(libs.androidx.datastore.preferences)
@@ -72,21 +74,36 @@ kotlin {
implementation(libs.compose.uiToolingPreview)
- implementation(libs.navigation.compose)
-
implementation(libs.kermit)
implementation(libs.kotlinx.datetime)
implementation(libs.kotlinx.serialization.json)
+ implementation(libs.kotlinx.serialization.cbor)
+
implementation(libs.ktor.client.core)
implementation(libs.ktor.client.cio)
implementation(libs.ktor.client.websockets)
implementation(libs.ktor.serialization.kotlinx.json)
+
+
+ implementation(libs.lightning.kmp.core)
+
+ implementation(libs.navigation.compose)
+
implementation(libs.okio)
+
+ implementation(libs.sqldelight.runtime)
+ implementation(libs.sqldelight.coroutines.extensions)
+
implementation(libs.vitorpamplona.quartz)
+
+ implementation("com.ionspin.kotlin:bignum:0.3.10")
+
+ implementation("no.synth:kmp-zip:0.8.0")
+ implementation("no.synth:kmp-zip-okio:0.8.0")
}
commonTest.dependencies {
implementation(libs.kotlin.test)
@@ -96,6 +113,7 @@ kotlin {
implementation(libs.kotlinx.coroutinesSwing)
}
iosMain.dependencies {
+ implementation(libs.sqldelight.native.driver)
}
}
}
@@ -104,6 +122,9 @@ android {
namespace = "ac.cord.auxiliary.android"
compileSdk = libs.versions.android.compileSdk.get().toInt()
+ buildFeatures {
+ buildConfig = true
+ }
defaultConfig {
applicationId = "ac.cord.auxiliary.android"
minSdk = libs.versions.android.minSdk.get().toInt()
@@ -147,6 +168,23 @@ room3 {
schemaDirectory("$projectDir/schemas")
}
+sqldelight {
+ databases {
+ create("ChannelsDatabase") {
+ packageName.set("fr.acinq.phoenix.db.sqldelight")
+ srcDirs.from("src/commonMain/sqldelight/channelsdb")
+ }
+ create("PaymentsDatabase") {
+ packageName.set("fr.acinq.phoenix.db.sqldelight")
+ srcDirs.from("src/commonMain/sqldelight/paymentsdb")
+ }
+ create("AppDatabase") {
+ packageName.set("fr.acinq.phoenix.db.sqldelight")
+ srcDirs.from("src/commonMain/sqldelight/appdb")
+ }
+ }
+}
+
compose.desktop {
application {
mainClass = "ac.cord.auxiliary.desktop.MainKt"
diff --git a/composeApp/src/androidMain/AndroidManifest.xml b/composeApp/src/androidMain/AndroidManifest.xml
index 9581de76..fbe5a1c1 100644
--- a/composeApp/src/androidMain/AndroidManifest.xml
+++ b/composeApp/src/androidMain/AndroidManifest.xml
@@ -4,7 +4,10 @@
+
+
by preferencesDataStore(name = "globalprefs")
+
+class InomboloApplication: Application() {
+ lateinit var globalPrefs: GlobalPrefs
+ lateinit var phoenixGlobal: PhoenixGlobal
+
+ override fun onCreate() {
+ super.onCreate()
+
+ phoenixGlobal = PhoenixGlobal(PlatformContext(applicationContext))
+ globalPrefs = GlobalPrefs(applicationContext.globalPrefs)
+ BusinessManager.initialize(applicationContext)
+
+ }
+}
\ No newline at end of file
diff --git a/composeApp/src/androidMain/kotlin/ac/cord/auxiliary/android/MainActivity.kt b/composeApp/src/androidMain/kotlin/ac/cord/auxiliary/android/MainActivity.kt
index c950c61a..2947484c 100644
--- a/composeApp/src/androidMain/kotlin/ac/cord/auxiliary/android/MainActivity.kt
+++ b/composeApp/src/androidMain/kotlin/ac/cord/auxiliary/android/MainActivity.kt
@@ -3,13 +3,21 @@ package ac.cord.auxiliary.android
import ac.cord.auxiliary.compose.AuxApp
import ac.cord.auxiliary.compose.AuxGlobal
import ac.cord.auxiliary.compose.PlatformContext
+import android.content.Intent
+import android.nfc.NfcAdapter
import android.os.Bundle
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.activity.enableEdgeToEdge
import androidx.navigation.compose.rememberNavController
+import com.machankura.compose.ui.composable.widgets.nfc.NfcState
+import com.machankura.compose.ui.composable.widgets.nfc.NfcStateRepository
+import fr.acinq.phoenix.android.services.HceService
class MainActivity : ComponentActivity() {
+
+ private var nfcAdapter: NfcAdapter? = null
+
override fun onCreate(savedInstanceState: Bundle?) {
enableEdgeToEdge()
super.onCreate(savedInstanceState)
@@ -27,4 +35,16 @@ class MainActivity : ComponentActivity() {
)
}
}
+
+ fun stopHceService() {
+ stopService(Intent(this@MainActivity, HceService::class.java))
+ }
+
+ fun stopNfcReader() {
+ if (NfcStateRepository.isReading()) {
+ NfcStateRepository.updateState(NfcState.Inactive)
+ }
+ nfcAdapter?.disableReaderMode(this@MainActivity)
+ }
+
}
\ No newline at end of file
diff --git a/composeApp/src/androidMain/kotlin/ac/cord/auxiliary/compose/AppVersion.android.kt b/composeApp/src/androidMain/kotlin/ac/cord/auxiliary/compose/AppVersion.android.kt
new file mode 100644
index 00000000..93198ec5
--- /dev/null
+++ b/composeApp/src/androidMain/kotlin/ac/cord/auxiliary/compose/AppVersion.android.kt
@@ -0,0 +1,10 @@
+package ac.cord.auxiliary.compose
+
+import ac.cord.auxiliary.android.BuildConfig
+
+actual object AppVersion {
+ actual val versionName: String
+ get() = BuildConfig.VERSION_NAME
+ actual val versionCode: String
+ get() = BuildConfig.VERSION_CODE.toString()
+}
\ No newline at end of file
diff --git a/composeApp/src/androidMain/kotlin/ac/cord/auxiliary/compose/ui/composable/widgets/nfc/HceMonitor.kt b/composeApp/src/androidMain/kotlin/ac/cord/auxiliary/compose/ui/composable/widgets/nfc/HceMonitor.kt
new file mode 100644
index 00000000..be61ebcb
--- /dev/null
+++ b/composeApp/src/androidMain/kotlin/ac/cord/auxiliary/compose/ui/composable/widgets/nfc/HceMonitor.kt
@@ -0,0 +1,82 @@
+/*
+ * Copyright 2025 ACINQ SAS
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.machankura.compose.ui.composable.widgets.nfc
+
+import androidx.compose.foundation.Image
+import androidx.compose.foundation.layout.PaddingValues
+import androidx.compose.foundation.layout.Spacer
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.height
+import androidx.compose.foundation.layout.size
+import androidx.compose.material3.FilledTonalButton
+import androidx.compose.material3.MaterialTheme
+import androidx.compose.material3.Text
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.collectAsState
+import androidx.compose.runtime.getValue
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.graphics.ColorFilter
+import androidx.compose.ui.platform.LocalContext
+import androidx.compose.ui.res.painterResource
+import androidx.compose.ui.res.stringResource
+import androidx.compose.ui.unit.dp
+import com.machankura.compose.ui.composable.widgets.dialogs.ModalBottomSheet
+import fr.acinq.phoenix.utils.extensions.findActivitySafe
+
+
+/** Displays a blocking bottom sheet when the HCE service is started, using the state in [NfcStateRepository]. */
+@Composable
+fun HceMonitorDialog() {
+ val context = LocalContext.current
+ val activity = context.findActivitySafe() ?: return
+
+ val state by NfcStateRepository.state.collectAsState(initial = null)
+
+ val onDone = { activity.stopHceService() }
+
+ when (state) {
+ is NfcState.EmulatingTag -> ModalBottomSheet(
+ onDismiss = onDone,
+ scrimAlpha = .5f,
+ horizontalAlignment = Alignment.CenterHorizontally,
+ internalPadding = PaddingValues(horizontal = 24.dp),
+ dismissOnScrimClick = false,
+ ) {
+ Text(text = "HCE Emulation", style = MaterialTheme.typography.headlineSmall)
+// TODO: Spacer(Modifier.height(16.dp))
+// Image(
+// painter = painterResource(id = R.drawable.ic_nfc),
+// contentDescription = stringResource(R.string.nfc_button),
+// modifier = Modifier.size(64.dp),
+// colorFilter = ColorFilter.tint(MaterialTheme.colors.primary)
+// )
+ Spacer(Modifier.height(24.dp))
+ FilledTonalButton(
+// TODO: icon = R.drawable.ic_cross,
+ onClick = onDone,
+ modifier = Modifier.fillMaxWidth()
+ ) {
+ Text("Cancel")
+ }
+ Spacer(Modifier.height(24.dp))
+ }
+ is NfcState.ShowReader -> {}
+ is NfcState.Inactive -> {}
+ null -> {}
+ }
+}
diff --git a/composeApp/src/androidMain/kotlin/ac/cord/auxiliary/compose/ui/composable/widgets/nfc/NfcReaderMonitor.kt b/composeApp/src/androidMain/kotlin/ac/cord/auxiliary/compose/ui/composable/widgets/nfc/NfcReaderMonitor.kt
new file mode 100644
index 00000000..266204fa
--- /dev/null
+++ b/composeApp/src/androidMain/kotlin/ac/cord/auxiliary/compose/ui/composable/widgets/nfc/NfcReaderMonitor.kt
@@ -0,0 +1,83 @@
+/*
+ * Copyright 2025 ACINQ SAS
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.machankura.compose.ui.composable.widgets.nfc
+
+import androidx.compose.foundation.Image
+import androidx.compose.foundation.layout.PaddingValues
+import androidx.compose.foundation.layout.Spacer
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.height
+import androidx.compose.material3.Button
+import androidx.compose.material3.FilledTonalButton
+import androidx.compose.material3.MaterialTheme
+import androidx.compose.material3.Text
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.collectAsState
+import androidx.compose.runtime.getValue
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.graphics.ColorFilter
+import androidx.compose.ui.platform.LocalContext
+import androidx.compose.ui.res.painterResource
+import androidx.compose.ui.res.stringResource
+import androidx.compose.ui.unit.dp
+import com.machankura.compose.ui.composable.widgets.dialogs.ModalBottomSheet
+import fr.acinq.phoenix.utils.extensions.findActivitySafe
+
+@Composable
+fun NfcReaderMonitor() {
+ val context = LocalContext.current
+ val state by NfcStateRepository.state.collectAsState(initial = null)
+ val activity = context.findActivitySafe() ?: return
+
+ val onDismiss = { activity.stopNfcReader() }
+
+ when (state) {
+ null -> {}
+ is NfcState.Inactive -> {}
+ is NfcState.EmulatingTag -> {}
+ is NfcState.ShowReader -> {
+ ModalBottomSheet(
+ onDismiss = onDismiss,
+ scrimAlpha = .5f,
+ horizontalAlignment = Alignment.CenterHorizontally,
+ internalPadding = PaddingValues(horizontal = 24.dp),
+ dismissOnScrimClick = false,
+ ) {
+ Text(text = "NFC Reader", style = MaterialTheme.typography.headlineSmall)
+// TODO: Spacer(Modifier.height(16.dp))
+// Image(
+// painter = painterResource(id = R.drawable.ic_nfc),
+// contentDescription = stringResource(R.string.nfc_button),
+// modifier = Modifier.size(64.dp),
+// colorFilter = ColorFilter.tint(MaterialTheme.colors.primary)
+// )
+ Spacer(Modifier.height(16.dp))
+// Text(text = stringResource(R.string.nfc_reader_desc), style = MaterialTheme.typography.subtitle2)
+ Spacer(Modifier.height(24.dp))
+ FilledTonalButton(
+// TODO: icon = R.drawable.ic_cross,
+ onClick = onDismiss,
+ modifier = Modifier.fillMaxWidth()
+ ) {
+ Text("Cancel")
+ }
+ Spacer(Modifier.height(24.dp))
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/composeApp/src/androidMain/kotlin/ac/cord/auxiliary/compose/ui/composable/widgets/nfc/NfcState.kt b/composeApp/src/androidMain/kotlin/ac/cord/auxiliary/compose/ui/composable/widgets/nfc/NfcState.kt
new file mode 100644
index 00000000..312a88a4
--- /dev/null
+++ b/composeApp/src/androidMain/kotlin/ac/cord/auxiliary/compose/ui/composable/widgets/nfc/NfcState.kt
@@ -0,0 +1,39 @@
+/*
+ * Copyright 2025 ACINQ SAS
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.machankura.compose.ui.composable.widgets.nfc
+
+import kotlinx.coroutines.flow.MutableStateFlow
+import kotlinx.coroutines.flow.asStateFlow
+
+sealed class NfcState {
+ data object Inactive: NfcState()
+ sealed class Busy : NfcState()
+ data object ShowReader: Busy()
+ data class EmulatingTag(val paymentRequest: String): Busy()
+}
+
+object NfcStateRepository {
+ private val _state = MutableStateFlow(null)
+ val state = _state.asStateFlow()
+
+ fun isReading() = state.value is NfcState.ShowReader
+ fun isEmulating() = state.value is NfcState.EmulatingTag
+
+ fun updateState(s: NfcState) {
+ _state.value = s
+ }
+}
diff --git a/composeApp/src/androidMain/kotlin/fr/acinq/phoenix/android/BusinessManager.kt b/composeApp/src/androidMain/kotlin/fr/acinq/phoenix/android/BusinessManager.kt
new file mode 100644
index 00000000..8b479274
--- /dev/null
+++ b/composeApp/src/androidMain/kotlin/fr/acinq/phoenix/android/BusinessManager.kt
@@ -0,0 +1,327 @@
+/*
+ * Copyright 2025 ACINQ SAS
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package fr.acinq.phoenix.android
+
+import ac.cord.auxiliary.android.InomboloApplication
+import ac.cord.auxiliary.compose.AppVersion
+import ac.cord.auxiliary.compose.ui.composable.widgets.wallet.WalletAvatars
+import android.content.Context
+import android.text.format.DateUtils
+import fr.acinq.lightning.LiquidityEvents
+import fr.acinq.lightning.PaymentEvents
+import fr.acinq.lightning.utils.Connection
+import fr.acinq.lightning.utils.currentTimestampMillis
+import fr.acinq.phoenix.BusinessMonitorJobs
+import fr.acinq.phoenix.BusinessRunning
+import fr.acinq.phoenix.PhoenixBusiness
+import fr.acinq.phoenix.android.services.InflightPaymentsWatcher
+import fr.acinq.phoenix.data.StartBusinessResult
+import fr.acinq.phoenix.data.StartupParams
+import fr.acinq.phoenix.data.WalletId
+import fr.acinq.phoenix.data.inFlightPaymentsCount
+import fr.acinq.phoenix.managers.AppConnectionsDaemon
+import fr.acinq.phoenix.managers.DataStoreManager
+import fr.acinq.phoenix.managers.NodeParamsManager
+import fr.acinq.phoenix.managers.PeerManager
+import fr.acinq.phoenix.managers.WalletManager
+import fr.acinq.phoenix.managers.global.CurrencyManager
+import fr.acinq.phoenix.utils.MnemonicLanguage
+import fr.acinq.phoenix.utils.SystemNotificationHelper
+import fr.acinq.phoenix.utils.preferences.InternalPrefs
+import fr.acinq.phoenix.utils.preferences.UserPrefs
+import fr.acinq.phoenix.utils.preferences.UserWalletMetadata
+import fr.acinq.phoenix.utils.preferences.getByWalletIdOrDefault
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.Job
+import kotlinx.coroutines.SupervisorJob
+import kotlinx.coroutines.delay
+import kotlinx.coroutines.flow.MutableStateFlow
+import kotlinx.coroutines.flow.asStateFlow
+import kotlinx.coroutines.flow.combine
+import kotlinx.coroutines.flow.filterNotNull
+import kotlinx.coroutines.flow.first
+import kotlinx.coroutines.launch
+import kotlinx.coroutines.sync.Mutex
+import kotlinx.coroutines.sync.withLock
+import org.slf4j.LoggerFactory
+import kotlin.time.Duration.Companion.hours
+
+object BusinessManager {
+
+ private val log = LoggerFactory.getLogger(this::class.java)
+ private val supervisor = SupervisorJob()
+ private val scope = CoroutineScope(Dispatchers.Default + supervisor)
+ private val startupMutex = Mutex()
+
+ // No memory leaks because this can only contain the application context.
+ private lateinit var appContext: Context
+ private val application by lazy { appContext as InomboloApplication }
+
+ /** A map of (walletId -> active businesses) */
+ private val _businessFlow = MutableStateFlow