diff --git a/CHANGELOG.md b/CHANGELOG.md
index 2b7591f96..518622d77 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -3,6 +3,20 @@ All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
+## [4.0.31] - 2024-07-02
+
+### Added
+- Allow disabling TLS on Personal Electrum servers
+
+### Changed
+- Refactor ViewModels to support Kotlin Multiplatform
+- Refactor various UI elements
+- Updated project dependencies
+- Bump Breez to version 0.5.1-rc4
+
+- ### Fixed
+- Fix F-Droid dependency issue
+
## [4.0.30] - 2024-06-10
### Added
diff --git a/base/build.gradle.kts b/base/build.gradle.kts
index fd85c1f59..88332d3dd 100644
--- a/base/build.gradle.kts
+++ b/base/build.gradle.kts
@@ -10,15 +10,10 @@ android {
defaultConfig {
minSdk = libs.versions.androidMinSdk.get().toInt()
}
-
- compileOptions {
- sourceCompatibility = JavaVersion.VERSION_17
- targetCompatibility = JavaVersion.VERSION_17
- }
}
kotlin {
- jvmToolchain(17)
+ jvmToolchain(libs.versions.jvm.get().toInt())
}
dependencies {
@@ -41,12 +36,15 @@ dependencies {
api(libs.androidx.browser)
api(libs.androidx.recyclerview)
api(libs.androidx.viewpager2)
- api(libs.installreferrer)
api(libs.androidx.startup.runtime)
api(libs.compose.material3)
api(libs.androidx.work.runtime.ktx)
/** ----------------------------------------------------------------------------------------- */
+ /** --- Countly ---------------------------------------------------------------------------- */
+ api(libs.countly.sdk.android)
+ /** ----------------------------------------------------------------------------------------- */
+
/** --- Logging ---------------------------------------------------------------------------- */
api(libs.slf4j.simple)
api(libs.kotlin.logging.jvm)
diff --git a/base/src/main/java/com/blockstream/base/InstallReferrer.kt b/base/src/main/java/com/blockstream/base/InstallReferrer.kt
new file mode 100644
index 000000000..ccb1c3fbd
--- /dev/null
+++ b/base/src/main/java/com/blockstream/base/InstallReferrer.kt
@@ -0,0 +1,10 @@
+package com.blockstream.base
+
+import ly.count.android.sdk.ModuleAttribution
+
+// No-Op Install Referrer for F-Droid
+open class InstallReferrer {
+ open fun handleReferrer(attribution: ModuleAttribution.Attribution, onComplete: (referrer: String) -> Unit) {
+ onComplete.invoke("")
+ }
+}
\ No newline at end of file
diff --git a/common/build.gradle.kts b/common/build.gradle.kts
index db247d41a..0ac429b66 100644
--- a/common/build.gradle.kts
+++ b/common/build.gradle.kts
@@ -1,5 +1,5 @@
-
import org.jetbrains.compose.desktop.application.dsl.TargetFormat
+import org.jetbrains.kotlin.gradle.ExperimentalKotlinGradlePluginApi
import org.jetbrains.kotlin.gradle.plugin.mpp.apple.XCFramework
plugins {
@@ -32,16 +32,19 @@ kotlin {
applyDefaultHierarchyTemplate()
androidTarget {
+ @OptIn(ExperimentalKotlinGradlePluginApi::class)
compilerOptions {
freeCompilerArgs.addAll("-P", "plugin:org.jetbrains.kotlin.parcelize:additionalAnnotation=com.blockstream.common.Parcelize")
}
- compilations.configureEach {
- kotlinOptions {
- jvmTarget = JavaVersion.VERSION_17.majorVersion
- }
- }
}
+ @OptIn(ExperimentalKotlinGradlePluginApi::class)
+ compilerOptions {
+ freeCompilerArgs.add("-Xexpect-actual-classes")
+ }
+
+ jvmToolchain(libs.versions.jvm.get().toInt())
+
jvm()
val xcf = XCFramework()
@@ -80,7 +83,7 @@ kotlin {
commit = "1892410d13fceccd7cf91f803f06f110efc215b3"
}
- // Support for Objective-C headers with @import directives
+ // Support for Objective-C headers with @import directives
// https://kotlinlang.org/docs/native-cocoapods-libraries.html#support-for-objective-c-headers-with-import-directives
extraOpts += listOf("-compiler-option", "-fmodules")
}
@@ -223,11 +226,6 @@ android {
defaultConfig {
minSdk = libs.versions.androidMinSdk.get().toInt()
}
-
- compileOptions {
- sourceCompatibility = JavaVersion.VERSION_17
- targetCompatibility = JavaVersion.VERSION_17
- }
}
diff --git a/common/src/commonMain/composeResources/values-cs/strings.xml b/common/src/commonMain/composeResources/values-cs/strings.xml
index 2930bfc25..5d9dcbbd9 100644
--- a/common/src/commonMain/composeResources/values-cs/strings.xml
+++ b/common/src/commonMain/composeResources/values-cs/strings.xml
@@ -1100,7 +1100,7 @@
SegWit (BIP84)
Vyberte fiat měnu a bitcoinovou denominaci, aby se v peněžence zobrazovaly správné částky
Vybrat účet
- Select account & asset
+ Select account & asset
Vyberte aplikaci na %1$s
Vybrat aktivum
Vybrat dobu trvání ochrany dvoufaktorovým ověřováním vašich mincí. Nová možnost se vztahuje na nově přijaté mince.
@@ -1413,6 +1413,7 @@
Čekání na transakci…
Peněženka
Peněženka již byla obnovena
+ Wallet already restored: %1$s
Aktiva peněženky
Záloha peněženky
Mince peněženky budou vyžadovat dvoufaktorovou reaktivaci jednou ročně, aby zůstaly chráněny dvoufaktorovou autentizací.
diff --git a/common/src/commonMain/composeResources/values-de/strings.xml b/common/src/commonMain/composeResources/values-de/strings.xml
index c15eb9628..9a8b0f51c 100644
--- a/common/src/commonMain/composeResources/values-de/strings.xml
+++ b/common/src/commonMain/composeResources/values-de/strings.xml
@@ -1100,7 +1100,7 @@
SegWit (BIP84)
Wähle eine Fiat-Währung und eine Bitcoin-Denominierung aus, um die Beträge in deiner Wallet anzuzeigen.
Wähle Konto
- Select account & asset
+ Select account & asset
Wähle eine App auf %1$s
Wählen Sie das Vermögen
Wähle die Dauer des Zwei-Faktor-Authentifizierungsschutzes für deine Coins. Die neu gewählte Option gilt nur für neu erhaltene Coins.
@@ -1413,6 +1413,7 @@
Warten auf Transaktion...
Wallet
Wallet bereits wiederhergestellt
+ Wallet already restored: %1$s
Wallet-Assets
Wallet Backup
Coins in der Wallet werden einmal im Jahr die Zwei-Faktor-Reaktivierung benötigen, um mit Zwei-Faktor-Authentifizierung beschützt zu bleiben
diff --git a/common/src/commonMain/composeResources/values-es/strings.xml b/common/src/commonMain/composeResources/values-es/strings.xml
index c2bb5ec16..3463fcf33 100644
--- a/common/src/commonMain/composeResources/values-es/strings.xml
+++ b/common/src/commonMain/composeResources/values-es/strings.xml
@@ -1100,7 +1100,7 @@
SegWit (BIP84)
Seleccione una divisa fiat y una denominación de bitcóin y le mostraremos los montos correspondientes en su cartera.
Seleccionar cuenta
- Select account & asset
+ Select account & asset
Seleccione una app en %1$s
Seleccionar activo
Seleccione el plazo de protección de sus monedas mediante autenticación de dos factores. Esta nueva opción se aplica a monedas recibidas recientemente.
@@ -1413,6 +1413,7 @@
Esperando transacción...
Cartera
La cartera ya se restauró
+ Wallet already restored: %1$s
Activos de la cartera
Respaldo de la cartera
Las monedas de la cartera seguirán estando protegidas por la autenticación de dos factores siempre y cuando la reactive una vez por año.
diff --git a/common/src/commonMain/composeResources/values-fr/strings.xml b/common/src/commonMain/composeResources/values-fr/strings.xml
index 322233d17..0d42b4734 100644
--- a/common/src/commonMain/composeResources/values-fr/strings.xml
+++ b/common/src/commonMain/composeResources/values-fr/strings.xml
@@ -1100,7 +1100,7 @@
SegWit (BIP84)
Sélectionnez une devise fiat et une dénomination bitcoin pour afficher les montants dans votre portefeuille.
Sélectionner le compte
- Select account & asset
+ Select account & asset
Sélectionner une app sur %1$s
Sélectionner l'actif
Sélectionnez la durée de la protection par authentification à deux facteurs pour vos pièces. La nouvelle option s'applique aux pièces nouvellement reçues.
@@ -1413,6 +1413,7 @@
En attente de la transaction...
Portefeuille
Portefeuille déjà restauré
+ Wallet already restored: %1$s
Actifs du portefeuille
Portefeuille Backup
Les pièces du portefeuille devront être réactivées une fois par an pour rester protégées par l'authentification à deux facteurs.
diff --git a/common/src/commonMain/composeResources/values-he/strings.xml b/common/src/commonMain/composeResources/values-he/strings.xml
index 7a33b5a13..d6c72a56b 100644
--- a/common/src/commonMain/composeResources/values-he/strings.xml
+++ b/common/src/commonMain/composeResources/values-he/strings.xml
@@ -1100,7 +1100,7 @@
SegWit (BIP84)
Select a fiat currency and bitcoin denomination to show amounts in your wallet
Select Account
- Select account & asset
+ Select account & asset
Select an app on %1$s
Select asset
Select duration of Two-Factor Authentication protection for your coins. The new option applies to newly received coins.
@@ -1413,6 +1413,7 @@
Waiting for transaction…
Wallet
Wallet already restored
+ Wallet already restored: %1$s
Wallet Assets
Wallet Backup
Wallet coins will require two-factor reactivation once a year to remain protected by two-factor authentication.
diff --git a/common/src/commonMain/composeResources/values-it/strings.xml b/common/src/commonMain/composeResources/values-it/strings.xml
index d87a8923a..bc220e583 100644
--- a/common/src/commonMain/composeResources/values-it/strings.xml
+++ b/common/src/commonMain/composeResources/values-it/strings.xml
@@ -1100,7 +1100,7 @@
SegWit (BIP84)
Seleziona una valuta fiat e una denominazione bitcoin per mostrare gli importi nel tuo wallet
Seleziona Account
- Select account & asset
+ Select account & asset
Seleziona un'app su %1$s
Seleziona asset
Seleziona la durata dell'Autenticazione a Due Fattori per i tuoi coin. La nuova opzione è valida per i coin appena ricevuti.
@@ -1413,6 +1413,7 @@
In attesa della transazione…
Wallet
Wallet già ripristinato
+ Wallet already restored: %1$s
Wallet Assets
Backup wallet
Per preservare l'autenticazione a due fattori dei coin del tuo wallet sarà necessaria una riattivazione a due fattori una volta all'anno.
diff --git a/common/src/commonMain/composeResources/values-ja/strings.xml b/common/src/commonMain/composeResources/values-ja/strings.xml
index f4eb4d77d..db8a32acf 100644
--- a/common/src/commonMain/composeResources/values-ja/strings.xml
+++ b/common/src/commonMain/composeResources/values-ja/strings.xml
@@ -1100,7 +1100,7 @@
SegWit (BIP84)
ウォレット内の金額表示に利用する法定通貨とビットコインの単位を選択してください
アカウントを選択
- Select account & asset
+ Select account & asset
%1$sでアプリを選択してください
アセットを選択
2段階認証による保護の期間を設定します。新しい設定はこれから受け取るコインにのみ適用されます。
@@ -1413,6 +1413,7 @@
トランザクション待機中…
ウォレット
このウォレットは復元済みです
+ Wallet already restored: %1$s
ウォレット内のアセット
ウォレットのバックアップ
ウォレット内のコインの2段階認証による保護を継続するには毎年保護の再アクティベーションが必要になります。
diff --git a/common/src/commonMain/composeResources/values-nl/strings.xml b/common/src/commonMain/composeResources/values-nl/strings.xml
index 55f68e92f..0705448c2 100644
--- a/common/src/commonMain/composeResources/values-nl/strings.xml
+++ b/common/src/commonMain/composeResources/values-nl/strings.xml
@@ -1100,7 +1100,7 @@
SegWit (BIP84)
Selecteer een fiat-munteenheid en bitcoin-denominatie om bedragen in je wallet weer te geven
Selecteer account
- Select account & asset
+ Select account & asset
Selecteer een app op %1$s
Selecteer asset
Selecteer de duur van de tweetrapsauthenticatie-bescherming voor je munten. De nieuwe optie is van toepassing op nieuw ontvangen munten.
@@ -1413,6 +1413,7 @@
Wachten op transactie…
Wallet
Wallet al hersteld
+ Wallet already restored: %1$s
Wallet-assets
Wallet-back-up
Wallet-munten moeten eenmaal per jaar opnieuw worden geactiveerd met tweetrapsauthenticatie om beschermd te blijven.
diff --git a/common/src/commonMain/composeResources/values-pt-rBR/strings.xml b/common/src/commonMain/composeResources/values-pt-rBR/strings.xml
index f6756174f..f7de4355e 100644
--- a/common/src/commonMain/composeResources/values-pt-rBR/strings.xml
+++ b/common/src/commonMain/composeResources/values-pt-rBR/strings.xml
@@ -1100,7 +1100,7 @@
SegWit (BIP84)
Escolha uma moeda fiduciária e a unidade de bitcoin que serão usadas para mostrar valores na sua carteira.
Selecionar conta
- Select account & asset
+ Select account & asset
Selecione um aplicativo em%1$s
Selecionar ativo
Selecione um intervalo de tempo em que suas moedas estarão protegidas por 2FA. A nova opção se aplica às moedas recém-recebidas.
@@ -1413,6 +1413,7 @@
Aguardando transação…
Carteira
Carteira já restaurada
+ Wallet already restored: %1$s
Ativos da carteira
Backup da carteira
É necessário reativar o 2FA a cada ano para que os fundos permaneçam protegidos por esta camada extra de segurança.
diff --git a/common/src/commonMain/composeResources/values-ro/strings.xml b/common/src/commonMain/composeResources/values-ro/strings.xml
index 5248016bf..aaaf4da85 100644
--- a/common/src/commonMain/composeResources/values-ro/strings.xml
+++ b/common/src/commonMain/composeResources/values-ro/strings.xml
@@ -1100,7 +1100,7 @@
SegWit (BIP84)
Selectați o monedă fiat și o denominare în bitcoin pentru a afișa cantitățile aflate în portofelul dvs
Selectați Contul
- Select account & asset
+ Select account & asset
Selectați o aplicație în %1$s
Selectați moneda
Selectați durata protecției cu Autentificare cu Doi Factori (2FA) pentru monedele dvs. Noua opțiune va fi aplicată monedelor nou-primite.
@@ -1413,6 +1413,7 @@
Se așteaptă tranzacția...
Wallet
Wallet already restored
+ Wallet already restored: %1$s
Wallet Assets
Copie de protecție pentru portofel
Monedele din portofel vor avea nevoie de o reactivare a autentificării cu doi factori o dată pe an, cu scopul de a păstra această protecție.
diff --git a/common/src/commonMain/composeResources/values-ru/strings.xml b/common/src/commonMain/composeResources/values-ru/strings.xml
index 90fa25f36..ce10c0fb8 100644
--- a/common/src/commonMain/composeResources/values-ru/strings.xml
+++ b/common/src/commonMain/composeResources/values-ru/strings.xml
@@ -1100,7 +1100,7 @@
SegWit (BIP84)
Выберите фиатную валюту и номинал биткоинов, чтобы отобразить суммы в вашем кошельке
Выберите аккаунт
- Select account & asset
+ Select account & asset
Выберите приложение на %1$s
Выберите актив
Выберите продолжительность защиты ваших монет двухфакторной аутентификацией. Эта новая опция применяется к вновь полученным монетам.
@@ -1413,6 +1413,7 @@
Ожидание транзакции ...
Кошелек
Кошелек уже восстановлен
+ Wallet already restored: %1$s
Активы кошелька
Резервное копирование кошелька
Монеты кошелька требуют двухфакторной реактивации один раз в год, чтобы оставаться защищенными двухфакторной аутентификацией.
diff --git a/common/src/commonMain/composeResources/values-uk/strings.xml b/common/src/commonMain/composeResources/values-uk/strings.xml
index 4420240fa..9986a9fb2 100644
--- a/common/src/commonMain/composeResources/values-uk/strings.xml
+++ b/common/src/commonMain/composeResources/values-uk/strings.xml
@@ -1100,7 +1100,7 @@
SegWit (BIP84)
Оберіть фіатную валюту і номінал біткоінів, щоб відображати суми у вашому гаманці
Оберіть акаунт
- Select account & asset
+ Select account & asset
Виберіть додаток на %1$s
Оберіть актив
Оберіть тривалість захисту ваших монет двофакторною аутентифікацією. Нова опція застосовується до монет, що нещодавно отримані.
@@ -1413,6 +1413,7 @@
Очікування транзакції...
Гаманець
Гаманець уже відновлений
+ Wallet already restored: %1$s
Активи гаманця
Створення резервної копії гаманця
Монети гаманця вимагатимуть двофакторну реактивацію один раз на рік, щоб залишатися захищеними двофакторною аутентифікацією.
diff --git a/common/src/commonMain/composeResources/values-vi/strings.xml b/common/src/commonMain/composeResources/values-vi/strings.xml
index b91cb242d..f7a93ff7c 100644
--- a/common/src/commonMain/composeResources/values-vi/strings.xml
+++ b/common/src/commonMain/composeResources/values-vi/strings.xml
@@ -1100,7 +1100,7 @@
SegWit (BIP84)
Select a fiat currency and bitcoin denomination to show amounts in your wallet
Chọn tài khoản
- Select account & asset
+ Select account & asset
Select an app on %1$s
Chọn tài sản số
Select duration of Two-Factor Authentication protection for your coins. The new option applies to newly received coins.
@@ -1413,6 +1413,7 @@
Đang chờ giao dịch...
Wallet
Wallet already restored
+ Wallet already restored: %1$s
Wallet Assets
Wallet Backup
Wallet coins will require two-factor reactivation once a year to remain protected by two-factor authentication.
diff --git a/common/src/commonMain/composeResources/values-zh/strings.xml b/common/src/commonMain/composeResources/values-zh/strings.xml
index 94bdb8d6b..deef93276 100644
--- a/common/src/commonMain/composeResources/values-zh/strings.xml
+++ b/common/src/commonMain/composeResources/values-zh/strings.xml
@@ -1100,7 +1100,7 @@
隔离见证(BIP84)
选择一种法币和比特币面额在账户中显示
选择账户
- Select account & asset
+ Select account & asset
在%1$s上选择一个app
选择资产
选择双重验证的持续时间来保护你的资产。新的选项只会影响新接收的资产。
@@ -1413,6 +1413,7 @@
等待转账...
钱包
钱包已恢复
+ Wallet already restored: %1$s
钱包资产
钱包备份
钱包中的代币需要每年重新激活一次双重验证,以保证受到双重身份验证的保护。
diff --git a/common/src/commonMain/composeResources/values/strings.xml b/common/src/commonMain/composeResources/values/strings.xml
index 7a33b5a13..84f3b604d 100644
--- a/common/src/commonMain/composeResources/values/strings.xml
+++ b/common/src/commonMain/composeResources/values/strings.xml
@@ -1100,7 +1100,7 @@
SegWit (BIP84)
Select a fiat currency and bitcoin denomination to show amounts in your wallet
Select Account
- Select account & asset
+ Select account & asset
Select an app on %1$s
Select asset
Select duration of Two-Factor Authentication protection for your coins. The new option applies to newly received coins.
@@ -1413,6 +1413,7 @@
Waiting for transaction…
Wallet
Wallet already restored
+ Wallet already restored: %1$s
Wallet Assets
Wallet Backup
Wallet coins will require two-factor reactivation once a year to remain protected by two-factor authentication.
@@ -1541,4 +1542,5 @@
Your wallet is not yet fully secured.\nPlease enable Two-Factor authentication.
Your watch-only username and password will be stored un-encrypted on this device. If your device is compromised third parties can get access to your transaction history. Press "OK" to continue.
You've entered an invalid PIN too many times.
+ Enable TLS connection
diff --git a/common/src/commonMain/kotlin/com/blockstream/common/CountlyBase.kt b/common/src/commonMain/kotlin/com/blockstream/common/CountlyBase.kt
index 50a5673c4..a72e38809 100644
--- a/common/src/commonMain/kotlin/com/blockstream/common/CountlyBase.kt
+++ b/common/src/commonMain/kotlin/com/blockstream/common/CountlyBase.kt
@@ -672,6 +672,16 @@ abstract class CountlyBase(
eventStart(Events.OTA_COMPLETE.toString())
}
+ fun jadeOtaRefuse(device: DeviceInterface, config: String, isDelta: Boolean, version: String) {
+ eventRecord(Events.OTA_REFUSE.toString(), deviceSegmentation(device , baseSegmentation()).also { segmentation ->
+ segmentation[PARAM_SELECTED_CONFIG] = config.lowercase()
+ segmentation[PARAM_SELECTED_DELTA] = isDelta
+ segmentation[PARAM_SELECTED_VERSION] = version
+ })
+
+ eventCancel(Events.OTA_COMPLETE.toString())
+ }
+
fun jadeOtaComplete(device: DeviceInterface, config: String, isDelta: Boolean, version: String) {
eventEnd(Events.OTA_COMPLETE.toString(), deviceSegmentation(device , baseSegmentation()).also { segmentation ->
segmentation[PARAM_SELECTED_CONFIG] = config
@@ -697,6 +707,7 @@ abstract class CountlyBase(
JADE_OTA("jade_ota"),
OTA_START("ota_start"),
+ OTA_REFUSE("ota_refuse"),
OTA_COMPLETE("ota_complete"),
WALLET_ADD("wallet_add"),
diff --git a/common/src/commonMain/kotlin/com/blockstream/common/data/ApplicationSettings.kt b/common/src/commonMain/kotlin/com/blockstream/common/data/ApplicationSettings.kt
index da4347df3..5d3c72eeb 100644
--- a/common/src/commonMain/kotlin/com/blockstream/common/data/ApplicationSettings.kt
+++ b/common/src/commonMain/kotlin/com/blockstream/common/data/ApplicationSettings.kt
@@ -39,6 +39,8 @@ data class ApplicationSettings constructor(
val personalTestnetElectrumServer: String? = null,
val personalTestnetLiquidElectrumServer: String? = null,
+ val personalElectrumServerTls: Boolean = true,
+
val spvBitcoinElectrumServer: String? = null,
val spvLiquidElectrumServer: String? = null,
val spvTestnetElectrumServer: String? = null,
@@ -94,6 +96,8 @@ data class ApplicationSettings constructor(
private const val PERSONAL_TESTNET_ELECTRUM_SERVER = "personalTestnetElectrumServer"
private const val PERSONAL_TESTNET_LIQUID_ELECTRUM_SERVER = "personalTestnetLiquidElectrumServer"
+ private const val PERSONAL_ELECTRUM_SERVER_TLS = "personalElectrumServerTls"
+
private const val SPV_BITCOIN_ELECTRUM_SERVER = "spvBitcoinElectrumServer"
private const val SPV_LIQUID_ELECTRUM_SERVER = "spvLiquidElectrumServer"
private const val SPV_TESTNET_ELECTRUM_SERVER = "spvTestnetElectrumServer"
@@ -129,6 +133,8 @@ data class ApplicationSettings constructor(
PERSONAL_TESTNET_LIQUID_ELECTRUM_SERVER
),
+ personalElectrumServerTls = settings.getBooleanOrNull(PERSONAL_ELECTRUM_SERVER_TLS) ?: true,
+
spvBitcoinElectrumServer = settings.getStringOrNull(SPV_BITCOIN_ELECTRUM_SERVER),
spvLiquidElectrumServer = settings.getStringOrNull(SPV_LIQUID_ELECTRUM_SERVER),
spvTestnetElectrumServer = settings.getStringOrNull(SPV_TESTNET_ELECTRUM_SERVER),
@@ -161,6 +167,8 @@ data class ApplicationSettings constructor(
it.putStringOrRemove(PERSONAL_TESTNET_ELECTRUM_SERVER, appSettings.personalTestnetElectrumServer)
it.putStringOrRemove(PERSONAL_TESTNET_LIQUID_ELECTRUM_SERVER, appSettings.personalTestnetLiquidElectrumServer)
+ it.putBoolean(PERSONAL_ELECTRUM_SERVER_TLS, appSettings.personalElectrumServerTls)
+
it.putStringOrRemove(SPV_BITCOIN_ELECTRUM_SERVER, appSettings.spvBitcoinElectrumServer)
it.putStringOrRemove(SPV_LIQUID_ELECTRUM_SERVER, appSettings.spvLiquidElectrumServer)
it.putStringOrRemove(SPV_TESTNET_ELECTRUM_SERVER, appSettings.spvTestnetElectrumServer)
diff --git a/common/src/commonMain/kotlin/com/blockstream/common/fcm/FcmCommon.kt b/common/src/commonMain/kotlin/com/blockstream/common/fcm/FcmCommon.kt
index dc9d611cb..e48c43dab 100644
--- a/common/src/commonMain/kotlin/com/blockstream/common/fcm/FcmCommon.kt
+++ b/common/src/commonMain/kotlin/com/blockstream/common/fcm/FcmCommon.kt
@@ -2,6 +2,7 @@ package com.blockstream.common.fcm
import breez_sdk.BreezEvent
import com.blockstream.common.crypto.GreenKeystore
+import com.blockstream.common.data.AppInfo
import com.blockstream.common.data.GreenWallet
import com.blockstream.common.database.Database
import com.blockstream.common.di.ApplicationScope
@@ -24,6 +25,7 @@ abstract class FcmCommon constructor(val applicationScope: ApplicationScope) : K
private val database: Database by inject()
private val greenKeystore: GreenKeystore by inject()
private val sessionManager: SessionManager by inject()
+ private val appInfo: AppInfo by inject()
private var _token: String? = null
@@ -53,12 +55,25 @@ abstract class FcmCommon constructor(val applicationScope: ApplicationScope) : K
breezNotification: BreezNotification
)
+ abstract fun showDebugNotification(
+ title: String,
+ message: String,
+ )
+
@NativeCoroutinesIgnore
protected suspend fun wallet(walletId: String) = database.getWallet(walletId)
@NativeCoroutinesIgnore
suspend fun doLightningBackgroundWork(walletId: String, breezNotification: BreezNotification) {
logger.d { "doLightningBackgroundWork for walletId:$walletId with data: $breezNotification" }
+
+ if(appInfo.isDevelopmentOrDebug) {
+ showDebugNotification(
+ title = "Background Work",
+ message = breezNotification.toString()
+ )
+ }
+
wallet(walletId)?.also { wallet ->
database.getLoginCredentials(wallet.id).lightningMnemonic?.encrypted_data?.let {
greenKeystore.decryptData(it).decodeToString()
@@ -67,7 +82,14 @@ abstract class FcmCommon constructor(val applicationScope: ApplicationScope) : K
it.connectToGreenlight(mnemonic = mnemonic)
// Wait maximum 2 minutes to complete all operations
- withTimeoutOrNull(120_000) {
+ val success = withTimeoutOrNull(120_000) {
+
+ if(appInfo.isDevelopmentOrDebug) {
+ showDebugNotification(
+ title = "Lightning connected and waiting",
+ message = breezNotification.toString()
+ )
+ }
if (breezNotification.paymentHash == "test") {
showLightningPaymentNotification(
@@ -94,6 +116,15 @@ abstract class FcmCommon constructor(val applicationScope: ApplicationScope) : K
}.firstOrNull()
}
+ if(appInfo.isDevelopmentOrDebug) {
+ showDebugNotification(
+ title = "Lightning disconnected: Success: $success",
+ message = breezNotification.toString()
+ )
+ }
+
+ logger.d { "doLightningBackgroundWork completed walletId:$walletId" }
+
it.release()
}
} ?: logger.d { "Couldn't decrypt mnemonic" }
diff --git a/common/src/commonMain/kotlin/com/blockstream/common/gdk/GdkSession.kt b/common/src/commonMain/kotlin/com/blockstream/common/gdk/GdkSession.kt
index d590d8b37..97965f7df 100644
--- a/common/src/commonMain/kotlin/com/blockstream/common/gdk/GdkSession.kt
+++ b/common/src/commonMain/kotlin/com/blockstream/common/gdk/GdkSession.kt
@@ -648,6 +648,7 @@ class GdkSession constructor(
proxy = applicationSettings.proxyUrl ?: "",
spvEnabled = spvEnabled,
spvMulti = spvMulti,
+ electrumTls = if(electrumUrl.isNotBlank()) applicationSettings.personalElectrumServerTls else true,
electrumUrl = electrumUrl,
electrumOnionUrl = electrumUrl.takeIf { useTor },
spvServers = spvServers
@@ -708,7 +709,7 @@ class GdkSession constructor(
gdk.connect(it.value, createConnectionParams(it.key))
it.key
} catch (e: Exception) {
- _failedNetworksStateFlow.value = _failedNetworksStateFlow.value + it.key
+ _failedNetworksStateFlow.value += it.key
null
}
}
diff --git a/common/src/commonMain/kotlin/com/blockstream/common/gdk/data/Network.kt b/common/src/commonMain/kotlin/com/blockstream/common/gdk/data/Network.kt
index ae69897a5..6c43a7049 100644
--- a/common/src/commonMain/kotlin/com/blockstream/common/gdk/data/Network.kt
+++ b/common/src/commonMain/kotlin/com/blockstream/common/gdk/data/Network.kt
@@ -99,7 +99,11 @@ data class Network(
@IgnoredOnParcel
val confirmationsRequired: Long
- get() = if(isLiquid) 2L else 6L
+ get() = when{
+ isLightning -> 1L
+ isLiquid -> 2L
+ else -> 6L
+ }
fun getVerPublic(): Int {
return if (isMainnet) BIP32_VER_MAIN_PUBLIC else BIP32_VER_TEST_PUBLIC
diff --git a/common/src/commonMain/kotlin/com/blockstream/common/gdk/params/ConnectionParams.kt b/common/src/commonMain/kotlin/com/blockstream/common/gdk/params/ConnectionParams.kt
index 4ffcca0e5..1c7e640a8 100644
--- a/common/src/commonMain/kotlin/com/blockstream/common/gdk/params/ConnectionParams.kt
+++ b/common/src/commonMain/kotlin/com/blockstream/common/gdk/params/ConnectionParams.kt
@@ -15,6 +15,7 @@ data class ConnectionParams constructor(
@SerialName("spv_enabled") val spvEnabled: Boolean = false,
@SerialName("spv_multi") val spvMulti: Boolean = false,
+ @SerialName("electrum_tls") val electrumTls: Boolean = true,
@SerialName("electrum_url") val electrumUrl: String? = null,
@SerialName("electrum_onion_url") val electrumOnionUrl: String? = null,
@SerialName("spv_servers") val spvServers: List? = null,
diff --git a/common/src/commonMain/kotlin/com/blockstream/common/lightning/Extensions.kt b/common/src/commonMain/kotlin/com/blockstream/common/lightning/Extensions.kt
index 090dc1060..f877ba8b2 100644
--- a/common/src/commonMain/kotlin/com/blockstream/common/lightning/Extensions.kt
+++ b/common/src/commonMain/kotlin/com/blockstream/common/lightning/Extensions.kt
@@ -11,6 +11,7 @@ import breez_sdk.OpenChannelFeeResponse
import breez_sdk.OpeningFeeParams
import breez_sdk.Payment
import breez_sdk.PaymentDetails
+import breez_sdk.PaymentStatus
import breez_sdk.PaymentType
import breez_sdk.ReceivePaymentResponse
import breez_sdk.RecommendedFees
@@ -172,8 +173,16 @@ fun Transaction.Companion.fromPayment(payment: Payment): Transaction {
val isPendingCloseChannel = payment.paymentType == PaymentType.CLOSED_CHANNEL && (payment.details as? PaymentDetails.ClosedChannel)?.data?.state == ChannelState.PENDING_CLOSE
+ val blockHeight = when {
+ isPendingCloseChannel || payment.status == PaymentStatus.PENDING -> 0
+ payment.status == PaymentStatus.COMPLETE -> payment.paymentTime
+ else -> {
+ 0
+ }
+ }
+
return Transaction(
- blockHeight = if(isPendingCloseChannel) 0 else payment.paymentTime,
+ blockHeight = blockHeight,
canRBF = false,
createdAtTs = payment.paymentTime * 1_000_000,
inputs = listOf(),
@@ -247,8 +256,8 @@ fun Transaction.Companion.fromReverseSwapInfo(account: Account, reverseSwapInfo:
fun AppGreenlightCredentials.Companion.fromGreenlightCredentials(greenlightCredentials: GreenlightCredentials): AppGreenlightCredentials {
return AppGreenlightCredentials(
- deviceKey = greenlightCredentials.deviceKey,
- deviceCert = greenlightCredentials.deviceCert
+ deviceKey = greenlightCredentials.developerKey,
+ deviceCert = greenlightCredentials.developerCert
)
}
diff --git a/common/src/commonMain/kotlin/com/blockstream/common/lightning/GreenlightKeys.kt b/common/src/commonMain/kotlin/com/blockstream/common/lightning/GreenlightKeys.kt
index 88c15fcbd..c4e15c0a6 100644
--- a/common/src/commonMain/kotlin/com/blockstream/common/lightning/GreenlightKeys.kt
+++ b/common/src/commonMain/kotlin/com/blockstream/common/lightning/GreenlightKeys.kt
@@ -11,8 +11,8 @@ data class GreenlightKeys(
fun toGreenlightCredentials(): GreenlightCredentials? {
return if (deviceKey != null && deviceCert != null) {
GreenlightCredentials(
- deviceKey = deviceKey,
- deviceCert = deviceCert,
+ developerKey = deviceKey,
+ developerCert = deviceCert,
)
} else null
}
diff --git a/common/src/commonMain/kotlin/com/blockstream/common/lightning/LightningBridge.kt b/common/src/commonMain/kotlin/com/blockstream/common/lightning/LightningBridge.kt
index ea55df90f..dfcdcf3c1 100644
--- a/common/src/commonMain/kotlin/com/blockstream/common/lightning/LightningBridge.kt
+++ b/common/src/commonMain/kotlin/com/blockstream/common/lightning/LightningBridge.kt
@@ -213,7 +213,7 @@ class LightningBridge constructor(
(if (appInfo.isDevelopment) GREEN_NOTIFY_DEVELOPMENT else GREEN_NOTIFY_PRODUCTION).let { backend ->
"$backend/api/v1/notify?platform=${platformName()}&token=$token&app_data=$xpubHashId"
}.also { url ->
- logger.d { "Registering webhook for wallet($xpubHashId) as $url" }
+ logger.i { "Registering webhook for wallet($xpubHashId) as $url" }
breezSdkOrNull?.registerWebhook(url)
}
}
diff --git a/common/src/commonMain/kotlin/com/blockstream/common/lightning/LightningManager.kt b/common/src/commonMain/kotlin/com/blockstream/common/lightning/LightningManager.kt
index dab7a02c2..a50e3bf6a 100644
--- a/common/src/commonMain/kotlin/com/blockstream/common/lightning/LightningManager.kt
+++ b/common/src/commonMain/kotlin/com/blockstream/common/lightning/LightningManager.kt
@@ -41,9 +41,9 @@ class LightningManager constructor(
if(appConfig.lightningFeatureEnabled) {
setLogStream(object : LogStream {
override fun log(l: LogEntry) {
- if (l.level == "DEBUG") {
+ if (l.level != "TRACE") {
logs.append("${Clock.System.now()} - ${l.line}\n")
- if (logs.length > 2_000_000) {
+ if (logs.length > 4_000_000) {
logger.d { "Clear Lightning Logs" }
logs.deleteRange(0, 1_000_000)
}
@@ -88,6 +88,18 @@ class LightningManager constructor(
return "${logDir}/greenlight_logs_${Clock.System.now()}.txt".toPath().also {
withContext(context = Dispatchers.IO) {
fileSystem.write(it) {
+
+ val nodeIds = bridges.map {
+ it.value.nodeInfoStateFlow.value
+ }
+
+ this.writeUtf8("------------------------------------------------------------------\n")
+ this.writeUtf8("Node IDs: --------------------------------------------------------\n")
+ this.writeUtf8(nodeIds.joinToString("\n") { it.id })
+ this.writeUtf8("\nNode Info: -------------------------------------------------------\n")
+ this.writeUtf8(nodeIds.joinToString("\n") { it.toString() })
+ this.writeUtf8("\n------------------------------------------------------------------\n")
+
this.writeUtf8(logs.toString())
}
}
diff --git a/common/src/commonMain/kotlin/com/blockstream/common/managers/SessionManager.kt b/common/src/commonMain/kotlin/com/blockstream/common/managers/SessionManager.kt
index e2d5c1321..1fd7dd979 100644
--- a/common/src/commonMain/kotlin/com/blockstream/common/managers/SessionManager.kt
+++ b/common/src/commonMain/kotlin/com/blockstream/common/managers/SessionManager.kt
@@ -144,7 +144,7 @@ class SessionManager constructor(
}
connectionChangeEvent.onEach {
- getConnectedEphemeralWalletSessions().filter { it.ephemeralWallet?.isHardware == true }.mapNotNull { it.ephemeralWallet }.let {
+ getConnectedEphemeralWalletSessions().filter { it.ephemeralWallet?.isLightning == false && it.ephemeralWallet?.isHardware == true }.mapNotNull { it.ephemeralWallet }.let {
_hardwareWallets.value = it
}
diff --git a/common/src/commonMain/kotlin/com/blockstream/common/models/GreenViewModel.kt b/common/src/commonMain/kotlin/com/blockstream/common/models/GreenViewModel.kt
index 33c0c3349..d17513c7e 100644
--- a/common/src/commonMain/kotlin/com/blockstream/common/models/GreenViewModel.kt
+++ b/common/src/commonMain/kotlin/com/blockstream/common/models/GreenViewModel.kt
@@ -153,7 +153,7 @@ open class GreenViewModel constructor(
// Main action validation
internal val _isValid = MutableStateFlow(viewModelScope, isPreview)
//@NativeCoroutinesState
- //val isValid = _isValid.asStateFlow()
+ val isValid: StateFlow = _isValid
// Main button enabled flag
private val _buttonEnabled = MutableStateFlow(isPreview)
diff --git a/common/src/commonMain/kotlin/com/blockstream/common/models/overview/WalletOverviewViewModel.kt b/common/src/commonMain/kotlin/com/blockstream/common/models/overview/WalletOverviewViewModel.kt
index 6f1854756..72bd220b8 100644
--- a/common/src/commonMain/kotlin/com/blockstream/common/models/overview/WalletOverviewViewModel.kt
+++ b/common/src/commonMain/kotlin/com/blockstream/common/models/overview/WalletOverviewViewModel.kt
@@ -173,17 +173,23 @@ class WalletOverviewViewModel(greenWallet: GreenWallet) :
} ?: emptyFlow()).stateIn(viewModelScope, SharingStarted.WhileSubscribed(), null)
- private val _transactions: StateFlow>> =
- session.walletTransactions.filter { session.isConnected }.map { transactionsLooks ->
- transactionsLooks.mapSuccess {
+ private val _transactions: StateFlow>> = combine(
+ session.walletTransactions.filter { session.isConnected },
+ session.settings()
+ ) { transactions, _ ->
+ transactions.mapSuccess {
it.map {
TransactionLook.create(it, session)
}
}
}.stateIn(viewModelScope, SharingStarted.WhileSubscribed(), DataState.Loading)
+ // Re-calculate if needed (hideAmount or denomination & exchange rate change)
override val transactions: StateFlow>> =
- combine(hideAmounts, _transactions) { hideAmounts, transactionsLooks ->
+ combine(
+ hideAmounts,
+ _transactions
+ ) { hideAmounts, transactionsLooks ->
if (transactionsLooks is DataState.Success && hideAmounts) {
DataState.Success(transactionsLooks.data.map { it.asMasked })
} else {
diff --git a/common/src/commonMain/kotlin/com/blockstream/common/models/receive/ReceiveViewModel.kt b/common/src/commonMain/kotlin/com/blockstream/common/models/receive/ReceiveViewModel.kt
index 0c04e2169..9f0e94333 100644
--- a/common/src/commonMain/kotlin/com/blockstream/common/models/receive/ReceiveViewModel.kt
+++ b/common/src/commonMain/kotlin/com/blockstream/common/models/receive/ReceiveViewModel.kt
@@ -209,7 +209,7 @@ class ReceiveViewModel(initialAccountAsset: AccountAsset, greenWallet: GreenWall
private val _generateAddressLock = Mutex()
init {
- combine(accountAsset, showLightningOnChainAddress) { accountAsset, showLightningOnChainAddress ->
+ combine(accountAsset, showLightningOnChainAddress, receiveAddress) { accountAsset, showLightningOnChainAddress, receiveAddress ->
_navData.value = NavData(
title = getString(Res.string.id_receive),
subtitle = greenWallet.name,
@@ -228,7 +228,7 @@ class ReceiveViewModel(initialAccountAsset: AccountAsset, greenWallet: GreenWall
)
)
}
- ).takeIf { accountAsset?.account?.isLightning == true && !showLightningOnChainAddress},
+ ).takeIf { accountAsset?.account?.isLightning == true && !showLightningOnChainAddress && receiveAddress == null},
NavAction(
title = getString(Res.string.id_reset),
icon = Res.drawable.question,
@@ -720,7 +720,7 @@ class ReceiveViewModel(initialAccountAsset: AccountAsset, greenWallet: GreenWall
) ?: "-"
_liquidityFee.value = when {
- amount.value.isBlank() -> {
+ amount.value.isBlank() || _amountError.value != null -> {
null
}
diff --git a/common/src/commonMain/kotlin/com/blockstream/common/models/send/SendViewModel.kt b/common/src/commonMain/kotlin/com/blockstream/common/models/send/SendViewModel.kt
index 035ce5d19..e693d9418 100644
--- a/common/src/commonMain/kotlin/com/blockstream/common/models/send/SendViewModel.kt
+++ b/common/src/commonMain/kotlin/com/blockstream/common/models/send/SendViewModel.kt
@@ -392,6 +392,7 @@ class SendViewModel(
_metadataDomain.value = null
_metadataImage.value = null
_metadataDescription.value = null
+ note.value = ""
return@doAsync null
}
@@ -405,6 +406,9 @@ class SendViewModel(
_error.value = null
}
+ // Mainly used in Lightning invoice
+ note.value = tx.memo ?: ""
+
tx.addressees.firstOrNull()?.also { addressee ->
_isAmountLocked.value = addressee.isAmountLocked == true
diff --git a/common/src/commonMain/kotlin/com/blockstream/common/models/settings/AppSettingsViewModel.kt b/common/src/commonMain/kotlin/com/blockstream/common/models/settings/AppSettingsViewModel.kt
index 14c084ee6..5022df0e1 100644
--- a/common/src/commonMain/kotlin/com/blockstream/common/models/settings/AppSettingsViewModel.kt
+++ b/common/src/commonMain/kotlin/com/blockstream/common/models/settings/AppSettingsViewModel.kt
@@ -67,6 +67,9 @@ abstract class AppSettingsViewModelAbstract() :
@NativeCoroutinesState
abstract val multiServerValidationEnabled: MutableStateFlow
+ @NativeCoroutinesState
+ abstract val personalElectrumServerTlsEnabled: MutableStateFlow
+
@NativeCoroutinesState
abstract val personalBitcoinElectrumServer: MutableStateFlow
@@ -149,6 +152,9 @@ class AppSettingsViewModel : AppSettingsViewModelAbstract() {
@NativeCoroutinesState
override val multiServerValidationEnabled: MutableStateFlow = MutableStateFlow(viewModelScope, appSettings.multiServerValidation)
+ @NativeCoroutinesState
+ override val personalElectrumServerTlsEnabled: MutableStateFlow = MutableStateFlow(viewModelScope, appSettings.personalElectrumServerTls)
+
@NativeCoroutinesState
override val personalBitcoinElectrumServer: MutableStateFlow = MutableStateFlow(viewModelScope, appSettings.personalBitcoinElectrumServer ?: "")
@@ -255,6 +261,8 @@ class AppSettingsViewModel : AppSettingsViewModelAbstract() {
personalTestnetElectrumServer = personalTestnetElectrumServer.value.takeIf { electrumNodeEnabled.value },
personalTestnetLiquidElectrumServer = personalTestnetLiquidElectrumServer.value.takeIf { electrumNodeEnabled.value },
+ personalElectrumServerTls = personalElectrumServerTlsEnabled.value,
+
spvBitcoinElectrumServer = spvBitcoinElectrumServer.value.takeIf { spvEnabled.value },
spvLiquidElectrumServer = spvLiquidElectrumServer.value.takeIf { spvEnabled.value },
spvTestnetElectrumServer = spvTestnetElectrumServer.value.takeIf { spvEnabled.value },
@@ -289,6 +297,7 @@ class AppSettingsViewModelPreview(initValue: Boolean = false) : AppSettingsViewM
override val electrumNodeEnabled: MutableStateFlow = MutableStateFlow(viewModelScope, initValue)
override val spvEnabled: MutableStateFlow = MutableStateFlow(viewModelScope, initValue)
override val multiServerValidationEnabled: MutableStateFlow = MutableStateFlow(viewModelScope, initValue)
+ override val personalElectrumServerTlsEnabled: MutableStateFlow = MutableStateFlow(viewModelScope, initValue)
override val personalBitcoinElectrumServer: MutableStateFlow = MutableStateFlow(viewModelScope, "")
override val personalLiquidElectrumServer: MutableStateFlow = MutableStateFlow(viewModelScope, "")
override val personalTestnetElectrumServer: MutableStateFlow = MutableStateFlow(viewModelScope, "")
diff --git a/common/src/commonMain/kotlin/com/blockstream/common/sideeffects/SideEffects.kt b/common/src/commonMain/kotlin/com/blockstream/common/sideeffects/SideEffects.kt
index eb41d2733..bca6ba74a 100644
--- a/common/src/commonMain/kotlin/com/blockstream/common/sideeffects/SideEffects.kt
+++ b/common/src/commonMain/kotlin/com/blockstream/common/sideeffects/SideEffects.kt
@@ -51,7 +51,7 @@ class SideEffects : SideEffect {
data class TransactionSent(val data: SendTransactionSuccess) : SideEffect
data class Logout(val reason: LogoutReason) : SideEffect
object WalletDelete : SideEffect
- data class CopyToClipboard(val value: String, val message: String? = null, val label: String? = null) : SideEffect
+ data class CopyToClipboard(val value: String, val message: String? = null, val label: String? = null, val isSensitive: Boolean = false) : SideEffect
data class AccountArchived(val account: Account) : SideEffect
data class AccountUnarchived(val account: Account) : SideEffect
data class AccountCreated(val accountAsset: AccountAsset): SideEffect
diff --git a/common/src/commonMain/kotlin/com/blockstream/common/utils/StringResources.kt b/common/src/commonMain/kotlin/com/blockstream/common/utils/StringResources.kt
index 0562d8fd2..1283e4cfc 100644
--- a/common/src/commonMain/kotlin/com/blockstream/common/utils/StringResources.kt
+++ b/common/src/commonMain/kotlin/com/blockstream/common/utils/StringResources.kt
@@ -1418,6 +1418,7 @@ object StringResourcesMap {
"id_waiting_for_transaction" to Res.string.id_waiting_for_transaction,
"id_wallet" to Res.string.id_wallet,
"id_wallet_already_restored" to Res.string.id_wallet_already_restored,
+ "id_wallet_already_restored_s" to Res.string.id_wallet_already_restored_s,
"id_wallet_assets" to Res.string.id_wallet_assets,
"id_wallet_backup" to Res.string.id_wallet_backup,
"id_wallet_coins_will_require" to Res.string.id_wallet_coins_will_require,
diff --git a/compose/build.gradle.kts b/compose/build.gradle.kts
index d24c787da..09ed3e5cf 100644
--- a/compose/build.gradle.kts
+++ b/compose/build.gradle.kts
@@ -13,14 +13,23 @@ plugins {
}
kotlin {
+ // Enable the default target hierarchy:
+ applyDefaultHierarchyTemplate()
+
androidTarget {
@OptIn(ExperimentalKotlinGradlePluginApi::class)
compilerOptions {
- jvmTarget.set(JvmTarget.JVM_17)
freeCompilerArgs.addAll("-P", "plugin:org.jetbrains.kotlin.parcelize:additionalAnnotation=com.blockstream.common.Parcelize")
}
}
+ @OptIn(ExperimentalKotlinGradlePluginApi::class)
+ compilerOptions {
+ freeCompilerArgs.add("-Xexpect-actual-classes")
+ }
+
+ jvmToolchain(libs.versions.jvm.get().toInt())
+
jvm("desktop")
val xcf = XCFramework()
@@ -139,10 +148,6 @@ android {
defaultConfig {
minSdk = libs.versions.androidMinSdk.get().toInt()
}
- compileOptions {
- sourceCompatibility = JavaVersion.VERSION_17
- targetCompatibility = JavaVersion.VERSION_17
- }
packaging {
jniLibs.pickFirsts.add("**/*.so")
diff --git a/compose/src/androidMain/kotlin/com/blockstream/compose/managers/PlatformManager.android.kt b/compose/src/androidMain/kotlin/com/blockstream/compose/managers/PlatformManager.android.kt
index 30e30834e..8589cbeb3 100644
--- a/compose/src/androidMain/kotlin/com/blockstream/compose/managers/PlatformManager.android.kt
+++ b/compose/src/androidMain/kotlin/com/blockstream/compose/managers/PlatformManager.android.kt
@@ -2,6 +2,7 @@ package com.blockstream.compose.managers
import android.Manifest
import android.content.ClipData
+import android.content.ClipDescription
import android.content.ClipboardManager
import android.content.Context
import android.content.Intent
@@ -13,11 +14,14 @@ import android.graphics.ImageDecoder
import android.graphics.Typeface
import android.net.Uri
import android.os.Build
+import android.os.Message
+import android.os.PersistableBundle
import android.provider.MediaStore
import android.text.Layout
import android.text.StaticLayout
import android.text.TextPaint
import android.text.TextUtils
+import android.widget.Toast
import androidx.activity.compose.rememberLauncherForActivityResult
import androidx.activity.result.contract.ActivityResultContracts
import androidx.browser.customtabs.CustomTabColorSchemeParams
@@ -110,6 +114,11 @@ actual fun askForNotificationPermissions(viewModel: GreenViewModel) {
actual class PlatformManager(val context: Context) {
+ actual fun openToast(content: String): Boolean {
+ Toast.makeText(context, content, Toast.LENGTH_SHORT).show()
+ return true
+ }
+
actual fun openBrowser(url: String) {
try {
val builder = CustomTabsIntent.Builder()
@@ -137,10 +146,20 @@ actual class PlatformManager(val context: Context) {
}
}
- actual fun copyToClipboard(content: String, label: String?) {
+ actual fun copyToClipboard(content: String, label: String?, isSensitive: Boolean): Boolean {
(context.getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager).setPrimaryClip(
- ClipData.newPlainText(label ?: "Green", content)
+ ClipData.newPlainText(label ?: "Green", content).apply {
+ if (isSensitive) {
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
+ description.extras = PersistableBundle().apply {
+ putBoolean(ClipDescription.EXTRA_IS_SENSITIVE, true)
+ }
+ }
+ }
+ }
)
+
+ return Build.VERSION.SDK_INT > Build.VERSION_CODES.S_V2
}
internal actual fun getClipboard(): String? {
diff --git a/compose/src/commonMain/kotlin/com/blockstream/compose/components/GreenAccountCard.kt b/compose/src/commonMain/kotlin/com/blockstream/compose/components/GreenAccountCard.kt
index fae298526..76c3eea56 100644
--- a/compose/src/commonMain/kotlin/com/blockstream/compose/components/GreenAccountCard.kt
+++ b/compose/src/commonMain/kotlin/com/blockstream/compose/components/GreenAccountCard.kt
@@ -63,11 +63,11 @@ fun GreenAccountCard(
account: AccountBalance,
isExpanded: Boolean,
session: GdkSession? = null,
- onCopyClick: (() -> Unit)? = null,
- onArrowClick: (() -> Unit)? = null,
- onWarningClick: (() -> Unit)? = null,
- onClick: () -> Unit = {},
- onLongClick: (offset: Offset) -> Unit = {},
+ onCopyClick: ((AccountBalance) -> Unit)? = null,
+ onArrowClick: ((AccountBalance) -> Unit)? = null,
+ onWarningClick: ((AccountBalance) -> Unit)? = null,
+ onClick: (AccountBalance) -> Unit = {},
+ onLongClick: (AccountBalance, offset: Offset) -> Unit = { _ , _ -> },
) {
Box(
modifier = Modifier
@@ -85,11 +85,11 @@ fun GreenAccountCard(
.fillMaxWidth()
.pointerInput(Unit){
detectTapGestures(
- onPress = {
- onClick()
+ onTap = {
+ onClick(account)
},
onLongPress = {
- onLongClick(it)
+ onLongClick(account, it)
}
)
}
@@ -201,11 +201,15 @@ fun GreenAccountCard(
type = GreenButtonType.OUTLINE,
color = GreenButtonColor.WHITE,
size = GreenButtonSize.SMALL,
- onClick = onCopyClick
+ onClick = {
+ onCopyClick(account)
+ }
)
} else if (onArrowClick != null) {
Card(
- onClick = onArrowClick,
+ onClick = {
+ onArrowClick(account)
+ },
modifier = Modifier
.align(Alignment.Bottom)
.size(42.dp),
@@ -243,7 +247,9 @@ fun GreenAccountCard(
painter = painterResource(Res.drawable.shield_warning),
contentDescription = null,
modifier = Modifier
- .noRippleClickable(onWarningClick)
+ .noRippleClickable {
+ onWarningClick(account)
+ }
.size(30.dp)
.clip(CircleShape)
.background(account.account.getAccountColor())
diff --git a/compose/src/commonMain/kotlin/com/blockstream/compose/components/GreenAddress.kt b/compose/src/commonMain/kotlin/com/blockstream/compose/components/GreenAddress.kt
index fb4ce9efd..2ae21ff47 100644
--- a/compose/src/commonMain/kotlin/com/blockstream/compose/components/GreenAddress.kt
+++ b/compose/src/commonMain/kotlin/com/blockstream/compose/components/GreenAddress.kt
@@ -1,5 +1,7 @@
package com.blockstream.compose.components
+import androidx.compose.foundation.clickable
+import androidx.compose.foundation.layout.Box
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
@@ -16,7 +18,8 @@ fun GreenAddress(
modifier: Modifier = Modifier,
address: String,
textAlign: TextAlign? = null,
- maxLines: Int = Int.MAX_VALUE
+ maxLines: Int = Int.MAX_VALUE,
+ onCopyClick: ((String) -> Unit)? = null
) {
val schemes = listOf("bitcoin", "liquidnetwork", "liquidtestnet", "lightning")
@@ -28,7 +31,7 @@ fun GreenAddress(
AnnotatedString(address)
}
- CopyContainer(value = address, withSelection = false) {
+ val content = @Composable {
Text(
text = text,
fontFamily = MonospaceFont(),
@@ -38,4 +41,16 @@ fun GreenAddress(
overflow = TextOverflow.Ellipsis
)
}
+
+ if (onCopyClick == null) {
+ CopyContainer(value = address, withSelection = false) {
+ content()
+ }
+ } else {
+ Box(modifier = Modifier.clickable {
+ onCopyClick(address)
+ }) {
+ content()
+ }
+ }
}
diff --git a/compose/src/commonMain/kotlin/com/blockstream/compose/components/GreenTransaction.kt b/compose/src/commonMain/kotlin/com/blockstream/compose/components/GreenTransaction.kt
index 0b994d0c8..a526e6125 100644
--- a/compose/src/commonMain/kotlin/com/blockstream/compose/components/GreenTransaction.kt
+++ b/compose/src/commonMain/kotlin/com/blockstream/compose/components/GreenTransaction.kt
@@ -53,10 +53,12 @@ fun GreenTransaction(
modifier: Modifier = Modifier,
transactionLook: TransactionLook,
showAccount: Boolean = true,
- onClick: () -> Unit
+ onClick: (TransactionLook) -> Unit
) {
Card(
- onClick = onClick,
+ onClick = {
+ onClick(transactionLook)
+ },
modifier = Modifier
.fillMaxWidth()
.padding(horizontal = 16.dp)
diff --git a/compose/src/commonMain/kotlin/com/blockstream/compose/managers/PlatformManager.kt b/compose/src/commonMain/kotlin/com/blockstream/compose/managers/PlatformManager.kt
index 0671d5d25..6d09fa54c 100644
--- a/compose/src/commonMain/kotlin/com/blockstream/compose/managers/PlatformManager.kt
+++ b/compose/src/commonMain/kotlin/com/blockstream/compose/managers/PlatformManager.kt
@@ -37,7 +37,9 @@ expect fun askForNotificationPermissions(viewModel: GreenViewModel)
expect class PlatformManager {
fun openBrowser(url: String)
- fun copyToClipboard(content: String, label: String? = null)
+ fun openToast(content: String): Boolean
+
+ fun copyToClipboard(content: String, label: String? = null, isSensitive: Boolean = false): Boolean
internal fun getClipboard(): String?
fun clearClipboard()
diff --git a/compose/src/commonMain/kotlin/com/blockstream/compose/screens/overview/AccountOverviewScreen.kt b/compose/src/commonMain/kotlin/com/blockstream/compose/screens/overview/AccountOverviewScreen.kt
index ae3620f35..1df02b6a2 100644
--- a/compose/src/commonMain/kotlin/com/blockstream/compose/screens/overview/AccountOverviewScreen.kt
+++ b/compose/src/commonMain/kotlin/com/blockstream/compose/screens/overview/AccountOverviewScreen.kt
@@ -293,7 +293,9 @@ fun AccountOverviewScreen(
}
assets.data()?.also {
- items(it) {
+ items(items = it, key = {
+ it.assetId
+ }) {
GreenAsset(
modifier = Modifier
.padding(horizontal = 16.dp)
@@ -344,9 +346,9 @@ fun AccountOverviewScreen(
}
transactions.data()?.let {
- itemsIndexed(it) { index, item ->
- GreenTransaction(transactionLook = item) {
- viewModel.postEvent(Events.Transaction(transaction = item.transaction))
+ items(items = it, key = { it.transaction.txHash.hashCode() + it.transaction.txType.gdkType.hashCode() }) {
+ GreenTransaction(transactionLook = it) {
+ viewModel.postEvent(Events.Transaction(transaction = it.transaction))
}
}
}
diff --git a/compose/src/commonMain/kotlin/com/blockstream/compose/screens/overview/WalletOverviewScreen.kt b/compose/src/commonMain/kotlin/com/blockstream/compose/screens/overview/WalletOverviewScreen.kt
index 5c1e7459b..be8cb1ef6 100644
--- a/compose/src/commonMain/kotlin/com/blockstream/compose/screens/overview/WalletOverviewScreen.kt
+++ b/compose/src/commonMain/kotlin/com/blockstream/compose/screens/overview/WalletOverviewScreen.kt
@@ -31,6 +31,7 @@ import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
+import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.input.nestedscroll.nestedScroll
import androidx.compose.ui.layout.onSizeChanged
@@ -65,12 +66,14 @@ import blockstream_green.common.generated.resources.trash
import cafe.adriel.voyager.core.screen.Screen
import cafe.adriel.voyager.core.screen.uniqueScreenKey
import cafe.adriel.voyager.koin.koinScreenModel
+import co.touchlab.kermit.Logger
import com.arkivanov.essenty.parcelable.IgnoredOnParcel
import com.blockstream.common.Parcelable
import com.blockstream.common.Parcelize
import com.blockstream.common.data.GreenWallet
import com.blockstream.common.events.Events
import com.blockstream.common.extensions.isNotBlank
+import com.blockstream.common.gdk.data.AccountBalance
import com.blockstream.common.models.SimpleGreenViewModel
import com.blockstream.common.models.archived.ArchivedAccountsViewModel
import com.blockstream.common.models.overview.WalletOverviewViewModel
@@ -261,7 +264,7 @@ fun WalletOverviewScreen(
if (!isWalletOnboarding) {
- items(alerts) {
+ items(items = alerts) {
GreenAlert(
modifier = Modifier
.padding(horizontal = 16.dp)
@@ -269,7 +272,9 @@ fun WalletOverviewScreen(
)
}
- items(accounts) {
+ items(items = accounts, key = {
+ it.account.id
+ }) {
val popupState = remember {
PopupState()
}
@@ -316,9 +321,9 @@ fun WalletOverviewScreen(
setAsActive = true
)
)
- }, onLongClick = {
+ }, onLongClick = { _: AccountBalance, offset: Offset ->
if (hasContextMenu) {
- popupState.offset.value = it.toMenuDpOffset(cardSize, density)
+ popupState.offset.value = offset.toMenuDpOffset(cardSize, density)
popupState.isContextMenuVisible.value = true
}
}
@@ -358,36 +363,6 @@ fun WalletOverviewScreen(
}
}
-// item {
-// val expandedAccount by viewModel.session.activeAccount.collectAsStateWithLifecycle()
-// AnimatedVisibility(visible = accounts.isNotEmpty()) {
-// GreenColumn(
-// padding = 0,
-// space = 1,
-// modifier = Modifier.padding(vertical = 8.dp)
-// ) {
-// accounts.forEach {
-// GreenAccountCard(
-// modifier = Modifier.padding(bottom = 1.dp),
-// accountBalance = it,
-// isExpanded = it.account.id == expandedAccount?.id,
-// session = viewModel.sessionOrNull,
-// onArrowClick = {
-//
-// }
-// ) {
-// viewModel.postEvent(
-// Events.SetAccountAsset(
-// accountAsset = it.account.accountAsset,
-// setAsActive = true
-// )
-// )
-// }
-// }
-// }
-// }
-// }
-
lightningInfo?.also { lightningInfo ->
item {
LightningInfo(lightningInfoLook = lightningInfo, onLearnMore = {
@@ -429,9 +404,11 @@ fun WalletOverviewScreen(
}
transactions.data()?.also {
- items(it) { item ->
+ items(items = it, key = {
+ it.transaction.txHash.hashCode() + it.transaction.txType.gdkType.hashCode()
+ }) { item ->
GreenTransaction(transactionLook = item) {
- viewModel.postEvent(Events.Transaction(transaction = item.transaction))
+ viewModel.postEvent(Events.Transaction(transaction = it.transaction))
}
}
}
diff --git a/compose/src/commonMain/kotlin/com/blockstream/compose/screens/receive/ReceiveScreen.kt b/compose/src/commonMain/kotlin/com/blockstream/compose/screens/receive/ReceiveScreen.kt
index b35769160..aa33311e6 100644
--- a/compose/src/commonMain/kotlin/com/blockstream/compose/screens/receive/ReceiveScreen.kt
+++ b/compose/src/commonMain/kotlin/com/blockstream/compose/screens/receive/ReceiveScreen.kt
@@ -38,6 +38,7 @@ import blockstream_green.common.generated.resources.arrows_counter_clockwise
import blockstream_green.common.generated.resources.id_account__asset
import blockstream_green.common.generated.resources.id_account_address
import blockstream_green.common.generated.resources.id_address
+import blockstream_green.common.generated.resources.id_address_copied_to_clipboard
import blockstream_green.common.generated.resources.id_amount
import blockstream_green.common.generated.resources.id_amount_to_receive
import blockstream_green.common.generated.resources.id_confirm
@@ -70,6 +71,7 @@ import com.blockstream.common.extensions.isNotBlank
import com.blockstream.common.gdk.data.AccountAsset
import com.blockstream.common.models.receive.ReceiveViewModel
import com.blockstream.common.models.receive.ReceiveViewModelAbstract
+import com.blockstream.common.models.send.SendConfirmViewModel
import com.blockstream.common.navigation.NavigateDestinations
import com.blockstream.common.sideeffects.SideEffects
import com.blockstream.compose.components.GreenAccountAsset
@@ -92,6 +94,7 @@ import com.blockstream.compose.sheets.DenominationBottomSheet
import com.blockstream.compose.sheets.LocalBottomSheetNavigatorM3
import com.blockstream.compose.sheets.MenuBottomSheet
import com.blockstream.compose.sheets.MenuEntry
+import com.blockstream.compose.sheets.NoteBottomSheet
import com.blockstream.compose.theme.bodyLarge
import com.blockstream.compose.theme.bodyMedium
import com.blockstream.compose.theme.bodySmall
@@ -104,6 +107,7 @@ import com.blockstream.compose.theme.whiteHigh
import com.blockstream.compose.theme.whiteLow
import com.blockstream.compose.theme.whiteMedium
import com.blockstream.compose.utils.AlphaPulse
+import com.blockstream.compose.utils.AnimatedNullableVisibility
import com.blockstream.compose.utils.AppBar
import com.blockstream.compose.utils.HandleSideEffect
import io.github.alexzhirkevich.qrose.QrCodePainter
@@ -148,6 +152,9 @@ fun ReceiveScreen(
viewModel.postEvent(Events.SetDenominatedValue(it))
}
+ NoteBottomSheet.getResult {
+ viewModel.postEvent(ReceiveViewModel.LocalEvents.SetNote(it))
+ }
val onProgress by viewModel.onProgress.collectAsStateWithLifecycle()
val accountAsset by viewModel.accountAsset.collectAsStateWithLifecycle()
@@ -281,14 +288,16 @@ fun ReceiveScreen(
AnimatedVisibility(visible = accountAsset?.account?.isLightning == true && !showLightningOnChainAddress || showRequestAmount) {
- GreenColumn(padding = 0, space = 8) {
+ GreenColumn(padding = 0, space = 8) {
GreenAmountField(
value = amount,
onValueChange = viewModel.amount.onValueChange(),
assetId = viewModel.accountAsset.value?.assetId,
session = viewModel.sessionOrNull,
- title = if(accountAsset?.account?.isLightning == false) stringResource(Res.string.id_request_amount) else stringResource(Res.string.id_amount),
+ title = if (accountAsset?.account?.isLightning == false) stringResource(
+ Res.string.id_request_amount
+ ) else stringResource(Res.string.id_amount),
error = amountError,
enabled = !onProgress,
denomination = denomination,
@@ -322,10 +331,9 @@ fun ReceiveScreen(
},
onDenominationClick = {
viewModel.postEvent(Events.SelectDenomination)
- }
- )
+ })
- liquidityFee?.also {
+ AnimatedNullableVisibility(liquidityFee) {
GreenCard(
padding = 0, colors = CardDefaults.elevatedCardColors(
containerColor = green20
@@ -345,7 +353,6 @@ fun ReceiveScreen(
}
}
}
-
}
}
@@ -408,7 +415,10 @@ fun ReceiveScreen(
GreenAddress(
address = receiveAddress ?: "",
textAlign = TextAlign.Center,
- maxLines = if (accountAsset?.account?.isLightning == true && !showLightningOnChainAddress) 1 else 6
+ maxLines = if (accountAsset?.account?.isLightning == true && !showLightningOnChainAddress) 1 else 6,
+ onCopyClick = {
+ viewModel.postEvent(ReceiveViewModel.LocalEvents.CopyAddress)
+ }
)
if (accountAsset?.account?.isLightning == true && showLightningOnChainAddress && onchainSwapMessage != null) {
diff --git a/compose/src/commonMain/kotlin/com/blockstream/compose/screens/send/SendScreen.kt b/compose/src/commonMain/kotlin/com/blockstream/compose/screens/send/SendScreen.kt
index 955f48a97..df610b8fd 100644
--- a/compose/src/commonMain/kotlin/com/blockstream/compose/screens/send/SendScreen.kt
+++ b/compose/src/commonMain/kotlin/com/blockstream/compose/screens/send/SendScreen.kt
@@ -29,6 +29,7 @@ import androidx.compose.ui.unit.dp
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import blockstream_green.common.generated.resources.Res
import blockstream_green.common.generated.resources.id_account__asset
+import blockstream_green.common.generated.resources.id_description
import blockstream_green.common.generated.resources.id_fee_rate
import blockstream_green.common.generated.resources.id_lightning_account
import blockstream_green.common.generated.resources.id_next
@@ -41,6 +42,7 @@ import com.blockstream.common.Parcelable
import com.blockstream.common.Parcelize
import com.blockstream.common.data.GreenWallet
import com.blockstream.common.events.Events
+import com.blockstream.common.extensions.isNotBlank
import com.blockstream.common.models.send.CreateTransactionViewModelAbstract
import com.blockstream.common.models.send.SendViewModel
import com.blockstream.common.models.send.SendViewModelAbstract
@@ -50,6 +52,7 @@ import com.blockstream.compose.components.GreenAccountAsset
import com.blockstream.compose.components.GreenAmountField
import com.blockstream.compose.components.GreenButton
import com.blockstream.compose.components.GreenColumn
+import com.blockstream.compose.components.GreenDataLayout
import com.blockstream.compose.components.GreenNetworkFee
import com.blockstream.compose.components.GreenTextField
import com.blockstream.compose.components.RiveAnimation
@@ -273,6 +276,23 @@ fun SendScreen(
)
}
+ val note by viewModel.note.collectAsStateWithLifecycle()
+ AnimatedVisibility(visible = note.isNotBlank()) {
+ GreenDataLayout(
+ title = stringResource(Res.string.id_description),
+ withPadding = false
+ ) {
+ Row {
+ Text(
+ text = note, modifier = Modifier
+ .weight(1f)
+ .padding(vertical = 16.dp)
+ .padding(start = 16.dp)
+ )
+ }
+ }
+ }
+
val metadataDomain by viewModel.metadataDomain.collectAsStateWithLifecycle()
AnimatedNullableVisibility(value = metadataDomain) {
Text(
@@ -351,6 +371,7 @@ fun SendScreen(
AnimatedNullableVisibility(value = accountAssetBalance) {
val buttonEnabled by viewModel.buttonEnabled.collectAsStateWithLifecycle()
+ val isValid by viewModel.isValid.collectAsStateWithLifecycle()
if (it.account.isLightning) {
SlideToUnlock(
@@ -363,7 +384,7 @@ fun SendScreen(
} else {
GreenButton(
text = stringResource(Res.string.id_next),
- enabled = buttonEnabled,
+ enabled = isValid,
modifier = Modifier.fillMaxWidth()
) {
viewModel.postEvent(Events.Continue)
diff --git a/compose/src/commonMain/kotlin/com/blockstream/compose/screens/settings/AppSettingsScreen.kt b/compose/src/commonMain/kotlin/com/blockstream/compose/screens/settings/AppSettingsScreen.kt
index bae6adf0a..ddec34204 100644
--- a/compose/src/commonMain/kotlin/com/blockstream/compose/screens/settings/AppSettingsScreen.kt
+++ b/compose/src/commonMain/kotlin/com/blockstream/compose/screens/settings/AppSettingsScreen.kt
@@ -53,6 +53,7 @@ import blockstream_green.common.generated.resources.id_double_check_spv_with_oth
import blockstream_green.common.generated.resources.id_enable_experimental_features
import blockstream_green.common.generated.resources.id_enable_limited_usage_data
import blockstream_green.common.generated.resources.id_enable_testnet
+import blockstream_green.common.generated.resources.id_enable_tls_connection
import blockstream_green.common.generated.resources.id_enhanced_privacy
import blockstream_green.common.generated.resources.id_experimental_features_might
import blockstream_green.common.generated.resources.id_help_green_improve
@@ -72,6 +73,7 @@ import blockstream_green.common.generated.resources.id_these_settings_apply_for_
import blockstream_green.common.generated.resources.id_use_secure_display_and_screen
import blockstream_green.common.generated.resources.id_verify_your_bitcoin
import blockstream_green.common.generated.resources.id_your_settings_are_unsavednndo
+import blockstream_green.common.generated.resources.lock_simple
import blockstream_green.common.generated.resources.shield_check
import blockstream_green.common.generated.resources.test_tube_fill
import blockstream_green.common.generated.resources.tor
@@ -339,6 +341,8 @@ fun AppSettingsScreen(
)
val electrumNodeEnabled by viewModel.electrumNodeEnabled.collectAsStateWithLifecycle()
+ val personalElectrumServerTlsEnabled by viewModel.personalElectrumServerTlsEnabled.collectAsStateWithLifecycle()
+
GreenSwitch(
title = stringResource(Res.string.id_personal_electrum_server),
caption = stringResource(Res.string.id_choose_the_electrum_servers_you),
@@ -413,6 +417,16 @@ fun AppSettingsScreen(
}
}
+ AnimatedVisibility(visible = electrumNodeEnabled) {
+ GreenSwitch(
+ title = stringResource(Res.string.id_enable_tls_connection),
+ checked = personalElectrumServerTlsEnabled,
+ painter = painterResource(Res.drawable.lock_simple),
+ onCheckedChange = viewModel.personalElectrumServerTlsEnabled.onValueChange(),
+ modifier = Modifier.padding(start = 42.dp)
+ )
+ }
+
HorizontalDivider(modifier = Modifier.padding(start = 54.dp))
val spvEnabled by viewModel.spvEnabled.collectAsStateWithLifecycle()
diff --git a/compose/src/commonMain/kotlin/com/blockstream/compose/sheets/NoteBottomSheet.kt b/compose/src/commonMain/kotlin/com/blockstream/compose/sheets/NoteBottomSheet.kt
index c10d917bf..13756c200 100644
--- a/compose/src/commonMain/kotlin/com/blockstream/compose/sheets/NoteBottomSheet.kt
+++ b/compose/src/commonMain/kotlin/com/blockstream/compose/sheets/NoteBottomSheet.kt
@@ -33,9 +33,11 @@ import com.blockstream.compose.components.GreenButton
import com.blockstream.compose.extensions.onValueChange
import com.blockstream.compose.navigation.getNavigationResult
import com.blockstream.compose.navigation.setNavigationResult
+import com.blockstream.compose.sheets.NoteBottomSheet.Companion.setResult
import com.blockstream.compose.utils.OpenKeyboard
import org.jetbrains.compose.resources.stringResource
import org.koin.core.parameter.parametersOf
+import kotlin.math.min
@Parcelize
data class NoteBottomSheet(
@@ -77,8 +79,7 @@ fun NoteBottomSheet(
sideEffectHandler = {
if (it is SideEffects.Success) {
(it.data as? String)?.also {
- NoteBottomSheet.setResult(it)
- onDismissRequest()
+ setResult(it)
}
}
},
@@ -91,13 +92,14 @@ fun NoteBottomSheet(
TextField(
value = note,
- onValueChange = viewModel.note.onValueChange(),
+ onValueChange = {
+ viewModel.note.value = it.substring(0 until it.length.coerceAtMost(200))
+ },
modifier = Modifier
.fillMaxWidth()
.focusRequester(focusRequester),
label = { Text(stringResource(if (viewModel.isLightning) Res.string.id_description else Res.string.id_add_note)) },
- minLines = 3,
- maxLines = 3,
+ maxLines = 5,
trailingIcon = {
Icon(
Icons.Default.Clear,
diff --git a/compose/src/commonMain/kotlin/com/blockstream/compose/utils/Containers.kt b/compose/src/commonMain/kotlin/com/blockstream/compose/utils/Containers.kt
index 3cadc56af..92f824ccd 100644
--- a/compose/src/commonMain/kotlin/com/blockstream/compose/utils/Containers.kt
+++ b/compose/src/commonMain/kotlin/com/blockstream/compose/utils/Containers.kt
@@ -5,7 +5,10 @@ import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.text.selection.SelectionContainer
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
+import com.blockstream.compose.LocalAppCoroutine
+import com.blockstream.compose.LocalSnackbar
import com.blockstream.compose.managers.LocalPlatformManager
+import kotlinx.coroutines.launch
@Composable
@@ -16,8 +19,9 @@ fun CopyContainer(
content: @Composable () -> Unit
) {
val platformManager = LocalPlatformManager.current
+
Box(modifier = modifier.clickable {
- platformManager.copyToClipboard(content = value )
+ platformManager.copyToClipboard(content = value)
}) {
if (withSelection) {
SelectionContainer {
diff --git a/compose/src/commonMain/kotlin/com/blockstream/compose/utils/SideEffects.kt b/compose/src/commonMain/kotlin/com/blockstream/compose/utils/SideEffects.kt
index e53712470..2b2064677 100644
--- a/compose/src/commonMain/kotlin/com/blockstream/compose/utils/SideEffects.kt
+++ b/compose/src/commonMain/kotlin/com/blockstream/compose/utils/SideEffects.kt
@@ -366,10 +366,14 @@ fun HandleSideEffect(
}
is SideEffects.CopyToClipboard -> {
- platformManager.copyToClipboard(content = it.value)
- it.message?.also {
- appCoroutine.launch {
- snackbar.showSnackbar(message = it)
+ if(!platformManager.copyToClipboard(content = it.value)){
+ it.message?.also {
+ if(!platformManager.openToast(it)) {
+ // In case openToast is not supported
+ appCoroutine.launch {
+ snackbar.showSnackbar(message = it)
+ }
+ }
}
}
}
diff --git a/compose/src/desktopMain/kotlin/com/blockstream/compose/di/KoinDesktop.kt b/compose/src/desktopMain/kotlin/com/blockstream/compose/di/KoinDesktop.kt
index 483523e55..6de104f7b 100644
--- a/compose/src/desktopMain/kotlin/com/blockstream/compose/di/KoinDesktop.kt
+++ b/compose/src/desktopMain/kotlin/com/blockstream/compose/di/KoinDesktop.kt
@@ -87,6 +87,10 @@ fun initKoinDesktop(appConfig: AppConfig, appInfo: AppInfo, doOnStartup: () -> U
}
single {
object : FcmCommon(get()){
+ override fun showDebugNotification(title: String, message: String) {
+
+ }
+
override fun scheduleLightningBackgroundJob(
walletId: String,
breezNotification: BreezNotification
diff --git a/compose/src/desktopMain/kotlin/com/blockstream/compose/managers/PlatformManager.desktop.kt b/compose/src/desktopMain/kotlin/com/blockstream/compose/managers/PlatformManager.desktop.kt
index eaae7d8b3..048ebf48e 100644
--- a/compose/src/desktopMain/kotlin/com/blockstream/compose/managers/PlatformManager.desktop.kt
+++ b/compose/src/desktopMain/kotlin/com/blockstream/compose/managers/PlatformManager.desktop.kt
@@ -37,13 +37,18 @@ actual fun askForNotificationPermissions(viewModel: GreenViewModel) {
}
actual class PlatformManager {
+ actual fun openToast(content: String): Boolean {
+ return false
+ }
+
actual fun openBrowser(url: String) {
if (Desktop.isDesktopSupported() && Desktop.getDesktop().isSupported(Desktop.Action.BROWSE)) {
Desktop.getDesktop().browse(URI(url))
}
}
- actual fun copyToClipboard(content: String, label: String?) {
+ actual fun copyToClipboard(content: String, label: String?, isSensitive: Boolean): Boolean {
+ return false
}
internal actual fun getClipboard(): String? {
diff --git a/compose/src/iosMain/kotlin/com/blockstream/compose/di/KoiniOS.kt b/compose/src/iosMain/kotlin/com/blockstream/compose/di/KoiniOS.kt
index 17a7d77c9..c0f3a8a06 100644
--- a/compose/src/iosMain/kotlin/com/blockstream/compose/di/KoiniOS.kt
+++ b/compose/src/iosMain/kotlin/com/blockstream/compose/di/KoiniOS.kt
@@ -66,6 +66,10 @@ fun startKoin(doOnStartup: () -> Unit = {}) {
}
single {
object : FcmCommon(get()){
+ override fun showDebugNotification(title: String, message: String) {
+
+ }
+
override fun scheduleLightningBackgroundJob(
walletId: String,
breezNotification: BreezNotification
diff --git a/compose/src/iosMain/kotlin/com/blockstream/compose/managers/PlatformManager.ios.kt b/compose/src/iosMain/kotlin/com/blockstream/compose/managers/PlatformManager.ios.kt
index 5c56eccbc..aa90d2754 100644
--- a/compose/src/iosMain/kotlin/com/blockstream/compose/managers/PlatformManager.ios.kt
+++ b/compose/src/iosMain/kotlin/com/blockstream/compose/managers/PlatformManager.ios.kt
@@ -55,14 +55,19 @@ actual fun askForNotificationPermissions(viewModel: GreenViewModel) {
@OptIn(ExperimentalForeignApi::class)
actual class PlatformManager(val application: UIApplication) {
+ actual fun openToast(content: String): Boolean {
+ return false
+ }
+
actual fun openBrowser(url: String) {
NSURL(string = url).takeIf { application.canOpenURL(it) }?.also {
application.openURL(it)
}
}
- actual fun copyToClipboard(content: String, label: String?) {
+ actual fun copyToClipboard(content: String, label: String?, isSensitive: Boolean): Boolean {
UIPasteboard.generalPasteboard().string = content
+ return false
}
internal actual fun getClipboard(): String? {
diff --git a/crypto/build.gradle.kts b/crypto/build.gradle.kts
deleted file mode 100644
index 9defef470..000000000
--- a/crypto/build.gradle.kts
+++ /dev/null
@@ -1,58 +0,0 @@
-import com.android.build.gradle.internal.cxx.configure.gradleLocalProperties
-
-plugins {
- alias(libs.plugins.androidLibrary)
- id("org.jetbrains.kotlin.android")
- alias(libs.plugins.kotlinParcelize)
- alias(libs.plugins.kotlinxSerialization)
-}
-
-android {
- namespace = "com.blockstream.crypto"
- compileSdk = 34
-
- defaultConfig {
- minSdk = 23
-
- val breezApiKey = System.getenv("BREEZ_API_KEY") ?: gradleLocalProperties(rootDir).getProperty("breez.apikey", "")
- val greenlightCertificate = System.getenv("GREENLIGHT_DEVICE_CERT") ?: gradleLocalProperties(rootDir).getProperty("greenlight.cert", "")
- val greenlightKey = System.getenv("GREENLIGHT_DEVICE_KEY") ?: gradleLocalProperties(rootDir).getProperty("greenlight.key", "")
-
- buildConfigField("String", "BREEZ_API_KEY", "\"${breezApiKey}\"")
- buildConfigField("String", "GREENLIGHT_DEVICE_CERT", "\"${greenlightCertificate}\"")
- buildConfigField("String", "GREENLIGHT_DEVICE_KEY", "\"${greenlightKey}\"")
-
- consumerProguardFiles("consumer-rules.pro")
- }
- compileOptions {
- sourceCompatibility = JavaVersion.VERSION_17
- targetCompatibility = JavaVersion.VERSION_17
- }
-}
-
-kotlin {
- jvmToolchain(17)
-}
-
-dependencies {
- /** --- Modules ---------------------------------------------------------------------------- */
- api(project(":gdk"))
- api(project(":common"))
- api(project(":lightning"))
- /** ----------------------------------------------------------------------------------------- */
-
- /** --- Kotlin & KotlinX ------------------------------------------------------------------- */
- implementation(libs.kotlinx.serialization.core)
- implementation(libs.kotlinx.serialization.json)
- implementation(libs.kotlinx.datetime)
- /** ----------------------------------------------------------------------------------------- */
-
- /** --- Logging ---------------------------------------------------------------------------- */
- implementation(libs.slf4j.simple)
- implementation(libs.kotlin.logging.jvm)
- /** ----------------------------------------------------------------------------------------- */
-
- /** --- Testing ---------------------------------------------------------------------------- */
- testImplementation(libs.junit)
- /** ----------------------------------------------------------------------------------------- */
-}
\ No newline at end of file
diff --git a/gdk/build.gradle.kts b/gdk/build.gradle.kts
index a24927741..6839b9045 100644
--- a/gdk/build.gradle.kts
+++ b/gdk/build.gradle.kts
@@ -11,14 +11,10 @@ android {
minSdk = libs.versions.androidMinSdk.get().toInt()
consumerProguardFiles("consumer-rules.pro")
}
- compileOptions {
- sourceCompatibility = JavaVersion.VERSION_17
- targetCompatibility = JavaVersion.VERSION_17
- }
}
kotlin {
- jvmToolchain(17)
+ jvmToolchain(libs.versions.jvm.get().toInt())
}
task("fetchAndroidBinaries") {
diff --git a/gms/build.gradle.kts b/gms/build.gradle.kts
index f57fa3254..7145984e1 100644
--- a/gms/build.gradle.kts
+++ b/gms/build.gradle.kts
@@ -13,17 +13,13 @@ android {
consumerProguardFiles("consumer-rules.pro")
}
- compileOptions {
- sourceCompatibility = JavaVersion.VERSION_17
- targetCompatibility = JavaVersion.VERSION_17
- }
buildFeatures {
buildConfig = true
}
}
kotlin {
- jvmToolchain(17)
+ jvmToolchain(libs.versions.jvm.get().toInt())
}
dependencies {
@@ -36,7 +32,7 @@ dependencies {
implementation(libs.review.ktx)
implementation(platform(libs.firebase.bom))
implementation(libs.firebase.messaging)
-
+ implementation(libs.installreferrer)
/** ----------------------------------------------------------------------------------------- */
/** --- Koin ----------------------------------------------------------------------------- */
diff --git a/gms/src/main/java/com/blockstream/gms/InstallReferrerImpl.kt b/gms/src/main/java/com/blockstream/gms/InstallReferrerImpl.kt
new file mode 100644
index 000000000..79b5f1121
--- /dev/null
+++ b/gms/src/main/java/com/blockstream/gms/InstallReferrerImpl.kt
@@ -0,0 +1,96 @@
+package com.blockstream.gms
+
+import android.content.Context
+import com.android.installreferrer.api.InstallReferrerClient
+import com.android.installreferrer.api.InstallReferrerStateListener
+import com.blockstream.base.InstallReferrer
+import com.blockstream.common.CountlyBase.Companion.GOOGLE_PLAY_ORGANIC_DEVELOPMENT
+import com.blockstream.common.CountlyBase.Companion.GOOGLE_PLAY_ORGANIC_PRODUCTION
+import com.blockstream.common.data.AppInfo
+import com.blockstream.common.utils.Loggable
+import kotlinx.serialization.json.buildJsonObject
+import kotlinx.serialization.json.put
+import ly.count.android.sdk.ModuleAttribution
+import java.net.URLDecoder
+
+class InstallReferrerImpl(val context: Context, val appInfo: AppInfo) : InstallReferrer() {
+
+ override fun handleReferrer(
+ attribution: ModuleAttribution.Attribution,
+ onComplete: (referrer: String) -> Unit
+ ) {
+ InstallReferrerClient.newBuilder(context).build().also { referrerClient ->
+ referrerClient.startConnection(object : InstallReferrerStateListener {
+ override fun onInstallReferrerSetupFinished(responseCode: Int) {
+ when (responseCode) {
+ InstallReferrerClient.InstallReferrerResponse.OK -> {
+ var cid: String? = null
+ var uid: String? = null
+ var referrer: String? = null
+
+ try {
+ // The string may be URL Encoded, so decode it just to be sure.
+ // eg. utm_source=google-play&utm_medium=organic
+ // eg. "cly_id=0eabe3eac38ff74556c69ed25a8275b19914ea9d&cly_uid=c27b33b16ac7947fae0ed9e60f3a5ceb96e0e545425dd431b791fe930fabafde4b96c69e0f63396202377a8025f008dfee2a9baf45fa30f7c80958bd5def6056"
+ referrer = URLDecoder.decode(
+ referrerClient.installReferrer.installReferrer,
+ "UTF-8"
+ )
+
+ logger.i { "Referrer: $referrer" }
+
+ val parts = referrer.split("&")
+
+ for (part in parts) {
+ // Countly campaign
+ if (part.startsWith("cly_id")) {
+ cid = part.replace("cly_id=", "").trim()
+ }
+ if (part.startsWith("cly_uid")) {
+ uid = part.replace("cly_uid=", "").trim()
+ }
+
+ // Google Play organic
+ if (part.trim() == "utm_medium=organic") {
+ cid =
+ if (appInfo.isDevelopment) GOOGLE_PLAY_ORGANIC_DEVELOPMENT else GOOGLE_PLAY_ORGANIC_PRODUCTION
+ }
+ }
+
+ attribution.recordDirectAttribution("countly", buildJsonObject {
+ put("cid", cid)
+ if (uid != null) {
+ put("cuid", uid)
+ }
+ }.toString())
+
+ } catch (e: Exception) {
+ e.printStackTrace()
+ }
+
+ onComplete.invoke(referrer ?: "")
+ }
+
+ InstallReferrerClient.InstallReferrerResponse.FEATURE_NOT_SUPPORTED -> {
+ // API not available on the current Play Store app.
+ // logger.info { "InstallReferrerService FEATURE_NOT_SUPPORTED" }
+ onComplete.invoke("")
+ }
+
+ InstallReferrerClient.InstallReferrerResponse.SERVICE_UNAVAILABLE -> {
+ // Connection couldn't be established.
+ // logger.info { "InstallReferrerService SERVICE_UNAVAILABLE" }
+ }
+ }
+
+ // Disconnect the client
+ referrerClient.endConnection()
+ }
+
+ override fun onInstallReferrerServiceDisconnected() {}
+ })
+ }
+ }
+
+ companion object : Loggable()
+}
\ No newline at end of file
diff --git a/gms/src/main/java/com/blockstream/gms/di/GmsModule.kt b/gms/src/main/java/com/blockstream/gms/di/GmsModule.kt
index b8a6a748e..3921bf3ef 100644
--- a/gms/src/main/java/com/blockstream/gms/di/GmsModule.kt
+++ b/gms/src/main/java/com/blockstream/gms/di/GmsModule.kt
@@ -3,11 +3,13 @@
package com.blockstream.gms.di
import com.blockstream.base.GooglePlay
+import com.blockstream.base.InstallReferrer
import com.blockstream.common.fcm.Firebase
import com.blockstream.common.ZendeskSdk
import com.blockstream.common.data.AppConfig
import com.blockstream.gms.FirebaseImpl
import com.blockstream.gms.GooglePlayImpl
+import com.blockstream.gms.InstallReferrerImpl
import com.blockstream.gms.ZendeskSdkAndroid
import com.google.android.play.core.review.ReviewManagerFactory
import okio.internal.commonToUtf8String
@@ -38,4 +40,8 @@ val gmsModule = module {
single {
FirebaseImpl(get())
} binds(arrayOf(Firebase::class))
+
+ single {
+ InstallReferrerImpl(get(), get())
+ } binds(arrayOf(InstallReferrer::class))
}
\ No newline at end of file
diff --git a/gms/src/main/java/com/blockstream/gms/services/FirebaseMessagingService.kt b/gms/src/main/java/com/blockstream/gms/services/FirebaseMessagingService.kt
index a7e43030a..863dd845b 100644
--- a/gms/src/main/java/com/blockstream/gms/services/FirebaseMessagingService.kt
+++ b/gms/src/main/java/com/blockstream/gms/services/FirebaseMessagingService.kt
@@ -1,5 +1,6 @@
package com.blockstream.gms.services
+import com.blockstream.common.data.AppInfo
import com.blockstream.common.fcm.FcmCommon
import com.blockstream.common.lightning.BreezNotification
import com.blockstream.common.utils.Loggable
@@ -12,6 +13,7 @@ import org.koin.core.component.inject
class FirebaseMessagingService : FirebaseMessagingService(), KoinComponent {
val fcm: FcmCommon by inject()
+ val appInfo: AppInfo by inject()
override fun onMessageReceived(remoteMessage: RemoteMessage) {
val data = remoteMessage.data
@@ -27,6 +29,10 @@ class FirebaseMessagingService : FirebaseMessagingService(), KoinComponent {
val xpubHashId = data["app_data"]
val breezNotification = BreezNotification.fromString(data["notification_payload"])
+ if(appInfo.isDevelopmentOrDebug){
+ fcm.showDebugNotification(title = "Notification Received", message = breezNotification.toString())
+ }
+
if (breezNotification != null && !xpubHashId.isNullOrBlank()) {
fcm.handleLightningPushNotification(xpubHashId, breezNotification)
} else {
diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml
index ecc5c60dd..342729378 100644
--- a/gradle/libs.versions.toml
+++ b/gradle/libs.versions.toml
@@ -1,19 +1,20 @@
[versions]
+jvm = "17"
accompanistPermissions = "0.34.0"
constraintlayoutComposeMultiplatform = "0.4.0"
kotlin = "2.0.0"
kotlin-logging = "3.0.5"
kotlinx-coroutines = "1.9.0-RC"
kotlinx-datetime = "0.6.0"
-kotlinx-serialization = "1.7.0-RC"
+kotlinx-serialization = "1.7.1"
kotlin-ksp = "2.0.0-1.0.21"
-android-gradle-plugin = "8.5.0"
+android-gradle-plugin = "8.5.1"
androidCompileSdk = "34"
androidTargetSdk = "34"
androidMinSdk = "24"
buildTools = "34.0.0"
-breez = "0.4.2-rc2"
-androidx-junit = "1.1.5"
+breez = "0.5.1-rc6"
+androidx-junit = "1.2.1"
biometric = "1.2.0-alpha05"
browser = "1.8.0"
constraintlayout = "2.1.4"
@@ -21,7 +22,7 @@ appcompat = "1.7.0"
core-ktx = "1.13.1"
core-testing = "2.2.0"
countly-sdk-android = "4626cb98b65ef9769296956022474d7658b3b116"
-espresso-core = "3.5.1"
+espresso-core = "3.6.1"
fastadapter = "5.7.0"
installreferrer = "2.2"
itemanimators = "1.1.0"
@@ -81,7 +82,7 @@ compose-constraint = "1.0.1"
jetbrains-compose = "1.6.11"
voyager = "1.1.0-beta02"
google-services = "4.4.2"
-firebase-bom = "33.1.0"
+firebase-bom = "33.1.2"
jna = "5.14.0"
[libraries]
diff --git a/gradle/verification-metadata.xml b/gradle/verification-metadata.xml
index 8152d8231..67666346b 100644
--- a/gradle/verification-metadata.xml
+++ b/gradle/verification-metadata.xml
@@ -13,7 +13,6 @@
-
diff --git a/green/build.gradle.kts b/green/build.gradle.kts
index 8af61b30a..ef7c3584f 100644
--- a/green/build.gradle.kts
+++ b/green/build.gradle.kts
@@ -42,8 +42,8 @@ android {
defaultConfig {
minSdk = libs.versions.androidMinSdk.get().toInt()
targetSdk = libs.versions.androidTargetSdk.get().toInt()
- versionCode = 430
- versionName = "4.0.30"
+ versionCode = 431
+ versionName = "4.0.31"
setProperty("archivesBaseName", "BlockstreamGreen-v$versionName")
proguardFiles(getDefaultProguardFile("proguard-android-optimize.txt"), "proguard-rules.pro")
@@ -130,8 +130,6 @@ android {
}
}
compileOptions {
- sourceCompatibility = JavaVersion.VERSION_17
- targetCompatibility = JavaVersion.VERSION_17
// SDK 23 support
isCoreLibraryDesugaringEnabled = true
}
@@ -161,7 +159,7 @@ composeCompiler {
}
kotlin {
- jvmToolchain(17)
+ jvmToolchain(libs.versions.jvm.get().toInt())
sourceSets {
all {
@@ -244,10 +242,6 @@ dependencies {
implementation(libs.zxing.android.embedded)
/** ----------------------------------------------------------------------------------------- */
- /** --- Countly ---------------------------------------------------------------------------- */
- implementation(libs.countly.sdk.android)
- /** ----------------------------------------------------------------------------------------- */
-
testImplementation(libs.junit)
testImplementation(libs.androidx.core.testing)
testImplementation(libs.kotlinx.coroutines.test)
diff --git a/green/src/main/AndroidManifest.xml b/green/src/main/AndroidManifest.xml
index fbc89d15f..072208296 100644
--- a/green/src/main/AndroidManifest.xml
+++ b/green/src/main/AndroidManifest.xml
@@ -90,6 +90,7 @@
diff --git a/green/src/main/java/com/blockstream/green/data/Countly.kt b/green/src/main/java/com/blockstream/green/data/Countly.kt
index b8748368b..ad75a1ce2 100644
--- a/green/src/main/java/com/blockstream/green/data/Countly.kt
+++ b/green/src/main/java/com/blockstream/green/data/Countly.kt
@@ -6,8 +6,7 @@ import android.content.SharedPreferences
import android.content.res.Configuration
import androidx.core.content.edit
import androidx.fragment.app.FragmentManager
-import com.android.installreferrer.api.InstallReferrerClient
-import com.android.installreferrer.api.InstallReferrerStateListener
+import com.blockstream.base.InstallReferrer
import com.blockstream.common.data.AppInfo
import com.blockstream.common.data.CountlyWidget
import com.blockstream.common.database.Database
@@ -21,8 +20,6 @@ import com.blockstream.green.ui.dialogs.CountlySurveyDialogFragment
import com.blockstream.green.utils.isDevelopmentOrDebug
import com.blockstream.green.utils.isProductionFlavor
import com.blockstream.green.views.GreenAlertView
-import kotlinx.serialization.json.buildJsonObject
-import kotlinx.serialization.json.put
import ly.count.android.sdk.Countly
import ly.count.android.sdk.CountlyConfig
import ly.count.android.sdk.ModuleAPM
@@ -36,7 +33,6 @@ import ly.count.android.sdk.ModuleRemoteConfig
import ly.count.android.sdk.ModuleRequestQueue
import ly.count.android.sdk.ModuleUserProfile
import ly.count.android.sdk.ModuleViews
-import java.net.URLDecoder
class Countly constructor(
private val context: Context,
@@ -45,6 +41,7 @@ class Countly constructor(
private val applicationScope: ApplicationScope,
private val settingsManager: SettingsManager,
private val database: Database,
+ private val installReferrer: InstallReferrer,
): CountlyAndroid(appInfo, applicationScope, settingsManager, database) {
private val _requestQueue: ModuleRequestQueue.RequestQueue
@@ -127,7 +124,7 @@ class Countly constructor(
// If no referrer is set, try to get it from the install referrer
// Empty string is also allowed
if (!this.sharedPreferences.contains(REFERRER_KEY)) {
- handleReferrer { referrer ->
+ installReferrer.handleReferrer(_attribution) { referrer ->
// Mark it as complete
sharedPreferences.edit {
putString(REFERRER_KEY, referrer)
@@ -239,77 +236,6 @@ class Countly constructor(
}
}
- private fun handleReferrer(onComplete: (referrer: String) -> Unit) {
- InstallReferrerClient.newBuilder(context).build().also { referrerClient ->
- referrerClient.startConnection(object : InstallReferrerStateListener {
- override fun onInstallReferrerSetupFinished(responseCode: Int) {
- when (responseCode) {
- InstallReferrerClient.InstallReferrerResponse.OK -> {
- var cid: String? = null
- var uid: String? = null
- var referrer: String? = null
-
- try {
- // The string may be URL Encoded, so decode it just to be sure.
- // eg. utm_source=google-play&utm_medium=organic
- // eg. "cly_id=0eabe3eac38ff74556c69ed25a8275b19914ea9d&cly_uid=c27b33b16ac7947fae0ed9e60f3a5ceb96e0e545425dd431b791fe930fabafde4b96c69e0f63396202377a8025f008dfee2a9baf45fa30f7c80958bd5def6056"
- referrer = URLDecoder.decode(
- referrerClient.installReferrer.installReferrer,
- "UTF-8"
- )
-
- logger.i { "Referrer: $referrer" }
-
- val parts = referrer.split("&")
-
- for (part in parts) {
- // Countly campaign
- if (part.startsWith("cly_id")) {
- cid = part.replace("cly_id=", "").trim()
- }
- if (part.startsWith("cly_uid")) {
- uid = part.replace("cly_uid=", "").trim()
- }
-
- // Google Play organic
- if (part.trim() == "utm_medium=organic") {
- cid = if (isProductionFlavor) GOOGLE_PLAY_ORGANIC_PRODUCTION else GOOGLE_PLAY_ORGANIC_DEVELOPMENT
- }
- }
-
- _attribution.recordDirectAttribution("countly", buildJsonObject {
- put("cid", cid)
- if (uid != null) {
- put("cuid", uid)
- }
- }.toString())
-
- } catch (e: Exception) {
- recordException(e)
- }
-
- onComplete.invoke(referrer ?: "")
- }
- InstallReferrerClient.InstallReferrerResponse.FEATURE_NOT_SUPPORTED -> {
- // API not available on the current Play Store app.
- // logger.info { "InstallReferrerService FEATURE_NOT_SUPPORTED" }
- onComplete.invoke("")
- }
- InstallReferrerClient.InstallReferrerResponse.SERVICE_UNAVAILABLE -> {
- // Connection couldn't be established.
- // logger.info { "InstallReferrerService SERVICE_UNAVAILABLE" }
- }
- }
-
- // Disconnect the client
- referrerClient.endConnection()
- }
-
- override fun onInstallReferrerServiceDisconnected() {}
- })
- }
- }
-
override fun updateUserWallets(wallets: Int) {
_userProfile.setProperty(USER_PROPERTY_TOTAL_WALLETS, wallets.toString())
_userProfile.save()
diff --git a/green/src/main/java/com/blockstream/green/di/KoinAndroid.kt b/green/src/main/java/com/blockstream/green/di/KoinAndroid.kt
index 8f2b1ce65..3c7ae395e 100644
--- a/green/src/main/java/com/blockstream/green/di/KoinAndroid.kt
+++ b/green/src/main/java/com/blockstream/green/di/KoinAndroid.kt
@@ -43,7 +43,7 @@ fun initKoinAndroid(context: Context, doOnStartup: () -> Unit = {}) {
}
single {
if (context.resources.getBoolean(R.bool.feature_analytics)) {
- Countly(get(), get(), get(), get(), get(), get())
+ Countly(get(), get(), get(), get(), get(), get(), get())
} else {
CountlyNoOp(get(), get(), get(), get())
}
diff --git a/green/src/main/java/com/blockstream/green/managers/FcmAndroid.kt b/green/src/main/java/com/blockstream/green/managers/FcmAndroid.kt
index 499e2eb6a..4e343deab 100644
--- a/green/src/main/java/com/blockstream/green/managers/FcmAndroid.kt
+++ b/green/src/main/java/com/blockstream/green/managers/FcmAndroid.kt
@@ -1,5 +1,6 @@
package com.blockstream.green.managers
+import android.app.Notification
import android.content.Context
import com.blockstream.common.data.GreenWallet
import com.blockstream.common.di.ApplicationScope
@@ -40,5 +41,12 @@ class FcmAndroid constructor(
notificationManager.createPaymentNotification(context, wallet, paymentHash, satoshi)
}
+ override fun showDebugNotification(
+ title: String,
+ message: String,
+ ) {
+ notificationManager.createDebugNotification(context = context, title = title, message = message)
+ }
+
companion object : Loggable()
}
\ No newline at end of file
diff --git a/green/src/main/java/com/blockstream/green/managers/NotificationManager.kt b/green/src/main/java/com/blockstream/green/managers/NotificationManager.kt
index ac025895d..2a97374ad 100644
--- a/green/src/main/java/com/blockstream/green/managers/NotificationManager.kt
+++ b/green/src/main/java/com/blockstream/green/managers/NotificationManager.kt
@@ -278,6 +278,31 @@ class NotificationManager constructor(
}
}
+ fun createDebugNotification(
+ context: Context,
+ title: String,
+ message: String,
+ ): Notification {
+
+ val notificationSound = RingtoneManager.getDefaultUri(RingtoneManager.TYPE_NOTIFICATION)
+
+ return NotificationCompat.Builder(context, LIGHTNING_CHANNEL_ID)
+ .setSmallIcon(R.drawable.ic_stat_green)
+ .setContentTitle(title)
+ .setContentText(message)
+ .setColorized(true)
+ .setColor(ContextCompat.getColor(context, R.color.brand_green))
+ .setSound(notificationSound)
+ .setPriority(NotificationCompat.PRIORITY_MAX)
+ .setAutoCancel(true)
+ .setProgress(0, 100, true)
+ .setOnlyAlertOnce(false)
+ .setVisibility(NotificationCompat.VISIBILITY_PUBLIC)
+ .build().also {
+ androidNotificationManager.notify(237_237, it)
+ }
+ }
+
suspend fun createForegroundServiceNotification(context: Context): Notification {
return NotificationCompat.Builder(context, LIGHTNING_CHANNEL_ID)
.setContentTitle(context.getString(R.string.id_lightning))
diff --git a/green/src/main/java/com/blockstream/green/ui/AppFragment.kt b/green/src/main/java/com/blockstream/green/ui/AppFragment.kt
index 7bdb05f56..1b83ada0c 100644
--- a/green/src/main/java/com/blockstream/green/ui/AppFragment.kt
+++ b/green/src/main/java/com/blockstream/green/ui/AppFragment.kt
@@ -165,7 +165,7 @@ abstract class AppFragment(
if(useCompose){
viewModel.navData.onEach {
updateToolbar()
- }
+ }.launchIn(lifecycleScope)
}
viewLifecycleOwner.lifecycleScope.launch {
diff --git a/green/src/main/java/com/blockstream/green/ui/bottomsheets/SelectUtxosBottomSheetDialogFragment.kt b/green/src/main/java/com/blockstream/green/ui/bottomsheets/SelectUtxosBottomSheetDialogFragment.kt
index d3b1ea5ba..353928d8b 100644
--- a/green/src/main/java/com/blockstream/green/ui/bottomsheets/SelectUtxosBottomSheetDialogFragment.kt
+++ b/green/src/main/java/com/blockstream/green/ui/bottomsheets/SelectUtxosBottomSheetDialogFragment.kt
@@ -9,9 +9,9 @@ import androidx.lifecycle.lifecycleScope
import androidx.recyclerview.widget.LinearLayoutManager
import com.blockstream.common.extensions.logException
import com.blockstream.common.gdk.data.Account
+import com.blockstream.common.models.GreenViewModel
import com.blockstream.green.databinding.SelectUtxosBottomSheetBinding
import com.blockstream.green.ui.items.UtxoListItem
-import com.blockstream.green.ui.send.SendViewModel
import com.mikepenz.fastadapter.FastAdapter
import com.mikepenz.fastadapter.adapters.ItemAdapter
import com.mikepenz.itemanimators.SlideDownAlphaAnimator
@@ -20,7 +20,7 @@ import kotlinx.coroutines.launch
import mu.KLogging
// WIP
-class SelectUtxosBottomSheetDialogFragment : WalletBottomSheetDialogFragment() {
+class SelectUtxosBottomSheetDialogFragment : WalletBottomSheetDialogFragment() {
override val screenName = "SelectUTXO"
override fun inflate(layoutInflater: LayoutInflater) = SelectUtxosBottomSheetBinding.inflate(layoutInflater)
diff --git a/green/src/main/java/com/blockstream/green/ui/receive/ReceiveFragment.kt b/green/src/main/java/com/blockstream/green/ui/receive/ReceiveFragment.kt
index 50c56aa57..e229e280c 100644
--- a/green/src/main/java/com/blockstream/green/ui/receive/ReceiveFragment.kt
+++ b/green/src/main/java/com/blockstream/green/ui/receive/ReceiveFragment.kt
@@ -31,7 +31,7 @@ import org.koin.core.parameter.parametersOf
class ReceiveFragment : AppFragment(
- layout = R.layout.compose_view
+ layout = R.layout.compose_view, menuRes = R.menu.menu_receive
) {
val args: ReceiveFragmentArgs by navArgs()
@@ -79,11 +79,25 @@ class ReceiveFragment : AppFragment(
(requireActivity() as MainActivity).lockDrawer(!it.isVisible)
}.launchIn(lifecycleScope)
+ viewModel.accountAsset.onEach {
+ invalidateMenu()
+ }.launchIn(lifecycleScope)
+
+ viewModel.onProgress.onEach {
+ // On HWWallet Block going back until address is generated
+ onBackCallback.isEnabled = viewModel.session.isHardwareWallet && it
+ invalidateMenu()
+ }.launchIn(lifecycleScope)
+
+ viewModel.navData.onEach {
+ invalidateMenu()
+ }.launchIn(lifecycleScope)
+
requireActivity().onBackPressedDispatcher.addCallback(viewLifecycleOwner, onBackCallback)
}
override fun onPrepareMenu(menu: Menu) {
- menu.findItem(R.id.add_description).isVisible = viewModel.account.isLightning
+ menu.findItem(R.id.add_description).isVisible = viewModel.account.isLightning && !viewModel.showLightningOnChainAddress.value && viewModel.receiveAddress.value == null
menu.findItem(R.id.add_description).isEnabled = !viewModel.onProgress.value
}
diff --git a/green/src/main/java/com/blockstream/green/ui/send/SendViewModel.kt b/green/src/main/java/com/blockstream/green/ui/send/SendViewModel.kt
deleted file mode 100644
index 7ad399cf9..000000000
--- a/green/src/main/java/com/blockstream/green/ui/send/SendViewModel.kt
+++ /dev/null
@@ -1,702 +0,0 @@
-package com.blockstream.green.ui.send;
-
-import android.graphics.Bitmap
-import androidx.lifecycle.MutableLiveData
-import androidx.lifecycle.asFlow
-import com.blockstream.common.AddressInputType
-import com.blockstream.common.TransactionSegmentation
-import com.blockstream.common.TransactionType
-import com.blockstream.common.data.DenominatedValue
-import com.blockstream.common.data.Denomination
-import com.blockstream.common.data.GreenWallet
-import com.blockstream.common.extensions.isNotBlank
-import com.blockstream.common.extensions.isPolicyAsset
-import com.blockstream.common.gdk.FeeBlockTarget
-import com.blockstream.common.gdk.GdkSession
-import com.blockstream.common.gdk.data.AccountAsset
-import com.blockstream.common.gdk.data.Assets
-import com.blockstream.common.gdk.data.CreateTransaction
-import com.blockstream.common.gdk.data.Network
-import com.blockstream.common.gdk.params.AddressParams
-import com.blockstream.common.gdk.params.CreateTransactionParams
-import com.blockstream.common.lightning.lnUrlPayDescription
-import com.blockstream.common.models.GreenViewModel
-import com.blockstream.common.sideeffects.SideEffects
-import com.blockstream.common.utils.UserInput
-import com.blockstream.common.utils.toAmountLook
-import com.blockstream.green.extensions.boolean
-import com.blockstream.green.extensions.lnUrlPayBitmap
-import com.blockstream.green.ui.bottomsheets.DenominationListener
-import com.blockstream.green.utils.feeRateWithUnit
-import com.rickclephas.kmp.observableviewmodel.coroutineScope
-import kotlinx.coroutines.Dispatchers
-import kotlinx.coroutines.FlowPreview
-import kotlinx.coroutines.flow.MutableStateFlow
-import kotlinx.coroutines.flow.debounce
-import kotlinx.coroutines.flow.distinctUntilChanged
-import kotlinx.coroutines.flow.drop
-import kotlinx.coroutines.flow.filterNot
-import kotlinx.coroutines.flow.filterNotNull
-import kotlinx.coroutines.flow.launchIn
-import kotlinx.coroutines.flow.map
-import kotlinx.coroutines.flow.onEach
-import kotlinx.coroutines.sync.Mutex
-import kotlinx.coroutines.sync.withLock
-import kotlinx.coroutines.withContext
-import kotlinx.serialization.json.JsonElement
-import kotlinx.serialization.json.jsonObject
-import kotlinx.serialization.json.jsonPrimitive
-import kotlinx.serialization.json.longOrNull
-import mu.KLogging
-import org.koin.android.annotation.KoinViewModel
-import org.koin.core.annotation.InjectedParam
-import kotlin.math.absoluteValue
-
-@OptIn(FlowPreview::class)
-@KoinViewModel
-class SendViewModel constructor(
- @InjectedParam wallet: GreenWallet,
- @InjectedParam initAccountAsset: AccountAsset,
- @InjectedParam address: String?,
- @InjectedParam addressType: AddressInputType?,
- @InjectedParam val bumpTransaction: JsonElement?,
-) : GreenViewModel(wallet, initAccountAsset), DenominationListener {
- val isSweep = false
- val isBump = bumpTransaction != null
- val isBumpOrSweep = isBump
-
- var activeRecipient = 0
-
- private val recipients = MutableStateFlow(mutableListOf(
- AddressParamsLiveData.create(
- session = session,
- index = 0,
- address = address,
- addressInputType = addressType,
- accountAsset = this.accountAsset
- )
- ))
- fun getRecipientsStateFlow() = recipients
-
- fun getRecipientStateFlow(index: Int) = recipients.value.getOrNull(index)
-
- val feeSlider = MutableLiveData() // SliderHighIndex.toFloat() // fee slider selection, 0 for custom
- val feeAmount = MutableLiveData("") // total tx fee
- val feeAmountFiat = MutableLiveData("") // total tx fee in fiat
- val feeAmountRate = MutableLiveData("") // fee rate
-
- // fee rate from sharedPreferences only for bitcoin
- var customFee: Long? = null
-
- var feeRate : Long? = null
- var feeEstimation: List? = null
-
- private var checkedTransaction: CreateTransaction? = null
- val transactionError: MutableLiveData = MutableLiveData("") // empty string as an initial error to disable next button
-
- val handledGdkErrors: List = listOf(
- "id_invalid_private_key",
- "id_invalid_address",
- "id_invalid_amount",
- "id_invalid_asset_id",
- "id_invoice_expired",
- "id_amount_must_be_at_least_s",
- "id_amount_must_be_at_most_s",
- "id_amount_below_the_dust_threshold"
- ) + listOfNotNull(if(!isBump) "id_insufficient_funds" else null) // On Bump, show fee error on errorTextView
-
- private val checkTransactionMutex = Mutex()
-
- init {
- // Update fee estimation on network change
- accountAsset.filterNotNull().map { it.account.network }.onEach { network ->
- updateFeeEstimation()
-
- // Set initial fee slider value
- if(feeSlider.value == null){
- feeSlider.value = SliderLowIndex.toFloat()
- }
-
- session.getSettings(network)?.let {
- FeeBlockTarget
- .indexOf(it.requiredNumBlocks)
- .takeIf { it > -1 }?.let {
- feeSlider.value = 3 - it.toFloat()
- }
- }
- }.launchIn(viewModelScope.coroutineScope)
-
- accountAsset.filterNotNull().onEach {
- setAccountAsset(0, it)
- }.launchIn(viewModelScope.coroutineScope)
-
- // Check transaction if we get a network event
- // we may have gotten an error "session is required"
- // TODO CHANGE THIS TO SUPPORT MULTI NETWORKS
- session.defaultNetworkOrNull?.also {
- session
- .networkEvents(it).filterNotNull()
- .onEach { event ->
- if (event.isConnected) {
- checkTransaction()
- }
- }.launchIn(viewModelScope.coroutineScope)
- }
-
-
- // Fee Slider
- feeSlider
- .asFlow()
- .drop(1) // drop initial value
- .distinctUntilChanged()
- .onEach {
- if(it.toInt() != SliderCustomIndex){
- feeRate = feeEstimation?.getOrNull(FeeBlockTarget[3 - (it.toInt())])
- }
-
- checkTransaction()
- }
- .launchIn(viewModelScope.coroutineScope)
-
- recipients.value.getOrNull(0)?.let {
- setupChangeObserve(it)
- }
-
- bootstrap()
- }
-
- fun createTransactionSegmentation(): TransactionSegmentation {
- return TransactionSegmentation(
- transactionType = when{
- isBump -> TransactionType.BUMP
- else -> TransactionType.SEND
- },
- addressInputType = recipients.value.get(0).addressInputType,
- sendAll = isSendAll()
- )
- }
-
- private fun getBumpTransactionFeeRate(): Long? {
- return bumpTransaction?.jsonObject?.get("fee_rate")?.jsonPrimitive?.longOrNull
- }
-
- private fun updateFeeEstimation() {
- feeRate = null // reset fee rate
-
- doAsync({
- logger.info { "updateFeeEstimation for ${account.network.id}" }
- session.getFeeEstimates(account.network)
- }, preAction = null, postAction = null, onSuccess = {
- feeEstimation = if(isBump){
-
- // Old fee rate + minimum relay
- val bumpFeeAndRelay = (getBumpTransactionFeeRate() ?: it.fees[0]) + (it.minimumRelayFee ?: account.network.defaultFee)
- it.fees.mapIndexed { index, fee ->
- if(index == 0) {
- fee
- } else {
- fee.coerceAtLeast(bumpFeeAndRelay)
- }
- }
- }else{
- it.fees
- }
-
- // skip if custom fee is selected
- if (feeSlider.value?.toInt() != SliderCustomIndex) {
- // update based on current slider selection
- feeRate = feeEstimation?.getOrNull(FeeBlockTarget[3 - (feeSlider.value ?: SliderLowIndex).toInt()])
-
- // Update fee
- checkTransaction()
- }
- })
- }
-
- private fun setupChangeObserve(addressParamsLiveData: AddressParamsLiveData) {
-
- // Pre select asset
-// assetsLiveData.value?.let { balances ->
-// if (balances.size == 1) {
-// if (addressParamsLiveData.accountAsset.value.isNullOrBlank()) {
-// addressParamsLiveData.assetId.value = balances.keys.first()
-// }
-// }
-// }
-
- // Address
- addressParamsLiveData.address
- .asFlow()
- .drop(1)// drop initial value
- .distinctUntilChanged()
- .filterNot { isBump }
- .debounce(50)
- .onEach {
- checkTransaction()
- }
- .launchIn(viewModelScope.coroutineScope)
-
- // Account
- addressParamsLiveData.accountAsset
- .drop(1)// drop initial value
- .distinctUntilChanged()
- .onEach {
- checkTransaction()
- }
- .launchIn(viewModelScope.coroutineScope)
-
- // Amount
- addressParamsLiveData.amount
- .asFlow()
- .drop(1)// drop initial value
- .distinctUntilChanged()
- .debounce(100) // debounce as user types
- .onEach {
- // Skip if is a bip21 field or is Send all or sweep or bump or Bolt11
- if (
- addressParamsLiveData.isSendAll.value == false
- && addressParamsLiveData.amountBip21.value == false
- && !isBumpOrSweep
- && !addressParamsLiveData.hasLockedAmount.boolean()
- ) {
- checkTransaction()
- }
-
- updateExchange(addressParamsLiveData)
- }
- .launchIn(viewModelScope.coroutineScope)
-
- // Send All
- addressParamsLiveData.isSendAll
- .asFlow()
- .drop(1)// drop initial value
- .filterNot { isBumpOrSweep }
- .distinctUntilChanged()
- .onEach { isSendAll ->
- // avoid checkTransaction when deselected as the event is fired from the amount field being set to ""
- if(isSendAll) {
- checkTransaction()
- }
- }
- .launchIn(viewModelScope.coroutineScope)
-
- if (session.hasLightning) {
- session.lightningSdk.nodeInfoStateFlow.drop(1).onEach {
- if (account.isLightning) {
- // Re-check the transaction on node_info update
- checkTransaction()
- }
- }.launchIn(viewModelScope.coroutineScope)
- }
- }
-
- private fun updateExchange(addressParamsLiveData: AddressParamsLiveData) {
- addressParamsLiveData.amount.value?.let { amount ->
- // Convert between BTC / Fiat
- doAsync({
- addressParamsLiveData.accountAsset.value?.assetId?.let { assetId ->
- if (assetId.isPolicyAsset(account.network)) {
-
- // TODO calculate exchange from string input or from satoshi ?
- UserInput.parseUserInputSafe(
- session = session,
- input = amount,
- denomination = addressParamsLiveData.denomination.value
- ).getBalance()?.let {
- "≈ " + it.toAmountLook(
- session = session,
- assetId = assetId,
- denomination = Denomination.exchange(session, addressParamsLiveData.denomination.value),
- withUnit = true,
- withGrouping = true,
- withMinimumDigits = false
- )
- } ?: ""
- } else {
- ""
- }
- }
- }, preAction = null, postAction = null, onSuccess = {
- addressParamsLiveData.exchange.value = it
- }, onError = {
- addressParamsLiveData.exchange.value = ""
- })
- }
- }
-
- fun addRecipient() {
- recipients.value = recipients.value.apply { add(AddressParamsLiveData.create(session, size, accountAsset = accountAsset)) }
- recipients.value.lastOrNull()?.let { setupChangeObserve(it) }
- }
-
- fun removeRecipient(index: Int) {
- recipients.value.let {
- if (it.size > 1) {
- recipients.value = it.apply { it.removeAt(index) }
- checkTransaction()
- }
- }
- }
-
- fun getFeeRate(): Long = if(feeSlider.value?.toInt() == SliderCustomIndex){
- // prevent custom fee lower than relay or default fee
- customFee?.coerceAtLeast(feeEstimation?.firstOrNull() ?: account.network.defaultFee) ?: account.network.defaultFee
- }else{
- feeRate ?: account.network.defaultFee.coerceAtLeast(feeEstimation?.getOrNull(0) ?: 0)
- }
-
- private suspend fun createTransactionParams(): CreateTransactionParams {
- if(account.isLightning){
- return recipients.value.map {
- it.toAddressParams(session = session, isGreedy = false)
- }.let { params ->
- CreateTransactionParams(
- addressees = params.map { it.toJsonElement() },
- addresseesAsParams = params
- )
- }
- }
-
- val unspentOutputs = session.getUnspentOutputs(account, isBump)
-
- return when{
- isBump -> {
- CreateTransactionParams(
- feeRate = getFeeRate(),
- utxos = unspentOutputs.unspentOutputsAsJsonElement,
- previousTransaction = bumpTransaction,
- )
- }
- else -> {
- recipients.value!!.map {
- it.toAddressParams(session = session, isGreedy = it.isSendAll.boolean())
- }.let { params ->
- CreateTransactionParams(
- addressees = params.map { it.toJsonElement() },
- addresseesAsParams = params,
- feeRate = getFeeRate(),
- utxos = unspentOutputs.unspentOutputsAsJsonElement
- )
- }
-
- }
- }
- }
-
- private fun checkTransaction(userInitiated: Boolean = false, finalCheckBeforeContinue: Boolean = false) {
- logger.info { "checkTransaction" }
-
- doAsync({
- // Prevent race condition
- checkTransactionMutex.withLock {
-
- val params = createTransactionParams()
-
- val tx = session.createTransaction(account.network, params)
- var balance: Assets? = null
-
- if(finalCheckBeforeContinue){
- balance = session.getBalance(account)
- }
-
- // Change UI based on the transaction
- recipients.value.let { recipients ->
- for(recipient in recipients){
- val hasLockedAmount = tx.addressees.getOrNull(recipient.index)?.isAmountLocked == true
-
- // If we have BIP21/sweep/bump, update the amounts from GDK side, and disable text input editing
- recipient.assetBip21.postValue(tx.addressees.getOrNull(recipient.index)?.bip21Params?.hasAssetId == true)
- recipient.amountBip21.postValue(tx.addressees.getOrNull(recipient.index)?.bip21Params?.hasAmount == true)
- recipient.domain.postValue(tx.addressees.getOrNull(recipient.index)?.domain ?: "")
-
- tx.addressees.getOrNull(recipient.index)?.metadata.also {
- recipient.description.postValue(it.lnUrlPayDescription() ?: "")
- recipient.image.postValue(it.lnUrlPayBitmap())
- }
- recipient.hasLockedAmount.postValue(hasLockedAmount)
-
- recipient.minAmount.postValue(
- tx.addressees.getOrNull(recipient.index)?.minAmount?.toAmountLook(
- session = session,
- withUnit = false
- )
- )
- recipient.maxAmount.postValue(
- tx.addressees.getOrNull(recipient.index)?.maxAmount?.toAmountLook(
- session = session,
- withUnit = true
- )
- )
-
- tx.addressees.getOrNull(recipient.index)?.let { addressee ->
- addressee.bip21Params?.assetId?.let { assetId ->
- withContext(context = Dispatchers.Main) {
- val assetAccount =
- recipient.accountAsset.value!!.account.let { account ->
-
- // Check if selected account as balance
- if (account.isLiquid && session.accountAssets(account).value.balance(assetId) > 0) {
- account
- } else {
- // Find an account with balance
- session.accountAsset.value.firstOrNull { accountAsset ->
- accountAsset.assetId == assetId && accountAsset.balance(
- session
- ) > 0
- }?.account
- ?: session.accountAsset.value.firstOrNull {
- it.account.isLiquid
- }?.account
- ?: account
- }
- }
-
- AccountAsset.fromAccountAsset(assetAccount, assetId, session).also {
- setAccountAsset(0, it)
- }
- }
- }
-
- if(isBump){
- recipient.address.postValue(addressee.address)
- }
-
- // Get amount from GDK if is a BIP21 or isSendAll or Sweep or Bump or Lightning
- if(addressee.bip21Params?.hasAmount == true || recipient.isSendAll.value == true || isBumpOrSweep || hasLockedAmount){
- val assetId = addressee.assetId ?: account.network.policyAsset
- if(!assetId.isPolicyAsset(account.network) && recipient.denomination.value?.isFiat == true){
- recipient.denomination.postValue(Denomination.default(session))
- }
-
- (tx.satoshi[assetId]?.absoluteValue?.let { sendAmount ->
- // Avoid UI glitches if isSweep and amount is zero (probably error)
- if(sendAmount == 0L){
- ""
- }else{
- sendAmount.toAmountLook(
- session = session,
- assetId = assetId,
- denomination = recipient.denomination.value,
- withUnit = false,
- withGrouping = false
- )
- }
- } ?: tx.addressees.getOrNull(recipient.index)?.bip21Params?.amount?.let { bip21Amount ->
- session.convert(
- assetId = assetId,
- asString = bip21Amount
- )?.toAmountLook(
- session = session,
- assetId = assetId,
- withUnit = false,
- withGrouping = false,
- withMinimumDigits = false,
- denomination = recipient.denomination.value,
- )
- }).also {
- recipient.amount.postValue(it)
- }
- }
- }
- }
- }
-
- // Check if the specified asset in the uri exists in the wallet, we do this check only if it's final
- if(balance != null){
- for (addressee in tx.addressees) {
- addressee.assetId?.let { assetId ->
- if (!balance.containsAsset(assetId)) {
- throw Exception("id_no_asset_in_this_account")
- }
- }
- }
- }
-
- checkedTransaction = tx
-
- feeAmount.postValue(tx.fee?.toAmountLook(session = session, assetId = account.network.policyAsset, denomination = getRecipientStateFlow(0)?.denomination?.value, withUnit = true, withGrouping = true, withMinimumDigits = false) ?: "")
- feeAmountRate.postValue(tx.feeRateWithUnit() ?: "")
- feeAmountFiat.postValue(tx.fee?.toAmountLook(session = session, denomination = Denomination.fiat(session), withUnit = true, withGrouping = true) ?: "")
-
- if(tx.error.isNotBlank()){
- throw Exception(tx.error)
- }
-
- Triple(params, tx, createTransactionSegmentation())
- }
- }, postAction = {
- // Avoid UI glitches
- onProgress.value = finalCheckBeforeContinue
- }, onSuccess = { triple ->
- transactionError.value = null
-
- if(finalCheckBeforeContinue){
- session.pendingTransaction = triple
- postSideEffect(SideEffects.Navigate())
- }
- }, onError = {
- transactionError.value = (it.cause?.message ?: it.message).let { error ->
- if(recipients.value[0].address.value.isNullOrBlank() && (error == "id_invalid_address" || error == "id_invalid_private_key")){
- "" // empty error to avoid ui glitches
- }else if(recipients.value[0].amount.value.isNullOrBlank() && !isSendAll() && (error == "id_invalid_amount" || error == "id_insufficient_funds" || error == "id_amount_below_the_dust_threshold")){
- "" // empty error to avoid ui glitches
- }else{
- error
- }
- }
-
- if(isBumpOrSweep && userInitiated) {
- postSideEffect(SideEffects.ErrorDialog(it))
- }
- })
- }
-
- fun confirmTransaction() {
- checkTransaction(finalCheckBeforeContinue = true)
- }
-
- fun setUri(uri: String) {
- recipients.value.getOrNull(activeRecipient)?.address?.value = uri
- }
-
- fun setAddress(index: Int, address: String, inputType: AddressInputType) {
- recipients.value?.getOrNull(index)?.let {
- it.address.value = address
- it.addressInputType = inputType
-
- if(account.isLightning && address.isBlank()){
- it.amount.value = ""
- }
- }
- }
-
- private fun setAccountAsset(index: Int, accountAsset: AccountAsset) {
- getRecipientStateFlow(index)?.let {
- // Clear amount if is new asset
- if (it.accountAsset.value?.assetId != accountAsset.assetId) {
- it.amount.value = ""
- }
- it.isSendAll.value = false
- // reset isFiat as we don't want to have inconsistencies between btc / assets
- if(it.denomination.value?.isFiat == true){
- it.denomination.value = Denomination.default(session)
- }
- it.accountAsset.value = accountAsset
- }
- }
-
- fun setCustomFeeRate(feeRate : Long?){
- customFee = feeRate ?: account.network.defaultFee
- feeSlider.value = SliderCustomIndex.toFloat()
- checkTransaction()
- }
-
- private fun isSendAll(): Boolean {
- return recipients.value.map {
- it.isSendAll.boolean()
- }.reduceOrNull { acc, b ->
- acc || b
- } ?: false
- }
-
- fun sendAll(index: Int, isSendAll : Boolean) {
- getRecipientStateFlow(index)?.let { addressParams ->
- addressParams.isSendAll.value = isSendAll
- if(!isSendAll){ // clear amount
- addressParams.amount.value = ""
- }
- }
- }
-
- companion object : KLogging() {
- const val SliderCustomIndex = 0
- const val SliderLowIndex = 1
- }
-
- suspend fun getAmountToConvert(): String{
- return getRecipientStateFlow(0)?.let { addressParams ->
- // Get value from the transaction object to get the actual send all amount
- if (checkedTransaction?.isSendAll == true) {
- addressParams.accountAsset.value?.assetId?.let { assetId ->
- checkedTransaction?.let {
- it.satoshi[assetId]?.absoluteValue
- }?.toAmountLook(
- session = session,
- assetId = assetId,
- denomination = addressParams.denomination.value,
- withUnit = false,
-
- withMinimumDigits = false,
- withGrouping = false
- ) ?: ""
- } ?: ""
- } else {
- addressParams.amount.value ?: ""
- }
- } ?: ""
- }
-
- override fun setDenominatedValue(denominatedValue: DenominatedValue) {
- getRecipientStateFlow(0)?.also {
- it.amount.value = denominatedValue.asInput(session) ?: ""
- it.denomination.value = denominatedValue.denomination
- }
- }
-}
-
-data class AddressParamsLiveData constructor(
- val index: Int,
- val address: MutableLiveData,
- var addressInputType: AddressInputType?,
- val accountAsset: MutableStateFlow,
- val amount: MutableLiveData,
- val denomination: MutableLiveData,
- val isSendAll: MutableLiveData = MutableLiveData(false),
- val hasLockedAmount: MutableLiveData = MutableLiveData(false),
- var minAmount: MutableLiveData = MutableLiveData(null),
- var maxAmount: MutableLiveData = MutableLiveData(null),
- val domain: MutableLiveData = MutableLiveData(""),
- val description: MutableLiveData = MutableLiveData(""),
- val image: MutableLiveData = MutableLiveData(null),
- val exchange: MutableLiveData = MutableLiveData(""),
- val assetBip21: MutableLiveData = MutableLiveData(false),
- val amountBip21: MutableLiveData = MutableLiveData(false)
-) {
-
- val network: Network
- get() = accountAsset.value!!.account.network
-
- suspend fun toAddressParams(session: GdkSession, isGreedy: Boolean): AddressParams {
-
- val satoshi = when {
- isGreedy -> 0
- accountAsset.value?.assetId.isPolicyAsset(session) -> {
- UserInput.parseUserInputSafe(session = session, input = amount.value, denomination = denomination.value)
- .getBalance()?.satoshi
- }
- else -> {
- UserInput.parseUserInputSafe(session = session, input = amount.value, assetId = accountAsset.value!!.assetId)
- .getBalance()?.satoshi
- }
- }
-
- return AddressParams(
- address = address.value ?: "",
- isGreedy = isGreedy,
- assetId = if (accountAsset.value?.account?.network?.isLiquid == true) accountAsset.value?.assetId else null,
- satoshi = satoshi ?: 0
- )
- }
-
- companion object : KLogging() {
- fun create(session: GdkSession, index: Int, address: String? = null, addressInputType : AddressInputType? = null, accountAsset: MutableStateFlow) = AddressParamsLiveData(
- index = index,
- address = MutableLiveData(address ?: ""),
- addressInputType = addressInputType,
- accountAsset = accountAsset,
- amount = MutableLiveData(""),
- denomination = MutableLiveData(Denomination.default(session))
- )
- }
-}
-
diff --git a/green/src/main/java/com/blockstream/green/ui/settings/ChangePinFragment.kt b/green/src/main/java/com/blockstream/green/ui/settings/ChangePinFragment.kt
index 120f4f0e7..a7ebb5783 100644
--- a/green/src/main/java/com/blockstream/green/ui/settings/ChangePinFragment.kt
+++ b/green/src/main/java/com/blockstream/green/ui/settings/ChangePinFragment.kt
@@ -2,20 +2,15 @@ package com.blockstream.green.ui.settings
import android.os.Bundle
import android.view.View
-import android.widget.Toast
import androidx.compose.ui.platform.ViewCompositionStrategy
import androidx.navigation.fragment.navArgs
-import com.blockstream.common.models.GreenViewModel
import com.blockstream.common.models.settings.WalletSettingsSection
import com.blockstream.common.models.settings.WalletSettingsViewModel
import com.blockstream.compose.AppFragmentBridge
import com.blockstream.compose.screens.settings.ChangePinScreen
-import com.blockstream.compose.screens.settings.WatchOnlyScreen
import com.blockstream.green.R
-import com.blockstream.green.databinding.ChangePinFragmentBinding
import com.blockstream.green.databinding.ComposeViewBinding
import com.blockstream.green.ui.AppFragment
-import com.blockstream.green.views.GreenPinViewListener
import org.koin.androidx.viewmodel.ext.android.viewModel
import org.koin.core.parameter.parametersOf
diff --git a/green/src/main/res/layout/account_2of3_fragment.xml b/green/src/main/res/layout/account_2of3_fragment.xml
deleted file mode 100644
index f12ff9958..000000000
--- a/green/src/main/res/layout/account_2of3_fragment.xml
+++ /dev/null
@@ -1,38 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
\ No newline at end of file
diff --git a/green/src/main/res/layout/account_overview_fragment.xml b/green/src/main/res/layout/account_overview_fragment.xml
deleted file mode 100644
index e33c0d76d..000000000
--- a/green/src/main/res/layout/account_overview_fragment.xml
+++ /dev/null
@@ -1,61 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
\ No newline at end of file
diff --git a/green/src/main/res/layout/addresses_fragment.xml b/green/src/main/res/layout/addresses_fragment.xml
deleted file mode 100644
index c57e2f579..000000000
--- a/green/src/main/res/layout/addresses_fragment.xml
+++ /dev/null
@@ -1,110 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
\ No newline at end of file
diff --git a/green/src/main/res/layout/app_settings_fragment.xml b/green/src/main/res/layout/app_settings_fragment.xml
deleted file mode 100644
index 9ace71228..000000000
--- a/green/src/main/res/layout/app_settings_fragment.xml
+++ /dev/null
@@ -1,441 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
diff --git a/green/src/main/res/layout/asset_details_bottom_sheet.xml b/green/src/main/res/layout/asset_details_bottom_sheet.xml
deleted file mode 100644
index e80417b6f..000000000
--- a/green/src/main/res/layout/asset_details_bottom_sheet.xml
+++ /dev/null
@@ -1,55 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
\ No newline at end of file
diff --git a/green/src/main/res/layout/bip39_passphrase_bottom_sheet.xml b/green/src/main/res/layout/bip39_passphrase_bottom_sheet.xml
deleted file mode 100644
index 1f0e05c14..000000000
--- a/green/src/main/res/layout/bip39_passphrase_bottom_sheet.xml
+++ /dev/null
@@ -1,156 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
\ No newline at end of file
diff --git a/green/src/main/res/layout/bottom_nav_layout.xml b/green/src/main/res/layout/bottom_nav_layout.xml
deleted file mode 100644
index 835e840a4..000000000
--- a/green/src/main/res/layout/bottom_nav_layout.xml
+++ /dev/null
@@ -1,145 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
\ No newline at end of file
diff --git a/green/src/main/res/layout/camera_bottom_sheet.xml b/green/src/main/res/layout/camera_bottom_sheet.xml
deleted file mode 100644
index 20fc2078f..000000000
--- a/green/src/main/res/layout/camera_bottom_sheet.xml
+++ /dev/null
@@ -1,102 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
\ No newline at end of file
diff --git a/green/src/main/res/layout/change_pin_fragment.xml b/green/src/main/res/layout/change_pin_fragment.xml
deleted file mode 100644
index 3bf4b7fea..000000000
--- a/green/src/main/res/layout/change_pin_fragment.xml
+++ /dev/null
@@ -1,79 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
diff --git a/green/src/main/res/layout/choose_account_type_fragment.xml b/green/src/main/res/layout/choose_account_type_fragment.xml
deleted file mode 100644
index 4182b45bb..000000000
--- a/green/src/main/res/layout/choose_account_type_fragment.xml
+++ /dev/null
@@ -1,133 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
\ No newline at end of file
diff --git a/green/src/main/res/layout/choose_network_fragment.xml b/green/src/main/res/layout/choose_network_fragment.xml
deleted file mode 100644
index 76e94f005..000000000
--- a/green/src/main/res/layout/choose_network_fragment.xml
+++ /dev/null
@@ -1,50 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
diff --git a/green/src/main/res/layout/consent_bottom_sheet.xml b/green/src/main/res/layout/consent_bottom_sheet.xml
deleted file mode 100644
index 86894005f..000000000
--- a/green/src/main/res/layout/consent_bottom_sheet.xml
+++ /dev/null
@@ -1,212 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
\ No newline at end of file
diff --git a/green/src/main/res/layout/list_item_transaction_recipient.xml b/green/src/main/res/layout/list_item_transaction_recipient.xml
deleted file mode 100644
index 227a134de..000000000
--- a/green/src/main/res/layout/list_item_transaction_recipient.xml
+++ /dev/null
@@ -1,517 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
diff --git a/green/src/main/res/layout/send_fragment.xml b/green/src/main/res/layout/send_fragment.xml
deleted file mode 100644
index d9e5afb71..000000000
--- a/green/src/main/res/layout/send_fragment.xml
+++ /dev/null
@@ -1,339 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
\ No newline at end of file
diff --git a/green/src/main/res/navigation/nav_graph.xml b/green/src/main/res/navigation/nav_graph.xml
index c2336714c..927eb2272 100644
--- a/green/src/main/res/navigation/nav_graph.xml
+++ b/green/src/main/res/navigation/nav_graph.xml
@@ -150,8 +150,7 @@
+ android:label="">
@@ -239,8 +238,7 @@
+ android:name="com.blockstream.green.ui.overview.WalletOverviewFragment">
@@ -416,8 +414,7 @@
+ android:name="com.blockstream.green.ui.send.SendFragment">
@@ -565,8 +562,7 @@
+ android:label="@string/id_setup_guide" />
@@ -631,11 +627,6 @@
android:id="@+id/action_global_sendFragment"
app:destination="@id/sendFragment" />
-