From 657cc5bfd29bc1cb62e03f58ba76cf0f7d145467 Mon Sep 17 00:00:00 2001 From: Tangem Date: Fri, 10 Jul 2026 16:20:49 +0400 Subject: [PATCH] Updated on 2026-08-14 --- app/src/main/assets/tangem-app-config | 2 +- features/tester/impl/build.gradle.kts | 1 + .../tester/presentation/TesterActivity.kt | 13 + .../backendauth/state/BackendAuthStatusUM.kt | 56 ++++ .../backendauth/ui/BackendAuthStatusScreen.kt | 182 +++++++++++++ .../viewmodels/BackendAuthStatusViewModel.kt | 251 ++++++++++++++++++ .../presentation/menu/state/TesterMenuUM.kt | 1 + .../presentation/navigation/TesterScreen.kt | 1 + .../impl/src/main/res/values/strings.xml | 1 + 9 files changed, 507 insertions(+), 1 deletion(-) create mode 100644 features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/backendauth/state/BackendAuthStatusUM.kt create mode 100644 features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/backendauth/ui/BackendAuthStatusScreen.kt create mode 100644 features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/backendauth/viewmodels/BackendAuthStatusViewModel.kt diff --git a/app/src/main/assets/tangem-app-config b/app/src/main/assets/tangem-app-config index f983c6defd..a51f37e5f3 160000 --- a/app/src/main/assets/tangem-app-config +++ b/app/src/main/assets/tangem-app-config @@ -1 +1 @@ -Subproject commit f983c6defd0b2240eb6f134aefe30f7e93696795 +Subproject commit a51f37e5f339fd83357443f4a083f7af0155fcdb diff --git a/features/tester/impl/build.gradle.kts b/features/tester/impl/build.gradle.kts index dc426eac96..342d35d5f8 100644 --- a/features/tester/impl/build.gradle.kts +++ b/features/tester/impl/build.gradle.kts @@ -83,6 +83,7 @@ dependencies { api(projects.libs.blockchainSdk) implementation(projects.common) implementation(projects.common.ui) + implementation(projects.libs.auth) implementation(projects.libs.crypto) implementation(projects.libs.tangemSdkApi) } \ No newline at end of file diff --git a/features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/TesterActivity.kt b/features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/TesterActivity.kt index a1ede089f5..1d34a19cde 100644 --- a/features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/TesterActivity.kt +++ b/features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/TesterActivity.kt @@ -23,6 +23,8 @@ import com.tangem.feature.tester.presentation.actions.TesterActionsScreen import com.tangem.feature.tester.presentation.actions.TesterActionsViewModel import com.tangem.feature.tester.presentation.addresses.ui.AddressesInfoScreen import com.tangem.feature.tester.presentation.addresses.viewmodels.AddressesInfoViewModel +import com.tangem.feature.tester.presentation.backendauth.ui.BackendAuthStatusScreen +import com.tangem.feature.tester.presentation.backendauth.viewmodels.BackendAuthStatusViewModel import com.tangem.feature.tester.presentation.environments.ui.EnvironmentTogglesScreen import com.tangem.feature.tester.presentation.environments.viewmodels.EnvironmentsTogglesViewModel import com.tangem.feature.tester.presentation.excludedblockchains.ExcludedBlockchainsScreen @@ -99,6 +101,7 @@ internal class TesterActivity : ComposeActivity() { ButtonUM.ADDRESSES_INFO, ButtonUM.STORY_BOOK, ButtonUM.SURVEY_SPARROW, + ButtonUM.BACKEND_AUTH_STATUS, ), onButtonClick = { buttonUM -> val route = when (buttonUM) { @@ -112,6 +115,7 @@ internal class TesterActivity : ComposeActivity() { ButtonUM.ADDRESSES_INFO -> TesterScreen.ADDRESSES_INFO ButtonUM.STORY_BOOK -> TesterScreen.STORY_BOOK ButtonUM.SURVEY_SPARROW -> TesterScreen.SURVEY_SPARROW + ButtonUM.BACKEND_AUTH_STATUS -> TesterScreen.BACKEND_AUTH_STATUS } innerTesterRouter.open(route) @@ -212,6 +216,15 @@ internal class TesterActivity : ComposeActivity() { } } } + + composable(route = TesterScreen.BACKEND_AUTH_STATUS.name) { + val viewModel = hiltViewModel().apply { + setupNavigation(innerTesterRouter) + } + val state by viewModel.uiState.collectAsStateWithLifecycle() + + BackendAuthStatusScreen(state) + } } } diff --git a/features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/backendauth/state/BackendAuthStatusUM.kt b/features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/backendauth/state/BackendAuthStatusUM.kt new file mode 100644 index 0000000000..03d953a26f --- /dev/null +++ b/features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/backendauth/state/BackendAuthStatusUM.kt @@ -0,0 +1,56 @@ +package com.tangem.feature.tester.presentation.backendauth.state + +import androidx.annotation.DrawableRes +import kotlinx.collections.immutable.ImmutableList +import kotlinx.collections.immutable.persistentListOf + +/** + * Content state of the Backend Authentication status screen. + * + * Grouped into [Section]s: each section shows a set of status [StatusRow]s followed by the action + * buttons that operate on them. + * + * @property onBackClick invoked when back button is pressed + * @property sections status/action groups to display + * @property onCopyClick invoked with a row whose [StatusRow.copyValue] is non-null to copy the full value + * @property runningAction label of the action currently running (shows progress, disables others) + */ +internal data class BackendAuthStatusUM( + val onBackClick: () -> Unit = {}, + val sections: ImmutableList
= persistentListOf(), + val onCopyClick: (StatusRow) -> Unit = {}, + val runningAction: String? = null, +) { + + /** A group of related status rows and the actions that operate on them. */ + data class Section( + val rows: ImmutableList, + val actions: ImmutableList = persistentListOf(), + ) + + /** + * A single label → value status row. + * + * @property value displayed value (may be shortened for keys/tokens) + * @property copyValue full value to copy; when non-null a copy action is shown + * @property subtitle optional extra line under the value (e.g. token expiry) + */ + data class StatusRow( + val label: String, + val value: String, + val copyValue: String? = null, + val subtitle: String? = null, + val iconActions: ImmutableList = persistentListOf(), + ) + + /** A full-width action button (mutates auth state, then refreshes the panel). */ + data class Action(val label: String, val onClick: () -> Unit) + + /** A compact icon action shown inline at the end of a row (like the copy icon). */ + data class IconAction( + val label: String, + @DrawableRes val iconRes: Int, + val isProgressShown: Boolean = true, + val onClick: () -> Unit, + ) +} \ No newline at end of file diff --git a/features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/backendauth/ui/BackendAuthStatusScreen.kt b/features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/backendauth/ui/BackendAuthStatusScreen.kt new file mode 100644 index 0000000000..d4258404d8 --- /dev/null +++ b/features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/backendauth/ui/BackendAuthStatusScreen.kt @@ -0,0 +1,182 @@ +package com.tangem.feature.tester.presentation.backendauth.ui + +import androidx.compose.animation.Crossfade +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.statusBarsPadding +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.items +import androidx.compose.foundation.lazy.itemsIndexed +import androidx.compose.foundation.text.selection.SelectionContainer +import androidx.compose.material3.CircularProgressIndicator +import androidx.compose.material3.Icon +import androidx.compose.material3.IconButton +import androidx.compose.material3.Scaffold +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.vector.ImageVector +import androidx.compose.ui.res.vectorResource +import androidx.compose.ui.unit.dp +import com.tangem.core.ui.components.SecondaryButton +import com.tangem.core.ui.components.appbar.AppBarWithBackButton +import com.tangem.core.ui.components.divider.DividerWithPadding +import com.tangem.core.ui.extensions.stringResourceSafe +import com.tangem.core.ui.res.TangemTheme +import com.tangem.feature.tester.impl.R +import com.tangem.feature.tester.presentation.backendauth.state.BackendAuthStatusUM +import com.tangem.core.ui.R as CoreUiR + +/** + * Read-only screen showing the current Backend Authentication state (toggle, environment, + * device key, registration flag, session tokens). Keys/tokens are shown shortened with a copy + * action; all values are also selectable. + * + * @param state screen state + */ +@Composable +internal fun BackendAuthStatusScreen(state: BackendAuthStatusUM) { + Scaffold( + topBar = { + AppBarWithBackButton( + onBackClick = state.onBackClick, + text = stringResourceSafe(id = R.string.backend_auth_status), + modifier = Modifier.statusBarsPadding(), + ) + }, + containerColor = TangemTheme.colors.background.secondary, + ) { paddingValues -> + SelectionContainer { + LazyColumn( + modifier = Modifier + .fillMaxSize() + .padding(paddingValues), + contentPadding = PaddingValues(vertical = 8.dp), + ) { + state.sections.forEachIndexed { sectionIndex, section -> + itemsIndexed(items = section.rows, key = { _, row -> "row_${row.label}" }) { index, row -> + Column(modifier = Modifier.animateItem()) { + StatusRow( + row = row, + runningAction = state.runningAction, + onCopyClick = { state.onCopyClick(row) }, + ) + if (index < section.rows.lastIndex) { + DividerWithPadding(start = 16.dp, end = 16.dp) + } + } + } + + items(items = section.actions, key = { "act_${it.label}" }) { action -> + SecondaryButton( + text = action.label, + onClick = action.onClick, + showProgress = state.runningAction == action.label, + enabled = state.runningAction == null, + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = 16.dp, vertical = 4.dp), + ) + } + + if (sectionIndex < state.sections.lastIndex) { + item(key = "gap_$sectionIndex") { + DividerWithPadding(start = 16.dp, end = 16.dp) + } + } + } + } + } + } +} + +@Composable +private fun StatusRow(row: BackendAuthStatusUM.StatusRow, runningAction: String?, onCopyClick: () -> Unit) { + Column( + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = 16.dp, vertical = 6.dp), + ) { + Text( + text = row.label, + style = TangemTheme.typography.caption1, + color = TangemTheme.colors.text.secondary, + ) + Row( + modifier = Modifier.fillMaxWidth(), + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.SpaceBetween, + ) { + Crossfade( + targetState = row.value, + modifier = Modifier.weight(1f), + label = "value", + ) { value -> + Text( + text = value, + style = TangemTheme.typography.body2, + color = TangemTheme.colors.text.primary1, + ) + } + RowTrailingIcons(row = row, runningAction = runningAction, onCopyClick = onCopyClick) + } + if (row.subtitle != null) { + Text( + text = row.subtitle, + style = TangemTheme.typography.caption2, + color = TangemTheme.colors.text.tertiary, + ) + } + } +} + +@Composable +private fun RowTrailingIcons(row: BackendAuthStatusUM.StatusRow, runningAction: String?, onCopyClick: () -> Unit) { + Row( + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(12.dp), + ) { + if (row.copyValue != null) { + IconButton( + onClick = onCopyClick, + modifier = Modifier.size(32.dp), + ) { + Icon( + imageVector = ImageVector.vectorResource(CoreUiR.drawable.ic_copy_24), + contentDescription = "Copy ${row.label}", + tint = TangemTheme.colors.icon.primary1, + ) + } + } + row.iconActions.forEach { iconAction -> + if (runningAction == iconAction.label && iconAction.isProgressShown) { + CircularProgressIndicator( + modifier = Modifier + .size(32.dp) + .padding(6.dp), + strokeWidth = 2.dp, + color = TangemTheme.colors.icon.primary1, + ) + } else { + IconButton( + onClick = iconAction.onClick, + enabled = runningAction == null, + modifier = Modifier.size(32.dp), + ) { + Icon( + imageVector = ImageVector.vectorResource(iconAction.iconRes), + contentDescription = iconAction.label, + tint = TangemTheme.colors.icon.primary1, + ) + } + } + } + } +} \ No newline at end of file diff --git a/features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/backendauth/viewmodels/BackendAuthStatusViewModel.kt b/features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/backendauth/viewmodels/BackendAuthStatusViewModel.kt new file mode 100644 index 0000000000..cdfc41207d --- /dev/null +++ b/features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/backendauth/viewmodels/BackendAuthStatusViewModel.kt @@ -0,0 +1,251 @@ +package com.tangem.feature.tester.presentation.backendauth.viewmodels + +import android.content.Context +import android.util.Base64 +import android.widget.Toast +import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope +import com.tangem.core.ui.clipboard.ClipboardManager +import com.tangem.datasource.api.common.config.ApiConfig +import com.tangem.datasource.api.common.config.managers.ApiConfigsManager +import com.tangem.datasource.local.preferences.AppPreferencesStore +import com.tangem.datasource.local.preferences.PreferencesKeys +import com.tangem.datasource.local.preferences.utils.getSyncOrDefault +import com.tangem.datasource.local.preferences.utils.store +import com.tangem.feature.tester.presentation.backendauth.state.BackendAuthStatusUM +import com.tangem.feature.tester.presentation.backendauth.state.BackendAuthStatusUM.Action +import com.tangem.feature.tester.presentation.backendauth.state.BackendAuthStatusUM.IconAction +import com.tangem.feature.tester.presentation.backendauth.state.BackendAuthStatusUM.Section +import com.tangem.feature.tester.presentation.backendauth.state.BackendAuthStatusUM.StatusRow +import com.tangem.core.ui.R as CoreUiR +import com.tangem.feature.tester.presentation.navigation.InnerTesterRouter +import com.tangem.lib.auth.AuthFeatureToggles +import com.tangem.lib.auth.devicekey.DeviceKeyManager +import com.tangem.lib.auth.session.DeviceRegistrar +import com.tangem.lib.auth.session.SessionTokenRefresher +import com.tangem.lib.auth.session.SessionTokens +import com.tangem.lib.auth.session.SessionTokensStore +import dagger.hilt.android.lifecycle.HiltViewModel +import dagger.hilt.android.qualifiers.ApplicationContext +import kotlinx.collections.immutable.ImmutableList +import kotlinx.collections.immutable.persistentListOf +import kotlinx.collections.immutable.toImmutableList +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.flow.update +import kotlinx.coroutines.launch +import kotlinx.datetime.Clock +import kotlinx.datetime.Instant +import kotlinx.datetime.toJavaInstant +import java.time.ZoneId +import java.time.format.DateTimeFormatter +import javax.inject.Inject +import kotlin.coroutines.cancellation.CancellationException +import kotlin.time.Duration.Companion.hours + +/** + * ViewModel for the Backend Authentication status screen. + * + * Aggregates the observable auth state (toggle, environment, device key, registration flag, + * session tokens) so QA doesn't have to read logcat, and exposes action buttons grouped by the + * state they operate on. Keys/tokens are shown shortened with a copy action for the full value. + */ +@Suppress("LongParameterList") +@HiltViewModel +internal class BackendAuthStatusViewModel @Inject constructor( + private val authFeatureToggles: AuthFeatureToggles, + private val deviceKeyManager: DeviceKeyManager, + private val sessionTokensStore: SessionTokensStore, + private val appPreferencesStore: AppPreferencesStore, + private val apiConfigsManager: ApiConfigsManager, + private val clipboardManager: ClipboardManager, + private val deviceRegistrar: DeviceRegistrar, + private val sessionTokenRefresher: SessionTokenRefresher, + @ApplicationContext private val context: Context, +) : ViewModel() { + + private val _uiState = MutableStateFlow(BackendAuthStatusUM(onCopyClick = ::onCopy)) + val uiState: StateFlow = _uiState.asStateFlow() + + init { + loadStatus() + } + + /** Setup navigation state property by router [router] */ + fun setupNavigation(router: InnerTesterRouter) { + _uiState.update { it.copy(onBackClick = router::back) } + } + + private fun loadStatus() { + viewModelScope.launch { + _uiState.update { it.copy(sections = buildSections()) } + } + } + + private suspend fun buildSections(): ImmutableList
{ + val publicKeyRow = deviceKeyManager.getPublicKeyEncoded().getOrNull()?.let { key -> + val spki = Base64.encodeToString(key, Base64.NO_WRAP) + StatusRow("Device public key (SPKI)", spki.shorten(), copyValue = spki) + } ?: StatusRow("Device public key (SPKI)", "absent") + + val environmentValue = runCatching { + val authEnv = apiConfigsManager.getEnvironmentConfig(ApiConfig.ID.Auth) + "${authEnv.environment.name} (${authEnv.baseUrl})" + }.getOrDefault("unavailable") + val isRegistered = appPreferencesStore.getSyncOrDefault( + PreferencesKeys.IS_DEVICE_REGISTERED_KEY, + default = false, + ) + val tokens = sessionTokensStore.get().getOrNull() + + return persistentListOf( + Section( + rows = persistentListOf( + StatusRow("Feature toggle", if (authFeatureToggles.isBackendAuthenticationEnabled) "ON" else "OFF"), + StatusRow("Environment", environmentValue), + publicKeyRow, + ), + ), + Section( + rows = persistentListOf( + StatusRow( + label = "Registered", + value = if (isRegistered) "✅" else "❌", + iconActions = persistentListOf( + iconAction("Register now", CoreUiR.drawable.ic_plus_24) { + deviceRegistrar.register().fold({ "failed: $it" }, { "ok" }) + }, + iconAction("Reset registration", CoreUiR.drawable.ic_close_24, isProgressShown = false) { + resetRegistration() + }, + ), + ), + ), + ), + Section( + rows = buildTokenRows(tokens), + actions = persistentListOf( + action("Force refresh") { sessionTokenRefresher.refresh().fold({ "failed: $it" }, { "ok" }) }, + action("Corrupt access token") { corruptAccessToken() }, + action("Expire access token") { expireAccessToken() }, + action("Clear session tokens") { sessionTokensStore.clear(); "done" }, + ), + ), + ) + } + + private fun buildTokenRows(tokens: SessionTokens?): ImmutableList = buildList { + add(StatusRow("Session tokens", if (tokens != null) "✅" else "❌")) + if (tokens != null) { + val isAccessBlank = tokens.accessToken.isBlank() + add( + StatusRow( + label = "Access token", + value = if (isAccessBlank) "—" else tokens.accessToken.shorten(), + copyValue = tokens.accessToken.ifBlank { null }, + subtitle = if (isAccessBlank) { + null + } else { + "expires ${tokens.accessTokenExpiresAt.formatWithCountdown(hoursMinutes = true)}" + }, + ), + ) + val refresh = tokens.refreshToken + add( + StatusRow( + label = "Refresh token", + value = refresh?.shorten() ?: "none", + copyValue = refresh, + subtitle = tokens.refreshTokenExpiresAt?.let { "expires ${it.formatWithCountdown()}" }, + ), + ) + add(StatusRow("Wallet IDs", tokens.walletIds.joinToString().ifEmpty { "—" })) + } + }.toImmutableList() + + private fun onCopy(row: StatusRow) { + val value = row.copyValue ?: return + clipboardManager.setText(text = value, isSensitive = false, label = row.label) + Toast.makeText(context, "Copied ${row.label}", Toast.LENGTH_SHORT).show() + } + + private fun action(label: String, block: suspend () -> String): Action = Action(label) { runAction(label, block) } + + private fun iconAction( + label: String, + iconRes: Int, + isProgressShown: Boolean = true, + block: suspend () -> String, + ): IconAction = IconAction( + label = label, + iconRes = iconRes, + isProgressShown = isProgressShown, + ) { runAction(label, block) } + + /** Shows a progress on the tapped button, runs [block], toasts its result, then refreshes the panel. */ + private fun runAction(label: String, block: suspend () -> String) { + if (_uiState.value.runningAction != null) return + viewModelScope.launch { + _uiState.update { it.copy(runningAction = label) } + val result = try { + block() + } catch (e: CancellationException) { + throw e + } catch (e: Exception) { + "error: ${e.message}" + } + Toast.makeText(context, "$label: $result", Toast.LENGTH_SHORT).show() + _uiState.update { it.copy(sections = buildSections(), runningAction = null) } + } + } + + /** Replaces the access token with a blank value (refresh token kept) so the next request 401s. */ + private suspend fun corruptAccessToken(): String { + val tokens = sessionTokensStore.get().getOrNull() ?: return "no tokens" + sessionTokensStore.save(tokens.copy(accessToken = "")) + return "done" + } + + /** Back-dates the access token expiry so it is treated as expired. */ + private suspend fun expireAccessToken(): String { + val tokens = sessionTokensStore.get().getOrNull() ?: return "no tokens" + sessionTokensStore.save(tokens.copy(accessTokenExpiresAt = Clock.System.now() - 1.hours)) + return "done" + } + + /** Clears tokens and the registration flag so the next launch re-registers (no `pm clear`). */ + private suspend fun resetRegistration(): String { + sessionTokensStore.clear() + appPreferencesStore.store(PreferencesKeys.IS_DEVICE_REGISTERED_KEY, value = false) + return "done" + } + + /** Shortens a long value to `prefix…suffix` for display; the full value stays copyable. */ + private fun String.shorten(): String = + if (length <= SHORTEN_KEEP * 2 + 1) this else "${take(SHORTEN_KEEP)}…${takeLast(SHORTEN_KEEP)}" + + /** + * Formats an [Instant] as `dd.MM.yyyy HH:mm` plus a remaining-time hint. + * @param hoursMinutes `true` → `… (3h 25m left)` (short-lived access token); + * `false` → `… (2d 5h left)` (long-lived refresh token). + */ + private fun Instant.formatWithCountdown(hoursMinutes: Boolean = false): String { + val readable = EXPIRY_FORMATTER.format(toJavaInstant()) + val left = this - Clock.System.now() + val hint = when { + left.isNegative() -> "expired" + hoursMinutes -> "${left.inWholeHours}h ${left.inWholeMinutes % MINUTES_IN_HOUR}m left" + else -> "${left.inWholeDays}d ${left.inWholeHours % HOURS_IN_DAY}h left" + } + return "$readable ($hint)" + } + + private companion object { + const val SHORTEN_KEEP = 6 + const val HOURS_IN_DAY = 24 + const val MINUTES_IN_HOUR = 60 + val EXPIRY_FORMATTER: DateTimeFormatter = + DateTimeFormatter.ofPattern("dd.MM.yyyy HH:mm").withZone(ZoneId.systemDefault()) + } +} \ No newline at end of file diff --git a/features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/menu/state/TesterMenuUM.kt b/features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/menu/state/TesterMenuUM.kt index 59523e315d..f73d03ea7b 100644 --- a/features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/menu/state/TesterMenuUM.kt +++ b/features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/menu/state/TesterMenuUM.kt @@ -28,5 +28,6 @@ data class TesterMenuUM( ADDRESSES_INFO(R.string.addresses_info), STORY_BOOK(R.string.story_book), SURVEY_SPARROW(R.string.survey_sparrow), + BACKEND_AUTH_STATUS(R.string.backend_auth_status), } } \ No newline at end of file diff --git a/features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/navigation/TesterScreen.kt b/features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/navigation/TesterScreen.kt index 09864f11b0..19df253534 100644 --- a/features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/navigation/TesterScreen.kt +++ b/features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/navigation/TesterScreen.kt @@ -17,4 +17,5 @@ internal enum class TesterScreen { ADDRESSES_INFO, STORY_BOOK, SURVEY_SPARROW, + BACKEND_AUTH_STATUS, } \ 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 cd08c14477..a332ecc23d 100644 --- a/features/tester/impl/src/main/res/values/strings.xml +++ b/features/tester/impl/src/main/res/values/strings.xml @@ -28,4 +28,5 @@ Addresses info Story book Survey Sparrow + Backend Auth