Skip to content

Commit 69a3eef

Browse files
authored
Merge pull request #196 from synonymdev/feat/high-balance-warning
High balance warning
2 parents 93e872b + f4c0e8e commit 69a3eef

File tree

7 files changed

+245
-12
lines changed

7 files changed

+245
-12
lines changed

app/src/main/java/to/bitkit/data/SettingsStore.kt

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -91,4 +91,6 @@ data class SettingsData(
9191
val enableSendAmountWarning: Boolean = false,
9292
val backupVerified: Boolean = false,
9393
val dismissedSuggestions: List<String> = emptyList(),
94+
val lastTimeAskedBalanceWarningMillis: Long = 0,
95+
val balanceWarningTimes: Int = 0,
9496
)

app/src/main/java/to/bitkit/env/Env.kt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -135,6 +135,6 @@ internal object Env {
135135
const val BITKIT_TELEGRAM = "https://t.me/bitkitchat"
136136
const val BITKIT_GITHUB = "https://github.com/synonymdev"
137137
const val BITKIT_HELP_CENTER = "https://help.bitkit.to"
138-
139138
const val TERMS_OF_USE_URL = "https://bitkit.to/terms-of-use"
139+
const val STORING_BITCOINS_URL = "https://en.bitcoin.it/wiki/Storing_bitcoins"
140140
}

app/src/main/java/to/bitkit/ui/ContentView.kt

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -370,7 +370,6 @@ fun ContentView(
370370
BottomSheetType.BackupNavigation -> BackupNavigationSheet(
371371
onDismiss = { appViewModel.hideSheet() },
372372
)
373-
374373
null -> Unit
375374
}
376375
}

app/src/main/java/to/bitkit/ui/screens/wallets/HomeScreen.kt

Lines changed: 20 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
package to.bitkit.ui.screens.wallets
22

33
import android.Manifest
4+
import android.content.Intent
45
import android.os.Build
56
import androidx.activity.compose.rememberLauncherForActivityResult
67
import androidx.activity.result.contract.ActivityResultContracts
@@ -60,6 +61,7 @@ import androidx.compose.ui.res.stringResource
6061
import androidx.compose.ui.tooling.preview.Preview
6162
import androidx.compose.ui.unit.dp
6263
import androidx.compose.ui.zIndex
64+
import androidx.core.net.toUri
6365
import androidx.hilt.navigation.compose.hiltViewModel
6466
import androidx.lifecycle.compose.collectAsStateWithLifecycle
6567
import androidx.navigation.NavController
@@ -101,6 +103,7 @@ import to.bitkit.ui.scaffold.AppAlertDialog
101103
import to.bitkit.ui.scaffold.AppScaffold
102104
import to.bitkit.ui.screens.wallets.activity.AllActivityScreen
103105
import to.bitkit.ui.screens.wallets.activity.components.ActivityListSimple
106+
import to.bitkit.ui.screens.wallets.sheets.HighBalanceWarningSheet
104107
import to.bitkit.ui.screens.widgets.DragAndDropWidget
105108
import to.bitkit.ui.screens.widgets.DragDropColumn
106109
import to.bitkit.ui.screens.widgets.blocks.BlockCard
@@ -254,7 +257,8 @@ fun HomeScreen(
254257
},
255258
onMoveWidget = { fromIndex, toIndex ->
256259
homeViewModel.moveWidget(fromIndex, toIndex)
257-
}
260+
},
261+
onDismissHighBalanceSheet = { homeViewModel.dismissHighBalanceSheet() },
258262
)
259263
}
260264
composable<HomeRoutes.Savings>(
@@ -336,9 +340,6 @@ fun HomeScreen(
336340
.systemBarsPadding()
337341
)
338342

339-
340-
// Drawer overlay and content - moved from AppScaffold to here
341-
// Semi-transparent overlay when drawer is open
342343
AnimatedVisibility(
343344
visible = drawerState.currentValue == DrawerValue.Open,
344345
modifier = Modifier
@@ -498,6 +499,7 @@ private fun HomeContentView(
498499
walletNavController: NavController,
499500
drawerState: DrawerState,
500501
onRefresh: () -> Unit,
502+
onDismissHighBalanceSheet: () -> Unit,
501503
) {
502504
val scope = rememberCoroutineScope()
503505

@@ -789,6 +791,19 @@ private fun HomeContentView(
789791
state = pullRefreshState,
790792
modifier = Modifier.align(Alignment.TopCenter)
791793
)
794+
795+
if (homeUiState.highBalanceSheetVisible) {
796+
val context = LocalContext.current
797+
HighBalanceWarningSheet(
798+
onDismiss = onDismissHighBalanceSheet,
799+
understoodClick = onDismissHighBalanceSheet,
800+
learnMoreClick = {
801+
val intent = Intent(Intent.ACTION_VIEW, Env.STORING_BITCOINS_URL.toUri())
802+
context.startActivity(intent)
803+
onDismissHighBalanceSheet()
804+
}
805+
)
806+
}
792807
}
793808
}
794809
}
@@ -845,6 +860,7 @@ private fun HomeContentViewPreview() {
845860
onClickEnableEdit = {},
846861
onClickEditWidget = {},
847862
onClickDeleteWidget = {},
863+
onDismissHighBalanceSheet = {},
848864
onMoveWidget = { _, _ -> },
849865
)
850866
}

app/src/main/java/to/bitkit/ui/screens/wallets/HomeUiState.kt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,4 +33,5 @@ data class HomeUiState(
3333
val currentPrice: PriceDTO? = null,
3434
val isEditingWidgets: Boolean = false,
3535
val deleteWidgetAlert: WidgetType? = null,
36+
val highBalanceSheetVisible: Boolean = false,
3637
)

app/src/main/java/to/bitkit/ui/screens/wallets/HomeViewModel.kt

Lines changed: 59 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -8,19 +8,23 @@ import kotlinx.coroutines.flow.MutableStateFlow
88
import kotlinx.coroutines.flow.StateFlow
99
import kotlinx.coroutines.flow.asStateFlow
1010
import kotlinx.coroutines.flow.combine
11+
import kotlinx.coroutines.flow.first
1112
import kotlinx.coroutines.flow.map
1213
import kotlinx.coroutines.flow.update
1314
import kotlinx.coroutines.launch
15+
import kotlinx.datetime.Clock
1416
import to.bitkit.data.SettingsStore
1517
import to.bitkit.models.Suggestion
1618
import to.bitkit.models.WidgetType
1719
import to.bitkit.models.toSuggestionOrNull
1820
import to.bitkit.models.widget.ArticleModel
1921
import to.bitkit.models.widget.toArticleModel
2022
import to.bitkit.models.widget.toBlockModel
23+
import to.bitkit.repositories.CurrencyRepo
2124
import to.bitkit.repositories.WalletRepo
2225
import to.bitkit.repositories.WidgetsRepo
2326
import to.bitkit.ui.screens.widgets.blocks.toWeatherModel
27+
import java.math.BigDecimal
2428
import javax.inject.Inject
2529
import kotlin.time.Duration.Companion.seconds
2630

@@ -29,6 +33,7 @@ class HomeViewModel @Inject constructor(
2933
private val walletRepo: WalletRepo,
3034
private val widgetsRepo: WidgetsRepo,
3135
private val settingsStore: SettingsStore,
36+
private val currencyRepo: CurrencyRepo
3237
) : ViewModel() {
3338

3439
private val _uiState = MutableStateFlow(HomeUiState())
@@ -41,6 +46,7 @@ class HomeViewModel @Inject constructor(
4146
setupStateObservation()
4247
setupArticleRotation()
4348
setupFactRotation()
49+
checkHighBalance()
4450
}
4551

4652
private fun setupStateObservation() {
@@ -124,21 +130,56 @@ class HomeViewModel @Inject constructor(
124130
_currentFact.value = null
125131
}
126132

127-
fun removeSuggestion(suggestion: Suggestion) {
133+
private fun checkHighBalance() {
128134
viewModelScope.launch {
129-
settingsStore.addDismissedSuggestion(suggestion)
135+
delay(CHECK_DELAY_MILLISECONDS)
136+
137+
val settings = settingsStore.data.first()
138+
139+
val totalOnChainSats = walletRepo.balanceState.value.totalSats
140+
val balanceUsd = satsToUsd(totalOnChainSats) ?: return@launch
141+
val thresholdReached = balanceUsd > BigDecimal(BALANCE_THRESHOLD_USD)
142+
val isTimeOutOver = settings.lastTimeAskedBalanceWarningMillis - ASK_INTERVAL_MILLIS > ASK_INTERVAL_MILLIS
143+
val belowMaxWarnings = settings.balanceWarningTimes < MAX_WARNINGS
144+
145+
if (thresholdReached && isTimeOutOver && belowMaxWarnings && !_uiState.value.highBalanceSheetVisible) {
146+
settingsStore.update {
147+
it.copy(
148+
balanceWarningTimes = it.balanceWarningTimes + 1,
149+
lastTimeAskedBalanceWarningMillis = Clock.System.now().toEpochMilliseconds()
150+
)
151+
}
152+
_uiState.update { it.copy(highBalanceSheetVisible = true) }
153+
}
154+
155+
if (!thresholdReached) {
156+
settingsStore.update {
157+
it.copy(
158+
balanceWarningTimes = 0,
159+
)
160+
}
161+
}
130162
}
131163
}
132164

133-
fun refreshWidgets() {
165+
private fun satsToUsd(sats: ULong): BigDecimal? {
166+
val converted = currencyRepo.convertSatsToFiat(sats = sats.toLong(), currency = "USD")
167+
return converted?.value
168+
}
169+
170+
fun dismissHighBalanceSheet() {
171+
_uiState.update { it.copy(highBalanceSheetVisible = false) }
172+
}
173+
174+
fun removeSuggestion(suggestion: Suggestion) {
134175
viewModelScope.launch {
135-
widgetsRepo.refreshEnabledWidgets()
176+
settingsStore.addDismissedSuggestion(suggestion)
136177
}
137178
}
138179

139-
fun refreshSpecificWidget(widgetType: WidgetType) {
180+
fun refreshWidgets() {
140181
viewModelScope.launch {
141-
widgetsRepo.refreshWidget(widgetType)
182+
widgetsRepo.refreshEnabledWidgets()
142183
}
143184
}
144185

@@ -240,4 +281,16 @@ class HomeViewModel @Inject constructor(
240281
val dismissedList = settings.dismissedSuggestions.mapNotNull { it.toSuggestionOrNull() }
241282
baseSuggestions.filterNot { it in dismissedList }
242283
}
284+
285+
companion object {
286+
/**How high the balance must be to show this warning to the user (in USD)*/
287+
private const val BALANCE_THRESHOLD_USD = 500L
288+
private const val MAX_WARNINGS = 3
289+
290+
/** 1 day - how long this prompt will be hidden if user taps Later*/
291+
private const val ASK_INTERVAL_MILLIS = 1000 * 60 * 60 * 24
292+
293+
/**How long user needs to stay on the home screen before he will see this prompt*/
294+
private const val CHECK_DELAY_MILLISECONDS = 2500L
295+
}
243296
}
Lines changed: 162 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,162 @@
1+
package to.bitkit.ui.screens.wallets.sheets
2+
3+
import androidx.compose.foundation.Image
4+
import androidx.compose.foundation.layout.Arrangement
5+
import androidx.compose.foundation.layout.Column
6+
import androidx.compose.foundation.layout.Row
7+
import androidx.compose.foundation.layout.fillMaxSize
8+
import androidx.compose.foundation.layout.fillMaxWidth
9+
import androidx.compose.foundation.layout.padding
10+
import androidx.compose.material3.ExperimentalMaterial3Api
11+
import androidx.compose.material3.ModalBottomSheet
12+
import androidx.compose.material3.SheetState
13+
import androidx.compose.material3.rememberModalBottomSheetState
14+
import androidx.compose.runtime.Composable
15+
import androidx.compose.ui.Alignment
16+
import androidx.compose.ui.Modifier
17+
import androidx.compose.ui.platform.testTag
18+
import androidx.compose.ui.res.painterResource
19+
import androidx.compose.ui.res.stringResource
20+
import androidx.compose.ui.text.SpanStyle
21+
import androidx.compose.ui.text.font.FontWeight
22+
import androidx.compose.ui.tooling.preview.Preview
23+
import androidx.compose.ui.unit.dp
24+
import androidx.compose.ui.unit.sp
25+
import to.bitkit.R
26+
import to.bitkit.ui.components.BodyM
27+
import to.bitkit.ui.components.Display
28+
import to.bitkit.ui.components.ModalBottomSheetHandle
29+
import to.bitkit.ui.components.PrimaryButton
30+
import to.bitkit.ui.components.SecondaryButton
31+
import to.bitkit.ui.components.VerticalSpacer
32+
import to.bitkit.ui.scaffold.ScreenColumn
33+
import to.bitkit.ui.scaffold.SheetTopBar
34+
import to.bitkit.ui.shared.util.gradientBackground
35+
import to.bitkit.ui.theme.AppShapes
36+
import to.bitkit.ui.theme.AppThemeSurface
37+
import to.bitkit.ui.theme.Colors
38+
import to.bitkit.ui.theme.InterFontFamily
39+
import to.bitkit.ui.theme.ModalSheetTopPadding
40+
import to.bitkit.ui.utils.withAccent
41+
42+
@OptIn(ExperimentalMaterial3Api::class)
43+
@Composable
44+
fun HighBalanceWarningSheet(
45+
onDismiss: () -> Unit,
46+
understoodClick: () -> Unit,
47+
learnMoreClick: () -> Unit,
48+
modifier: Modifier = Modifier,
49+
) {
50+
val sheetState: SheetState = rememberModalBottomSheetState(skipPartiallyExpanded = true)
51+
52+
ModalBottomSheet(
53+
onDismissRequest = onDismiss,
54+
sheetState = sheetState,
55+
shape = AppShapes.sheet,
56+
containerColor = Colors.Black,
57+
dragHandle = { ModalBottomSheetHandle() },
58+
modifier = Modifier
59+
.fillMaxSize()
60+
.padding(top = ModalSheetTopPadding)
61+
) {
62+
HighBalanceWarningContent(
63+
understoodClick = understoodClick,
64+
learnMoreClick = learnMoreClick,
65+
modifier = modifier
66+
)
67+
}
68+
}
69+
70+
@Composable
71+
fun HighBalanceWarningContent(
72+
understoodClick: () -> Unit,
73+
learnMoreClick: () -> Unit,
74+
modifier: Modifier = Modifier,
75+
) {
76+
Column(
77+
modifier = modifier
78+
.fillMaxWidth()
79+
.gradientBackground()
80+
.testTag("high_balance_intro_screen")
81+
) {
82+
SheetTopBar(stringResource(R.string.other__high_balance__nav_title))
83+
84+
Column(
85+
modifier = Modifier.padding(horizontal = 16.dp),
86+
horizontalAlignment = Alignment.CenterHorizontally
87+
) {
88+
Image(
89+
painter = painterResource(R.drawable.exclamation_mark),
90+
contentDescription = null,
91+
modifier = Modifier
92+
.fillMaxWidth()
93+
.weight(1f)
94+
.testTag("high_balance_image")
95+
)
96+
97+
Display(
98+
text = stringResource(R.string.other__high_balance__title).withAccent(accentColor = Colors.Yellow),
99+
color = Colors.White,
100+
modifier = Modifier
101+
.fillMaxWidth()
102+
.testTag("high_balance_title")
103+
)
104+
VerticalSpacer(8.dp)
105+
BodyM(
106+
text =
107+
stringResource(R.string.other__high_balance__text).withAccent(
108+
defaultColor = Colors.White64,
109+
accentStyle = SpanStyle(
110+
fontWeight = FontWeight.Bold,
111+
fontSize = 17.sp,
112+
letterSpacing = 0.4.sp,
113+
fontFamily = InterFontFamily,
114+
color = Colors.White,
115+
)
116+
),
117+
color = Colors.White64,
118+
modifier = Modifier
119+
.testTag("high_balance_description")
120+
)
121+
VerticalSpacer(32.dp)
122+
Row(
123+
modifier = Modifier
124+
.fillMaxWidth()
125+
.testTag("buttons_row"),
126+
horizontalArrangement = Arrangement.spacedBy(16.dp)
127+
) {
128+
SecondaryButton(
129+
text = stringResource(R.string.other__high_balance__cancel),
130+
fullWidth = false,
131+
onClick = learnMoreClick,
132+
modifier = Modifier
133+
.weight(1f)
134+
.testTag("learn_more_button"),
135+
)
136+
137+
PrimaryButton(
138+
text = stringResource(R.string.other__high_balance__continue),
139+
fullWidth = false,
140+
onClick = understoodClick,
141+
modifier = Modifier
142+
.weight(1f)
143+
.testTag("understood_button"),
144+
)
145+
}
146+
VerticalSpacer(16.dp)
147+
}
148+
}
149+
}
150+
151+
152+
@OptIn(ExperimentalMaterial3Api::class)
153+
@Preview(showBackground = true)
154+
@Composable
155+
private fun Preview() {
156+
AppThemeSurface {
157+
HighBalanceWarningContent(
158+
understoodClick = {},
159+
learnMoreClick = {},
160+
)
161+
}
162+
}

0 commit comments

Comments
 (0)