Skip to content
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
2 changes: 2 additions & 0 deletions app/src/main/java/to/bitkit/data/SettingsStore.kt
Original file line number Diff line number Diff line change
Expand Up @@ -91,4 +91,6 @@ data class SettingsData(
val enableSendAmountWarning: Boolean = false,
val backupVerified: Boolean = false,
val dismissedSuggestions: List<String> = emptyList(),
val lastTimeAskedBalanceWarningMillis: Long = 0,
val balanceWarningTimes: Int = 0,
)
2 changes: 1 addition & 1 deletion app/src/main/java/to/bitkit/env/Env.kt
Original file line number Diff line number Diff line change
Expand Up @@ -135,6 +135,6 @@ internal object Env {
const val BITKIT_TELEGRAM = "https://t.me/bitkitchat"
const val BITKIT_GITHUB = "https://github.com/synonymdev"
const val BITKIT_HELP_CENTER = "https://help.bitkit.to"

const val TERMS_OF_USE_URL = "https://bitkit.to/terms-of-use"
const val STORING_BITCOINS_URL = "https://en.bitcoin.it/wiki/Storing_bitcoins"
}
1 change: 0 additions & 1 deletion app/src/main/java/to/bitkit/ui/ContentView.kt
Original file line number Diff line number Diff line change
Expand Up @@ -370,7 +370,6 @@ fun ContentView(
BottomSheetType.BackupNavigation -> BackupNavigationSheet(
onDismiss = { appViewModel.hideSheet() },
)

null -> Unit
}
}
Expand Down
24 changes: 20 additions & 4 deletions app/src/main/java/to/bitkit/ui/screens/wallets/HomeScreen.kt
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package to.bitkit.ui.screens.wallets

import android.Manifest
import android.content.Intent
import android.os.Build
import androidx.activity.compose.rememberLauncherForActivityResult
import androidx.activity.result.contract.ActivityResultContracts
Expand Down Expand Up @@ -60,6 +61,7 @@ import androidx.compose.ui.res.stringResource
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import androidx.compose.ui.zIndex
import androidx.core.net.toUri
import androidx.hilt.navigation.compose.hiltViewModel
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import androidx.navigation.NavController
Expand Down Expand Up @@ -101,6 +103,7 @@ import to.bitkit.ui.scaffold.AppAlertDialog
import to.bitkit.ui.scaffold.AppScaffold
import to.bitkit.ui.screens.wallets.activity.AllActivityScreen
import to.bitkit.ui.screens.wallets.activity.components.ActivityListSimple
import to.bitkit.ui.screens.wallets.sheets.HighBalanceWarningSheet
import to.bitkit.ui.screens.widgets.DragAndDropWidget
import to.bitkit.ui.screens.widgets.DragDropColumn
import to.bitkit.ui.screens.widgets.blocks.BlockCard
Expand Down Expand Up @@ -254,7 +257,8 @@ fun HomeScreen(
},
onMoveWidget = { fromIndex, toIndex ->
homeViewModel.moveWidget(fromIndex, toIndex)
}
},
onDismissHighBalanceSheet = { homeViewModel.dismissHighBalanceSheet() },
)
}
composable<HomeRoutes.Savings>(
Expand Down Expand Up @@ -336,9 +340,6 @@ fun HomeScreen(
.systemBarsPadding()
)


// Drawer overlay and content - moved from AppScaffold to here
// Semi-transparent overlay when drawer is open
AnimatedVisibility(
visible = drawerState.currentValue == DrawerValue.Open,
modifier = Modifier
Expand Down Expand Up @@ -498,6 +499,7 @@ private fun HomeContentView(
walletNavController: NavController,
drawerState: DrawerState,
onRefresh: () -> Unit,
onDismissHighBalanceSheet: () -> Unit,
) {
val scope = rememberCoroutineScope()

Expand Down Expand Up @@ -789,6 +791,19 @@ private fun HomeContentView(
state = pullRefreshState,
modifier = Modifier.align(Alignment.TopCenter)
)

if (homeUiState.highBalanceSheetVisible) {
val context = LocalContext.current
HighBalanceWarningSheet(
onDismiss = onDismissHighBalanceSheet,
understoodClick = onDismissHighBalanceSheet,
learnMoreClick = {
val intent = Intent(Intent.ACTION_VIEW, Env.STORING_BITCOINS_URL.toUri())
context.startActivity(intent)
onDismissHighBalanceSheet()
}
)
}
}
}
}
Expand Down Expand Up @@ -845,6 +860,7 @@ private fun HomeContentViewPreview() {
onClickEnableEdit = {},
onClickEditWidget = {},
onClickDeleteWidget = {},
onDismissHighBalanceSheet = {},
onMoveWidget = { _, _ -> },
)
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -33,4 +33,5 @@ data class HomeUiState(
val currentPrice: PriceDTO? = null,
val isEditingWidgets: Boolean = false,
val deleteWidgetAlert: WidgetType? = null,
val highBalanceSheetVisible: Boolean = false,
)
65 changes: 59 additions & 6 deletions app/src/main/java/to/bitkit/ui/screens/wallets/HomeViewModel.kt
Original file line number Diff line number Diff line change
Expand Up @@ -8,19 +8,23 @@ import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.update
import kotlinx.coroutines.launch
import kotlinx.datetime.Clock
import to.bitkit.data.SettingsStore
import to.bitkit.models.Suggestion
import to.bitkit.models.WidgetType
import to.bitkit.models.toSuggestionOrNull
import to.bitkit.models.widget.ArticleModel
import to.bitkit.models.widget.toArticleModel
import to.bitkit.models.widget.toBlockModel
import to.bitkit.repositories.CurrencyRepo
import to.bitkit.repositories.WalletRepo
import to.bitkit.repositories.WidgetsRepo
import to.bitkit.ui.screens.widgets.blocks.toWeatherModel
import java.math.BigDecimal
import javax.inject.Inject
import kotlin.time.Duration.Companion.seconds

Expand All @@ -29,6 +33,7 @@ class HomeViewModel @Inject constructor(
private val walletRepo: WalletRepo,
private val widgetsRepo: WidgetsRepo,
private val settingsStore: SettingsStore,
private val currencyRepo: CurrencyRepo
) : ViewModel() {

private val _uiState = MutableStateFlow(HomeUiState())
Expand All @@ -41,6 +46,7 @@ class HomeViewModel @Inject constructor(
setupStateObservation()
setupArticleRotation()
setupFactRotation()
checkHighBalance()
}

private fun setupStateObservation() {
Expand Down Expand Up @@ -124,21 +130,56 @@ class HomeViewModel @Inject constructor(
_currentFact.value = null
}

fun removeSuggestion(suggestion: Suggestion) {
private fun checkHighBalance() {
viewModelScope.launch {
settingsStore.addDismissedSuggestion(suggestion)
delay(CHECK_DELAY_MILLISECONDS)

val settings = settingsStore.data.first()

val totalOnChainSats = walletRepo.balanceState.value.totalSats
val balanceUsd = satsToUsd(totalOnChainSats) ?: return@launch
val thresholdReached = balanceUsd > BigDecimal(BALANCE_THRESHOLD_USD)
val isTimeOutOver = settings.lastTimeAskedBalanceWarningMillis - ASK_INTERVAL_MILLIS > ASK_INTERVAL_MILLIS
val belowMaxWarnings = settings.balanceWarningTimes < MAX_WARNINGS

if (thresholdReached && isTimeOutOver && belowMaxWarnings && !_uiState.value.highBalanceSheetVisible) {
settingsStore.update {
it.copy(
balanceWarningTimes = it.balanceWarningTimes + 1,
lastTimeAskedBalanceWarningMillis = Clock.System.now().toEpochMilliseconds()
)
}
_uiState.update { it.copy(highBalanceSheetVisible = true) }
}

if (!thresholdReached) {
settingsStore.update {
it.copy(
balanceWarningTimes = 0,
)
}
}
}
}

fun refreshWidgets() {
private fun satsToUsd(sats: ULong): BigDecimal? {
val converted = currencyRepo.convertSatsToFiat(sats = sats.toLong(), currency = "USD")
return converted?.value
}

fun dismissHighBalanceSheet() {
_uiState.update { it.copy(highBalanceSheetVisible = false) }
}

fun removeSuggestion(suggestion: Suggestion) {
viewModelScope.launch {
widgetsRepo.refreshEnabledWidgets()
settingsStore.addDismissedSuggestion(suggestion)
}
}

fun refreshSpecificWidget(widgetType: WidgetType) {
fun refreshWidgets() {
viewModelScope.launch {
widgetsRepo.refreshWidget(widgetType)
widgetsRepo.refreshEnabledWidgets()
}
}

Expand Down Expand Up @@ -240,4 +281,16 @@ class HomeViewModel @Inject constructor(
val dismissedList = settings.dismissedSuggestions.mapNotNull { it.toSuggestionOrNull() }
baseSuggestions.filterNot { it in dismissedList }
}

companion object {
/**How high the balance must be to show this warning to the user (in USD)*/
private const val BALANCE_THRESHOLD_USD = 500L
private const val MAX_WARNINGS = 3

/** 1 day - how long this prompt will be hidden if user taps Later*/
private const val ASK_INTERVAL_MILLIS = 1000 * 60 * 60 * 24

/**How long user needs to stay on the home screen before he will see this prompt*/
private const val CHECK_DELAY_MILLISECONDS = 2500L
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,162 @@
package to.bitkit.ui.screens.wallets.sheets

import androidx.compose.foundation.Image
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.ModalBottomSheet
import androidx.compose.material3.SheetState
import androidx.compose.material3.rememberModalBottomSheetState
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.testTag
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.SpanStyle
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import to.bitkit.R
import to.bitkit.ui.components.BodyM
import to.bitkit.ui.components.Display
import to.bitkit.ui.components.ModalBottomSheetHandle
import to.bitkit.ui.components.PrimaryButton
import to.bitkit.ui.components.SecondaryButton
import to.bitkit.ui.components.VerticalSpacer
import to.bitkit.ui.scaffold.ScreenColumn
import to.bitkit.ui.scaffold.SheetTopBar
import to.bitkit.ui.shared.util.gradientBackground
import to.bitkit.ui.theme.AppShapes
import to.bitkit.ui.theme.AppThemeSurface
import to.bitkit.ui.theme.Colors
import to.bitkit.ui.theme.InterFontFamily
import to.bitkit.ui.theme.ModalSheetTopPadding
import to.bitkit.ui.utils.withAccent

@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun HighBalanceWarningSheet(
onDismiss: () -> Unit,
understoodClick: () -> Unit,
learnMoreClick: () -> Unit,
modifier: Modifier = Modifier,
) {
val sheetState: SheetState = rememberModalBottomSheetState(skipPartiallyExpanded = true)

ModalBottomSheet(
onDismissRequest = onDismiss,
sheetState = sheetState,
shape = AppShapes.sheet,
containerColor = Colors.Black,
dragHandle = { ModalBottomSheetHandle() },
modifier = Modifier
.fillMaxSize()
.padding(top = ModalSheetTopPadding)
) {
HighBalanceWarningContent(
understoodClick = understoodClick,
learnMoreClick = learnMoreClick,
modifier = modifier
)
}
}

@Composable
fun HighBalanceWarningContent(
understoodClick: () -> Unit,
learnMoreClick: () -> Unit,
modifier: Modifier = Modifier,
) {
Column(
modifier = modifier
.fillMaxWidth()
.gradientBackground()
.testTag("high_balance_intro_screen")
) {
SheetTopBar(stringResource(R.string.other__high_balance__nav_title))

Column(
modifier = Modifier.padding(horizontal = 16.dp),
horizontalAlignment = Alignment.CenterHorizontally
) {
Image(
painter = painterResource(R.drawable.exclamation_mark),
contentDescription = null,
modifier = Modifier
.fillMaxWidth()
.weight(1f)
.testTag("high_balance_image")
)

Display(
text = stringResource(R.string.other__high_balance__title).withAccent(accentColor = Colors.Yellow),
color = Colors.White,
modifier = Modifier
.fillMaxWidth()
.testTag("high_balance_title")
)
VerticalSpacer(8.dp)
BodyM(
text =
stringResource(R.string.other__high_balance__text).withAccent(
defaultColor = Colors.White64,
accentStyle = SpanStyle(
fontWeight = FontWeight.Bold,
fontSize = 17.sp,
letterSpacing = 0.4.sp,
fontFamily = InterFontFamily,
color = Colors.White,
)
),
color = Colors.White64,
modifier = Modifier
.testTag("high_balance_description")
)
VerticalSpacer(32.dp)
Row(
modifier = Modifier
.fillMaxWidth()
.testTag("buttons_row"),
horizontalArrangement = Arrangement.spacedBy(16.dp)
) {
SecondaryButton(
text = stringResource(R.string.other__high_balance__cancel),
fullWidth = false,
onClick = learnMoreClick,
modifier = Modifier
.weight(1f)
.testTag("learn_more_button"),
)

PrimaryButton(
text = stringResource(R.string.other__high_balance__continue),
fullWidth = false,
onClick = understoodClick,
modifier = Modifier
.weight(1f)
.testTag("understood_button"),
)
}
VerticalSpacer(16.dp)
}
}
}


@OptIn(ExperimentalMaterial3Api::class)
@Preview(showBackground = true)
@Composable
private fun Preview() {
AppThemeSurface {
HighBalanceWarningContent(
understoodClick = {},
learnMoreClick = {},
)
}
}