Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Sats/USD currency switcher on Wallet Dashboard #262

Merged
merged 16 commits into from
Dec 25, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions app/detekt-baseline.xml
Original file line number Diff line number Diff line change
Expand Up @@ -97,6 +97,7 @@
<ID>LongParameterList:NoteEditorViewModel.kt$NoteEditorViewModel$( @Assisted private val args: NoteEditorArgs, private val dispatcherProvider: CoroutineDispatcherProvider, private val fileAnalyser: FileAnalyser, private val activeAccountStore: ActiveAccountStore, private val feedRepository: FeedRepository, private val notePublishHandler: NotePublishHandler, private val attachmentRepository: AttachmentsRepository, private val exploreRepository: ExploreRepository, private val profileRepository: ProfileRepository, private val articleRepository: ArticleRepository, )</ID>
<ID>LongParameterList:ProfileDetailsViewModel.kt$ProfileDetailsViewModel$( savedStateHandle: SavedStateHandle, private val dispatcherProvider: CoroutineDispatcherProvider, private val activeAccountStore: ActiveAccountStore, private val feedsRepository: FeedsRepository, private val profileRepository: ProfileRepository, private val mutedUserRepository: MutedUserRepository, private val zapHandler: ZapHandler, )</ID>
<ID>LongParameterList:SubscriptionsManager.kt$SubscriptionsManager$( dispatcherProvider: CoroutineDispatcherProvider, private val activeAccountStore: ActiveAccountStore, private val userRepository: UserRepository, private val nostrNotary: NostrNotary, private val appConfigProvider: AppConfigProvider, @PrimalCacheApiClient private val cacheApiClient: PrimalApiClient, @PrimalWalletApiClient private val walletApiClient: PrimalApiClient, )</ID>
<ID>LongParameterList:TransactionDetailsViewModel.kt$TransactionDetailsViewModel$( savedStateHandle: SavedStateHandle, private val dispatcherProvider: CoroutineDispatcherProvider, private val activeAccountStore: ActiveAccountStore, private val walletRepository: WalletRepository, private val feedRepository: FeedRepository, private val articleRepository: ArticleRepository, private val exchangeRateHandler: ExchangeRateHandler, )</ID>
<ID>LongParameterList:UserDataUpdater.kt$UserDataUpdater$( @Assisted val userId: String, private val settingsRepository: SettingsRepository, private val userRepository: UserRepository, private val walletRepository: WalletRepository, private val relayRepository: RelayRepository, private val bookmarksRepository: BookmarksRepository, private val premiumRepository: PremiumRepository, )</ID>
<ID>LongParameterList:UserRepository.kt$UserRepository$( private val database: PrimalDatabase, private val dispatchers: CoroutineDispatcherProvider, private val userAccountFetcher: UserAccountFetcher, private val accountsStore: UserAccountsStore, private val fileUploader: PrimalFileUploader, private val usersApi: UsersApi, private val nostrPublisher: NostrPublisher, )</ID>
<ID>LongParameterList:ZapHandler.kt$ZapHandler$( private val dispatcherProvider: CoroutineDispatcherProvider, private val accountsStore: UserAccountsStore, private val nwcNostrZapperFactory: NwcNostrZapperFactory, private val primalWalletZapper: WalletNostrZapper, private val relayRepository: RelayRepository, private val notary: NostrNotary, private val database: PrimalDatabase, )</ID>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -130,6 +130,7 @@ import net.primal.android.core.compose.icons.primaliconpack.UserFeedRemove
import net.primal.android.core.compose.icons.primaliconpack.Verified
import net.primal.android.core.compose.icons.primaliconpack.VerifiedFilled
import net.primal.android.core.compose.icons.primaliconpack.WalletBitcoinPayment
import net.primal.android.core.compose.icons.primaliconpack.WalletChangeCurrency
import net.primal.android.core.compose.icons.primaliconpack.WalletError
import net.primal.android.core.compose.icons.primaliconpack.WalletLightningPayment
import net.primal.android.core.compose.icons.primaliconpack.WalletLightningPaymentAlt
Expand Down Expand Up @@ -293,6 +294,7 @@ val PrimalIcons.PrimalIcons: ____KtList<ImageVector>
DrawerSignOut,
OnboardingZapsExplained,
Document,
WalletChangeCurrency,
)
return __PrimalIcons!!
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
package net.primal.android.core.compose.icons.primaliconpack

import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.PathFillType
import androidx.compose.ui.graphics.SolidColor
import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.graphics.vector.path
import androidx.compose.ui.unit.dp
import net.primal.android.core.compose.icons.PrimalIcons

val PrimalIcons.WalletChangeCurrency: ImageVector
get() {
if (_WalletChangeCurrency != null) {
return _WalletChangeCurrency!!
}
_WalletChangeCurrency = ImageVector.Builder(
name = "WalletChangeCurrency",
defaultWidth = 18.dp,
defaultHeight = 14.dp,
viewportWidth = 18f,
viewportHeight = 14f
).apply {
path(
fill = SolidColor(Color(0xFFCA079F)),
pathFillType = PathFillType.EvenOdd
) {
moveTo(0.224f, 3.919f)
curveTo(-0.075f, 4.207f, -0.075f, 4.673f, 0.224f, 4.961f)
curveTo(0.522f, 5.248f, 1.005f, 5.248f, 1.304f, 4.961f)
lineTo(3.542f, 2.802f)
verticalLineTo(11.651f)
curveTo(3.542f, 12.058f, 3.885f, 12.389f, 4.307f, 12.389f)
curveTo(4.73f, 12.389f, 5.072f, 12.058f, 5.072f, 11.651f)
verticalLineTo(2.605f)
lineTo(7.516f, 4.961f)
curveTo(7.814f, 5.248f, 8.297f, 5.248f, 8.596f, 4.961f)
curveTo(8.894f, 4.673f, 8.894f, 4.207f, 8.596f, 3.919f)
lineTo(4.616f, 0.083f)
curveTo(4.502f, -0.028f, 4.317f, -0.028f, 4.203f, 0.083f)
lineTo(0.224f, 3.919f)
close()
}
path(
fill = SolidColor(Color(0xFFCA077C)),
pathFillType = PathFillType.EvenOdd
) {
moveTo(0.224f, 3.919f)
curveTo(-0.075f, 4.207f, -0.075f, 4.673f, 0.224f, 4.961f)
curveTo(0.522f, 5.248f, 1.005f, 5.248f, 1.304f, 4.961f)
lineTo(3.542f, 2.802f)
verticalLineTo(11.651f)
curveTo(3.542f, 12.058f, 3.885f, 12.389f, 4.307f, 12.389f)
curveTo(4.73f, 12.389f, 5.072f, 12.058f, 5.072f, 11.651f)
verticalLineTo(2.605f)
lineTo(7.516f, 4.961f)
curveTo(7.814f, 5.248f, 8.297f, 5.248f, 8.596f, 4.961f)
curveTo(8.894f, 4.673f, 8.894f, 4.207f, 8.596f, 3.919f)
lineTo(4.616f, 0.083f)
curveTo(4.502f, -0.028f, 4.317f, -0.028f, 4.203f, 0.083f)
lineTo(0.224f, 3.919f)
close()
}
path(
fill = SolidColor(Color(0xFFCA079F)),
pathFillType = PathFillType.EvenOdd
) {
moveTo(17.776f, 10.08f)
curveTo(18.075f, 9.793f, 18.075f, 9.327f, 17.776f, 9.039f)
curveTo(17.478f, 8.752f, 16.995f, 8.752f, 16.696f, 9.039f)
lineTo(14.253f, 11.395f)
verticalLineTo(2.349f)
curveTo(14.253f, 1.942f, 13.911f, 1.611f, 13.488f, 1.611f)
curveTo(13.066f, 1.611f, 12.723f, 1.942f, 12.723f, 2.349f)
verticalLineTo(11.198f)
lineTo(10.484f, 9.039f)
curveTo(10.186f, 8.752f, 9.703f, 8.752f, 9.404f, 9.039f)
curveTo(9.106f, 9.327f, 9.106f, 9.793f, 9.404f, 10.08f)
lineTo(13.384f, 13.917f)
curveTo(13.498f, 14.028f, 13.683f, 14.028f, 13.797f, 13.917f)
lineTo(17.776f, 10.08f)
close()
}
path(
fill = SolidColor(Color(0xFFCA077C)),
pathFillType = PathFillType.EvenOdd
) {
moveTo(17.776f, 10.08f)
curveTo(18.075f, 9.793f, 18.075f, 9.327f, 17.776f, 9.039f)
curveTo(17.478f, 8.752f, 16.995f, 8.752f, 16.696f, 9.039f)
lineTo(14.253f, 11.395f)
verticalLineTo(2.349f)
curveTo(14.253f, 1.942f, 13.911f, 1.611f, 13.488f, 1.611f)
curveTo(13.066f, 1.611f, 12.723f, 1.942f, 12.723f, 2.349f)
verticalLineTo(11.198f)
lineTo(10.484f, 9.039f)
curveTo(10.186f, 8.752f, 9.703f, 8.752f, 9.404f, 9.039f)
curveTo(9.106f, 9.327f, 9.106f, 9.793f, 9.404f, 10.08f)
lineTo(13.384f, 13.917f)
curveTo(13.498f, 14.028f, 13.683f, 14.028f, 13.797f, 13.917f)
lineTo(17.776f, 10.08f)
close()
}
}.build()

return _WalletChangeCurrency!!
}

@Suppress("ObjectPropertyName")
private var _WalletChangeCurrency: ImageVector? = null
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ interface WalletDashboardContract {
val primalWallet: PrimalWallet? = null,
val walletPreference: WalletPreference = WalletPreference.Undefined,
val walletBalance: BigDecimal? = null,
val exchangeBtcUsdRate: Double? = null,
val lastWalletUpdatedAt: Long? = null,
val lowBalance: Boolean = false,
val error: DashboardError? = null,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -149,11 +149,11 @@ fun WalletDashboardScreen(

val isScrolledToTop by remember(listState) { derivedStateOf { listState.firstVisibleItemScrollOffset == 0 } }
val dashboardExpanded by rememberSaveable(isScrolledToTop) { mutableStateOf(isScrolledToTop) }

val dashboardLiteHeightDp = 80.dp
var topBarHeight by remember { mutableIntStateOf(0) }
var topBarFooterHeight by remember { mutableIntStateOf(0) }
val density = LocalDensity.current
var currencyMode by rememberSaveable { mutableStateOf(CurrencyMode.SATS) }

var shouldAddFooter by remember { mutableStateOf(false) }
LaunchedEffect(pagingItems.itemCount, listState) {
Expand Down Expand Up @@ -212,6 +212,7 @@ fun WalletDashboardScreen(
.wrapContentSize(align = Alignment.Center)
.padding(horizontal = 32.dp)
.padding(vertical = 32.dp)
.animateContentSize()
.then(
if (state.primalWallet?.kycLevel == WalletKycLevel.None) {
Modifier.graphicsLayer { alpha = DISABLED_WALLET_ALPHA }
Expand All @@ -229,14 +230,18 @@ fun WalletDashboardScreen(
WalletAction.Receive -> onReceiveClick()
}
},
currencyMode = currencyMode,
onSwitchCurrencyMode = { currencyMode = it },
exchangeBtcUsdRate = state.exchangeBtcUsdRate,
)

false -> WalletDashboardLite(
modifier = Modifier
.fillMaxWidth()
.height(dashboardLiteHeightDp)
.background(color = AppTheme.colorScheme.surface)
.padding(horizontal = 10.dp, vertical = 16.dp),
.padding(horizontal = 10.dp, vertical = 16.dp)
.animateContentSize(),
walletBalance = state.walletBalance,
actions = listOf(WalletAction.Send, WalletAction.Scan, WalletAction.Receive),
onWalletAction = { action ->
Expand All @@ -246,6 +251,9 @@ fun WalletDashboardScreen(
WalletAction.Receive -> onReceiveClick()
}
},
currencyMode = currencyMode,
onSwitchCurrencyMode = { currencyMode = it },
exchangeBtcUsdRate = state.exchangeBtcUsdRate,
)
}
}
Expand Down Expand Up @@ -310,6 +318,8 @@ fun WalletDashboardScreen(
.background(color = AppTheme.colorScheme.surfaceVariant)
.padding(top = with(LocalDensity.current) { topBarHeight.toDp() }),
pagingItems = pagingItems,
currencyMode = currencyMode,
exchangeBtcUsdRate = state.exchangeBtcUsdRate,
listState = listState,
onProfileClick = onProfileClick,
onTransactionClick = onTransactionClick,
Expand Down Expand Up @@ -349,4 +359,9 @@ fun WalletDashboardScreen(
)
}

enum class CurrencyMode {
FIAT,
SATS,
}

private const val DISABLED_WALLET_ALPHA = 0.42f
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ import net.primal.android.user.subscriptions.SubscriptionsManager
import net.primal.android.wallet.dashboard.WalletDashboardContract.UiEvent
import net.primal.android.wallet.dashboard.WalletDashboardContract.UiState
import net.primal.android.wallet.db.WalletTransaction
import net.primal.android.wallet.repository.ExchangeRateHandler
import net.primal.android.wallet.repository.WalletRepository
import net.primal.android.wallet.store.PrimalBillingClient
import net.primal.android.wallet.store.domain.SatsPurchase
Expand All @@ -39,6 +40,7 @@ class WalletDashboardViewModel @Inject constructor(
private val userRepository: UserRepository,
private val primalBillingClient: PrimalBillingClient,
private val subscriptionsManager: SubscriptionsManager,
private val exchangeRateHandler: ExchangeRateHandler,
) : ViewModel() {

private val activeUserId = activeAccountStore.activeUserId()
Expand All @@ -58,6 +60,7 @@ class WalletDashboardViewModel @Inject constructor(

init {
fetchWalletBalance()
observeUsdExchangeRate()
subscribeToEvents()
subscribeToActiveAccount()
subscribeToPurchases()
Expand Down Expand Up @@ -116,6 +119,22 @@ class WalletDashboardViewModel @Inject constructor(
}
}

private fun observeUsdExchangeRate() {
viewModelScope.launch {
fetchExchangeRate()
exchangeRateHandler.usdExchangeRate.collect {
setState { copy(exchangeBtcUsdRate = it) }
}
}
}

private fun fetchExchangeRate() =
viewModelScope.launch {
exchangeRateHandler.updateExchangeRate(
userId = activeAccountStore.activeUserId(),
)
}

private fun enablePrimalWallet() =
viewModelScope.launch {
userRepository.updateWalletPreference(
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
package net.primal.android.wallet.dashboard.ui

import androidx.compose.animation.animateContentSize
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.padding
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.TextUnit
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import java.math.BigDecimal
import java.text.NumberFormat
import net.primal.android.R
import net.primal.android.theme.AppTheme
import net.primal.android.wallet.utils.CurrencyConversionUtils.toUsd

@Composable
fun FiatAmountText(
modifier: Modifier,
amount: BigDecimal?,
textSize: TextUnit = 42.sp,
amountColor: Color = Color.Unspecified,
currencyColor: Color = AppTheme.extraColorScheme.onSurfaceVariantAlt3,
exchangeBtcUsdRate: Double?,
) {
val numberFormat = remember { NumberFormat.getNumberInstance() }

Row(
modifier = modifier
.animateContentSize(),
verticalAlignment = Alignment.Bottom,
horizontalArrangement = Arrangement.Start,
) {
if (amount != null) {
Text(
modifier = Modifier.padding(bottom = (textSize.value / 2 - 5).dp),
text = "${stringResource(id = R.string.wallet_usd_prefix)} ",
textAlign = TextAlign.Center,
maxLines = 1,
style = AppTheme.typography.bodyMedium,
fontSize = textSize / 2,
color = currencyColor,
)
}

Text(
text = amount?.toUsd(exchangeBtcUsdRate)?.let { numberFormat.format(it.toFloat()) } ?: "⌛",
mehmedalijaK marked this conversation as resolved.
Show resolved Hide resolved
textAlign = TextAlign.Center,
style = AppTheme.typography.displayMedium,
fontSize = textSize,
color = amountColor,
)

if (amount != null) {
Text(
modifier = Modifier.padding(bottom = (textSize.value / 6).dp),
text = " ${stringResource(id = R.string.wallet_usd_suffix)}",
textAlign = TextAlign.Center,
maxLines = 1,
style = AppTheme.typography.bodyMedium,
color = currencyColor,
)
}
}
}
Loading