Updated on 2026-08-14
This commit is contained in:
parent
c509046a8b
commit
adb8c50ae2
13 changed files with 448 additions and 1 deletions
|
|
@ -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<Long> =
|
||||||
|
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<Long> = 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")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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<Long> = MutableStateFlow(UsedeskTokenTtlManager.DEFAULT_TTL_MILLIS)
|
||||||
|
|
||||||
|
override fun getTokenTtlMillis(): StateFlow<Long> = state
|
||||||
|
override fun getTokenTtlMillisSync(): Long = UsedeskTokenTtlManager.DEFAULT_TTL_MILLIS
|
||||||
|
override suspend fun setTokenTtlMillis(millis: Long) = Unit
|
||||||
|
}
|
||||||
|
|
@ -6,11 +6,14 @@ import com.tangem.data.settings.DefaultAppRatingRepository
|
||||||
import com.tangem.data.settings.DefaultPermissionRepository
|
import com.tangem.data.settings.DefaultPermissionRepository
|
||||||
import com.tangem.data.settings.DefaultSettingsRepository
|
import com.tangem.data.settings.DefaultSettingsRepository
|
||||||
import com.tangem.data.settings.DevHotWalletRestrictionManager
|
import com.tangem.data.settings.DevHotWalletRestrictionManager
|
||||||
|
import com.tangem.data.settings.DevUsedeskTokenTtlManager
|
||||||
import com.tangem.data.settings.ProdHotWalletRestrictionManager
|
import com.tangem.data.settings.ProdHotWalletRestrictionManager
|
||||||
|
import com.tangem.data.settings.ProdUsedeskTokenTtlManager
|
||||||
import com.tangem.datasource.api.tangemTech.TangemTechApi
|
import com.tangem.datasource.api.tangemTech.TangemTechApi
|
||||||
import com.tangem.datasource.local.logs.AppLogsStore
|
import com.tangem.datasource.local.logs.AppLogsStore
|
||||||
import com.tangem.datasource.local.preferences.AppPreferencesStore
|
import com.tangem.datasource.local.preferences.AppPreferencesStore
|
||||||
import com.tangem.domain.settings.HotWalletRestrictionManager
|
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.AppRatingRepository
|
||||||
import com.tangem.domain.settings.repositories.PermissionRepository
|
import com.tangem.domain.settings.repositories.PermissionRepository
|
||||||
import com.tangem.domain.settings.repositories.SettingsRepository
|
import com.tangem.domain.settings.repositories.SettingsRepository
|
||||||
|
|
@ -72,4 +75,17 @@ internal object SettingsDataModule {
|
||||||
ProdHotWalletRestrictionManager()
|
ProdHotWalletRestrictionManager()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Provides
|
||||||
|
@Singleton
|
||||||
|
fun provideUsedeskTokenTtlManager(
|
||||||
|
appPreferencesStore: AppPreferencesStore,
|
||||||
|
dispatchers: CoroutineDispatcherProvider,
|
||||||
|
): UsedeskTokenTtlManager {
|
||||||
|
return if (BuildConfig.TESTER_MENU_ENABLED) {
|
||||||
|
DevUsedeskTokenTtlManager(appPreferencesStore, dispatchers)
|
||||||
|
} else {
|
||||||
|
ProdUsedeskTokenTtlManager()
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -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<AppPreferencesStore>(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<Preferences.Key<Long>>(), 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<Preferences.Key<Long>>(), 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<Preferences.Key<Long>>(), 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<Preferences.Key<Long>>(), 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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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<Long>
|
||||||
|
fun getTokenTtlMillisSync(): Long
|
||||||
|
suspend fun setTokenTtlMillis(millis: Long)
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
val DEFAULT_TTL_MILLIS: Long = 7.days.inWholeMilliseconds
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -1,11 +1,13 @@
|
||||||
package com.tangem.feature.tester.presentation.actions
|
package com.tangem.feature.tester.presentation.actions
|
||||||
|
|
||||||
import androidx.compose.runtime.Immutable
|
import androidx.compose.runtime.Immutable
|
||||||
|
import kotlinx.collections.immutable.ImmutableList
|
||||||
import java.io.File
|
import java.io.File
|
||||||
|
|
||||||
internal data class TesterActionsContentState(
|
internal data class TesterActionsContentState(
|
||||||
val hideAllCurrenciesUM: HideAllCurrenciesUM,
|
val hideAllCurrenciesUM: HideAllCurrenciesUM,
|
||||||
val toggleHotWalletRestrictionUM: ToggleHotWalletRestrictionUM,
|
val toggleHotWalletRestrictionUM: ToggleHotWalletRestrictionUM,
|
||||||
|
val usedeskTokenTtlUM: UsedeskTokenTtlUM,
|
||||||
val shareLogsUM: ShareLogsUM,
|
val shareLogsUM: ShareLogsUM,
|
||||||
val onBackClick: () -> Unit,
|
val onBackClick: () -> Unit,
|
||||||
) {
|
) {
|
||||||
|
|
@ -21,6 +23,24 @@ internal data class TesterActionsContentState(
|
||||||
val onClick: () -> Unit,
|
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<Preset>,
|
||||||
|
val onPresetSelected: (Long) -> Unit,
|
||||||
|
val onCustomMinutesSelected: (Long) -> Unit,
|
||||||
|
) {
|
||||||
|
data class Preset(val label: String, val millis: Long)
|
||||||
|
}
|
||||||
|
|
||||||
@Immutable
|
@Immutable
|
||||||
data class ShareLogsUM(val file: File?)
|
data class ShareLogsUM(val file: File?)
|
||||||
}
|
}
|
||||||
|
|
@ -8,15 +8,23 @@ import androidx.compose.foundation.background
|
||||||
import androidx.compose.foundation.layout.*
|
import androidx.compose.foundation.layout.*
|
||||||
import androidx.compose.foundation.lazy.LazyColumn
|
import androidx.compose.foundation.lazy.LazyColumn
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.getValue
|
||||||
|
import androidx.compose.runtime.mutableStateOf
|
||||||
import androidx.compose.runtime.remember
|
import androidx.compose.runtime.remember
|
||||||
|
import androidx.compose.runtime.setValue
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.platform.LocalContext
|
import androidx.compose.ui.platform.LocalContext
|
||||||
|
import androidx.compose.ui.text.input.TextFieldValue
|
||||||
import androidx.compose.ui.tooling.preview.Preview
|
import androidx.compose.ui.tooling.preview.Preview
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import androidx.core.app.ShareCompat
|
import androidx.core.app.ShareCompat
|
||||||
import androidx.core.content.ContextCompat
|
import androidx.core.content.ContextCompat
|
||||||
import androidx.core.content.FileProvider
|
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.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.components.appbar.AppBarWithBackButton
|
||||||
import com.tangem.core.ui.extensions.stringResourceSafe
|
import com.tangem.core.ui.extensions.stringResourceSafe
|
||||||
import com.tangem.core.ui.res.TangemTheme
|
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.impl.R
|
||||||
import com.tangem.feature.tester.presentation.actions.TesterActionsContentState.HideAllCurrenciesUM
|
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.ToggleHotWalletRestrictionUM
|
||||||
|
import com.tangem.feature.tester.presentation.actions.TesterActionsContentState.UsedeskTokenTtlUM
|
||||||
import com.tangem.utils.logging.TangemLogger
|
import com.tangem.utils.logging.TangemLogger
|
||||||
|
import kotlinx.collections.immutable.toImmutableList
|
||||||
import java.io.File
|
import java.io.File
|
||||||
|
import kotlin.time.Duration.Companion.days
|
||||||
|
import kotlin.time.Duration.Companion.minutes
|
||||||
|
|
||||||
@OptIn(ExperimentalFoundationApi::class)
|
@OptIn(ExperimentalFoundationApi::class)
|
||||||
@Composable
|
@Composable
|
||||||
internal fun TesterActionsScreen(state: TesterActionsContentState, modifier: Modifier = Modifier) {
|
internal fun TesterActionsScreen(state: TesterActionsContentState, modifier: Modifier = Modifier) {
|
||||||
|
var shouldShowTtlSelector by remember { mutableStateOf(value = false) }
|
||||||
|
var shouldShowTtlCustomInput by remember { mutableStateOf(value = false) }
|
||||||
|
|
||||||
LazyColumn(
|
LazyColumn(
|
||||||
modifier = modifier
|
modifier = modifier
|
||||||
.fillMaxSize()
|
.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 {
|
item {
|
||||||
val activity = LocalContext.current.findActivity()
|
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
|
@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?) {
|
private fun Activity.shareFile(file: File?) {
|
||||||
val originalIntent = createEmailShareIntent(activity = this, file = file)
|
val originalIntent = createEmailShareIntent(activity = this, file = file)
|
||||||
|
|
||||||
|
|
@ -126,6 +222,15 @@ private fun TesterActionsScreenSample(modifier: Modifier = Modifier) {
|
||||||
state = TesterActionsContentState(
|
state = TesterActionsContentState(
|
||||||
hideAllCurrenciesUM = HideAllCurrenciesUM.Clickable {},
|
hideAllCurrenciesUM = HideAllCurrenciesUM.Clickable {},
|
||||||
toggleHotWalletRestrictionUM = ToggleHotWalletRestrictionUM(isEnabled = true) {},
|
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),
|
shareLogsUM = TesterActionsContentState.ShareLogsUM(file = null),
|
||||||
onBackClick = {},
|
onBackClick = {},
|
||||||
),
|
),
|
||||||
|
|
|
||||||
|
|
@ -9,14 +9,21 @@ import com.tangem.data.common.account.WalletAccountsSaver
|
||||||
import com.tangem.domain.common.wallets.UserWalletsListRepository
|
import com.tangem.domain.common.wallets.UserWalletsListRepository
|
||||||
import com.tangem.domain.feedback.repository.FeedbackRepository
|
import com.tangem.domain.feedback.repository.FeedbackRepository
|
||||||
import com.tangem.domain.settings.HotWalletRestrictionManager
|
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.HideAllCurrenciesUM
|
||||||
import com.tangem.feature.tester.presentation.actions.TesterActionsContentState.ToggleHotWalletRestrictionUM
|
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 com.tangem.feature.tester.presentation.navigation.InnerTesterRouter
|
||||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||||
|
import kotlinx.collections.immutable.persistentListOf
|
||||||
import kotlinx.coroutines.flow.launchIn
|
import kotlinx.coroutines.flow.launchIn
|
||||||
import kotlinx.coroutines.flow.onEach
|
import kotlinx.coroutines.flow.onEach
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import javax.inject.Inject
|
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
|
@HiltViewModel
|
||||||
internal class TesterActionsViewModel @Inject constructor(
|
internal class TesterActionsViewModel @Inject constructor(
|
||||||
|
|
@ -24,6 +31,7 @@ internal class TesterActionsViewModel @Inject constructor(
|
||||||
private val userWalletsListRepository: UserWalletsListRepository,
|
private val userWalletsListRepository: UserWalletsListRepository,
|
||||||
private val walletAccountsSaver: WalletAccountsSaver,
|
private val walletAccountsSaver: WalletAccountsSaver,
|
||||||
private val hotWalletRestrictionManager: HotWalletRestrictionManager,
|
private val hotWalletRestrictionManager: HotWalletRestrictionManager,
|
||||||
|
private val usedeskTokenTtlManager: UsedeskTokenTtlManager,
|
||||||
) : ViewModel() {
|
) : ViewModel() {
|
||||||
|
|
||||||
var uiState: TesterActionsContentState by mutableStateOf(initialState)
|
var uiState: TesterActionsContentState by mutableStateOf(initialState)
|
||||||
|
|
@ -36,12 +44,19 @@ internal class TesterActionsViewModel @Inject constructor(
|
||||||
isEnabled = hotWalletRestrictionManager.isCreationEnabledSync(),
|
isEnabled = hotWalletRestrictionManager.isCreationEnabledSync(),
|
||||||
onClick = this::toggleHotWalletRestriction,
|
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()),
|
shareLogsUM = TesterActionsContentState.ShareLogsUM(file = feedbackRepository.getLogFile()),
|
||||||
onBackClick = { /* no-op */ },
|
onBackClick = { /* no-op */ },
|
||||||
)
|
)
|
||||||
|
|
||||||
init {
|
init {
|
||||||
bootstrapHotWalletRestrictionUpdates()
|
bootstrapHotWalletRestrictionUpdates()
|
||||||
|
bootstrapUsedeskTokenTtlUpdates()
|
||||||
}
|
}
|
||||||
|
|
||||||
fun setupNavigation(router: InnerTesterRouter) {
|
fun setupNavigation(router: InnerTesterRouter) {
|
||||||
|
|
@ -76,6 +91,10 @@ internal class TesterActionsViewModel @Inject constructor(
|
||||||
hotWalletRestrictionManager.toggleCreationEnabled()
|
hotWalletRestrictionManager.toggleCreationEnabled()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun setUsedeskTokenTtl(millis: Long) = viewModelScope.launch {
|
||||||
|
usedeskTokenTtlManager.setTokenTtlMillis(millis)
|
||||||
|
}
|
||||||
|
|
||||||
private fun bootstrapHotWalletRestrictionUpdates() {
|
private fun bootstrapHotWalletRestrictionUpdates() {
|
||||||
hotWalletRestrictionManager.isCreationEnabled()
|
hotWalletRestrictionManager.isCreationEnabled()
|
||||||
.onEach { isEnabled ->
|
.onEach { isEnabled ->
|
||||||
|
|
@ -87,4 +106,52 @@ internal class TesterActionsViewModel @Inject constructor(
|
||||||
}
|
}
|
||||||
.launchIn(viewModelScope)
|
.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"}"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -15,6 +15,9 @@
|
||||||
<string name="excluded_blockchains_search_placeholder" translatable="false">Filter by name or symbol</string>
|
<string name="excluded_blockchains_search_placeholder" translatable="false">Filter by name or symbol</string>
|
||||||
<string name="blockchain_providers" translatable="false">Blockchain providers</string>
|
<string name="blockchain_providers" translatable="false">Blockchain providers</string>
|
||||||
<string name="toggle_hot_wallet_restriction" translatable="false">Hot wallet creation restriction - %s</string>
|
<string name="toggle_hot_wallet_restriction" translatable="false">Hot wallet creation restriction - %s</string>
|
||||||
|
<string name="usedesk_token_ttl" translatable="false">Usedesk chat token TTL - %s</string>
|
||||||
|
<string name="usedesk_token_ttl_title" translatable="false">Usedesk chat token TTL</string>
|
||||||
|
<string name="usedesk_token_ttl_custom" translatable="false">Custom (minutes)</string>
|
||||||
<string name="share_logs" translatable="false">Share logs</string>
|
<string name="share_logs" translatable="false">Share logs</string>
|
||||||
<string name="test_push" translatable="false">Test push</string>
|
<string name="test_push" translatable="false">Test push</string>
|
||||||
<string name="accounts" translatable="false">Accounts</string>
|
<string name="accounts" translatable="false">Accounts</string>
|
||||||
|
|
|
||||||
|
|
@ -26,6 +26,7 @@ dependencies {
|
||||||
|
|
||||||
/** Domain */
|
/** Domain */
|
||||||
implementation(projects.domain.feedback)
|
implementation(projects.domain.feedback)
|
||||||
|
implementation(projects.domain.settings)
|
||||||
|
|
||||||
/** DI */
|
/** DI */
|
||||||
implementation(deps.hilt.android)
|
implementation(deps.hilt.android)
|
||||||
|
|
|
||||||
|
|
@ -9,6 +9,7 @@ import com.tangem.core.decompose.model.ParamsContainer
|
||||||
import com.tangem.datasource.local.preferences.AppPreferencesStore
|
import com.tangem.datasource.local.preferences.AppPreferencesStore
|
||||||
import com.tangem.datasource.local.preferences.PreferencesKeys
|
import com.tangem.datasource.local.preferences.PreferencesKeys
|
||||||
import com.tangem.domain.feedback.repository.FeedbackRepository
|
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.analytics.UsedeskAnalyticsEvents
|
||||||
import com.tangem.feature.usedesk.api.UsedeskComponent
|
import com.tangem.feature.usedesk.api.UsedeskComponent
|
||||||
import com.tangem.usedesk.chat_sdk.entity.UsedeskChatConfiguration
|
import com.tangem.usedesk.chat_sdk.entity.UsedeskChatConfiguration
|
||||||
|
|
@ -29,6 +30,7 @@ internal class UsedeskModel @Inject constructor(
|
||||||
private val appPreferencesStore: AppPreferencesStore,
|
private val appPreferencesStore: AppPreferencesStore,
|
||||||
private val analyticsEventHandler: AnalyticsEventHandler,
|
private val analyticsEventHandler: AnalyticsEventHandler,
|
||||||
private val feedbackRepository: FeedbackRepository,
|
private val feedbackRepository: FeedbackRepository,
|
||||||
|
private val usedeskTokenTtlManager: UsedeskTokenTtlManager,
|
||||||
paramsContainer: ParamsContainer,
|
paramsContainer: ParamsContainer,
|
||||||
) : Model() {
|
) : Model() {
|
||||||
|
|
||||||
|
|
@ -54,6 +56,7 @@ internal class UsedeskModel @Inject constructor(
|
||||||
clientEmail = params.userWalletId,
|
clientEmail = params.userWalletId,
|
||||||
clientInitMessage = params.prefilledMessage,
|
clientInitMessage = params.prefilledMessage,
|
||||||
additionalFields = additionalFieldsFor(params.source),
|
additionalFields = additionalFieldsFor(params.source),
|
||||||
|
tokenTtlMillis = usedeskTokenTtlManager.getTokenTtlMillisSync(),
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -13,7 +13,7 @@ tangemVico = "tangem-master-21"
|
||||||
#tangemVico = "0.0.1" # Keep it! - used for local builds ^
|
#tangemVico = "0.0.1" # Keep it! - used for local builds ^
|
||||||
tangemHotSdk = "develop-550"
|
tangemHotSdk = "develop-550"
|
||||||
#tangemHotSdk = "0.0.1" # Keep it! - used for local builds ^
|
#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 ^
|
#tangemUsedeskSdk = "0.0.1" # Keep it! - used for local builds ^
|
||||||
|
|
||||||
[libraries]
|
[libraries]
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue