From adb8c50ae2edce2e907be6d3cb6daa02e6f06f5b Mon Sep 17 00:00:00 2001 From: Tangem Date: Wed, 1 Jul 2026 22:26:21 +0500 Subject: [PATCH] Updated on 2026-08-14 --- .../settings/DevUsedeskTokenTtlManager.kt | 50 +++++++++ .../settings/ProdUsedeskTokenTtlManager.kt | 21 ++++ .../data/settings/di/SettingsDataModule.kt | 16 +++ .../settings/DevUsedeskTokenTtlManagerTest.kt | 104 +++++++++++++++++ .../ProdUsedeskTokenTtlManagerTest.kt | 33 ++++++ .../domain/settings/UsedeskTokenTtlManager.kt | 24 ++++ .../actions/TesterActionsContentState.kt | 20 ++++ .../actions/TesterActionsScreen.kt | 105 ++++++++++++++++++ .../actions/TesterActionsViewModel.kt | 67 +++++++++++ .../impl/src/main/res/values/strings.xml | 3 + features/usedesk/impl/build.gradle.kts | 1 + .../feature/usedesk/model/UsedeskModel.kt | 3 + gradle/tangem_dependencies.toml | 2 +- 13 files changed, 448 insertions(+), 1 deletion(-) create mode 100644 data/settings/src/main/java/com/tangem/data/settings/DevUsedeskTokenTtlManager.kt create mode 100644 data/settings/src/main/java/com/tangem/data/settings/ProdUsedeskTokenTtlManager.kt create mode 100644 data/settings/src/test/java/com/tangem/data/settings/DevUsedeskTokenTtlManagerTest.kt create mode 100644 data/settings/src/test/java/com/tangem/data/settings/ProdUsedeskTokenTtlManagerTest.kt create mode 100644 domain/settings/src/main/java/com/tangem/domain/settings/UsedeskTokenTtlManager.kt diff --git a/data/settings/src/main/java/com/tangem/data/settings/DevUsedeskTokenTtlManager.kt b/data/settings/src/main/java/com/tangem/data/settings/DevUsedeskTokenTtlManager.kt new file mode 100644 index 0000000000..02031848bb --- /dev/null +++ b/data/settings/src/main/java/com/tangem/data/settings/DevUsedeskTokenTtlManager.kt @@ -0,0 +1,50 @@ +package com.tangem.data.settings + +import androidx.datastore.preferences.core.longPreferencesKey +import com.tangem.datasource.local.preferences.AppPreferencesStore +import com.tangem.datasource.local.preferences.utils.get +import com.tangem.domain.settings.UsedeskTokenTtlManager +import com.tangem.utils.coroutines.CoroutineDispatcherProvider +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.SupervisorJob +import kotlinx.coroutines.flow.SharingStarted +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.stateIn + +/** + * Development implementation of [UsedeskTokenTtlManager]. + * + * Reads and writes the Usedesk chat token TTL from [AppPreferencesStore], + * allowing testers to override it via the Tester Menu. + * The preference [kotlinx.coroutines.flow.Flow] is converted to a [StateFlow] on construction, + * so [getTokenTtlMillisSync] can be called from non-suspending contexts. + * Defaults to [UsedeskTokenTtlManager.DEFAULT_TTL_MILLIS] when no value is stored. + */ +internal class DevUsedeskTokenTtlManager( + private val appPreferencesStore: AppPreferencesStore, + dispatchers: CoroutineDispatcherProvider, +) : UsedeskTokenTtlManager { + + private val ttlMillisState: StateFlow = + appPreferencesStore + .get(key = USEDESK_TOKEN_TTL_MILLIS_KEY, default = UsedeskTokenTtlManager.DEFAULT_TTL_MILLIS) + .stateIn( + scope = CoroutineScope(dispatchers.io + SupervisorJob()), + started = SharingStarted.Eagerly, + initialValue = UsedeskTokenTtlManager.DEFAULT_TTL_MILLIS, + ) + + override fun getTokenTtlMillis(): StateFlow = ttlMillisState + + override fun getTokenTtlMillisSync(): Long = ttlMillisState.value + + override suspend fun setTokenTtlMillis(millis: Long) { + appPreferencesStore.editData { preferences -> + preferences[USEDESK_TOKEN_TTL_MILLIS_KEY] = millis + } + } + + private companion object { + val USEDESK_TOKEN_TTL_MILLIS_KEY = longPreferencesKey(name = "usedeskTokenTtlMillis") + } +} \ No newline at end of file diff --git a/data/settings/src/main/java/com/tangem/data/settings/ProdUsedeskTokenTtlManager.kt b/data/settings/src/main/java/com/tangem/data/settings/ProdUsedeskTokenTtlManager.kt new file mode 100644 index 0000000000..785d5a6092 --- /dev/null +++ b/data/settings/src/main/java/com/tangem/data/settings/ProdUsedeskTokenTtlManager.kt @@ -0,0 +1,21 @@ +package com.tangem.data.settings + +import com.tangem.domain.settings.UsedeskTokenTtlManager +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow + +/** + * Production implementation of [UsedeskTokenTtlManager]. + * + * The Usedesk chat token TTL is fixed to [UsedeskTokenTtlManager.DEFAULT_TTL_MILLIS] in production — + * a tester override must never leak into release builds. + * [setTokenTtlMillis] is a no-op since the TTL cannot be changed at runtime. + */ +internal class ProdUsedeskTokenTtlManager : UsedeskTokenTtlManager { + + private val state: StateFlow = MutableStateFlow(UsedeskTokenTtlManager.DEFAULT_TTL_MILLIS) + + override fun getTokenTtlMillis(): StateFlow = state + override fun getTokenTtlMillisSync(): Long = UsedeskTokenTtlManager.DEFAULT_TTL_MILLIS + override suspend fun setTokenTtlMillis(millis: Long) = Unit +} \ No newline at end of file diff --git a/data/settings/src/main/java/com/tangem/data/settings/di/SettingsDataModule.kt b/data/settings/src/main/java/com/tangem/data/settings/di/SettingsDataModule.kt index ebc5c40001..d5d5be07b8 100644 --- a/data/settings/src/main/java/com/tangem/data/settings/di/SettingsDataModule.kt +++ b/data/settings/src/main/java/com/tangem/data/settings/di/SettingsDataModule.kt @@ -6,11 +6,14 @@ import com.tangem.data.settings.DefaultAppRatingRepository import com.tangem.data.settings.DefaultPermissionRepository import com.tangem.data.settings.DefaultSettingsRepository import com.tangem.data.settings.DevHotWalletRestrictionManager +import com.tangem.data.settings.DevUsedeskTokenTtlManager import com.tangem.data.settings.ProdHotWalletRestrictionManager +import com.tangem.data.settings.ProdUsedeskTokenTtlManager import com.tangem.datasource.api.tangemTech.TangemTechApi import com.tangem.datasource.local.logs.AppLogsStore import com.tangem.datasource.local.preferences.AppPreferencesStore import com.tangem.domain.settings.HotWalletRestrictionManager +import com.tangem.domain.settings.UsedeskTokenTtlManager import com.tangem.domain.settings.repositories.AppRatingRepository import com.tangem.domain.settings.repositories.PermissionRepository import com.tangem.domain.settings.repositories.SettingsRepository @@ -72,4 +75,17 @@ internal object SettingsDataModule { ProdHotWalletRestrictionManager() } } + + @Provides + @Singleton + fun provideUsedeskTokenTtlManager( + appPreferencesStore: AppPreferencesStore, + dispatchers: CoroutineDispatcherProvider, + ): UsedeskTokenTtlManager { + return if (BuildConfig.TESTER_MENU_ENABLED) { + DevUsedeskTokenTtlManager(appPreferencesStore, dispatchers) + } else { + ProdUsedeskTokenTtlManager() + } + } } \ No newline at end of file diff --git a/data/settings/src/test/java/com/tangem/data/settings/DevUsedeskTokenTtlManagerTest.kt b/data/settings/src/test/java/com/tangem/data/settings/DevUsedeskTokenTtlManagerTest.kt new file mode 100644 index 0000000000..31efefaab1 --- /dev/null +++ b/data/settings/src/test/java/com/tangem/data/settings/DevUsedeskTokenTtlManagerTest.kt @@ -0,0 +1,104 @@ +package com.tangem.data.settings + +import androidx.datastore.preferences.core.Preferences +import com.google.common.truth.Truth.assertThat +import com.tangem.datasource.local.preferences.AppPreferencesStore +import com.tangem.datasource.local.preferences.utils.get +import com.tangem.domain.settings.UsedeskTokenTtlManager +import com.tangem.test.core.getEmittedValues +import com.tangem.utils.coroutines.CoroutineDispatcherProvider +import com.tangem.utils.coroutines.TestingCoroutineDispatcherProvider +import io.mockk.coEvery +import io.mockk.coVerify +import io.mockk.every +import io.mockk.mockk +import io.mockk.mockkStatic +import io.mockk.unmockkStatic +import kotlinx.coroutines.flow.flowOf +import kotlinx.coroutines.test.runTest +import org.junit.jupiter.api.AfterEach +import org.junit.jupiter.api.BeforeEach +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.TestInstance + +@TestInstance(TestInstance.Lifecycle.PER_CLASS) +class DevUsedeskTokenTtlManagerTest { + + private val appPreferencesStore = mockk(relaxed = true) + private val dispatchers: CoroutineDispatcherProvider = TestingCoroutineDispatcherProvider() + + @BeforeEach + fun setup() { + mockkStatic("com.tangem.datasource.local.preferences.utils.PreferencesDataStoreExtKt") + } + + @AfterEach + fun tearDown() { + unmockkStatic("com.tangem.datasource.local.preferences.utils.PreferencesDataStoreExtKt") + } + + @Test + fun `GIVEN stored ttl WHEN getTokenTtlMillis THEN emits stored value`() = runTest { + // Arrange + val storedTtl = 15L * 60 * 1000 + every { + appPreferencesStore.get(key = any>(), default = DEFAULT) + } returns flowOf(storedTtl) + + val manager = DevUsedeskTokenTtlManager(appPreferencesStore, dispatchers) + + // Act + val emitted = getEmittedValues(manager.getTokenTtlMillis()) + + // Assert + assertThat(emitted).containsExactly(storedTtl) + } + + @Test + fun `GIVEN no stored ttl WHEN getTokenTtlMillisSync THEN returns default`() = runTest { + // Arrange + every { + appPreferencesStore.get(key = any>(), default = DEFAULT) + } returns flowOf(DEFAULT) + + val manager = DevUsedeskTokenTtlManager(appPreferencesStore, dispatchers) + + // Act & Assert + assertThat(manager.getTokenTtlMillisSync()).isEqualTo(DEFAULT) + } + + @Test + fun `GIVEN stored ttl WHEN getTokenTtlMillisSync THEN returns cached value`() = runTest { + // Arrange + val storedTtl = 60L * 60 * 1000 + every { + appPreferencesStore.get(key = any>(), default = DEFAULT) + } returns flowOf(storedTtl) + + val manager = DevUsedeskTokenTtlManager(appPreferencesStore, dispatchers) + + // Act & Assert + assertThat(manager.getTokenTtlMillisSync()).isEqualTo(storedTtl) + } + + @Test + fun `WHEN setTokenTtlMillis THEN opens editData transaction`() = runTest { + // Arrange + every { + appPreferencesStore.get(key = any>(), default = DEFAULT) + } returns flowOf(DEFAULT) + coEvery { appPreferencesStore.editData(any()) } returns mockk(relaxed = true) + + val manager = DevUsedeskTokenTtlManager(appPreferencesStore, dispatchers) + + // Act + manager.setTokenTtlMillis(millis = 15L * 60 * 1000) + + // Assert + coVerify { appPreferencesStore.editData(any()) } + } + + private companion object { + val DEFAULT = UsedeskTokenTtlManager.DEFAULT_TTL_MILLIS + } +} \ No newline at end of file diff --git a/data/settings/src/test/java/com/tangem/data/settings/ProdUsedeskTokenTtlManagerTest.kt b/data/settings/src/test/java/com/tangem/data/settings/ProdUsedeskTokenTtlManagerTest.kt new file mode 100644 index 0000000000..8fd4fd4a28 --- /dev/null +++ b/data/settings/src/test/java/com/tangem/data/settings/ProdUsedeskTokenTtlManagerTest.kt @@ -0,0 +1,33 @@ +package com.tangem.data.settings + +import com.google.common.truth.Truth.assertThat +import com.tangem.domain.settings.UsedeskTokenTtlManager +import com.tangem.test.core.getEmittedValues +import kotlinx.coroutines.test.runTest +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.TestInstance + +@TestInstance(TestInstance.Lifecycle.PER_CLASS) +class ProdUsedeskTokenTtlManagerTest { + + private val manager = ProdUsedeskTokenTtlManager() + + @Test + fun `WHEN getTokenTtlMillis THEN always emits default`() = runTest { + val emitted = getEmittedValues(manager.getTokenTtlMillis()) + + assertThat(emitted).containsExactly(UsedeskTokenTtlManager.DEFAULT_TTL_MILLIS) + } + + @Test + fun `WHEN getTokenTtlMillisSync THEN returns default`() = runTest { + assertThat(manager.getTokenTtlMillisSync()).isEqualTo(UsedeskTokenTtlManager.DEFAULT_TTL_MILLIS) + } + + @Test + fun `GIVEN override attempt WHEN setTokenTtlMillis THEN sync still returns default`() = runTest { + manager.setTokenTtlMillis(millis = 15L * 60 * 1000) + + assertThat(manager.getTokenTtlMillisSync()).isEqualTo(UsedeskTokenTtlManager.DEFAULT_TTL_MILLIS) + } +} \ No newline at end of file diff --git a/domain/settings/src/main/java/com/tangem/domain/settings/UsedeskTokenTtlManager.kt b/domain/settings/src/main/java/com/tangem/domain/settings/UsedeskTokenTtlManager.kt new file mode 100644 index 0000000000..40dc506f48 --- /dev/null +++ b/domain/settings/src/main/java/com/tangem/domain/settings/UsedeskTokenTtlManager.kt @@ -0,0 +1,24 @@ +package com.tangem.domain.settings + +import kotlinx.coroutines.flow.StateFlow +import kotlin.time.Duration.Companion.days + +/** + * Manages the lifetime (TTL) of the Usedesk support-chat token. + * + * The token is reused while the time since the last chat interaction is below this TTL; + * once it expires the SDK obtains a fresh token on the next chat init. + * + * In production the TTL is fixed to [DEFAULT_TTL_MILLIS]. Debug builds let testers override it + * via the Tester Menu (e.g. shorten it to a few minutes to verify token-refresh behaviour). + */ +interface UsedeskTokenTtlManager { + + fun getTokenTtlMillis(): StateFlow + fun getTokenTtlMillisSync(): Long + suspend fun setTokenTtlMillis(millis: Long) + + companion object { + val DEFAULT_TTL_MILLIS: Long = 7.days.inWholeMilliseconds + } +} \ No newline at end of file diff --git a/features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/actions/TesterActionsContentState.kt b/features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/actions/TesterActionsContentState.kt index fe09060e52..cf18cccf1e 100644 --- a/features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/actions/TesterActionsContentState.kt +++ b/features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/actions/TesterActionsContentState.kt @@ -1,11 +1,13 @@ package com.tangem.feature.tester.presentation.actions import androidx.compose.runtime.Immutable +import kotlinx.collections.immutable.ImmutableList import java.io.File internal data class TesterActionsContentState( val hideAllCurrenciesUM: HideAllCurrenciesUM, val toggleHotWalletRestrictionUM: ToggleHotWalletRestrictionUM, + val usedeskTokenTtlUM: UsedeskTokenTtlUM, val shareLogsUM: ShareLogsUM, val onBackClick: () -> Unit, ) { @@ -21,6 +23,24 @@ internal data class TesterActionsContentState( val onClick: () -> Unit, ) + /** + * State of the Usedesk chat token TTL setting. + * + * @param currentLabel human-readable current TTL (e.g. "7 days", "15 minutes") + * @param presets selectable preset durations + * @param onPresetSelected applies one of the [presets] (value in milliseconds) + * @param onCustomMinutesSelected applies a custom TTL entered in minutes + */ + @Immutable + data class UsedeskTokenTtlUM( + val currentLabel: String, + val presets: ImmutableList, + val onPresetSelected: (Long) -> Unit, + val onCustomMinutesSelected: (Long) -> Unit, + ) { + data class Preset(val label: String, val millis: Long) + } + @Immutable data class ShareLogsUM(val file: File?) } \ No newline at end of file diff --git a/features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/actions/TesterActionsScreen.kt b/features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/actions/TesterActionsScreen.kt index a3e947b392..7ca004864b 100644 --- a/features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/actions/TesterActionsScreen.kt +++ b/features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/actions/TesterActionsScreen.kt @@ -8,15 +8,23 @@ import androidx.compose.foundation.background import androidx.compose.foundation.layout.* import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue import androidx.compose.ui.Modifier import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.text.input.TextFieldValue import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import androidx.core.app.ShareCompat import androidx.core.content.ContextCompat import androidx.core.content.FileProvider +import com.tangem.core.ui.components.AdditionalTextInputDialogUM +import com.tangem.core.ui.components.DialogButtonUM import com.tangem.core.ui.components.PrimaryButton +import com.tangem.core.ui.components.SelectorDialog +import com.tangem.core.ui.components.TextInputDialog import com.tangem.core.ui.components.appbar.AppBarWithBackButton import com.tangem.core.ui.extensions.stringResourceSafe import com.tangem.core.ui.res.TangemTheme @@ -25,12 +33,19 @@ import com.tangem.core.ui.utils.findActivity import com.tangem.feature.tester.impl.R import com.tangem.feature.tester.presentation.actions.TesterActionsContentState.HideAllCurrenciesUM import com.tangem.feature.tester.presentation.actions.TesterActionsContentState.ToggleHotWalletRestrictionUM +import com.tangem.feature.tester.presentation.actions.TesterActionsContentState.UsedeskTokenTtlUM import com.tangem.utils.logging.TangemLogger +import kotlinx.collections.immutable.toImmutableList import java.io.File +import kotlin.time.Duration.Companion.days +import kotlin.time.Duration.Companion.minutes @OptIn(ExperimentalFoundationApi::class) @Composable internal fun TesterActionsScreen(state: TesterActionsContentState, modifier: Modifier = Modifier) { + var shouldShowTtlSelector by remember { mutableStateOf(value = false) } + var shouldShowTtlCustomInput by remember { mutableStateOf(value = false) } + LazyColumn( modifier = modifier .fillMaxSize() @@ -65,6 +80,13 @@ internal fun TesterActionsScreen(state: TesterActionsContentState, modifier: Mod ) } + item { + TesterActionItem( + name = stringResourceSafe(id = R.string.usedesk_token_ttl, state.usedeskTokenTtlUM.currentLabel), + onClick = { shouldShowTtlSelector = true }, + ) + } + item { val activity = LocalContext.current.findActivity() @@ -75,6 +97,24 @@ internal fun TesterActionsScreen(state: TesterActionsContentState, modifier: Mod ) } } + + if (shouldShowTtlSelector) { + UsedeskTtlSelectorDialog( + um = state.usedeskTokenTtlUM, + onCustomRequest = { + shouldShowTtlSelector = false + shouldShowTtlCustomInput = true + }, + onDismiss = { shouldShowTtlSelector = false }, + ) + } + + if (shouldShowTtlCustomInput) { + UsedeskTtlCustomInputDialog( + um = state.usedeskTokenTtlUM, + onDismiss = { shouldShowTtlCustomInput = false }, + ) + } } @Composable @@ -90,6 +130,62 @@ private fun TesterActionItem(name: String, onClick: () -> Unit, progress: Boolea ) } +@Composable +private fun UsedeskTtlSelectorDialog(um: UsedeskTokenTtlUM, onCustomRequest: () -> Unit, onDismiss: () -> Unit) { + val customLabel = stringResourceSafe(R.string.usedesk_token_ttl_custom) + val items = remember(um.presets, customLabel) { + (um.presets.map(UsedeskTokenTtlUM.Preset::label) + customLabel).toImmutableList() + } + val selectedIndex = remember(um.presets, um.currentLabel) { + um.presets.indexOfFirst { it.label == um.currentLabel } + .takeIf { it >= 0 } + ?: um.presets.size + } + + SelectorDialog( + title = stringResourceSafe(R.string.usedesk_token_ttl_title), + items = items, + selectedItemIndex = selectedIndex, + confirmButton = DialogButtonUM(title = stringResourceSafe(id = R.string.common_close), onClick = onDismiss), + onSelect = { index -> + val preset = um.presets.getOrNull(index) + if (preset != null) { + um.onPresetSelected(preset.millis) + onDismiss() + } else { + onCustomRequest() + } + }, + onDismissDialog = onDismiss, + ) +} + +@Composable +private fun UsedeskTtlCustomInputDialog(um: UsedeskTokenTtlUM, onDismiss: () -> Unit) { + var value by remember { mutableStateOf(TextFieldValue()) } + val minutes = value.text.toLongOrNull() + val isValid = minutes != null && minutes > 0 + + TextInputDialog( + title = stringResourceSafe(R.string.usedesk_token_ttl_title), + fieldValue = value, + onValueChange = { newValue -> value = newValue.copy(text = newValue.text.filter(Char::isDigit)) }, + textFieldParams = AdditionalTextInputDialogUM( + label = stringResourceSafe(R.string.usedesk_token_ttl_custom), + placeholder = "15", + ), + confirmButton = DialogButtonUM( + isEnabled = isValid, + onClick = { + if (isValid) um.onCustomMinutesSelected(minutes) + onDismiss() + }, + ), + dismissButton = DialogButtonUM(title = stringResourceSafe(id = R.string.common_close), onClick = onDismiss), + onDismissDialog = onDismiss, + ) +} + private fun Activity.shareFile(file: File?) { val originalIntent = createEmailShareIntent(activity = this, file = file) @@ -126,6 +222,15 @@ private fun TesterActionsScreenSample(modifier: Modifier = Modifier) { state = TesterActionsContentState( hideAllCurrenciesUM = HideAllCurrenciesUM.Clickable {}, toggleHotWalletRestrictionUM = ToggleHotWalletRestrictionUM(isEnabled = true) {}, + usedeskTokenTtlUM = UsedeskTokenTtlUM( + currentLabel = "7 days", + presets = listOf( + UsedeskTokenTtlUM.Preset(label = "7 days", millis = 7.days.inWholeMilliseconds), + UsedeskTokenTtlUM.Preset(label = "15 minutes", millis = 15.minutes.inWholeMilliseconds), + ).toImmutableList(), + onPresetSelected = {}, + onCustomMinutesSelected = {}, + ), shareLogsUM = TesterActionsContentState.ShareLogsUM(file = null), onBackClick = {}, ), diff --git a/features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/actions/TesterActionsViewModel.kt b/features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/actions/TesterActionsViewModel.kt index fb6e223f88..aa7cb851ee 100644 --- a/features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/actions/TesterActionsViewModel.kt +++ b/features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/actions/TesterActionsViewModel.kt @@ -9,14 +9,21 @@ import com.tangem.data.common.account.WalletAccountsSaver import com.tangem.domain.common.wallets.UserWalletsListRepository import com.tangem.domain.feedback.repository.FeedbackRepository import com.tangem.domain.settings.HotWalletRestrictionManager +import com.tangem.domain.settings.UsedeskTokenTtlManager import com.tangem.feature.tester.presentation.actions.TesterActionsContentState.HideAllCurrenciesUM import com.tangem.feature.tester.presentation.actions.TesterActionsContentState.ToggleHotWalletRestrictionUM +import com.tangem.feature.tester.presentation.actions.TesterActionsContentState.UsedeskTokenTtlUM import com.tangem.feature.tester.presentation.navigation.InnerTesterRouter import dagger.hilt.android.lifecycle.HiltViewModel +import kotlinx.collections.immutable.persistentListOf import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.launch import javax.inject.Inject +import kotlin.time.Duration.Companion.days +import kotlin.time.Duration.Companion.hours +import kotlin.time.Duration.Companion.milliseconds +import kotlin.time.Duration.Companion.minutes @HiltViewModel internal class TesterActionsViewModel @Inject constructor( @@ -24,6 +31,7 @@ internal class TesterActionsViewModel @Inject constructor( private val userWalletsListRepository: UserWalletsListRepository, private val walletAccountsSaver: WalletAccountsSaver, private val hotWalletRestrictionManager: HotWalletRestrictionManager, + private val usedeskTokenTtlManager: UsedeskTokenTtlManager, ) : ViewModel() { var uiState: TesterActionsContentState by mutableStateOf(initialState) @@ -36,12 +44,19 @@ internal class TesterActionsViewModel @Inject constructor( isEnabled = hotWalletRestrictionManager.isCreationEnabledSync(), onClick = this::toggleHotWalletRestriction, ), + usedeskTokenTtlUM = UsedeskTokenTtlUM( + currentLabel = formatTtl(usedeskTokenTtlManager.getTokenTtlMillisSync()), + presets = TTL_PRESETS, + onPresetSelected = this::setUsedeskTokenTtl, + onCustomMinutesSelected = { minutes -> setUsedeskTokenTtl(minutes.minutes.inWholeMilliseconds) }, + ), shareLogsUM = TesterActionsContentState.ShareLogsUM(file = feedbackRepository.getLogFile()), onBackClick = { /* no-op */ }, ) init { bootstrapHotWalletRestrictionUpdates() + bootstrapUsedeskTokenTtlUpdates() } fun setupNavigation(router: InnerTesterRouter) { @@ -76,6 +91,10 @@ internal class TesterActionsViewModel @Inject constructor( hotWalletRestrictionManager.toggleCreationEnabled() } + private fun setUsedeskTokenTtl(millis: Long) = viewModelScope.launch { + usedeskTokenTtlManager.setTokenTtlMillis(millis) + } + private fun bootstrapHotWalletRestrictionUpdates() { hotWalletRestrictionManager.isCreationEnabled() .onEach { isEnabled -> @@ -87,4 +106,52 @@ internal class TesterActionsViewModel @Inject constructor( } .launchIn(viewModelScope) } + + private fun bootstrapUsedeskTokenTtlUpdates() { + usedeskTokenTtlManager.getTokenTtlMillis() + .onEach { millis -> + uiState = uiState.copy( + usedeskTokenTtlUM = uiState.usedeskTokenTtlUM.copy( + currentLabel = formatTtl(millis), + ), + ) + } + .launchIn(viewModelScope) + } + + private companion object { + + val TTL_PRESETS = persistentListOf( + UsedeskTokenTtlUM.Preset( + label = formatTtl(7.days.inWholeMilliseconds), + millis = 7.days.inWholeMilliseconds, + ), + UsedeskTokenTtlUM.Preset( + label = formatTtl(1.days.inWholeMilliseconds), + millis = 1.days.inWholeMilliseconds, + ), + UsedeskTokenTtlUM.Preset( + label = formatTtl(1.hours.inWholeMilliseconds), + millis = 1.hours.inWholeMilliseconds, + ), + UsedeskTokenTtlUM.Preset( + label = formatTtl(15.minutes.inWholeMilliseconds), + millis = 15.minutes.inWholeMilliseconds, + ), + ) + + /** Picks the largest whole unit (days → hours → minutes → seconds) for a readable label. */ + fun formatTtl(millis: Long): String { + val duration = millis.milliseconds + return when { + duration.inWholeDays >= 1 && duration == duration.inWholeDays.days -> duration.inWholeDays.unit("day") + duration.inWholeHours >= 1 && duration == duration.inWholeHours.hours -> + duration.inWholeHours.unit("hour") + duration.inWholeMinutes >= 1 -> duration.inWholeMinutes.unit("minute") + else -> duration.inWholeSeconds.unit("second") + } + } + + fun Long.unit(name: String): String = "$this $name${if (this == 1L) "" else "s"}" + } } \ No newline at end of file diff --git a/features/tester/impl/src/main/res/values/strings.xml b/features/tester/impl/src/main/res/values/strings.xml index 5edc7e890f..83de0df1e4 100644 --- a/features/tester/impl/src/main/res/values/strings.xml +++ b/features/tester/impl/src/main/res/values/strings.xml @@ -15,6 +15,9 @@ Filter by name or symbol Blockchain providers Hot wallet creation restriction - %s + Usedesk chat token TTL - %s + Usedesk chat token TTL + Custom (minutes) Share logs Test push Accounts diff --git a/features/usedesk/impl/build.gradle.kts b/features/usedesk/impl/build.gradle.kts index 1a0b339e56..6d662492fe 100644 --- a/features/usedesk/impl/build.gradle.kts +++ b/features/usedesk/impl/build.gradle.kts @@ -26,6 +26,7 @@ dependencies { /** Domain */ implementation(projects.domain.feedback) + implementation(projects.domain.settings) /** DI */ implementation(deps.hilt.android) diff --git a/features/usedesk/impl/src/main/java/com/tangem/feature/usedesk/model/UsedeskModel.kt b/features/usedesk/impl/src/main/java/com/tangem/feature/usedesk/model/UsedeskModel.kt index e99816ae43..935740ad09 100644 --- a/features/usedesk/impl/src/main/java/com/tangem/feature/usedesk/model/UsedeskModel.kt +++ b/features/usedesk/impl/src/main/java/com/tangem/feature/usedesk/model/UsedeskModel.kt @@ -9,6 +9,7 @@ import com.tangem.core.decompose.model.ParamsContainer import com.tangem.datasource.local.preferences.AppPreferencesStore import com.tangem.datasource.local.preferences.PreferencesKeys import com.tangem.domain.feedback.repository.FeedbackRepository +import com.tangem.domain.settings.UsedeskTokenTtlManager import com.tangem.feature.usedesk.analytics.UsedeskAnalyticsEvents import com.tangem.feature.usedesk.api.UsedeskComponent import com.tangem.usedesk.chat_sdk.entity.UsedeskChatConfiguration @@ -29,6 +30,7 @@ internal class UsedeskModel @Inject constructor( private val appPreferencesStore: AppPreferencesStore, private val analyticsEventHandler: AnalyticsEventHandler, private val feedbackRepository: FeedbackRepository, + private val usedeskTokenTtlManager: UsedeskTokenTtlManager, paramsContainer: ParamsContainer, ) : Model() { @@ -54,6 +56,7 @@ internal class UsedeskModel @Inject constructor( clientEmail = params.userWalletId, clientInitMessage = params.prefilledMessage, additionalFields = additionalFieldsFor(params.source), + tokenTtlMillis = usedeskTokenTtlManager.getTokenTtlMillisSync(), ), ) } diff --git a/gradle/tangem_dependencies.toml b/gradle/tangem_dependencies.toml index 76bd3373b5..6e0f27042e 100644 --- a/gradle/tangem_dependencies.toml +++ b/gradle/tangem_dependencies.toml @@ -13,7 +13,7 @@ tangemVico = "tangem-master-21" #tangemVico = "0.0.1" # Keep it! - used for local builds ^ tangemHotSdk = "develop-550" #tangemHotSdk = "0.0.1" # Keep it! - used for local builds ^ -tangemUsedeskSdk = "main-9" +tangemUsedeskSdk = "main-10" #tangemUsedeskSdk = "0.0.1" # Keep it! - used for local builds ^ [libraries]