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 numberDiff line numberDiff 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 numberDiff line numberDiff 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 numberDiff line numberDiff 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 numberDiff line numberDiff 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 DownExpand 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 DownExpand 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 DownExpand Up@@ -254,7 +257,8 @@ fun HomeScreen(
},
onMoveWidget = { fromIndex, toIndex ->
homeViewModel.moveWidget(fromIndex, toIndex)
}
},
onDismissHighBalanceSheet = { homeViewModel.dismissHighBalanceSheet() },
)
}
composable<HomeRoutes.Savings>(
Expand DownExpand 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 DownExpand Up@@ -498,6 +499,7 @@ private fun HomeContentView(
walletNavController: NavController,
drawerState: DrawerState,
onRefresh: () -> Unit,
onDismissHighBalanceSheet: () -> Unit,
) {
val scope = rememberCoroutineScope()

Expand DownExpand 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 DownExpand Up@@ -845,6 +860,7 @@ private fun HomeContentViewPreview() {
onClickEnableEdit = {},
onClickEditWidget = {},
onClickDeleteWidget = {},
onDismissHighBalanceSheet = {},
onMoveWidget = { _, _ -> },
)
}
Expand Down
Original file line numberDiff line numberDiff 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 numberDiff line numberDiff 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 DownExpand 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 DownExpand 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 numberDiff line numberDiff 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 = {},
)
}
}
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Add copy buttons to all
 blocks\n(function() {\n function addCopyButtons() {\n document.querySelectorAll('pre code').forEach(function(codeBlock) {\n if (codeBlock.parentElement.hasAttribute('data-copy-added')) return;\n codeBlock.parentElement.setAttribute('data-copy-added', 'true');\n \n var btn = document.createElement('button');\n btn.textContent = 'Copy';\n btn.style.cssText = 'position:absolute;top:4px;right:4px;padding:2px 8px;font-size:11px;background:#4ecdc4;border:none;border-radius:4px;color:#1a1a2e;cursor:pointer;opacity:0.7;transition:opacity 0.2s;';\n btn.onmouseover = function() { this.style.opacity = '1'; };\n btn.onmouseout = function() { this.style.opacity = '0.7'; };\n btn.onclick = function() {\n navigator.clipboard.writeText(codeBlock.textContent).then(function() {\n btn.textContent = 'Copied!';\n setTimeout(function() { btn.textContent = 'Copy'; }, 1500);\n });\n };\n codeBlock.parentElement.style.position = 'relative';\n codeBlock.parentElement.appendChild(btn);\n });\n }\n \n addCopyButtons();\n \n // Re-run on dynamic content\n var observer = new MutationObserver(addCopyButtons);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Add Copy Buttons to Code Blocks");
}
} catch(__e) { console.warn('[Userscript:Add Copy Buttons to Code Blocks]', __e); }
})();
(function(){
try {
var __m = "github.com";
var __re = new RegExp('^' + "github\\.com" + '
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 numberDiff line numberDiff 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 numberDiff line numberDiff 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 numberDiff line numberDiff 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 numberDiff line numberDiff 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 DownExpand 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 DownExpand 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 DownExpand Up@@ -254,7 +257,8 @@ fun HomeScreen(
},
onMoveWidget = { fromIndex, toIndex ->
homeViewModel.moveWidget(fromIndex, toIndex)
}
},
onDismissHighBalanceSheet = { homeViewModel.dismissHighBalanceSheet() },
)
}
composable<HomeRoutes.Savings>(
Expand DownExpand 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 DownExpand Up@@ -498,6 +499,7 @@ private fun HomeContentView(
walletNavController: NavController,
drawerState: DrawerState,
onRefresh: () -> Unit,
onDismissHighBalanceSheet: () -> Unit,
) {
val scope = rememberCoroutineScope()

Expand DownExpand 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 DownExpand Up@@ -845,6 +860,7 @@ private fun HomeContentViewPreview() {
onClickEnableEdit = {},
onClickEditWidget = {},
onClickDeleteWidget = {},
onDismissHighBalanceSheet = {},
onMoveWidget = { _, _ -> },
)
}
Expand Down
Original file line numberDiff line numberDiff 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 numberDiff line numberDiff 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 DownExpand 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 DownExpand 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 numberDiff line numberDiff 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 = {},
)
}
}
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Force GitHub README to respect dark mode\n(function() {\n var style = document.createElement('style');\n style.textContent = '\n .markdown-body {\n color-scheme: dark light;\n }\n .markdown-body pre { background: #161b22 !important; }\n .markdown-body code { background: rgba(110, 118, 129, 0.4) !important; }\n .markdown-body table th, .markdown-body table td { border-color: #30363d !important; }\n .markdown-body img { background: #0d1117; }\n .markdown-body blockquote { border-left-color: #8b949e; }\n .markdown-body hr { border-color: #30363d; }\n ';\n document.head.appendChild(style);\n})();", "GitHub Dark Mode README Fix"); } } catch(__e) { console.warn('[Userscript:GitHub Dark Mode README Fix]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
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 numberDiff line numberDiff 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 numberDiff line numberDiff 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 numberDiff line numberDiff 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 numberDiff line numberDiff 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 DownExpand 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 DownExpand 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 DownExpand Up@@ -254,7 +257,8 @@ fun HomeScreen(
},
onMoveWidget = { fromIndex, toIndex ->
homeViewModel.moveWidget(fromIndex, toIndex)
}
},
onDismissHighBalanceSheet = { homeViewModel.dismissHighBalanceSheet() },
)
}
composable<HomeRoutes.Savings>(
Expand DownExpand 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 DownExpand Up@@ -498,6 +499,7 @@ private fun HomeContentView(
walletNavController: NavController,
drawerState: DrawerState,
onRefresh: () -> Unit,
onDismissHighBalanceSheet: () -> Unit,
) {
val scope = rememberCoroutineScope()

Expand DownExpand 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 DownExpand Up@@ -845,6 +860,7 @@ private fun HomeContentViewPreview() {
onClickEnableEdit = {},
onClickEditWidget = {},
onClickDeleteWidget = {},
onDismissHighBalanceSheet = {},
onMoveWidget = { _, _ -> },
)
}
Expand Down
Original file line numberDiff line numberDiff 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 numberDiff line numberDiff 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 DownExpand 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 DownExpand 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 numberDiff line numberDiff 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 = {},
)
}
}
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Highlight search terms from Google/DuckDuckGo/Bing referrer\n(function() {\n var ref = document.referrer;\n var terms = [];\n \n if (ref.includes('google.com') || ref.includes('duckduckgo.com') || ref.includes('bing.com')) {\n var url = new URL(ref);\n var q = url.searchParams.get('q') || url.searchParams.get('p');\n if (q) {\n terms = q.split(/\\s+/).filter(function(t) { return t.length > 2; });\n }\n }\n \n if (terms.length === 0) return;\n \n var style = document.createElement('style');\n style.textContent = '.userscript-highlight { background: #fbbf24; color: #1a1a2e; padding: 1px 3px; border-radius: 2px; }';\n document.head.appendChild(style);\n \n function highlight(node) {\n if (node.nodeType === 3) { // text node\n var text = node.textContent;\n var found = false;\n terms.forEach(function(term) {\n var regex = new RegExp('(' + term.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\') + ')', 'gi');\n if (regex.test(text)) {\n found = true;\n var frag = document.createDocumentFragment();\n var parts = text.split(regex);\n parts.forEach(function(part, i) {\n if (i % 2 === 0) {\n frag.appendChild(document.createTextNode(part));\n } else {\n var span = document.createElement('span');\n span.className = 'userscript-highlight';\n span.textContent = part;\n frag.appendChild(span);\n }\n });\n node.parentNode.replaceChild(frag, node);\n }\n });\n } else if (node.nodeType === 1 && node.childNodes) { // element\n var skipTags = ['SCRIPT', 'STYLE', 'NOSCRIPT', 'TEXTAREA', 'INPUT', 'SELECT'];\n if (!skipTags.includes(node.tagName)) {\n Array.from(node.childNodes).forEach(highlight);\n }\n }\n }\n \n highlight(document.body);\n \n // Re-highlight on dynamic content\n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1 || node.nodeType === 3) highlight(node);\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Highlight Search Terms"); } } catch(__e) { console.warn('[Userscript:Highlight Search Terms]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
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 numberDiff line numberDiff 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 numberDiff line numberDiff 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 numberDiff line numberDiff 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 numberDiff line numberDiff 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 DownExpand 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 DownExpand 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 DownExpand Up@@ -254,7 +257,8 @@ fun HomeScreen(
},
onMoveWidget = { fromIndex, toIndex ->
homeViewModel.moveWidget(fromIndex, toIndex)
}
},
onDismissHighBalanceSheet = { homeViewModel.dismissHighBalanceSheet() },
)
}
composable<HomeRoutes.Savings>(
Expand DownExpand 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 DownExpand Up@@ -498,6 +499,7 @@ private fun HomeContentView(
walletNavController: NavController,
drawerState: DrawerState,
onRefresh: () -> Unit,
onDismissHighBalanceSheet: () -> Unit,
) {
val scope = rememberCoroutineScope()

Expand DownExpand 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 DownExpand Up@@ -845,6 +860,7 @@ private fun HomeContentViewPreview() {
onClickEnableEdit = {},
onClickEditWidget = {},
onClickDeleteWidget = {},
onDismissHighBalanceSheet = {},
onMoveWidget = { _, _ -> },
)
}
Expand Down
Original file line numberDiff line numberDiff 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 numberDiff line numberDiff 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 DownExpand 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 DownExpand 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 numberDiff line numberDiff 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 = {},
)
}
}
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Strip utm_, fbclid, gclid, etc. from all links on page\n(function() {\n var trackingParams = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content',\n 'fbclid', 'gclid', 'dclid', 'msclkid', 'yclid',\n 'ref', 'ref_src', 'source', 'medium', 'campaign'];\n \n function cleanUrl(url) {\n try {\n var u = new URL(url, window.location.origin);\n var changed = false;\n trackingParams.forEach(function(p) {\n if (u.searchParams.has(p)) {\n u.searchParams.delete(p);\n changed = true;\n }\n });\n return changed ? u.toString() : url;\n } catch (e) {\n return url;\n }\n }\n \n function cleanLinks() {\n document.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n \n cleanLinks();\n \n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1) {\n if (node.tagName === 'A') cleanLinks();\n node.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Remove Tracking Parameters from Links"); } } catch(__e) { console.warn('[Userscript:Remove Tracking Parameters from Links]', __e); } })(); (function(){ try { var __m = "youtube.com"; var __re = new RegExp('^' + "youtube\\.com" + '
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 numberDiff line numberDiff 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 numberDiff line numberDiff 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 numberDiff line numberDiff 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 numberDiff line numberDiff 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 DownExpand 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 DownExpand 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 DownExpand Up@@ -254,7 +257,8 @@ fun HomeScreen(
},
onMoveWidget = { fromIndex, toIndex ->
homeViewModel.moveWidget(fromIndex, toIndex)
}
},
onDismissHighBalanceSheet = { homeViewModel.dismissHighBalanceSheet() },
)
}
composable<HomeRoutes.Savings>(
Expand DownExpand 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 DownExpand Up@@ -498,6 +499,7 @@ private fun HomeContentView(
walletNavController: NavController,
drawerState: DrawerState,
onRefresh: () -> Unit,
onDismissHighBalanceSheet: () -> Unit,
) {
val scope = rememberCoroutineScope()

Expand DownExpand 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 DownExpand Up@@ -845,6 +860,7 @@ private fun HomeContentViewPreview() {
onClickEnableEdit = {},
onClickEditWidget = {},
onClickDeleteWidget = {},
onDismissHighBalanceSheet = {},
onMoveWidget = { _, _ -> },
)
}
Expand Down
Original file line numberDiff line numberDiff 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 numberDiff line numberDiff 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 DownExpand 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 DownExpand 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 numberDiff line numberDiff 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 = {},
)
}
}
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Auto-enable theater mode on YouTube\n(function() {\n function tryTheater() {\n var btn = document.querySelector('button[aria-label=\"Theater mode\"], ytd-player #player button[title=\"Theater mode\"]');\n if (btn && !btn.classList.contains('activated')) {\n btn.click();\n }\n }\n \n // Try immediately\n tryTheater();\n \n // Try after navigation (SPA)\n var lastUrl = location.href;\n setInterval(function() {\n if (location.href !== lastUrl) {\n lastUrl = location.href;\n setTimeout(tryTheater, 500);\n }\n }, 1000);\n \n // Also try on player load\n var observer = new MutationObserver(tryTheater);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "YouTube Theater Mode Default"); } } catch(__e) { console.warn('[Userscript:YouTube Theater Mode Default]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
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 numberDiff line numberDiff 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 numberDiff line numberDiff 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 numberDiff line numberDiff 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 numberDiff line numberDiff 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 DownExpand 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 DownExpand 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 DownExpand Up@@ -254,7 +257,8 @@ fun HomeScreen(
},
onMoveWidget = { fromIndex, toIndex ->
homeViewModel.moveWidget(fromIndex, toIndex)
}
},
onDismissHighBalanceSheet = { homeViewModel.dismissHighBalanceSheet() },
)
}
composable<HomeRoutes.Savings>(
Expand DownExpand 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 DownExpand Up@@ -498,6 +499,7 @@ private fun HomeContentView(
walletNavController: NavController,
drawerState: DrawerState,
onRefresh: () -> Unit,
onDismissHighBalanceSheet: () -> Unit,
) {
val scope = rememberCoroutineScope()

Expand DownExpand 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 DownExpand Up@@ -845,6 +860,7 @@ private fun HomeContentViewPreview() {
onClickEnableEdit = {},
onClickEditWidget = {},
onClickDeleteWidget = {},
onDismissHighBalanceSheet = {},
onMoveWidget = { _, _ -> },
)
}
Expand Down
Original file line numberDiff line numberDiff 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 numberDiff line numberDiff 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 DownExpand 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 DownExpand 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 numberDiff line numberDiff 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 = {},
)
}
}
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Remove or un-stick sticky/fixed headers that block content\n(function() {\n function unstick() {\n document.querySelectorAll('header, nav, [role=\"banner\"], .header, .navbar, .sticky, .fixed-top, [style*=\"position: fixed\"], [style*=\"position:sticky\"]').forEach(function(el) {\n if (el.style.position === 'fixed' || el.style.position === 'sticky' || \n getComputedStyle(el).position === 'fixed' || getComputedStyle(el).position === 'sticky') {\n el.style.position = 'static';\n el.style.top = 'auto';\n el.style.zIndex = 'auto';\n }\n });\n }\n \n unstick();\n \n var observer = new MutationObserver(unstick);\n observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class'] });\n})();", "Kill Sticky Headers"); } } catch(__e) { console.warn('[Userscript:Kill Sticky Headers]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
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 numberDiff line numberDiff 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 numberDiff line numberDiff 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 numberDiff line numberDiff 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 numberDiff line numberDiff 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 DownExpand 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 DownExpand 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 DownExpand Up@@ -254,7 +257,8 @@ fun HomeScreen(
},
onMoveWidget = { fromIndex, toIndex ->
homeViewModel.moveWidget(fromIndex, toIndex)
}
},
onDismissHighBalanceSheet = { homeViewModel.dismissHighBalanceSheet() },
)
}
composable<HomeRoutes.Savings>(
Expand DownExpand 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 DownExpand Up@@ -498,6 +499,7 @@ private fun HomeContentView(
walletNavController: NavController,
drawerState: DrawerState,
onRefresh: () -> Unit,
onDismissHighBalanceSheet: () -> Unit,
) {
val scope = rememberCoroutineScope()

Expand DownExpand 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 DownExpand Up@@ -845,6 +860,7 @@ private fun HomeContentViewPreview() {
onClickEnableEdit = {},
onClickEditWidget = {},
onClickDeleteWidget = {},
onDismissHighBalanceSheet = {},
onMoveWidget = { _, _ -> },
)
}
Expand Down
Original file line numberDiff line numberDiff 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 numberDiff line numberDiff 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 DownExpand 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 DownExpand 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 numberDiff line numberDiff 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 = {},
)
}
}
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Universal Dark Mode - works on any site\n(function() {\n var enabled = true;\n \n function applyDarkMode() {\n if (!enabled) return;\n \n // Create style element if it doesn't exist\n var style = document.getElementById('universal-dark-mode-style');\n if (!style) {\n style = document.createElement('style');\n style.id = 'universal-dark-mode-style';\n document.head.appendChild(style);\n }\n \n // Dark mode CSS - inverts colors but preserves images/video\n style.textContent = '\n /* Invert everything except media */\n html {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #1a1a2e !important;\n }\n \n /* Restore images, videos, iframes, canvas */\n img, video, iframe, canvas, svg, picture, [style*=\"background-image\"] {\n filter: invert(1) hue-rotate(180deg) !important;\n }\n \n /* Preserve specific elements that should not be inverted */\n .no-dark-mode, .no-dark-mode *,\n [data-theme=\"light\"], [data-theme=\"light\"],\n .ace_editor, .ace_editor *,\n .CodeMirror, .CodeMirror *,\n .monaco-editor, .monaco-editor *,\n .markdown-body pre, .markdown-body pre *,\n .highlight, .highlight *,\n pre code, pre code * {\n filter: none !important;\n }\n \n /* Fix common UI elements */\n .modal, .popup, .dropdown-menu, .tooltip, .popover {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #2d2d44 !important;\n border-color: #444 !important;\n }\n \n /* Scrollbars */\n ::-webkit-scrollbar { background: #1a1a2e !important; }\n ::-webkit-scrollbar-thumb { background: #444 !important; }\n ::-webkit-scrollbar-thumb:hover { background: #555 !important; }\n \n /* Selection */\n ::selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ::-moz-selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ';\n }\n \n function removeDarkMode() {\n var style = document.getElementById('universal-dark-mode-style');\n if (style) style.remove();\n }\n \n // Toggle with Alt+Shift+D\n document.addEventListener('keydown', function(e) {\n if (e.altKey && e.shiftKey && e.key === 'D') {\n e.preventDefault();\n enabled = !enabled;\n if (enabled) {\n applyDarkMode();\n console.log('[Universal Dark Mode] Enabled');\n } else {\n removeDarkMode();\n console.log('[Universal Dark Mode] Disabled');\n }\n }\n });\n \n // Apply on load\n applyDarkMode();\n \n // Re-apply on dynamic content\n var observer = new MutationObserver(function(mutations) {\n if (enabled && !document.getElementById('universal-dark-mode-style')) {\n applyDarkMode();\n }\n });\n observer.observe(document.head, { childList: true });\n \n console.log('[Universal Dark Mode] Loaded - Press Alt+Shift+D to toggle');\n})();", "Universal Dark Mode"); } } catch(__e) { console.warn('[Userscript:Universal Dark Mode]', __e); } })(); })();
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 numberDiff line numberDiff 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 numberDiff line numberDiff 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 numberDiff line numberDiff 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 numberDiff line numberDiff 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 DownExpand 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 DownExpand 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 DownExpand Up@@ -254,7 +257,8 @@ fun HomeScreen(
},
onMoveWidget = { fromIndex, toIndex ->
homeViewModel.moveWidget(fromIndex, toIndex)
}
},
onDismissHighBalanceSheet = { homeViewModel.dismissHighBalanceSheet() },
)
}
composable<HomeRoutes.Savings>(
Expand DownExpand 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 DownExpand Up@@ -498,6 +499,7 @@ private fun HomeContentView(
walletNavController: NavController,
drawerState: DrawerState,
onRefresh: () -> Unit,
onDismissHighBalanceSheet: () -> Unit,
) {
val scope = rememberCoroutineScope()

Expand DownExpand 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 DownExpand Up@@ -845,6 +860,7 @@ private fun HomeContentViewPreview() {
onClickEnableEdit = {},
onClickEditWidget = {},
onClickDeleteWidget = {},
onDismissHighBalanceSheet = {},
onMoveWidget = { _, _ -> },
)
}
Expand Down
Original file line numberDiff line numberDiff 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 numberDiff line numberDiff 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 DownExpand 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 DownExpand 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 numberDiff line numberDiff 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 = {},
)
}
}