diff --git a/api/src/main/java/com/getcode/analytics/AnalyticsManager.kt b/api/src/main/java/com/getcode/analytics/AnalyticsManager.kt index e7cea61d1..166f1e379 100644 --- a/api/src/main/java/com/getcode/analytics/AnalyticsManager.kt +++ b/api/src/main/java/com/getcode/analytics/AnalyticsManager.kt @@ -343,6 +343,8 @@ class AnalyticsManager @Inject constructor( Backup("Backup Screen"), Withdraw("Withdraw Screen"), Debug("Debug Screen"), + Share("Share Screen"), + AppSettings("App Settings"), ForceUpgrade("Force Upgrade"), BuyMoreKin("Buy More Kin Screen"), SendKin("Send Kin Screen"), diff --git a/api/src/main/java/com/getcode/model/PrefBool.kt b/api/src/main/java/com/getcode/model/PrefBool.kt index 614e9ae44..a344eb757 100644 --- a/api/src/main/java/com/getcode/model/PrefBool.kt +++ b/api/src/main/java/com/getcode/model/PrefBool.kt @@ -11,18 +11,24 @@ data class PrefBool( val value: Boolean ) - +sealed interface InternalRouting +sealed interface AppSetting sealed interface BetaFlag + + sealed class PrefsBool(val value: String) { // internal routing - data object IS_DEBUG_ACTIVE: PrefsBool("debug_menu_active") - data object IS_DEBUG_ALLOWED: PrefsBool("debug_menu_allowed") - data object IS_ELIGIBLE_GET_FIRST_KIN_AIRDROP: PrefsBool("is_eligible_get_first_kin_airdrop") - data object IS_ELIGIBLE_GIVE_FIRST_KIN_AIRDROP: PrefsBool("is_eligible_give_first_kin_airdrop") - data object HAS_REMOVED_LOCAL_CURRENCY: PrefsBool("removed_local_currency") - data object SEEN_TIP_CARD : PrefsBool("seen_tip_card") + data object IS_DEBUG_ACTIVE: PrefsBool("debug_menu_active"), InternalRouting + data object IS_DEBUG_ALLOWED: PrefsBool("debug_menu_allowed"), InternalRouting + data object IS_ELIGIBLE_GET_FIRST_KIN_AIRDROP: PrefsBool("is_eligible_get_first_kin_airdrop"), InternalRouting + data object IS_ELIGIBLE_GIVE_FIRST_KIN_AIRDROP: PrefsBool("is_eligible_give_first_kin_airdrop"), InternalRouting + data object HAS_REMOVED_LOCAL_CURRENCY: PrefsBool("removed_local_currency"), InternalRouting + data object SEEN_TIP_CARD : PrefsBool("seen_tip_card"), InternalRouting - data object BUY_MODULE_AVAILABLE : PrefsBool("buy_module_available") + data object BUY_MODULE_AVAILABLE : PrefsBool("buy_module_available"), InternalRouting + + // app settings + data object CAMERA_START_BY_DEFAULT: PrefsBool("camera_start_default"), AppSetting // beta flags data object BUCKET_DEBUGGER_ENABLED: PrefsBool("debug_buckets"), BetaFlag @@ -39,4 +45,6 @@ sealed class PrefsBool(val value: String) { data object TIPS_CHAT_ENABLED: PrefsBool("tips_chat_enabled"), BetaFlag data object TIPS_CHAT_CASH_ENABLED: PrefsBool("tips_chat_cash_enabled"), BetaFlag data object BALANCE_CURRENCY_SELECTION_ENABLED: PrefsBool("balance_currency_enabled"), BetaFlag -} \ No newline at end of file +} + +val APP_SETTINGS: List = listOf(PrefsBool.CAMERA_START_BY_DEFAULT) \ No newline at end of file diff --git a/api/src/main/java/com/getcode/network/repository/AppSettingsRepository.kt b/api/src/main/java/com/getcode/network/repository/AppSettingsRepository.kt new file mode 100644 index 000000000..915d89418 --- /dev/null +++ b/api/src/main/java/com/getcode/network/repository/AppSettingsRepository.kt @@ -0,0 +1,43 @@ +package com.getcode.network.repository + +import com.getcode.model.AppSetting +import com.getcode.model.PrefsBool +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.map +import javax.inject.Inject + +data class AppSettings( + val cameraStartByDefault: Boolean +) { + companion object { + val Defaults = AppSettings( + cameraStartByDefault = true + ) + } +} +class AppSettingsRepository @Inject constructor( + private val prefRepository: PrefRepository, +) { + + fun observe(): Flow = AppSettings.Defaults.let { defaults -> + prefRepository.observeOrDefault(PrefsBool.CAMERA_START_BY_DEFAULT, defaults.cameraStartByDefault) + .map { AppSettings(it) } + } + + suspend fun get(setting: AppSetting): Boolean { + return when (setting) { + PrefsBool.CAMERA_START_BY_DEFAULT -> prefRepository.get( + PrefsBool.CAMERA_START_BY_DEFAULT, + AppSettings.Defaults.cameraStartByDefault + ) + } + } + + fun update(setting: AppSetting, value: Boolean) { + when (setting) { + PrefsBool.CAMERA_START_BY_DEFAULT -> { + prefRepository.set(PrefsBool.CAMERA_START_BY_DEFAULT, value) + } + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/getcode/navigation/screens/MainScreens.kt b/app/src/main/java/com/getcode/navigation/screens/MainScreens.kt index d602b35ca..9c48545a1 100644 --- a/app/src/main/java/com/getcode/navigation/screens/MainScreens.kt +++ b/app/src/main/java/com/getcode/navigation/screens/MainScreens.kt @@ -240,6 +240,11 @@ data object ShareDownloadLinkModal : MainGraph, ModalRoot { ) { ShareDownloadScreen() } + + AnalyticsScreenWatcher( + lifecycleOwner = LocalLifecycleOwner.current, + event = AnalyticsManager.Screen.Share + ) } } diff --git a/app/src/main/java/com/getcode/navigation/screens/ModalScreens.kt b/app/src/main/java/com/getcode/navigation/screens/ModalScreens.kt index d60fda5db..e2c7e7108 100644 --- a/app/src/main/java/com/getcode/navigation/screens/ModalScreens.kt +++ b/app/src/main/java/com/getcode/navigation/screens/ModalScreens.kt @@ -20,6 +20,7 @@ import com.getcode.view.main.account.AccountDeposit import com.getcode.view.main.account.AccountDetails import com.getcode.view.main.account.AccountFaq import com.getcode.view.main.account.AccountPhone +import com.getcode.view.main.account.AppSettingsScreen import com.getcode.view.main.account.BackupKey import com.getcode.view.main.account.BetaFlagsScreen import com.getcode.view.main.account.ConfirmDeleteAccount @@ -101,6 +102,27 @@ data object AccountDebugOptionsScreen : MainGraph, ModalContent { } } +@Parcelize +data object AppSettingsScreen : MainGraph, ModalContent { + @IgnoredOnParcel + override val key: ScreenKey = uniqueScreenKey + + override val name: String + @Composable get() = stringResource(id = R.string.title_appSettings) + + @Composable + override fun Content() { + ModalContainer(backButtonEnabled = { it is AppSettingsScreen }) { + AppSettingsScreen(getViewModel()) + } + + AnalyticsScreenWatcher( + lifecycleOwner = LocalLifecycleOwner.current, + event = AnalyticsManager.Screen.AppSettings + ) + } +} + @Parcelize data object AccountDetailsScreen : MainGraph, ModalContent { @IgnoredOnParcel diff --git a/app/src/main/java/com/getcode/ui/components/CodeButton.kt b/app/src/main/java/com/getcode/ui/components/CodeButton.kt index f8c1d2079..495652064 100644 --- a/app/src/main/java/com/getcode/ui/components/CodeButton.kt +++ b/app/src/main/java/com/getcode/ui/components/CodeButton.kt @@ -44,6 +44,10 @@ fun CodeButton( isLoading: Boolean = false, isSuccess: Boolean = false, enabled: Boolean = true, + contentPadding: PaddingValues = PaddingValues( + top = CodeTheme.dimens.grid.x3, + bottom = CodeTheme.dimens.grid.x3, + ), buttonState: ButtonState = ButtonState.Bordered, textColor: Color = Color.Unspecified, shape: Shape = CodeTheme.shapes.small, @@ -55,6 +59,7 @@ fun CodeButton( isSuccess = isSuccess, enabled = enabled, buttonState = buttonState, + contentPadding = contentPadding, shape = shape, contentColor = textColor, ) { @@ -72,6 +77,10 @@ fun CodeButton( enabled: Boolean = true, buttonState: ButtonState = ButtonState.Bordered, shape: Shape = CodeTheme.shapes.small, + contentPadding: PaddingValues = PaddingValues( + top = CodeTheme.dimens.grid.x3, + bottom = CodeTheme.dimens.grid.x3, + ), contentColor: Color = Color.Unspecified, content: @Composable RowScope.() -> Unit, ) { @@ -111,10 +120,7 @@ fun CodeButton( pressedElevation = 0.dp ), shape = shape, - contentPadding = ButtonDefaults.ContentPadding.plus( - top = CodeTheme.dimens.grid.x3, - bottom = CodeTheme.dimens.grid.x3, - ) + contentPadding = ButtonDefaults.ContentPadding.plus(contentPadding) ) { when { isLoading -> { diff --git a/app/src/main/java/com/getcode/ui/components/CodeSwitch.kt b/app/src/main/java/com/getcode/ui/components/CodeSwitch.kt index 2d6590cb9..5cec48bd6 100644 --- a/app/src/main/java/com/getcode/ui/components/CodeSwitch.kt +++ b/app/src/main/java/com/getcode/ui/components/CodeSwitch.kt @@ -1,37 +1,85 @@ package com.getcode.ui.components -import androidx.compose.foundation.interaction.MutableInteractionSource -import androidx.compose.material.ExperimentalMaterialApi -import androidx.compose.material.Switch +import androidx.compose.animation.core.animateFloatAsState +import androidx.compose.foundation.Canvas +import androidx.compose.foundation.gestures.detectTapGestures +import androidx.compose.foundation.layout.size +import androidx.compose.material.SwitchColors import androidx.compose.material.SwitchDefaults import androidx.compose.runtime.Composable -import androidx.compose.runtime.remember +import androidx.compose.runtime.getValue import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.scale +import androidx.compose.ui.geometry.CornerRadius +import androidx.compose.ui.geometry.Offset import androidx.compose.ui.graphics.Color -import com.getcode.theme.CodeTheme +import androidx.compose.ui.input.pointer.pointerInput +import androidx.compose.ui.platform.LocalDensity +import androidx.compose.ui.unit.dp +import com.getcode.theme.SystemGreen import com.getcode.theme.White -import com.getcode.theme.White50 +import com.getcode.ui.utils.addIf + +object CodeToggleSwitchDefaults { + + val colors: SwitchColors + @Composable get() = SwitchDefaults.colors( + checkedThumbColor = White, + uncheckedThumbColor = White, + checkedTrackColor = SystemGreen, + checkedTrackAlpha = 1f, + uncheckedTrackAlpha = 1f, + uncheckedTrackColor = Color(0xFF201D2F) + ) +} @Composable -fun CodeSwitch( - checked: Boolean, - onCheckedChange: ((Boolean) -> Unit)?, +fun CodeToggleSwitch( modifier: Modifier = Modifier, enabled: Boolean = true, - interactionSource: MutableInteractionSource = remember { MutableInteractionSource() }, + checked: Boolean, + onCheckedChange: ((Boolean) -> Unit)? = null, ) { - Switch( - checked = checked, - onCheckedChange = onCheckedChange, - modifier = modifier, - enabled = enabled, - interactionSource = interactionSource, - colors = SwitchDefaults.colors( - checkedThumbColor = White, - uncheckedThumbColor = White50, - checkedTrackColor = CodeTheme.colors.brandLight, - checkedTrackAlpha = 1f, - uncheckedTrackColor = CodeTheme.colors.brandLight - ) + val width = 51.dp + val height = 31.dp + val gapBetweenThumbAndTrackEdge = 2.dp + + val thumbRadius = (height / 2) - gapBetweenThumbAndTrackEdge + val animatePosition = animateFloatAsState( + targetValue = if (checked) { + with(LocalDensity.current) { (width - thumbRadius - gapBetweenThumbAndTrackEdge).toPx() } + } else { + with (LocalDensity.current) { (thumbRadius + gapBetweenThumbAndTrackEdge).toPx() } + }, label = "thumb position" ) + + val colors = CodeToggleSwitchDefaults.colors + val trackColor by colors.trackColor(enabled = enabled, checked = checked) + val thumbColor by colors.thumbColor(enabled = enabled, checked = checked) + Canvas( + modifier = Modifier + .size(width = width, height = height) + .addIf(onCheckedChange != null) { + Modifier.pointerInput(Unit) { + detectTapGestures( + onTap = { + onCheckedChange?.invoke(!checked) + } + ) + } + }.then(modifier) + ) { + drawRoundRect( + color = trackColor, + cornerRadius = CornerRadius(x = 45.dp.toPx(), y = 45.dp.toPx()) + ) + drawCircle( + color = thumbColor, + radius = thumbRadius.toPx(), + center = Offset( + x = animatePosition.value, + y = size.height / 2 + ) + ) + } } \ No newline at end of file diff --git a/app/src/main/java/com/getcode/ui/components/SettingsRow.kt b/app/src/main/java/com/getcode/ui/components/SettingsRow.kt new file mode 100644 index 000000000..9314960f3 --- /dev/null +++ b/app/src/main/java/com/getcode/ui/components/SettingsRow.kt @@ -0,0 +1,68 @@ +package com.getcode.ui.components + +import androidx.compose.foundation.Image +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.width +import androidx.compose.foundation.layout.wrapContentSize +import androidx.compose.material.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.painterResource +import com.getcode.theme.CodeTheme +import com.getcode.ui.utils.rememberedClickable + +@Composable +fun SettingsRow( + modifier: Modifier = Modifier, + title: String, + icon: Int? = null, + subtitle: String? = null, + checked: Boolean, + onClick: () -> Unit) { + Row( + modifier = modifier + .rememberedClickable { onClick() } + .padding(horizontal = CodeTheme.dimens.grid.x3) + .padding(end = CodeTheme.dimens.grid.x3), + verticalAlignment = Alignment.CenterVertically + ) { + if (icon != null) { + Image( + modifier = Modifier + .padding(end = CodeTheme.dimens.inset) + .height(CodeTheme.dimens.staticGrid.x5) + .width(CodeTheme.dimens.staticGrid.x5), + painter = painterResource(id = icon), + contentDescription = "" + ) + } + Column( + modifier = Modifier + .weight(1f) + .padding(vertical = CodeTheme.dimens.grid.x3) + ) { + Text( + modifier = Modifier + .padding(vertical = CodeTheme.dimens.grid.x2), + text = title, + ) + if (!subtitle.isNullOrEmpty()) { + Text( + modifier = Modifier + .padding(vertical = CodeTheme.dimens.grid.x1), + text = subtitle, + style = CodeTheme.typography.caption, + color = CodeTheme.colors.textSecondary + ) + } + } + + CodeToggleSwitch( + checked = checked, + onCheckedChange = null, + ) + } +} \ No newline at end of file diff --git a/app/src/main/java/com/getcode/ui/components/TopBarContainer.kt b/app/src/main/java/com/getcode/ui/components/TopBarContainer.kt index aca009edb..a1130c214 100644 --- a/app/src/main/java/com/getcode/ui/components/TopBarContainer.kt +++ b/app/src/main/java/com/getcode/ui/components/TopBarContainer.kt @@ -10,7 +10,6 @@ import androidx.compose.foundation.interaction.MutableInteractionSource import androidx.compose.foundation.layout.* import androidx.compose.material.* import androidx.compose.runtime.* -import androidx.compose.runtime.livedata.observeAsState import androidx.compose.ui.Alignment.Companion.CenterVertically import androidx.compose.ui.Modifier import androidx.compose.ui.draw.drawBehind @@ -92,9 +91,9 @@ private fun TopBarView( when (topBarMessage.type) { ERROR_NETWORK, ERROR -> CodeTheme.colors.error WARNING -> Warning - NOTIFICATION -> topNotification - NEUTRAL -> topNeutral - SUCCESS -> topSuccess + NOTIFICATION -> TopNotification + NEUTRAL -> TopNeutral + SUCCESS -> TopSuccess } ) diff --git a/app/src/main/java/com/getcode/ui/utils/PaddingValues.kt b/app/src/main/java/com/getcode/ui/utils/PaddingValues.kt index 707c48825..490a50b56 100644 --- a/app/src/main/java/com/getcode/ui/utils/PaddingValues.kt +++ b/app/src/main/java/com/getcode/ui/utils/PaddingValues.kt @@ -55,3 +55,15 @@ fun PaddingValues.plus( bottom = calculateBottomPadding() + bottom, ) } + +@Composable +fun PaddingValues.plus( + other: PaddingValues, +): PaddingValues { + return PaddingValues( + start = calculateStartPadding() + other.calculateStartPadding(), + top = calculateTopPadding() + other.calculateTopPadding(), + end = calculateEndPadding() + other.calculateEndPadding(), + bottom = calculateBottomPadding() + other.calculateBottomPadding(), + ) +} \ No newline at end of file diff --git a/app/src/main/java/com/getcode/view/main/account/AccountHome.kt b/app/src/main/java/com/getcode/view/main/account/AccountHome.kt index e87d8b2d8..4d4abefaf 100644 --- a/app/src/main/java/com/getcode/view/main/account/AccountHome.kt +++ b/app/src/main/java/com/getcode/view/main/account/AccountHome.kt @@ -41,6 +41,7 @@ import com.getcode.manager.TopBarManager import com.getcode.navigation.core.LocalCodeNavigator import com.getcode.navigation.screens.AccountDebugOptionsScreen import com.getcode.navigation.screens.AccountDetailsScreen +import com.getcode.navigation.screens.AppSettingsScreen import com.getcode.navigation.screens.BuyMoreKinModal import com.getcode.navigation.screens.BuySellScreen import com.getcode.navigation.screens.DepositKinScreen @@ -90,6 +91,7 @@ fun AccountHome( AccountPage.FAQ -> navigator.push(FaqScreen) AccountPage.ACCOUNT_DETAILS -> navigator.push(AccountDetailsScreen) AccountPage.ACCOUNT_DEBUG_OPTIONS -> navigator.push(AccountDebugOptionsScreen) + AccountPage.APP_SETTINGS -> navigator.push(AppSettingsScreen) AccountPage.LOGOUT -> { BottomBarManager.showMessage( BottomBarManager.BottomBarMessage( diff --git a/app/src/main/java/com/getcode/view/main/account/AccountSheetViewModel.kt b/app/src/main/java/com/getcode/view/main/account/AccountSheetViewModel.kt index 751820654..cd26e9eaf 100644 --- a/app/src/main/java/com/getcode/view/main/account/AccountSheetViewModel.kt +++ b/app/src/main/java/com/getcode/view/main/account/AccountSheetViewModel.kt @@ -27,6 +27,7 @@ import javax.inject.Inject data class AccountMainItem( val type: AccountPage, val name: Int, + val description: Int? = null, val icon: Int, val isPhoneLinked: Boolean? = null, ) @@ -40,6 +41,7 @@ enum class AccountPage { ACCESS_KEY, FAQ, ACCOUNT_DETAILS, + APP_SETTINGS, ACCOUNT_DEBUG_OPTIONS, LOGOUT } @@ -135,6 +137,11 @@ class AccountSheetViewModel @Inject constructor( name = R.string.title_myAccount, icon = R.drawable.ic_menu_account ), + AccountMainItem( + type = AccountPage.APP_SETTINGS, + name = R.string.title_appSettings, + icon = R.drawable.ic_settings_outline, + ), AccountMainItem( type = AccountPage.ACCOUNT_DEBUG_OPTIONS, name = R.string.title_betaFlags, diff --git a/app/src/main/java/com/getcode/view/main/account/AppSettingsScreen.kt b/app/src/main/java/com/getcode/view/main/account/AppSettingsScreen.kt new file mode 100644 index 000000000..01bfc4fc9 --- /dev/null +++ b/app/src/main/java/com/getcode/view/main/account/AppSettingsScreen.kt @@ -0,0 +1,36 @@ +package com.getcode.view.main.account + +import androidx.compose.foundation.ExperimentalFoundationApi +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.items +import androidx.compose.runtime.Composable +import androidx.compose.runtime.collectAsState +import androidx.compose.runtime.getValue +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.stringResource +import com.getcode.navigation.screens.AppSettingsScreen +import com.getcode.ui.components.SettingsRow + +@OptIn(ExperimentalFoundationApi::class) +@Composable +fun AppSettingsScreen( + viewModel: AppSettingsViewModel +) { + val state by viewModel.stateFlow.collectAsState() + + LazyColumn { + items(state.settings) { option -> + SettingsRow( + modifier = Modifier.animateItemPlacement(), + title = stringResource(id = option.name), + icon = option.icon, + subtitle = option.description?.let { stringResource(id = it) }, + checked = option.enabled + ) { + viewModel.dispatchEvent( + AppSettingsViewModel.Event.SettingChanged(option.type, !option.enabled) + ) + } + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/getcode/view/main/account/AppSettingsViewModel.kt b/app/src/main/java/com/getcode/view/main/account/AppSettingsViewModel.kt new file mode 100644 index 000000000..31e5439b7 --- /dev/null +++ b/app/src/main/java/com/getcode/view/main/account/AppSettingsViewModel.kt @@ -0,0 +1,78 @@ +package com.getcode.view.main.account + +import androidx.lifecycle.viewModelScope +import com.getcode.R +import com.getcode.model.APP_SETTINGS +import com.getcode.model.AppSetting +import com.getcode.model.PrefsBool +import com.getcode.network.repository.AppSettings +import com.getcode.network.repository.AppSettingsRepository +import com.getcode.view.BaseViewModel2 +import dagger.hilt.android.lifecycle.HiltViewModel +import kotlinx.coroutines.flow.distinctUntilChanged +import kotlinx.coroutines.flow.filterIsInstance +import kotlinx.coroutines.flow.launchIn +import kotlinx.coroutines.flow.map +import kotlinx.coroutines.flow.onEach +import javax.inject.Inject + +data class SettingItem( + val type: AppSetting, + val name: Int, + val description: Int? = null, + val icon: Int, + val enabled: Boolean, +) + +@HiltViewModel +class AppSettingsViewModel @Inject constructor( + appSettings: AppSettingsRepository, +) : BaseViewModel2( + initialState = State(emptyList()), + updateStateForEvent = updateStateForEvent +) { + data class State( + val settings: List, + ) + + sealed interface Event { + data class UpdateSettings(val settings: List) : Event + data class SettingChanged(val setting: AppSetting, val value: Boolean): Event + } + + init { + appSettings.observe() + .distinctUntilChanged() + .map { settings -> + APP_SETTINGS.map { setting -> + when (setting) { + PrefsBool.CAMERA_START_BY_DEFAULT -> SettingItem( + type = setting, + name = R.string.title_autoStartCamera, + icon = R.drawable.ic_camera_outline, + enabled = settings.cameraStartByDefault + ) + } + } + } + .onEach { settings -> + dispatchEvent(Event.UpdateSettings(settings)) + }.launchIn(viewModelScope) + + eventFlow + .filterIsInstance() + .map { it.setting to it.value } + .onEach { (setting, value) -> + appSettings.update(setting, value) + }.launchIn(viewModelScope) + } + + companion object { + val updateStateForEvent: (Event) -> ((State) -> State) = { event -> + when (event) { + is Event.SettingChanged -> { state -> state } + is Event.UpdateSettings -> { state -> state.copy(settings = event.settings) } + } + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/getcode/view/main/account/BetaFlagsScreen.kt b/app/src/main/java/com/getcode/view/main/account/BetaFlagsScreen.kt index b910caca0..7962368a3 100644 --- a/app/src/main/java/com/getcode/view/main/account/BetaFlagsScreen.kt +++ b/app/src/main/java/com/getcode/view/main/account/BetaFlagsScreen.kt @@ -2,18 +2,13 @@ package com.getcode.view.main.account import android.widget.Toast import androidx.compose.foundation.ExperimentalFoundationApi -import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.layout.wrapContentSize import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.items -import androidx.compose.material.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.collectAsState import androidx.compose.runtime.getValue -import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.res.stringResource @@ -22,8 +17,7 @@ import com.getcode.model.PrefsBool import com.getcode.theme.CodeTheme import com.getcode.ui.components.ButtonState import com.getcode.ui.components.CodeButton -import com.getcode.ui.utils.rememberedClickable -import com.getcode.ui.components.CodeSwitch +import com.getcode.ui.components.SettingsRow import dev.bmcreations.tipkit.engines.LocalTipsEngine @OptIn(ExperimentalFoundationApi::class) @@ -120,40 +114,13 @@ fun BetaFlagsScreen( LazyColumn { items(options) { option -> - Row( - modifier = Modifier - .animateItemPlacement() - .rememberedClickable { option.onChange(!option.dataState) } - .padding(horizontal = CodeTheme.dimens.grid.x3) - .padding(end = CodeTheme.dimens.grid.x3), - verticalAlignment = Alignment.CenterVertically + SettingsRow( + modifier = Modifier.animateItemPlacement(), + title = stringResource(id = option.titleResId), + subtitle = option.subtitleText, + checked = option.dataState ) { - Column( - modifier = Modifier - .weight(1f) - .padding(vertical = CodeTheme.dimens.grid.x3) - ) { - Text( - modifier = Modifier - .padding(vertical = CodeTheme.dimens.grid.x2), - text = stringResource(id = option.titleResId) - ) - if (option.subtitleText.isNotEmpty()) { - Text( - modifier = Modifier - .padding(vertical = CodeTheme.dimens.grid.x1), - text = option.subtitleText, - style = CodeTheme.typography.caption, - color = CodeTheme.colors.textSecondary - ) - } - } - - CodeSwitch( - modifier = Modifier.wrapContentSize(), - checked = option.dataState, - onCheckedChange = null, - ) + option.onChange(!option.dataState) } } @@ -177,7 +144,7 @@ fun BetaFlagsScreen( private fun BetaFlagsViewModel.State.canMutate(flag: PrefsBool): Boolean { return when (flag) { PrefsBool.BUY_MODULE_ENABLED -> false - PrefsBool.TIPS_ENABLED -> false + PrefsBool.BALANCE_CURRENCY_SELECTION_ENABLED -> false PrefsBool.TIPS_CHAT_CASH_ENABLED -> tipsChatEnabled else -> true } diff --git a/app/src/main/java/com/getcode/view/main/home/HomeScan.kt b/app/src/main/java/com/getcode/view/main/home/HomeScan.kt index 3a010c697..c5737eb9d 100644 --- a/app/src/main/java/com/getcode/view/main/home/HomeScan.kt +++ b/app/src/main/java/com/getcode/view/main/home/HomeScan.kt @@ -63,6 +63,7 @@ import com.getcode.ui.utils.ModalAnimationSpeed import com.getcode.ui.utils.addIf import com.getcode.ui.utils.measured import com.getcode.view.main.home.components.BillManagementOptions +import com.getcode.view.main.home.components.CameraDisabledView import com.getcode.view.main.home.components.CodeScanner import com.getcode.view.main.home.components.HomeBill import com.getcode.view.main.home.components.LoginConfirmation @@ -122,6 +123,7 @@ fun HomeScreen( HomeEvent.PresentTipEntry -> { navigator.show(EnterTipModal()) } + is HomeEvent.SendIntent -> { context.startActivity(it.intent) } @@ -300,20 +302,30 @@ private fun BillContainer( Box( modifier = Modifier .fillMaxSize() - .addIf(dataState.isCameraPermissionGranted != true) { Modifier.background(Color.Black) } .then(modifier) ) { - if (dataState.isCameraPermissionGranted == true || dataState.isCameraPermissionGranted == null) { - scannerView() - } else { - PermissionsBlockingView( - modifier = Modifier - .align(Center) - .fillMaxWidth(0.85f), - context = context, - onPermissionResult = onPermissionResult, - launcher = launcher - ) + when { + dataState.isCameraPermissionGranted == true + || dataState.isCameraPermissionGranted == null -> { + if (dataState.autoStartCamera == null) { + // waiting for result + } else if (!dataState.autoStartCamera) { + CameraDisabledView(modifier = Modifier.fillMaxSize()) { + homeViewModel.onStartCamera() + } + } else { + scannerView() + } + } + + else -> { + PermissionsBlockingView( + modifier = Modifier.fillMaxSize(), + context = context, + onPermissionResult = onPermissionResult, + launcher = launcher + ) + } } val updatedState by rememberUpdatedState(newValue = dataState) diff --git a/app/src/main/java/com/getcode/view/main/home/HomeViewModel.kt b/app/src/main/java/com/getcode/view/main/home/HomeViewModel.kt index 03bf3df44..2d2db4d30 100644 --- a/app/src/main/java/com/getcode/view/main/home/HomeViewModel.kt +++ b/app/src/main/java/com/getcode/view/main/home/HomeViewModel.kt @@ -17,6 +17,7 @@ import com.getcode.manager.GiftCardManager import com.getcode.manager.MnemonicManager import com.getcode.manager.SessionManager import com.getcode.manager.TopBarManager +import com.getcode.model.AppSetting import com.getcode.model.BuyModuleFeature import com.getcode.model.CodePayload import com.getcode.model.Currency @@ -60,6 +61,8 @@ import com.getcode.network.client.requestFirstKinAirdrop import com.getcode.network.client.sendRemotely import com.getcode.network.client.sendRequestToReceiveBill import com.getcode.network.exchange.Exchange +import com.getcode.network.repository.AppSettings +import com.getcode.network.repository.AppSettingsRepository import com.getcode.network.repository.BetaFlagsRepository import com.getcode.network.repository.FeatureRepository import com.getcode.network.repository.PaymentRepository @@ -138,6 +141,7 @@ data class HomeUiModel( val balance: KinAmount? = null, val logScanTimes: Boolean = false, val showNetworkOffline: Boolean = false, + val autoStartCamera: Boolean? = null, val isCameraScanEnabled: Boolean = true, val presentationStyle: PresentationStyle = PresentationStyle.Hidden, val billState: BillState = BillState.Default, @@ -181,6 +185,7 @@ class HomeViewModel @Inject constructor( private val exchange: Exchange, private val giftCardManager: GiftCardManager, private val mnemonicManager: MnemonicManager, + appSettings: AppSettingsRepository, betaFlags: BetaFlagsRepository, features: FeatureRepository, ) : BaseViewModel(resources), ScreenModel { @@ -196,6 +201,13 @@ class HomeViewModel @Inject constructor( init { onDrawn() + viewModelScope.launch { + val cameraAutoStart = appSettings.get(PrefsBool.CAMERA_START_BY_DEFAULT) + uiFlow.update { + it.copy(autoStartCamera = cameraAutoStart) + } + } + betaFlags.observe() .distinctUntilChanged() .onEach { beta -> @@ -367,6 +379,10 @@ class HomeViewModel @Inject constructor( } } + fun onStartCamera() { + uiFlow.update { it.copy(autoStartCamera = true) } + } + fun onCameraPermissionChanged(isGranted: Boolean) { uiFlow.update { it.copy(isCameraPermissionGranted = isGranted) } } diff --git a/app/src/main/java/com/getcode/view/main/home/components/CameraDisabledView.kt b/app/src/main/java/com/getcode/view/main/home/components/CameraDisabledView.kt new file mode 100644 index 000000000..c7b87b74e --- /dev/null +++ b/app/src/main/java/com/getcode/view/main/home/components/CameraDisabledView.kt @@ -0,0 +1,50 @@ +package com.getcode.view.main.home.components + +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.shape.CircleShape +import androidx.compose.material.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.unit.dp +import com.getcode.R +import com.getcode.theme.CodeTheme +import com.getcode.ui.components.ButtonState +import com.getcode.ui.components.CodeButton + +@Composable +internal fun CameraDisabledView( + modifier: Modifier = Modifier, + onClick: () -> Unit +) { + Box( + modifier = modifier.background(Color.Black), + contentAlignment = Alignment.Center + ) { + Column(Modifier.fillMaxWidth(0.85f)) { + Text( + modifier = Modifier + .fillMaxWidth() + .padding(bottom = 30.dp), + style = CodeTheme.typography.textMedium.copy(textAlign = TextAlign.Center), + text = stringResource(R.string.subtitle_cameraRequiredToScanCodes) + ) + CodeButton( + onClick = onClick, + modifier = Modifier.align(Alignment.CenterHorizontally), + text = stringResource(id = R.string.action_startCamera), + contentPadding = PaddingValues(), + shape = CircleShape, + buttonState = ButtonState.Filled + ) + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/getcode/view/main/home/components/PermissionsBlockingView.kt b/app/src/main/java/com/getcode/view/main/home/components/PermissionsBlockingView.kt index 877b6f20c..de71a4930 100644 --- a/app/src/main/java/com/getcode/view/main/home/components/PermissionsBlockingView.kt +++ b/app/src/main/java/com/getcode/view/main/home/components/PermissionsBlockingView.kt @@ -2,15 +2,20 @@ package com.getcode.view.main.home.components import android.Manifest import android.content.Context +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.shape.CircleShape import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material.MaterialTheme import androidx.compose.material.Text import androidx.compose.runtime.Composable import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.unit.dp @@ -28,28 +33,35 @@ internal fun PermissionsBlockingView( onPermissionResult: (Boolean) -> Unit, launcher: PermissionsLauncher, ) { - Column( - modifier = modifier, + Box( + modifier = modifier.background(Color.Black), + contentAlignment = Alignment.Center ) { - Text( - modifier = Modifier.padding(bottom = 30.dp), - style = CodeTheme.typography.textMedium.copy(textAlign = TextAlign.Center), - text = stringResource(R.string.subtitle_allowCameraAccess) - ) - CodeButton( - onClick = { - PermissionCheck.requestPermission( - context = context, - permission = Manifest.permission.CAMERA, - shouldRequest = true, - onPermissionResult = onPermissionResult, - launcher = launcher - ) - }, - modifier = Modifier.align(Alignment.CenterHorizontally), - text = stringResource(id = R.string.action_allowCameraAccess), - shape = RoundedCornerShape(45.dp), - buttonState = ButtonState.Filled - ) + Column(Modifier.fillMaxWidth(0.85f)) { + Text( + modifier = Modifier + .fillMaxWidth() + .padding(bottom = 30.dp), + style = CodeTheme.typography.textMedium.copy(textAlign = TextAlign.Center), + text = stringResource(R.string.subtitle_allowCameraAccess) + ) + CodeButton( + onClick = { + PermissionCheck.requestPermission( + context = context, + permission = Manifest.permission.CAMERA, + shouldRequest = true, + onPermissionResult = onPermissionResult, + launcher = launcher + ) + }, + modifier = Modifier.align(Alignment.CenterHorizontally), + contentPadding = PaddingValues(), + text = stringResource(id = R.string.action_allowCameraAccess), + shape = CircleShape, + buttonState = ButtonState.Filled + ) + } } + } \ No newline at end of file diff --git a/app/src/main/res/drawable/ic_camera_outline.xml b/app/src/main/res/drawable/ic_camera_outline.xml new file mode 100644 index 000000000..60b4f3dd9 --- /dev/null +++ b/app/src/main/res/drawable/ic_camera_outline.xml @@ -0,0 +1,12 @@ + + + + diff --git a/app/src/main/res/drawable/ic_settings_outline.xml b/app/src/main/res/drawable/ic_settings_outline.xml new file mode 100644 index 000000000..2def22aef --- /dev/null +++ b/app/src/main/res/drawable/ic_settings_outline.xml @@ -0,0 +1,12 @@ + + + + diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index dc92ec8d3..5897d504d 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -34,4 +34,9 @@ Failed to Collect Something went wrong. This Kin could not be collected. + App Settings + Auto Start Camera + You need to start your camera\nto scan Codes + Start Camera + diff --git a/common/theme/src/main/kotlin/com/getcode/theme/Color.kt b/common/theme/src/main/kotlin/com/getcode/theme/Color.kt index 24d1b30c9..0fb339f63 100644 --- a/common/theme/src/main/kotlin/com/getcode/theme/Color.kt +++ b/common/theme/src/main/kotlin/com/getcode/theme/Color.kt @@ -8,7 +8,6 @@ val BrandLight = Color(0xFF7379A0) val BrandSubtle = Color(0xFF565C86) val BrandMuted = Color(0xFF45464E) val BrandDark = Color(0xFF1F1A34) -val BrandOverlay = Color(0xFFFF2E2934) val Brand01 = Color(0xFF130F27) val White = Color(0xffffffff) @@ -32,11 +31,13 @@ val Warning = Color(0xFFf1ab1f) val Success = Color(0xFF87D300) val Error = Color(0xFFA42D2D) +val SystemGreen = Color(0xFF04C759) + val ChatOutgoing = Color(0xFF443091) -val topNotification = Color(0xFF4f49ce) -val topNeutral = Color(0xFF747474) -val topSuccess = Brand +val TopNotification = Color(0xFF4f49ce) +val TopNeutral = Color(0xFF747474) +val TopSuccess = Brand val textSelectionColors = TextSelectionColors( handleColor = White, diff --git a/vendor/tipkit/tipkit-m2/src/main/kotlin/dev/bmcreations/tipkit/TipScaffold.kt b/vendor/tipkit/tipkit-m2/src/main/kotlin/dev/bmcreations/tipkit/TipScaffold.kt index 83c1aa993..7162aecff 100644 --- a/vendor/tipkit/tipkit-m2/src/main/kotlin/dev/bmcreations/tipkit/TipScaffold.kt +++ b/vendor/tipkit/tipkit-m2/src/main/kotlin/dev/bmcreations/tipkit/TipScaffold.kt @@ -16,7 +16,6 @@ import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.width import androidx.compose.foundation.layout.widthIn -import androidx.compose.foundation.shape.CircleShape import androidx.compose.foundation.shape.CornerBasedShape import androidx.compose.material.Divider import androidx.compose.material.Icon @@ -34,10 +33,8 @@ import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment.Companion.CenterVertically import androidx.compose.ui.Modifier import androidx.compose.ui.geometry.Offset -import androidx.compose.ui.geometry.Size import androidx.compose.ui.graphics.Color import androidx.compose.ui.layout.layout -import androidx.compose.ui.layout.onPlaced import androidx.compose.ui.platform.LocalConfiguration import androidx.compose.ui.platform.LocalDensity import androidx.compose.ui.platform.LocalLayoutDirection @@ -46,7 +43,6 @@ import androidx.compose.ui.unit.IntSize import androidx.compose.ui.unit.dp import com.getcode.theme.Brand import com.getcode.theme.BrandLight -import com.getcode.theme.BrandOverlay import com.getcode.theme.CodeTheme import com.getcode.theme.White import dev.bmcreations.tipkit.data.InlineTipData