diff --git a/AGENTS.md b/AGENTS.md index e7a0b0da0..0e6e8a7fb 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -210,6 +210,7 @@ suspend fun getData(): Result = withContext(Dispatchers.IO) { - PREFER to use one-liners with `run {}` when applicable, e.g. `override fun someCall(value: String) = run { this.value = value }` - ALWAYS add imports instead of inline fully-qualified names - PREFER to place `@Suppress()` annotations at the narrowest possible scope +- ALWAYS wrap suspend functions in `withContext(bgDispatcher)` if in domain layer, using ctor injected prop `@BgDispatcher private val bgDispatcher: CoroutineDispatcher` ### Architecture Guidelines diff --git a/app/src/main/java/to/bitkit/di/EnvModule.kt b/app/src/main/java/to/bitkit/di/EnvModule.kt index d413c24af..24621dab0 100644 --- a/app/src/main/java/to/bitkit/di/EnvModule.kt +++ b/app/src/main/java/to/bitkit/di/EnvModule.kt @@ -1,11 +1,14 @@ package to.bitkit.di +import android.content.Context import dagger.Module import dagger.Provides import dagger.hilt.InstallIn +import dagger.hilt.android.qualifiers.ApplicationContext import dagger.hilt.components.SingletonComponent import org.lightningdevkit.ldknode.Network import to.bitkit.env.Env +import java.util.Locale import kotlin.time.Clock import kotlin.time.ExperimentalTime @@ -14,13 +17,12 @@ import kotlin.time.ExperimentalTime object EnvModule { @Provides - fun provideNetwork(): Network { - return Env.network - } + fun provideNetwork(): Network = Env.network @OptIn(ExperimentalTime::class) @Provides - fun provideClock(): Clock { - return Clock.System - } + fun provideClock(): Clock = Clock.System + + @Provides + fun provideLocale(@ApplicationContext context: Context): Locale = context.resources.configuration.locales[0] } diff --git a/app/src/main/java/to/bitkit/models/Currency.kt b/app/src/main/java/to/bitkit/models/Currency.kt index fe4e8157c..9d5256caa 100644 --- a/app/src/main/java/to/bitkit/models/Currency.kt +++ b/app/src/main/java/to/bitkit/models/Currency.kt @@ -82,10 +82,7 @@ data class ConvertedAmount( ) fun bitcoinDisplay(unit: BitcoinDisplayUnit): BitcoinDisplayComponents { - val formattedValue = when (unit) { - BitcoinDisplayUnit.MODERN -> sats.formatToModernDisplay(locale) - BitcoinDisplayUnit.CLASSIC -> sats.formatToClassicDisplay(locale) - } + val formattedValue = sats.formatMoney(unit, locale) return BitcoinDisplayComponents( symbol = BITCOIN_SYMBOL, value = formattedValue, @@ -93,6 +90,19 @@ data class ConvertedAmount( } } +fun Long.formatMoney( + unit: BitcoinDisplayUnit, + locale: Locale = Locale.getDefault(), +): String = when (unit) { + BitcoinDisplayUnit.MODERN -> formatToModernDisplay(locale) + BitcoinDisplayUnit.CLASSIC -> formatToClassicDisplay(locale) +} + +fun ULong.formatMoney( + unit: BitcoinDisplayUnit, + locale: Locale = Locale.getDefault(), +): String = toLong().formatMoney(unit, locale) + fun Long.formatToModernDisplay(locale: Locale = Locale.getDefault()): String { val sats = this val symbols = DecimalFormatSymbols(locale).apply { diff --git a/app/src/main/java/to/bitkit/services/CoreService.kt b/app/src/main/java/to/bitkit/services/CoreService.kt index 25abc125a..85fa8d988 100644 --- a/app/src/main/java/to/bitkit/services/CoreService.kt +++ b/app/src/main/java/to/bitkit/services/CoreService.kt @@ -629,6 +629,7 @@ class ActivityService( ) } + @Suppress("CyclomaticComplexMethod") private suspend fun processOnchainPayment( kind: PaymentKind.Onchain, payment: PaymentDetails, diff --git a/app/src/main/java/to/bitkit/services/MigrationService.kt b/app/src/main/java/to/bitkit/services/MigrationService.kt index 068b4c5a8..7c8ff75cd 100644 --- a/app/src/main/java/to/bitkit/services/MigrationService.kt +++ b/app/src/main/java/to/bitkit/services/MigrationService.kt @@ -1394,7 +1394,7 @@ class MigrationService @Inject constructor( } } - @Suppress("CyclomaticComplexMethod") + @Suppress("CyclomaticComplexMethod", "LongMethod") private suspend fun updateOnchainActivityMetadata( item: RNActivityItem, onchain: OnchainActivity, diff --git a/app/src/main/java/to/bitkit/usecases/FormatMoneyValue.kt b/app/src/main/java/to/bitkit/usecases/FormatMoneyValue.kt new file mode 100644 index 000000000..e5029c42e --- /dev/null +++ b/app/src/main/java/to/bitkit/usecases/FormatMoneyValue.kt @@ -0,0 +1,23 @@ +package to.bitkit.usecases + +import kotlinx.coroutines.CoroutineDispatcher +import kotlinx.coroutines.flow.first +import kotlinx.coroutines.withContext +import to.bitkit.data.SettingsStore +import to.bitkit.di.BgDispatcher +import to.bitkit.models.formatMoney +import java.util.Locale +import javax.inject.Inject +import javax.inject.Singleton + +@Singleton +class FormatMoneyValue @Inject constructor( + @BgDispatcher private val bgDispatcher: CoroutineDispatcher, + private val settingsStore: SettingsStore, + private val locale: Locale, +) { + suspend operator fun invoke(sats: ULong): String = withContext(bgDispatcher) { + val unit = settingsStore.data.first().displayUnit + sats.formatMoney(unit, locale) + } +} diff --git a/app/src/main/java/to/bitkit/viewmodels/AppViewModel.kt b/app/src/main/java/to/bitkit/viewmodels/AppViewModel.kt index c9be82b68..019b447e3 100644 --- a/app/src/main/java/to/bitkit/viewmodels/AppViewModel.kt +++ b/app/src/main/java/to/bitkit/viewmodels/AppViewModel.kt @@ -108,6 +108,7 @@ import to.bitkit.ui.shared.toast.ToastEventBus import to.bitkit.ui.shared.toast.ToastQueueManager import to.bitkit.ui.sheets.SendRoute import to.bitkit.ui.theme.TRANSITION_SCREEN_MS +import to.bitkit.usecases.FormatMoneyValue import to.bitkit.utils.Bip21Utils import to.bitkit.utils.Logger import to.bitkit.utils.NetworkValidationHelper @@ -153,6 +154,7 @@ class AppViewModel @Inject constructor( private val notificationsSheet: NotificationsTimedSheet, private val quickPaySheet: QuickPayTimedSheet, private val highBalanceSheet: HighBalanceTimedSheet, + private val formatMoneyValue: FormatMoneyValue, ) : ViewModel() { val healthState = healthRepo.healthState @@ -748,7 +750,7 @@ class AppViewModel @Inject constructor( showAddressValidationError( titleRes = R.string.other__pay_insufficient_spending, descriptionRes = R.string.other__pay_insufficient_spending_amount_description, - descriptionArgs = mapOf("amount" to shortfall.toString()), + descriptionArgs = mapOf("amount" to formatMoneyValue(shortfall)), testTag = "InsufficientSpendingToast", ) return @@ -758,7 +760,7 @@ class AppViewModel @Inject constructor( _sendUiState.update { it.copy(isAddressInputValid = true) } } - private fun validateOnChainAddress(invoice: OnChainInvoice) { + private suspend fun validateOnChainAddress(invoice: OnChainInvoice) { val validatedAddress = runCatching { validateBitcoinAddress(invoice.address) } .getOrElse { showAddressValidationError( @@ -794,7 +796,7 @@ class AppViewModel @Inject constructor( showAddressValidationError( titleRes = R.string.other__pay_insufficient_savings, descriptionRes = R.string.other__pay_insufficient_savings_amount_description, - descriptionArgs = mapOf("amount" to shortfall.toString()), + descriptionArgs = mapOf("amount" to formatMoneyValue(shortfall)), testTag = "InsufficientSavingsToast", ) return @@ -915,7 +917,7 @@ class AppViewModel @Inject constructor( type = Toast.ToastType.ERROR, title = context.getString(R.string.wallet__lnurl_pay__error_min__title), description = context.getString(R.string.wallet__lnurl_pay__error_min__description) - .replace("{amount}", minSendable.toString()), + .replace("{amount}", formatMoneyValue(minSendable)), testTag = "LnurlPayAmountTooLowToast", ) return @@ -1125,7 +1127,7 @@ class AppViewModel @Inject constructor( type = Toast.ToastType.ERROR, title = context.getString(R.string.other__pay_insufficient_savings), description = context.getString(R.string.other__pay_insufficient_savings_amount_description) - .replace("{amount}", shortfall.toString()), + .replace("{amount}", formatMoneyValue(shortfall)), testTag = "InsufficientSavingsToast", ) return @@ -1167,7 +1169,7 @@ class AppViewModel @Inject constructor( type = Toast.ToastType.ERROR, title = context.getString(R.string.other__pay_insufficient_spending), description = context.getString(R.string.other__pay_insufficient_spending_amount_description) - .replace("{amount}", shortfall.toString()), + .replace("{amount}", formatMoneyValue(shortfall)), testTag = "InsufficientSpendingToast", ) return