From 04c9cf4a4286c4a2ac8ca65191adcceb36a10569 Mon Sep 17 00:00:00 2001 From: Angelos Veglektsis Date: Tue, 2 Jul 2024 15:07:05 +0300 Subject: [PATCH] Increment to version 4.0.31 --- CHANGELOG.md | 14 + base/build.gradle.kts | 12 +- .../com/blockstream/base/InstallReferrer.kt | 10 + common/build.gradle.kts | 22 +- .../composeResources/values-cs/strings.xml | 3 +- .../composeResources/values-de/strings.xml | 3 +- .../composeResources/values-es/strings.xml | 3 +- .../composeResources/values-fr/strings.xml | 3 +- .../composeResources/values-he/strings.xml | 3 +- .../composeResources/values-it/strings.xml | 3 +- .../composeResources/values-ja/strings.xml | 3 +- .../composeResources/values-nl/strings.xml | 3 +- .../values-pt-rBR/strings.xml | 3 +- .../composeResources/values-ro/strings.xml | 3 +- .../composeResources/values-ru/strings.xml | 3 +- .../composeResources/values-uk/strings.xml | 3 +- .../composeResources/values-vi/strings.xml | 3 +- .../composeResources/values-zh/strings.xml | 3 +- .../composeResources/values/strings.xml | 4 +- .../com/blockstream/common/CountlyBase.kt | 11 + .../common/data/ApplicationSettings.kt | 8 + .../com/blockstream/common/fcm/FcmCommon.kt | 33 +- .../com/blockstream/common/gdk/GdkSession.kt | 3 +- .../blockstream/common/gdk/data/Network.kt | 6 +- .../common/gdk/params/ConnectionParams.kt | 1 + .../common/lightning/Extensions.kt | 15 +- .../common/lightning/GreenlightKeys.kt | 4 +- .../common/lightning/LightningBridge.kt | 2 +- .../common/lightning/LightningManager.kt | 16 +- .../common/managers/SessionManager.kt | 2 +- .../common/models/GreenViewModel.kt | 2 +- .../overview/WalletOverviewViewModel.kt | 14 +- .../common/models/receive/ReceiveViewModel.kt | 6 +- .../common/models/send/SendViewModel.kt | 4 + .../models/settings/AppSettingsViewModel.kt | 9 + .../common/sideeffects/SideEffects.kt | 2 +- .../common/utils/StringResources.kt | 1 + compose/build.gradle.kts | 15 +- .../managers/PlatformManager.android.kt | 23 +- .../compose/components/GreenAccountCard.kt | 28 +- .../compose/components/GreenAddress.kt | 19 +- .../compose/components/GreenTransaction.kt | 6 +- .../compose/managers/PlatformManager.kt | 4 +- .../screens/overview/AccountOverviewScreen.kt | 10 +- .../screens/overview/WalletOverviewScreen.kt | 49 +- .../compose/screens/receive/ReceiveScreen.kt | 24 +- .../compose/screens/send/SendScreen.kt | 23 +- .../screens/settings/AppSettingsScreen.kt | 14 + .../compose/sheets/NoteBottomSheet.kt | 12 +- .../blockstream/compose/utils/Containers.kt | 6 +- .../blockstream/compose/utils/SideEffects.kt | 12 +- .../com/blockstream/compose/di/KoinDesktop.kt | 4 + .../managers/PlatformManager.desktop.kt | 7 +- .../com/blockstream/compose/di/KoiniOS.kt | 4 + .../compose/managers/PlatformManager.ios.kt | 7 +- crypto/build.gradle.kts | 58 -- gdk/build.gradle.kts | 6 +- gms/build.gradle.kts | 8 +- .../blockstream/gms/InstallReferrerImpl.kt | 96 +++ .../java/com/blockstream/gms/di/GmsModule.kt | 6 + .../gms/services/FirebaseMessagingService.kt | 6 + gradle/libs.versions.toml | 13 +- gradle/verification-metadata.xml | 1 - green/build.gradle.kts | 12 +- green/src/main/AndroidManifest.xml | 1 + .../com/blockstream/green/data/Countly.kt | 80 +- .../com/blockstream/green/di/KoinAndroid.kt | 2 +- .../blockstream/green/managers/FcmAndroid.kt | 8 + .../green/managers/NotificationManager.kt | 25 + .../com/blockstream/green/ui/AppFragment.kt | 2 +- .../SelectUtxosBottomSheetDialogFragment.kt | 4 +- .../green/ui/receive/ReceiveFragment.kt | 18 +- .../green/ui/send/SendViewModel.kt | 702 ------------------ .../green/ui/settings/ChangePinFragment.kt | 5 - .../main/res/layout/account_2of3_fragment.xml | 38 - .../res/layout/account_overview_fragment.xml | 61 -- .../main/res/layout/addresses_fragment.xml | 110 --- .../main/res/layout/app_settings_fragment.xml | 441 ----------- .../res/layout/asset_details_bottom_sheet.xml | 55 -- .../layout/bip39_passphrase_bottom_sheet.xml | 156 ---- .../src/main/res/layout/bottom_nav_layout.xml | 145 ---- .../main/res/layout/camera_bottom_sheet.xml | 102 --- .../main/res/layout/change_pin_fragment.xml | 79 -- .../layout/choose_account_type_fragment.xml | 133 ---- .../res/layout/choose_network_fragment.xml | 50 -- .../main/res/layout/consent_bottom_sheet.xml | 212 ------ .../list_item_transaction_recipient.xml | 517 ------------- green/src/main/res/layout/send_fragment.xml | 339 --------- green/src/main/res/navigation/nav_graph.xml | 17 +- hardware/build.gradle.kts | 6 +- jade/build.gradle.kts | 6 +- no-gms/build.gradle.kts | 6 +- .../java/com/blockstream/gms/di/GmsModule.kt | 4 + 93 files changed, 561 insertions(+), 3479 deletions(-) create mode 100644 base/src/main/java/com/blockstream/base/InstallReferrer.kt delete mode 100644 crypto/build.gradle.kts create mode 100644 gms/src/main/java/com/blockstream/gms/InstallReferrerImpl.kt delete mode 100644 green/src/main/java/com/blockstream/green/ui/send/SendViewModel.kt delete mode 100644 green/src/main/res/layout/account_2of3_fragment.xml delete mode 100644 green/src/main/res/layout/account_overview_fragment.xml delete mode 100644 green/src/main/res/layout/addresses_fragment.xml delete mode 100644 green/src/main/res/layout/app_settings_fragment.xml delete mode 100644 green/src/main/res/layout/asset_details_bottom_sheet.xml delete mode 100644 green/src/main/res/layout/bip39_passphrase_bottom_sheet.xml delete mode 100644 green/src/main/res/layout/bottom_nav_layout.xml delete mode 100644 green/src/main/res/layout/camera_bottom_sheet.xml delete mode 100644 green/src/main/res/layout/change_pin_fragment.xml delete mode 100644 green/src/main/res/layout/choose_account_type_fragment.xml delete mode 100644 green/src/main/res/layout/choose_network_fragment.xml delete mode 100644 green/src/main/res/layout/consent_bottom_sheet.xml delete mode 100644 green/src/main/res/layout/list_item_transaction_recipient.xml delete mode 100644 green/src/main/res/layout/send_fragment.xml 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 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -