diff --git a/.github/workflows/notify.yml b/.github/workflows/notify.yml index e78bfba1..2b1ba765 100644 --- a/.github/workflows/notify.yml +++ b/.github/workflows/notify.yml @@ -1,9 +1,9 @@ -name: Notify Mirror on PR +name: Notify 🪞 on PR on: - pull_request: - types: [opened, synchronize, reopened] - + workflow_dispatch: + #pull_request: + # types: [opened, synchronize, reopened] jobs: trigger-mirror: runs-on: ubuntu-latest @@ -17,11 +17,11 @@ jobs: owner: 'gruntsoftware', repo: 'android', workflow_id: 'sync.yml', - ref: 'main', + ref: 'develop', inputs: { source: '${{ github.event.repository.name }}', pr_number: '${{ github.event.pull_request.number }}', pr_title: '${{ github.event.pull_request.title }}', branch: '${{ github.head_ref }}' } - }) \ No newline at end of file + }) diff --git a/.github/workflows/pr-summary-copilot.yml b/.github/workflows/pr-summary-copilot.yml index f631db1a..cd3c3beb 100644 --- a/.github/workflows/pr-summary-copilot.yml +++ b/.github/workflows/pr-summary-copilot.yml @@ -6,6 +6,7 @@ on: branches: - develop - main + workflow_dispatch: permissions: contents: read diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 8d589b90..882f1184 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -35,7 +35,7 @@ android { applicationId = "ltd.grunt.brainwallet" minSdk = 29 targetSdk = 35 - versionCode = 202506326 + versionCode = 202506327 versionName = "v4.9.1" multiDexEnabled = true base.archivesName.set("${defaultConfig.versionName}(${defaultConfig.versionCode})") diff --git a/app/src/main/java/com/brainwallet/ui/composable/passcode/PasscodeKeypad.kt b/app/src/main/java/com/brainwallet/ui/composable/passcode/PasscodeKeypad.kt index e32330cf..6017f928 100644 --- a/app/src/main/java/com/brainwallet/ui/composable/passcode/PasscodeKeypad.kt +++ b/app/src/main/java/com/brainwallet/ui/composable/passcode/PasscodeKeypad.kt @@ -12,11 +12,13 @@ import androidx.compose.material.icons.automirrored.filled.ArrowBack import androidx.compose.material.icons.filled.Face import androidx.compose.material3.Icon import androidx.compose.material3.IconButtonDefaults +import androidx.compose.material3.LocalContentColor import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.takeOrElse import androidx.compose.ui.platform.testTag import androidx.compose.ui.unit.dp @@ -90,16 +92,13 @@ fun PasscodeKeypad( _root_ide_package_.com.brainwallet.ui.composable.CircleButton( modifier = modifierCircleButton, - onClick = { - onEvent.invoke(PasscodeKeypadEvent.OnDelete) - }, - colors = IconButtonDefaults.filledIconButtonColors( - containerColor = Color.Transparent - ), + onClick = { onEvent.invoke(PasscodeKeypadEvent.OnDelete) }, ) { Icon( Icons.AutoMirrored.Filled.ArrowBack, contentDescription = "Delete", + tint = MaterialTheme.typography.headlineMedium.color + .takeOrElse { LocalContentColor.current } ) } } diff --git a/app/src/main/java/com/brainwallet/ui/screens/send/SendViewModel.kt b/app/src/main/java/com/brainwallet/ui/screens/send/SendViewModel.kt index de7c2209..a3dc3275 100644 --- a/app/src/main/java/com/brainwallet/ui/screens/send/SendViewModel.kt +++ b/app/src/main/java/com/brainwallet/ui/screens/send/SendViewModel.kt @@ -15,9 +15,11 @@ import com.brainwallet.tools.manager.BRSharedPrefs import com.brainwallet.tools.manager.FeeManager import com.brainwallet.tools.security.BRKeyStore import com.brainwallet.tools.util.BRExchange +import com.brainwallet.tools.util.BRExchange.ONE_LITECOIN_OF_LITOSHIS import com.brainwallet.tools.util.Utils import com.brainwallet.ui.BrainwalletViewModel import com.brainwallet.ui.screens.send.BWSendResult.Error +import com.brainwallet.util.CurrencyDataGetter import com.brainwallet.util.EventBus import com.brainwallet.wallet.BRWalletManager import kotlinx.coroutines.CoroutineDispatcher @@ -43,6 +45,7 @@ class SendViewModel( private val bwSender: BWSender, private val txRepository: TxRepository, private val settingRepository: SettingRepository, + private val currencyDataGetter: CurrencyDataGetter, private val isWalletCreated: () -> Boolean = { BRWalletManager.getInstance().isCreated() }, private val validateAddress: (String) -> Boolean = { BRWalletManager.getInstance().validateAddress(it) }, private val getBalance: () -> Long = { BRWalletManager.getInstance().getBalance(app) }, @@ -138,13 +141,19 @@ class SendViewModel( } is SendEvent.OnToggleFiatOrLTC -> { val currentState = _state.value - val rate = _state.value.selectedCurrency.rate + val rate = currencyDataGetter + .getCurrencyByIso(currentState.selectedCurrency.code)?.rate + ?.takeIf { it > 0f } val convertedAmount = if (rate != null && currentState.amountString.isNotBlank()) { val current = currentState.amountString.toBigDecimalOrNull() ?: BigDecimal.ZERO if (currentState.userViewsFiat) { // switching fiat → LTC: divide by rate - current.divide(BigDecimal(rate.toString()), 8, BWConstants.ROUNDING_MODE) + current.divide( + BigDecimal(rate.toString()), + 8, + BWConstants.ROUNDING_MODE + ) } else { // switching LTC → fiat: multiply by rate current.multiply(BigDecimal(rate.toString())) @@ -153,19 +162,24 @@ class SendViewModel( } else { null } + + val newAmountInLitoshi = when { + currentState.userViewsFiat && convertedAmount != null -> { + // was fiat, now LTC — convertedAmount is LTC + convertedAmount + .multiply(BigDecimal(ONE_LITECOIN_OF_LITOSHIS)) + } + !currentState.userViewsFiat -> { + // was LTC, now fiat — litoshi amount is unchanged + currentState.amountInLitoshi + } + else -> BigDecimal.ZERO + } _state.update { it.copy( userViewsFiat = !it.userViewsFiat, amountString = convertedAmount?.toPlainString() ?: it.amountString, - amountInLitoshi = if (currentState.userViewsFiat) { - BigDecimal(rate.toDouble()).divide( - convertedAmount, - 8, - BWConstants.ROUNDING_MODE - ) - } else { - convertedAmount ?: BigDecimal.ZERO - } + amountInLitoshi = newAmountInLitoshi ) } } diff --git a/app/src/main/res/values-ar/strings.xml b/app/src/main/res/values-ar/strings.xml index a9072767..f55a3768 100644 --- a/app/src/main/res/values-ar/strings.xml +++ b/app/src/main/res/values-ar/strings.xml @@ -304,4 +304,6 @@ تحقق قبل الإرسال! امسح أو الصق عنوان الوجهة. تحقق مرتين! غيّر قيمتك بالعملة المحلية لمعرفة المبلغ الذي يمكنك إرساله. أضف ملاحظة كتذكير مفيد. اضغط متابعة. السماح بالوصول إلى الكاميرا من الإعدادات - + المتجر + اشترِ بطاقات هدايا باستخدام LTC! + \ No newline at end of file diff --git a/app/src/main/res/values-de/strings.xml b/app/src/main/res/values-de/strings.xml index 930815f2..c4759610 100644 --- a/app/src/main/res/values-de/strings.xml +++ b/app/src/main/res/values-de/strings.xml @@ -434,4 +434,6 @@ Überprüfe vor dem Senden! Scanne oder füge die Zieladresse ein. Überprüfe sie genau! Ändere deine lokale Währung, um herauszufinden, wie viel du senden kannst. Füge eine Notiz als praktische Erinnerung hinzu. Tippe auf Weiter. Kamerazugriff in den Einstellungen aktivieren - + Shop + Kaufe Geschenkgutscheine mit LTC! + \ No newline at end of file diff --git a/app/src/main/res/values-es/strings.xml b/app/src/main/res/values-es/strings.xml index 9daa34b9..b83dbf63 100644 --- a/app/src/main/res/values-es/strings.xml +++ b/app/src/main/res/values-es/strings.xml @@ -509,4 +509,6 @@ ¡Verifica antes de enviar! Escanea o pega la dirección de destino. ¡Verifica dos veces! Cambia tu moneda fiduciaria local para determinar cuánto puedes enviar. Añade una nota como recordatorio útil. Toca Continuar. Permitir acceso a la cámara en Configuración - + Tienda + ¡Compra Tarjetas de Regalo con LTC! + \ No newline at end of file diff --git a/app/src/main/res/values-fa/strings.xml b/app/src/main/res/values-fa/strings.xml index 58af62d4..7ca6cee3 100644 --- a/app/src/main/res/values-fa/strings.xml +++ b/app/src/main/res/values-fa/strings.xml @@ -645,4 +645,6 @@ قبل از ارسال تأیید کنید! آدرس مقصد را اسکن یا جایگذاری کنید. دوبار بررسی کنید! ارز محلی خود را تغییر دهید تا بفهمید چقدر می‌توانید ارسال کنید. یادداشتی به عنوان یادآوری اضافه کنید. بر روی ادامه ضربه بزنید. دسترسی دوربین را در تنظیمات فعال کنید - + فروشگاه + خرید کارت هدیه با LTC! + \ No newline at end of file diff --git a/app/src/main/res/values-fr/strings.xml b/app/src/main/res/values-fr/strings.xml index 911a9499..e05eb164 100644 --- a/app/src/main/res/values-fr/strings.xml +++ b/app/src/main/res/values-fr/strings.xml @@ -660,4 +660,6 @@ Vérifiez avant d\'envoyer! Scannez ou collez l\'adresse de destination. Vérifiez bien! Convertissez en devise locale pour déterminer le montant à envoyer. Ajoutez un mémo comme aide-mémoire. Appuyez sur Continuer. Autoriser l\'accès à la caméra dans les Paramètres - + Boutique + Achetez des Cartes Cadeaux avec LTC ! + \ No newline at end of file diff --git a/app/src/main/res/values-hi/strings.xml b/app/src/main/res/values-hi/strings.xml index e0f839d5..b75967b5 100644 --- a/app/src/main/res/values-hi/strings.xml +++ b/app/src/main/res/values-hi/strings.xml @@ -304,4 +304,6 @@ भेजने से पहले सत्यापित करें! गंतव्य पता स्कैन या पेस्ट करें। दोबारा जांचें! आप कितना भेज सकते हैं यह जानने के लिए अपनी स्थानीय फिएट को बदलें। एक सहायक स्मरणीय के रूप में एक मेमो जोड़ें। जारी रखें पर टैप करें। सेटिंग्स में कैमरा एक्सेस की अनुमति दें - + दुकान + LTC के साथ गिफ्ट कार्ड खरीदें! + \ No newline at end of file diff --git a/app/src/main/res/values-in/strings.xml b/app/src/main/res/values-in/strings.xml index 0cd1aece..8e91ddc1 100644 --- a/app/src/main/res/values-in/strings.xml +++ b/app/src/main/res/values-in/strings.xml @@ -660,4 +660,6 @@ Verifikasi sebelum mengirim! Pindai atau tempel alamat tujuan. Periksa dua kali! Ubah fiat lokal Anda untuk mengetahui berapa banyak yang dapat Anda kirim. Tambahkan memo sebagai pengingat praktis. Ketuk Lanjutkan. Izinkan akses kamera di Pengaturan - + Toko + Beli Kartu Hadiah dengan LTC! + \ No newline at end of file diff --git a/app/src/main/res/values-it/strings.xml b/app/src/main/res/values-it/strings.xml index ccf1ad45..4b39065e 100644 --- a/app/src/main/res/values-it/strings.xml +++ b/app/src/main/res/values-it/strings.xml @@ -660,4 +660,6 @@ Verifica prima di inviare! Scansiona o incolla l\'indirizzo di destinazione. Verifica! Cambia la tua valuta fiat locale per capire quanto puoi inviare. Aggiungi una nota come promemoria utile. Tocca Continua. Consenti l\'accesso alla fotocamera in Impostazioni - + Negozio + Acquista Carte Regalo con LTC! + \ No newline at end of file diff --git a/app/src/main/res/values-ja/strings.xml b/app/src/main/res/values-ja/strings.xml index da1398a3..ec3d2212 100644 --- a/app/src/main/res/values-ja/strings.xml +++ b/app/src/main/res/values-ja/strings.xml @@ -660,4 +660,6 @@ 送信前に確認! 宛先アドレスをスキャンまたは貼り付けます。よく確認してください!現地通貨で計算して送信可能な金額を確認します。メモを追加して便利に管理できます。続けるをタップします。 設定からカメラへのアクセスを許可してください - + ショップ + LTCでギフトカードを購入! + \ No newline at end of file diff --git a/app/src/main/res/values-ko/strings.xml b/app/src/main/res/values-ko/strings.xml index 0ca33d4c..98d1ddbb 100644 --- a/app/src/main/res/values-ko/strings.xml +++ b/app/src/main/res/values-ko/strings.xml @@ -660,4 +660,6 @@ 보내기 전에 확인하세요! 수신자 주소를 스캔하거나 붙여넣기하세요. 확인하세요! 현지 통화로 변경하여 보낼 수 있는 금액을 계산하세요. 편리한 참고를 위해 메모를 추가하세요. 계속을 탭하세요. 설정에서 카메라 접근을 허용해주세요 - + 상점 + LTC로 기프트 카드를 구매하세요! + \ No newline at end of file diff --git a/app/src/main/res/values-nl/strings.xml b/app/src/main/res/values-nl/strings.xml index a97d6571..23850a43 100644 --- a/app/src/main/res/values-nl/strings.xml +++ b/app/src/main/res/values-nl/strings.xml @@ -304,4 +304,6 @@ Verifieer voordat u verzendt! Scan of plak het doeladdres. Dubbel controleren! Wijzig uw lokale fiat om uit te rekenen hoeveel u kunt verzenden. Voeg een memo toe als handige herinnering. Tik op Doorgaan. Sta cameratoegang toe in Instellingen - + Winkel + Koop cadeaukaarten met LTC! + \ No newline at end of file diff --git a/app/src/main/res/values-pa/strings.xml b/app/src/main/res/values-pa/strings.xml index 8860f8b3..72fce716 100644 --- a/app/src/main/res/values-pa/strings.xml +++ b/app/src/main/res/values-pa/strings.xml @@ -670,4 +670,6 @@ ਭੇਜਣ ਤੋਂ ਪਹਿਲਾਂ ਤਸਦੀਕ ਕਰੋ! ਮੰਜ਼ਿਲ ਦਾ ਪਤਾ ਸਕੈਨ ਕਰੋ ਜਾਂ ਚਿਪਕਾਓ। ਦੋ ਵਾਰ ਚੈਕ ਕਰੋ! ਇਹ ਪਤਾ ਲਗਾਉਣ ਲਈ ਆਪਣੀ ਸਥਾਨਕ ਫਿਆਟ ਨੂੰ ਬਦਲੋ ਕਿ ਤੁਸੀਂ ਕਿੰਨਾ ਭੇਜ ਸਕਦੇ ਹੋ। ਆਸਾਨ ਯਾਦ ਦਿਖਾਉਣ ਲਈ ਇੱਕ ਯਾਦ ਸ਼ਾਮਲ ਕਰੋ। ਜਾਰੀ ਰੱਖੋ \'ਤੇ ਟੈਪ ਕਰੋ। ਸੈਟਿੰਗਜ਼ ਵਿੱਚ ਕੈਮਰੇ ਦੀ ਅਨੁਮਤੀ ਦਿਓ - + ਦੁਕਾਨ + LTC ਨਾਲ ਗਿਫ਼ਟ ਕਾਰਡ ਖਰੀਦੋ! + \ No newline at end of file diff --git a/app/src/main/res/values-pl/strings.xml b/app/src/main/res/values-pl/strings.xml index b034e957..ea238811 100644 --- a/app/src/main/res/values-pl/strings.xml +++ b/app/src/main/res/values-pl/strings.xml @@ -665,4 +665,6 @@ Sprawdź przed wysłaniem! Zeskanuj lub wklej adres docelowy. Sprawdź dokładnie! Zmień swoją lokalną walutę fiat, aby sprawdzić, ile możesz wysłać. Dodaj notatkę jako przydatne przypomnienie. Dotknij Kontynuuj. Zezwól na dostęp do aparatu w Ustawieniach - + Sklep + Kup karty podarunkowe za LTC! + \ No newline at end of file diff --git a/app/src/main/res/values-pt-rBR/strings.xml b/app/src/main/res/values-pt-rBR/strings.xml index 07c5db4b..f1e88df2 100644 --- a/app/src/main/res/values-pt-rBR/strings.xml +++ b/app/src/main/res/values-pt-rBR/strings.xml @@ -304,4 +304,6 @@ Verifique antes de enviar! Escaneie ou cole o endereço de destino. Verifique bem! Mude sua moeda fiduciária local para calcular quanto pode enviar. Adicione um memorando como lembrete útil. Toque em Continuar. Permita o acesso à câmera nas Configurações - + Loja + Compre Cartões Presentes com LTC! + \ No newline at end of file diff --git a/app/src/main/res/values-ru/strings.xml b/app/src/main/res/values-ru/strings.xml index 08c2779e..c45af3d4 100644 --- a/app/src/main/res/values-ru/strings.xml +++ b/app/src/main/res/values-ru/strings.xml @@ -660,4 +660,6 @@ Проверьте перед отправкой! Отсканируйте или вставьте адрес получателя. Проверьте дважды! Конвертируйте между фиатом и LTC, чтобы узнать сумму отправки. Добавьте заметку в качестве напоминания. Нажмите Продолжить. Разрешите доступ к камере в Настройках - + Магазин + Покупайте подарочные карты за LTC! + \ No newline at end of file diff --git a/app/src/main/res/values-sv/strings.xml b/app/src/main/res/values-sv/strings.xml index efbb0b93..a655a7c6 100644 --- a/app/src/main/res/values-sv/strings.xml +++ b/app/src/main/res/values-sv/strings.xml @@ -304,4 +304,6 @@ Verifiera innan du skickar! Skanna eller klistra in destinationsadressen. Dubbelkolla! Ändra din lokala fiat-valuta för att räkna ut hur mycket du kan skicka. Lägg till en anteckning som en praktisk påminnelse. Tryck Fortsätt. Tillåt kameraåtkomst i Inställningar - + Butik + Köp presentkort med LTC! + \ No newline at end of file diff --git a/app/src/main/res/values-th/strings.xml b/app/src/main/res/values-th/strings.xml index 858b18f3..0c90e5ce 100644 --- a/app/src/main/res/values-th/strings.xml +++ b/app/src/main/res/values-th/strings.xml @@ -304,4 +304,6 @@ ตรวจสอบก่อนส่ง! สแกนหรือวาง address ปลายทาง ตรวจสอบอีกครั้ง! เปลี่ยนตัวเลขเงินฟิแอตท้องถิ่นของคุณเพื่อคำนวณจำนวนที่คุณสามารถส่งได้ เพิ่มบันทึกเป็นการเตือนที่มีประโยชน์ แตะ ดำเนินการต่อ อนุญาตการเข้าถึงกล้องในการตั้งค่า - + ร้านค้า + ซื้อบัตรของขวัญด้วย LTC! + \ No newline at end of file diff --git a/app/src/main/res/values-tr/strings.xml b/app/src/main/res/values-tr/strings.xml index 866f998c..02bc40cf 100644 --- a/app/src/main/res/values-tr/strings.xml +++ b/app/src/main/res/values-tr/strings.xml @@ -679,4 +679,6 @@ Göndermeden Önce Doğrulayın! Hedef adresini tarayın veya yapıştırın. İki kez kontrol edin! Ne kadar gönderebileceğinizi öğrenmek için yerel para biriminizi değiştirin. Hatırlatıcı olması için not ekleyin. Devam Et\'e dokunun. Ayarlar\'da kamera erişimine izin verin - + Mağaza + LTC ile Hediye Kartı Satın Al! + \ No newline at end of file diff --git a/app/src/main/res/values-uk/strings.xml b/app/src/main/res/values-uk/strings.xml index d2ef237a..81d90e99 100644 --- a/app/src/main/res/values-uk/strings.xml +++ b/app/src/main/res/values-uk/strings.xml @@ -662,4 +662,6 @@ Перевірте перед відправленням! Відсканюйте або вставте адресу призначення. Перевірте двічі! Змініть вашу місцеву валюту на фіатну, щоб дізнатися, скільки можна відправити. Додайте замітку як корисне нагадування. Натисніть «Продовжити». Дозвольте доступ до камери в Налаштуваннях - + Магазин + Купуйте подарункові карти за LTC! + \ No newline at end of file diff --git a/app/src/main/res/values-zh-rCN/strings.xml b/app/src/main/res/values-zh-rCN/strings.xml index 2d4b4cf8..90a78ab7 100644 --- a/app/src/main/res/values-zh-rCN/strings.xml +++ b/app/src/main/res/values-zh-rCN/strings.xml @@ -660,4 +660,6 @@ 发送前验证! 扫描或粘贴目标地址。仔细检查!更改您的本地法币以计算可以发送的金额。添加备忘录作为便捷提醒。点击继续。 在设置中允许摄像头访问 - + 商店 + 用 LTC 购买礼品卡! + \ No newline at end of file diff --git a/app/src/main/res/values-zh-rTW/strings.xml b/app/src/main/res/values-zh-rTW/strings.xml index 1a746ae4..08c5214d 100644 --- a/app/src/main/res/values-zh-rTW/strings.xml +++ b/app/src/main/res/values-zh-rTW/strings.xml @@ -660,4 +660,6 @@ 發送前驗證! 掃描或貼上目標地址。仔細核實!將本地法幣轉換為LTC以確定可發送金額。新增備忘錄作為方便的提醒。點擊繼續。 在設定中允許相機存取 - + 商店 + 用 LTC 購買禮品卡! + \ No newline at end of file diff --git a/app/src/test/java/com/brainwallet/ui/screens/send/SendViewModelTest.kt b/app/src/test/java/com/brainwallet/ui/screens/send/SendViewModelTest.kt index 25e0eda1..eb16f87a 100644 --- a/app/src/test/java/com/brainwallet/ui/screens/send/SendViewModelTest.kt +++ b/app/src/test/java/com/brainwallet/ui/screens/send/SendViewModelTest.kt @@ -9,6 +9,7 @@ import com.brainwallet.presenter.entities.TransactionItem import com.brainwallet.tools.manager.AnalyticsManager import com.brainwallet.tools.security.BRKeyStore import com.brainwallet.tools.util.Utils +import com.brainwallet.util.CurrencyDataGetter import com.brainwallet.util.EventBus import io.mockk.coEvery import io.mockk.coVerify @@ -33,15 +34,6 @@ import org.junit.Before import org.junit.Test import java.math.BigDecimal -/** - * Regression tests for the Send flow. These cover the state transitions that, - * if broken, would leave the Send button disabled or cause a crash before a - * transaction could be submitted — matching the production incident. - * - * JNI-backed methods (BRWalletManager.validateAddress, getBalance, FeeManager, - * Utils.tieredOpsFee) are injected as lambdas so these tests never trigger the - * native linker. - */ @OptIn(ExperimentalCoroutinesApi::class) class SendViewModelTest { @@ -51,21 +43,16 @@ class SendViewModelTest { private lateinit var bwSender: BWSender private lateinit var txRepository: TxRepository private lateinit var settingRepository: SettingRepository + private lateinit var currencyDataGetter: CurrencyDataGetter + + private val usdCurrency = CurrencyEntity("USD", "US Dollar", 100f, "$") private val settingsFlow = MutableStateFlow( - AppSetting( - isDarkMode = false, - currency = CurrencyEntity("USD", "US Dollar", 100f, "$") - ) + AppSetting(isDarkMode = false, currency = usdCurrency) ) - /** - * Build a ViewModel with sensible test defaults. Individual tests override - * whatever they need to exercise specific branches. - */ - private fun TestScope.buildViewModel( - getBalance: () -> Long = { 1_000_000_000L }, // 10 LTC in litoshis + getBalance: () -> Long = { 1_000_000_000L }, validateAddress: (String) -> Boolean = { it.startsWith("L") }, getCurrentFee: () -> Long = { 2_000L }, getOpsFee: (Long) -> Long = { 500L }, @@ -80,6 +67,7 @@ class SendViewModelTest { getBalance = getBalance, getCurrentFee = getCurrentFee, getOpsFee = getOpsFee, + currencyDataGetter = currencyDataGetter ).also { advanceUntilIdle() } @Before @@ -96,10 +84,14 @@ class SendViewModelTest { every { currentSettings } returns settingsFlow } + // Default: getCurrencyByIso returns USD at 100f + currencyDataGetter = mockk { + every { getCurrencyByIso("USD") } returns usdCurrency + } + mockkStatic(BRKeyStore::class) every { BRKeyStore.getPinCode(any()) } returns "1234" - // Stub the asset/Firebase boundary that OnSend hits. mockkStatic(Utils::class) every { Utils.fetchServiceItem(any(), any()) } returns "LOpsAddr" every { Utils.tieredOpsFee(any(), any()) } returns 500L @@ -133,6 +125,7 @@ class SendViewModelTest { fun `settings update propagates currency and darkMode`() = runTest { val vm = buildViewModel() val newCurrency = CurrencyEntity("GBP", "Pound", 80f, "£") + every { currencyDataGetter.getCurrencyByIso("GBP") } returns newCurrency settingsFlow.value = AppSetting(isDarkMode = true, currency = newCurrency) advanceUntilIdle() @@ -142,35 +135,45 @@ class SendViewModelTest { } // ------------------------------------------------------------------------- - // REGRESSION: OnToggleFiatOrLTC null / negative rate handling + // OnToggleFiatOrLTC — uses currencyDataGetter (not selectedCurrency.rate) // ------------------------------------------------------------------------- @Test - fun `toggle does not crash when rate is negative sentinel`() = runTest { - // The default CurrencyEntity in SendState has rate = -1f. If the user - // toggles before rates load, the VM must not crash. - settingsFlow.value = AppSetting( - currency = CurrencyEntity("USD", "US Dollar", -1f, "$") - ) - val vm = buildViewModel() + fun `toggle does not crash when currencyDataGetter returns no rate`() = runTest { + // Simulates the case where rates haven't loaded yet — getCurrencyByIso + // returns null, so the takeIf guard fires and convertedAmount stays null. + every { currencyDataGetter.getCurrencyByIso(any()) } returns null + val vm = buildViewModel() vm.onEvent(SendEvent.OnAmountChanged("0.5")) advanceUntilIdle() vm.onEvent(SendEvent.OnToggleFiatOrLTC) advanceUntilIdle() - // Expectation: no crash. The amount string should either stay put or - // clear — but the VM must remain functional. + // amountString must be unchanged; VM must remain functional + assertEquals("0.5", vm.state.value.amountString) assertFalse(vm.state.value.brainwalletIsPublishing) } + @Test + fun `toggle does not crash when rate is zero`() = runTest { + // rate = 0f fails the takeIf { it > 0f } guard — same safe path as null + every { currencyDataGetter.getCurrencyByIso("USD") } returns + CurrencyEntity("USD", "US Dollar", 0f, "$") + + val vm = buildViewModel() + vm.onEvent(SendEvent.OnAmountChanged("0.5")) + advanceUntilIdle() + vm.onEvent(SendEvent.OnToggleFiatOrLTC) + advanceUntilIdle() + + assertEquals("0.5", vm.state.value.amountString) + } + @Test fun `toggle LTC to fiat multiplies by rate with 2dp rounding`() = runTest { - settingsFlow.value = AppSetting( - currency = CurrencyEntity("USD", "US Dollar", 100f, "$") - ) val vm = buildViewModel() - // Start in LTC view, enter 0.5 LTC. + // Start in LTC view, enter 0.5 LTC vm.onEvent(SendEvent.OnAmountChanged("0.5")) advanceUntilIdle() @@ -184,11 +187,9 @@ class SendViewModelTest { @Test fun `toggle fiat to LTC divides by rate with 8dp precision`() = runTest { - settingsFlow.value = AppSetting( - currency = CurrencyEntity("USD", "US Dollar", 100f, "$") - ) val vm = buildViewModel() vm.onEvent(SendEvent.OnToggleFiatOrLTC) // → fiat view + advanceUntilIdle() vm.onEvent(SendEvent.OnAmountChanged("50")) advanceUntilIdle() @@ -200,14 +201,44 @@ class SendViewModelTest { assertEquals(BigDecimal("0.50000000"), BigDecimal(vm.state.value.amountString)) } + @Test + fun `toggle LTC to fiat preserves amountInLitoshi`() = runTest { + val vm = buildViewModel() + vm.onEvent(SendEvent.OnAmountChanged("1")) // 1 LTC = 100_000_000 litoshis + advanceUntilIdle() + val litoshiBefore = vm.state.value.amountInLitoshi + + vm.onEvent(SendEvent.OnToggleFiatOrLTC) // → fiat, litoshi unchanged + advanceUntilIdle() + + assertEquals(litoshiBefore, vm.state.value.amountInLitoshi) + } + + @Test + fun `toggle fiat to LTC updates amountInLitoshi`() = runTest { + val vm = buildViewModel() + vm.onEvent(SendEvent.OnToggleFiatOrLTC) // → fiat + advanceUntilIdle() + vm.onEvent(SendEvent.OnAmountChanged("100")) // 100 USD / 100 rate = 1 LTC + advanceUntilIdle() + + vm.onEvent(SendEvent.OnToggleFiatOrLTC) // → LTC + advanceUntilIdle() + + // 1 LTC = 100_000_000 litoshis + assertEquals( + 0, + BigDecimal("100000000").compareTo(vm.state.value.amountInLitoshi) + ) + } + // ------------------------------------------------------------------------- - // REGRESSION: Amount validation drives isReadyToSend + // OnAmountChanged — uses selectedCurrency.rate (Float, always present) // ------------------------------------------------------------------------- @Test fun `valid address alone does not enable send`() = runTest { val vm = buildViewModel() - vm.onEvent(SendEvent.OnRecipientAddressChanged("LValidAddr")) advanceUntilIdle() @@ -221,23 +252,19 @@ class SendViewModelTest { @Test fun `valid address plus valid amount enables send`() = runTest { val vm = buildViewModel() - vm.onEvent(SendEvent.OnRecipientAddressChanged("LValidAddr")) vm.onEvent(SendEvent.OnAmountChanged("0.1")) advanceUntilIdle() - // Note: this asserts the intended contract. It will currently fail - // unless the getBalance/getCurrentFee/getOpsFee lambdas are wired into - // the ViewModel — which is exactly the point of this regression test. assertTrue(vm.state.value.isReadyToSend) } @Test fun `amount exceeding balance disables send`() = runTest { - val vm = buildViewModel(getBalance = { 1000L }) // 1000 litoshis ~ tiny + val vm = buildViewModel(getBalance = { 1000L }) vm.onEvent(SendEvent.OnRecipientAddressChanged("LValidAddr")) - vm.onEvent(SendEvent.OnAmountChanged("100")) // 100 LTC — way over + vm.onEvent(SendEvent.OnAmountChanged("100")) advanceUntilIdle() assertFalse(vm.state.value.isAmountBelowBalance) @@ -247,7 +274,6 @@ class SendViewModelTest { @Test fun `invalid address disables send even with valid amount`() = runTest { val vm = buildViewModel(validateAddress = { false }) - vm.onEvent(SendEvent.OnRecipientAddressChanged("not-a-real-addr")) vm.onEvent(SendEvent.OnAmountChanged("0.1")) advanceUntilIdle() @@ -259,7 +285,6 @@ class SendViewModelTest { @Test fun `zero amount does not enable send`() = runTest { val vm = buildViewModel() - vm.onEvent(SendEvent.OnRecipientAddressChanged("LValidAddr")) vm.onEvent(SendEvent.OnAmountChanged("0")) advanceUntilIdle() @@ -273,7 +298,6 @@ class SendViewModelTest { @Test fun `empty amount string does not enable send`() = runTest { val vm = buildViewModel() - vm.onEvent(SendEvent.OnRecipientAddressChanged("LValidAddr")) vm.onEvent(SendEvent.OnAmountChanged("")) advanceUntilIdle() @@ -281,8 +305,23 @@ class SendViewModelTest { assertFalse(vm.state.value.isReadyToSend) } + @Test + fun `fiat amount converts to litoshis correctly for balance check`() = runTest { + // 50 USD / 100 rate = 0.5 LTC = 50_000_000 litoshis + // balance = 1_000_000_000 litoshis (10 LTC) → should be valid + val vm = buildViewModel() + vm.onEvent(SendEvent.OnToggleFiatOrLTC) // → fiat + advanceUntilIdle() + vm.onEvent(SendEvent.OnRecipientAddressChanged("LValidAddr")) + vm.onEvent(SendEvent.OnAmountChanged("50")) + advanceUntilIdle() + + assertTrue(vm.state.value.isAmountBelowBalance) + assertTrue(vm.state.value.isReadyToSend) + } + // ------------------------------------------------------------------------- - // REGRESSION: Send button lock-up (isSending never clears) + // Send result handling — brainwalletIsPublishing always clears // ------------------------------------------------------------------------- @Test @@ -293,17 +332,11 @@ class SendViewModelTest { vm.onEvent(SendEvent.OnSend(dummyTransactionItem())) advanceUntilIdle() - assertFalse( - "brainwalletIsPublishing must be cleared after success", - vm.state.value.brainwalletIsPublishing - ) + assertFalse(vm.state.value.brainwalletIsPublishing) } @Test fun `send AlreadySending clears publishing flag`() = runTest { - // This is the core regression: if AlreadySending leaves the VM in - // brainwalletIsPublishing = true, the Send button never re-enables - // and the user is permanently blocked. coEvery { bwSender.prepareTransaction(any()) } returns BWSendResult.Error.AlreadySending val vm = buildViewModel() @@ -351,6 +384,19 @@ class SendViewModelTest { assertTrue(vm.state.value.errorResultString.isNotEmpty()) } + @Test + fun `send InsufficientFunds surfaces error and clears publishing`() = runTest { + coEvery { bwSender.prepareTransaction(any()) } returns + BWSendResult.Error.InsufficientFunds + val vm = buildViewModel() + + vm.onEvent(SendEvent.OnSend(dummyTransactionItem())) + advanceUntilIdle() + + assertFalse(vm.state.value.brainwalletIsPublishing) + assertTrue(vm.state.value.errorResultString.isNotEmpty()) + } + @Test fun `onSend calls bwSender exactly once`() = runTest { coEvery { bwSender.prepareTransaction(any()) } returns BWSendResult.Success @@ -417,13 +463,12 @@ class SendViewModelTest { } // ------------------------------------------------------------------------- - // QR scan / EventBus wiring + // QR scan / EventBus // ------------------------------------------------------------------------- @Test fun `QR scan event populates recipient address`() = runTest { val vm = buildViewModel() - EventBus.emit(EventBus.Event.QRCodeScanned(url = "LQRScannedAddr")) advanceUntilIdle() @@ -433,7 +478,6 @@ class SendViewModelTest { @Test fun `QR scan with null url sets empty string not crash`() = runTest { val vm = buildViewModel() - EventBus.emit(EventBus.Event.QRCodeScanned(url = null)) advanceUntilIdle() diff --git a/app/src/test/kotlin/com/brainwallet/testing/FlakyTest.kt b/app/src/test/kotlin/com/brainwallet/testing/FlakyTest.kt new file mode 100644 index 00000000..e6005238 --- /dev/null +++ b/app/src/test/kotlin/com/brainwallet/testing/FlakyTest.kt @@ -0,0 +1,4 @@ +package com.brainwallet.testing +@Retention(AnnotationRetention.RUNTIME) +@Target(AnnotationTarget.FUNCTION, AnnotationTarget.CLASS) +annotation class FlakyTest(val reason: String = "") diff --git a/app/src/test/kotlin/com/brainwallet/ui/bentosections/shopbento/ShopBentoViewModelTest.kt b/app/src/test/kotlin/com/brainwallet/ui/bentosections/shopbento/ShopBentoViewModelTest.kt index 70d5a71a..699ce614 100644 --- a/app/src/test/kotlin/com/brainwallet/ui/bentosections/shopbento/ShopBentoViewModelTest.kt +++ b/app/src/test/kotlin/com/brainwallet/ui/bentosections/shopbento/ShopBentoViewModelTest.kt @@ -7,6 +7,7 @@ import app.cash.turbine.testIn import app.cash.turbine.turbineScope import com.brainwallet.data.model.AppSetting import com.brainwallet.data.repository.SettingRepository +import com.brainwallet.testing.FlakyTest import io.mockk.every import io.mockk.mockk import kotlinx.coroutines.flow.MutableStateFlow @@ -75,6 +76,7 @@ class ShopBentoViewModelTest { } } + @FlakyTest(reason = "advanceTimeBy timing sensitive under load and in remote test runner server") @Test fun `onEvent OnTapShop - updates shouldSlide to true`() = runTest { turbineScope { @@ -82,10 +84,10 @@ class ShopBentoViewModelTest { val turbine = viewModel.state.testIn(backgroundScope) settingsFlow.emit(AppSetting()) - advanceTimeBy(200) + advanceTimeBy(500) viewModel.onEvent(ShopBentoEvent.OnTapShop) - advanceTimeBy(200) + advanceTimeBy(500) assertEquals(true, turbine.expectMostRecentItem().shouldSlide) turbine.cancelAndIgnoreRemainingEvents()