From 2453d636336834e7b6fd3b23dfae6ce047fb9a4f Mon Sep 17 00:00:00 2001 From: Tangem Date: Wed, 3 Jun 2026 17:04:46 +0200 Subject: [PATCH] Updated on 2026-08-14 --- .claude/skills/write-ui-test/SKILL.md | 10 +- .../write-ui-test/reference/compose-traps.md | 24 +++ .../reference/running-and-debugging.md | 74 ++++++++- .../scenarios/DeviceSetingsScenarios.kt | 6 + .../screens/DeviceSettingsPageObject.kt | 16 ++ .../tangem/screens/SecurityModePageObject.kt | 27 ++++ .../kotlin/com/tangem/tests/DetailsTest.kt | 142 ++++++++++++++++++ .../com/tangem/tests/SecurityModeTest.kt | 68 +++++++++ .../tap/domain/sdk/mocks/MockProvider.kt | 4 + .../sdk/mocks/content/S2CMockContent.kt | 112 ++++++++++++++ .../content/SingleCurrencyMockContent.kt | 112 ++++++++++++++ .../domain/sdk/mocks/content/V3MockContent.kt | 112 ++++++++++++++ 12 files changed, 702 insertions(+), 5 deletions(-) create mode 100644 app/src/androidTest/kotlin/com/tangem/screens/SecurityModePageObject.kt create mode 100644 app/src/androidTest/kotlin/com/tangem/tests/SecurityModeTest.kt create mode 100644 app/src/main/java/com/tangem/tap/domain/sdk/mocks/content/S2CMockContent.kt create mode 100644 app/src/main/java/com/tangem/tap/domain/sdk/mocks/content/SingleCurrencyMockContent.kt create mode 100644 app/src/main/java/com/tangem/tap/domain/sdk/mocks/content/V3MockContent.kt diff --git a/.claude/skills/write-ui-test/SKILL.md b/.claude/skills/write-ui-test/SKILL.md index 11686c0e7c..d830f62b04 100644 --- a/.claude/skills/write-ui-test/SKILL.md +++ b/.claude/skills/write-ui-test/SKILL.md @@ -71,8 +71,9 @@ When the user asks to **port** an iOS test to Android: strings inside `step(...)`. - **Each click is its own** `step("Click on '$x' button")`. Combining clicks into one step hides which click failed in the Allure report. -- **Step naming**: `Click on 'X' button` (not "Tap X"); `Assert is displayed` (not "Check X - visible"). Keep it consistent with the existing suite. +- **Step naming**: `Click on 'X' button` (not "Tap X"); `Assert is displayed` / `is not displayed`. + Reviewers reject `is visible`, `does not exist`, `Check X visible` — the convention is **`is displayed` / + `is not displayed`** even though older tests in the file may still use the old phrasing (don't copy it). - **No conditional `if (foo.isDisplayedSafely()) foo.performClick()`** for elements that are deterministically present after `pm clear` — the `if` is dead code. Use a straight `performClick()`. @@ -131,5 +132,6 @@ Delete anything explaining WHAT a step does. - **`reference/compose-traps.md`** — read when the screen uses `PullToRefreshBox`, `TangemHoldToConfirmButton`, a Decompose model that fetches in `init {}`, or a hot-wallet import with an access code. These have silent failure modes that look like passing tests. -- **`reference/running-and-debugging.md`** — read when building, installing, running a single test, - interpreting CLI/Allure output, using `@Ignore`, or driving WireMock scenarios. \ No newline at end of file +- **`reference/running-and-debugging.md`** — read when building, installing, running tests (orchestrator + vs. raw `am instrument`), running against a local WireMock, interpreting CLI/Allure output, using + `@Ignore`, or driving WireMock scenarios. \ No newline at end of file diff --git a/.claude/skills/write-ui-test/reference/compose-traps.md b/.claude/skills/write-ui-test/reference/compose-traps.md index 03a06c524f..413a932acd 100644 --- a/.claude/skills/write-ui-test/reference/compose-traps.md +++ b/.claude/skills/write-ui-test/reference/compose-traps.md @@ -41,6 +41,30 @@ so the hold gesture is silently swallowed: the button looks fine, the user holds 3. Snapshot again — byte-identical trees mean `onConfirm` didn't run. 4. Or check WireMock request stats for the downstream API call expected after `onConfirm`. +## Asserting enabled/disabled on a `Modifier.clickable` row + +When a settings/list row puts `Modifier.clickable(enabled = isClickable, ...)` on the row **container** +(not the title `Text`), the enabled/disabled state lives on that container; the child Texts only carry +`testTag`/text. So `assertIsEnabled()` / `assertIsNotEnabled()` must target the container, matched by a +descendant text — not the title node itself. + +Match the container in BOTH states with **click-action OR disabled-semantics**. Do NOT rely on +`hasClickAction()` alone: depending on the Compose version a `clickable(enabled = false)` row may not +expose an onClick action, so a `hasClickAction()`-only matcher finds no node and `assertIsNotEnabled()` +fails with "No node found". + +```kotlin +import androidx.compose.ui.test.hasClickAction as withClickAction +import androidx.compose.ui.test.isNotEnabled as withDisabled + +val row: KNode = child { + addSemanticsMatcher(withClickAction() or withDisabled()) // matches enabled AND disabled rows + hasAnyDescendant(withText(getResourceString(R.string.row_title))) // narrows to the specific row + useUnmergedTree = true +} +// enabled card: row.assertIsEnabled() ; disabled card: row.assertIsNotEnabled() +``` + ## `assertTextContains(x)` defaults to exact-segment match, not substring `SemanticsNodeInteraction.assertTextContains(value, substring = false, ignoreCase = false)` defaults to diff --git a/.claude/skills/write-ui-test/reference/running-and-debugging.md b/.claude/skills/write-ui-test/reference/running-and-debugging.md index 8cf1f04834..5698b4e87f 100644 --- a/.claude/skills/write-ui-test/reference/running-and-debugging.md +++ b/.claude/skills/write-ui-test/reference/running-and-debugging.md @@ -29,6 +29,77 @@ adb shell am instrument -w \ com.tangem.wallet.mocked.test/com.tangem.common.HiltTestRunner ``` +## Harness: orchestrator vs. raw `am instrument` + +The app is configured `execution = "ANDROIDX_TEST_ORCHESTRATOR"` (`app/build.gradle.kts`). The orchestrator +runs **each test method in its own process** (and can clear app data between them). It is still 100% +local — it runs on the same emulator; nothing remote about it. + +Raw `adb shell am instrument` runs **all selected tests in one shared process**, which has two failure +modes that look like test bugs but aren't: + +- Running several tests in one invocation → `IllegalStateException: There are multiple DataStores active + for the same file` mid-run. Run them one at a time (with `pm clear` between) if you must use raw + `am instrument`. +- Tests that re-scan the card inside **Card/Device Settings** (the "Scan card or ring" gate) → + `IllegalStateException: Tangem SDK is null after re-registering with foreground activity`. The existing + `ResetCardTest` crashes identically under raw `am instrument`. These only pass via the orchestrator. + +**Prefer the orchestrator** (it's what CI/Marathon use). Run a class or method through Gradle: + +```bash +./gradlew :app:connectedGoogleMockedAndroidTest \ + -Pandroid.testInstrumentationRunnerArguments.class=com.tangem.tests.DetailsTest +# or a single method: ...class=com.tangem.tests.DetailsTest#someTest +# or several classes: ...class=com.tangem.tests.DetailsTest,com.tangem.tests.SecurityModeTest +``` + +Gradle installs both APKs, runs via the orchestrator, then **uninstalls them** — so a following raw +`am instrument` reports `Unable to find instrumentation info`; reinstall both APKs first. Read results +from the JUnit XML (authoritative pass/fail counts), not just stdout: + +```bash +ls -t app/build/outputs/androidTest-results/connected/mocked/flavors/google/*.xml | head -1 +# inspect tests="…" failures="…" errors="…" skipped="…" and the / nodes +``` + +## Running against local WireMock + +Every instrumentation test runs with `ApiEnvironment.MOCK` (forced in `BaseTestCase.setupHooks`), so the +app's API base URLs point at `wiremock.tests-d.com` — i.e. tests **always** talk to WireMock, never the +real backend. By default that's the **remote** WireMock at `wiremock.tests-d.com`. To use a **local** +WireMock instead, pass `wiremockBaseUrl`: `WireMockRedirectInterceptor` then rewrites every +`wiremock.tests-d.com` request to your local instance. + +Emulator addressing matters — `localhost` inside an emulator is the **emulator itself**, not your host: + +- Use the host alias **`http://10.0.2.2:8081`** (no extra setup), **or** +- `http://localhost:8081` **with** `adb reverse tcp:8081 tcp:8081` run first. + +Pass it through the orchestrator (recommended): + +```bash +curl -s -X POST http://localhost:8081/__admin/scenarios/reset # start clean +./gradlew :app:connectedGoogleMockedAndroidTest \ + -Pandroid.testInstrumentationRunnerArguments.class=com.tangem.tests.DetailsTest \ + -Pandroid.testInstrumentationRunnerArguments.wiremockBaseUrl=http://10.0.2.2:8081 +``` + +(Raw `am instrument` equivalent: `-e wiremockBaseUrl http://10.0.2.2:8081` — subject to the harness +caveats above.) + +**If a screen hangs / you get `ComposeNotIdleException` (infinite recomposition):** that usually means a +request the app made wasn't served (endless retry/loading), *not* a test bug. Ask WireMock what it +didn't match — this is the smoking gun: + +```bash +curl -s http://localhost:8081/__admin/requests/unmatched | jq '.requests[] | "\(.method) \(.url)"' +``` + +`unmatched: 0` means the URL plumbing is correct and local WireMock served everything — look elsewhere +(harness/emulator) for the hang. A non-empty list names exactly which mapping (or scenario state) the +local instance is missing. + ## Classify the result — Allure noise vs. real failure After `pm clear`, `/data/user/0//files/original_screenshots` doesn't exist → @@ -51,7 +122,8 @@ Distinguish: ## WireMock cheatsheet -Local override is detected; otherwise hits remote. Default local port: `8081`. +Without a `wiremockBaseUrl` arg the app hits the **remote** WireMock (`wiremock.tests-d.com`); pass the +arg to redirect to a local instance (see "Running against local WireMock"). Default local port: `8081`. ```bash # Set a scenario state — PUT, not POST diff --git a/app/src/androidTest/kotlin/com/tangem/scenarios/DeviceSetingsScenarios.kt b/app/src/androidTest/kotlin/com/tangem/scenarios/DeviceSetingsScenarios.kt index d58f6ba8e3..41afda9061 100644 --- a/app/src/androidTest/kotlin/com/tangem/scenarios/DeviceSetingsScenarios.kt +++ b/app/src/androidTest/kotlin/com/tangem/scenarios/DeviceSetingsScenarios.kt @@ -5,6 +5,12 @@ import com.tangem.common.extensions.clickWithAssertion import com.tangem.screens.onDeviceSettingsScreen import io.qameta.allure.kotlin.Allure.step +fun BaseTestCase.scanCardInDeviceSettings() { + step("Click on 'Scan card or ring' button") { + onDeviceSettingsScreen { scanCardOrRingButton.clickWithAssertion() } + } +} + fun BaseTestCase.openResetCardScreen(withBackup: Boolean = false) { step("Click on 'Scan card or ring' button") { onDeviceSettingsScreen { scanCardOrRingButton.clickWithAssertion() } diff --git a/app/src/androidTest/kotlin/com/tangem/screens/DeviceSettingsPageObject.kt b/app/src/androidTest/kotlin/com/tangem/screens/DeviceSettingsPageObject.kt index 7822f4b34e..f390b77959 100644 --- a/app/src/androidTest/kotlin/com/tangem/screens/DeviceSettingsPageObject.kt +++ b/app/src/androidTest/kotlin/com/tangem/screens/DeviceSettingsPageObject.kt @@ -13,6 +13,9 @@ import io.github.kakaocup.compose.node.element.ComposeScreen.Companion.onCompose import io.github.kakaocup.compose.node.element.KNode import io.github.kakaocup.compose.node.element.lazylist.KLazyListNode import io.github.kakaocup.kakao.common.utilities.getResourceString +import androidx.compose.ui.test.hasClickAction as withClickAction +import androidx.compose.ui.test.hasText as withText +import androidx.compose.ui.test.isNotEnabled as withDisabled class DeviceSettingsPageObject(semanticsProvider: SemanticsNodeInteractionsProvider) : ComposeScreen(semanticsProvider = semanticsProvider) { @@ -46,6 +49,19 @@ class DeviceSettingsPageObject(semanticsProvider: SemanticsNodeInteractionsProvi useUnmergedTree = true } + val securityModeRowTitle: KNode = child { + hasTestTag(DeviceSettingsScreenTestTags.ITEM_TITLE) + hasText(getResourceString(R.string.card_settings_security_mode)) + useUnmergedTree = true + } + + // Match the row container (not the title Text): enabled exposes a click action, disabled exposes disabled semantics. + val securityModeRow: KNode = child { + addSemanticsMatcher(withClickAction() or withDisabled()) + hasAnyDescendant(withText(getResourceString(R.string.card_settings_security_mode))) + useUnmergedTree = true + } + fun resetToFactorySettingsButtonSubtitle(withBackup: Boolean = false): KNode = child { hasTestTag(DeviceSettingsScreenTestTags.ITEM_SUBTITLE) useUnmergedTree = true diff --git a/app/src/androidTest/kotlin/com/tangem/screens/SecurityModePageObject.kt b/app/src/androidTest/kotlin/com/tangem/screens/SecurityModePageObject.kt new file mode 100644 index 0000000000..3b668e21d0 --- /dev/null +++ b/app/src/androidTest/kotlin/com/tangem/screens/SecurityModePageObject.kt @@ -0,0 +1,27 @@ +package com.tangem.screens + +import androidx.compose.ui.test.SemanticsNodeInteractionsProvider +import com.tangem.common.BaseTestCase +import com.tangem.wallet.R +import io.github.kakaocup.compose.node.element.ComposeScreen +import io.github.kakaocup.compose.node.element.ComposeScreen.Companion.onComposeScreen +import io.github.kakaocup.compose.node.element.KNode +import io.github.kakaocup.kakao.common.utilities.getResourceString + +class SecurityModePageObject(semanticsProvider: SemanticsNodeInteractionsProvider) : + ComposeScreen(semanticsProvider = semanticsProvider) { + + // Description only appears on the Security Mode screen — unambiguous "screen opened" signal. + val longTapOptionDescription: KNode = child { + hasText(getResourceString(R.string.details_manage_security_long_tap_description)) + useUnmergedTree = true + } + + val saveChangesButton: KNode = child { + hasText(getResourceString(R.string.common_save_changes)) + useUnmergedTree = true + } +} + +internal fun BaseTestCase.onSecurityModeScreen(function: SecurityModePageObject.() -> Unit) = + onComposeScreen(composeTestRule, function) \ No newline at end of file diff --git a/app/src/androidTest/kotlin/com/tangem/tests/DetailsTest.kt b/app/src/androidTest/kotlin/com/tangem/tests/DetailsTest.kt index a4b8e21a22..c8ea09ea94 100644 --- a/app/src/androidTest/kotlin/com/tangem/tests/DetailsTest.kt +++ b/app/src/androidTest/kotlin/com/tangem/tests/DetailsTest.kt @@ -6,9 +6,13 @@ import com.tangem.domain.models.scan.ProductType import com.tangem.scenarios.openMainScreen import com.tangem.screens.* import com.tangem.tap.domain.sdk.mocks.content.Firmware412MockContent +import com.tangem.tap.domain.sdk.mocks.content.S2CMockContent +import com.tangem.tap.domain.sdk.mocks.content.SingleCurrencyMockContent +import com.tangem.tap.domain.sdk.mocks.content.V3MockContent import dagger.hilt.android.testing.HiltAndroidTest import io.qameta.allure.kotlin.AllureId import io.qameta.allure.kotlin.junit4.DisplayName +import org.junit.Ignore import org.junit.Test @HiltAndroidTest @@ -183,6 +187,144 @@ class DetailsTest : BaseTestCase() { } } + @AllureId("838") + @DisplayName("Details: (v3 multicurrency) fields") + @Test + fun v3MultiCurrencyDetailsTest() = + setupHooks().run { + step("Open 'Main Screen'") { + openMainScreen(mockContent = V3MockContent) + } + onMainScreenTopBar { + step("Open wallet details") { + moreButton.clickWithAssertion() + } + } + onDetailsScreen { + step("Assert 'Wallet connect' button is displayed") { + walletConnectButton.assertIsDisplayed() + } + step("Assert 'Buy Tangem card' button is displayed") { + buyTangemButton.assertIsDisplayed() + } + step("Assert 'App settings' button is displayed") { + appSettingsButton.assertIsDisplayed() + } + step("Assert 'Contact support' button is displayed") { + contactSupportButton.assertIsDisplayed() + } + step("Assert 'Terms of service' button is displayed") { + toSButton.assertIsDisplayed() + } + step("Assert app version is displayed") { + versionName.assertIsDisplayed() + } + } + } + + @AllureId("9832") + @DisplayName("Details: (single currency) fields") + @Test + fun singleCurrencyDetailsTest() = + setupHooks().run { + step("Open 'Main Screen'") { + openMainScreen(mockContent = SingleCurrencyMockContent) + } + onMainScreenTopBar { + step("Open wallet details") { + moreButton.clickWithAssertion() + } + } + onDetailsScreen { + step("Assert 'Wallet connect' button is not displayed") { + walletConnectButton.assertIsNotDisplayed() + } + step("Assert 'Buy Tangem card' button is displayed") { + buyTangemButton.assertIsDisplayed() + } + step("Assert 'App settings' button is displayed") { + appSettingsButton.assertIsDisplayed() + } + step("Assert 'Contact support' button is displayed") { + contactSupportButton.assertIsDisplayed() + } + step("Assert 'Terms of service' button is displayed") { + toSButton.assertIsDisplayed() + } + step("Assert app version is displayed") { + versionName.assertIsDisplayed() + } + } + } + + @AllureId("841") + @DisplayName("Details: (S2C) fields") + @Test + fun s2cDetailsTest() = + setupHooks().run { + step("Open 'Main Screen'") { + openMainScreen(mockContent = S2CMockContent) + } + onMainScreenTopBar { + step("Open wallet details") { + moreButton.clickWithAssertion() + } + } + onDetailsScreen { + step("Assert 'Wallet connect' button is not displayed") { + walletConnectButton.assertIsNotDisplayed() + } + step("Assert 'Buy Tangem card' button is displayed") { + buyTangemButton.assertIsDisplayed() + } + step("Assert 'App settings' button is displayed") { + appSettingsButton.assertIsDisplayed() + } + step("Assert 'Contact support' button is displayed") { + contactSupportButton.assertIsDisplayed() + } + step("Assert 'Terms of service' button is displayed") { + toSButton.assertIsDisplayed() + } + step("Assert app version is displayed") { + versionName.assertIsDisplayed() + } + } + } + + // Parked: createWalletActions adds Sell for single-wallet cards with no isStart2Coin() check. + @Ignore("[REDACTED_JIRA]") + @AllureId("2869") + @DisplayName("Details: (S2C) no trade buttons and standard details") + @Test + fun s2cNoTradeButtonsDetailsTest() = + setupHooks().run { + step("Open 'Main Screen'") { + openMainScreen(mockContent = S2CMockContent) + } + onMainScreen { + step("Assert 'Buy' button is not displayed") { + buyButton.assertIsNotDisplayed() + } + step("Assert 'Sell' button is not displayed") { + sellButton.assertIsNotDisplayed() + } + step("Assert 'Swap' button is not displayed") { + swapButton.assertIsNotDisplayed() + } + } + onMainScreenTopBar { + step("Open wallet details") { + moreButton.clickWithAssertion() + } + } + onDetailsScreen { + step("Assert 'Wallet connect' button is not displayed") { + walletConnectButton.assertIsNotDisplayed() + } + } + } + @AllureId("3647") @DisplayName("Referral program: validate screen") @Test diff --git a/app/src/androidTest/kotlin/com/tangem/tests/SecurityModeTest.kt b/app/src/androidTest/kotlin/com/tangem/tests/SecurityModeTest.kt new file mode 100644 index 0000000000..8542e67bca --- /dev/null +++ b/app/src/androidTest/kotlin/com/tangem/tests/SecurityModeTest.kt @@ -0,0 +1,68 @@ +package com.tangem.tests + +import com.tangem.common.BaseTestCase +import com.tangem.domain.models.scan.ProductType +import com.tangem.scenarios.openDeviceSettingsScreen +import com.tangem.scenarios.openMainScreen +import com.tangem.scenarios.scanCardInDeviceSettings +import com.tangem.screens.* +import dagger.hilt.android.testing.HiltAndroidTest +import io.qameta.allure.kotlin.AllureId +import io.qameta.allure.kotlin.junit4.DisplayName +import org.junit.Test + +@HiltAndroidTest +class SecurityModeTest : BaseTestCase() { + + @AllureId("2267") + @DisplayName("Security Mode: available for Twin cards") + @Test + fun securityModeOpensForTwinsTest() = + setupHooks().run { + step("Open 'Main Screen'") { + openMainScreen(productType = ProductType.Twins, isTwinsCard = true) + } + step("Open 'Device settings' screen") { + openDeviceSettingsScreen() + } + step("Scan card in 'Device settings'") { + scanCardInDeviceSettings() + } + step("Assert 'Security mode' row is enabled") { + onDeviceSettingsScreen { securityModeRow.assertIsEnabled() } + } + step("Click on 'Security mode' button") { + onDeviceSettingsScreen { securityModeRow.performClick() } + } + onSecurityModeScreen { + step("Assert 'Long tap' option is displayed") { + longTapOptionDescription.assertIsDisplayed() + } + step("Assert 'Save changes' button is displayed") { + saveChangesButton.assertIsDisplayed() + } + } + } + + @AllureId("9831") + @DisplayName("Security Mode: unavailable for single-capability cards") + @Test + fun securityModeRowDisabledForOtherCardsTest() = + setupHooks().run { + step("Open 'Main Screen'") { + openMainScreen() + } + step("Open 'Device settings' screen") { + openDeviceSettingsScreen() + } + step("Scan card in 'Device settings'") { + scanCardInDeviceSettings() + } + step("Assert 'Security mode' row title is displayed") { + onDeviceSettingsScreen { securityModeRowTitle.assertIsDisplayed() } + } + step("Assert 'Security mode' row is disabled") { + onDeviceSettingsScreen { securityModeRow.assertIsNotEnabled() } + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/domain/sdk/mocks/MockProvider.kt b/app/src/main/java/com/tangem/tap/domain/sdk/mocks/MockProvider.kt index ed23617f90..ecdb384b6d 100644 --- a/app/src/main/java/com/tangem/tap/domain/sdk/mocks/MockProvider.kt +++ b/app/src/main/java/com/tangem/tap/domain/sdk/mocks/MockProvider.kt @@ -36,6 +36,9 @@ object MockProvider { MockOption("Backup Wallet") { BackupWalletMockContent }, MockOption("Dev Wallet") { DevWalletMockContent }, MockOption("Firmware 4.12") { Firmware412MockContent }, + MockOption("V3 Multicurrency") { V3MockContent }, + MockOption("Single Currency") { SingleCurrencyMockContent }, + MockOption("Start2Coin") { S2CMockContent }, MockOption("Cobrand") { showCobrandConfigDialog(it) }, ) @@ -99,6 +102,7 @@ object MockProvider { ProductType.Note -> NoteMockContent ProductType.Ring -> RingMockContent ProductType.Twins -> TwinsMockContent + ProductType.Start2Coin -> S2CMockContent else -> TODO() } } diff --git a/app/src/main/java/com/tangem/tap/domain/sdk/mocks/content/S2CMockContent.kt b/app/src/main/java/com/tangem/tap/domain/sdk/mocks/content/S2CMockContent.kt new file mode 100644 index 0000000000..27f873c0f5 --- /dev/null +++ b/app/src/main/java/com/tangem/tap/domain/sdk/mocks/content/S2CMockContent.kt @@ -0,0 +1,112 @@ +package com.tangem.tap.domain.sdk.mocks.content + +import com.tangem.common.SuccessResponse +import com.tangem.common.card.* +import com.tangem.domain.models.scan.CardDTO +import com.tangem.domain.models.scan.ProductType +import com.tangem.domain.models.scan.ScanResponse +import com.tangem.operations.attestation.Attestation +import com.tangem.operations.derivation.DerivationTaskResponse +import com.tangem.operations.wallet.CreateWalletResponse +import com.tangem.sdk.api.CreateProductWalletTaskResponse +import com.tangem.tap.domain.sdk.mocks.MockContent +import java.util.Date + +// Start2Coin (S2C): issuer "Start2Coin" trips isStart2Coin → single currency, WalletConnect hidden. +object S2CMockContent : MockContent { + + override val cardDto = CardDTO( + cardId = "1198724260000000", + batchId = "CD04", + cardPublicKey = byteArrayOf(2, 102, 3, -106, -14, -87, -118, 120, 10, 93, 17, 55, 26, -44, 5, 115, 88, 35, 49, -88, -69, 116, 0, -72, -27, 57, 50, -55, 80, -16, 39, -70, 119), + firmwareVersion = CardDTO.FirmwareVersion( + major = 4, + minor = 52, + patch = 0, + type = FirmwareVersion.FirmwareType.Release, + ), + manufacturer = CardDTO.Manufacturer( + name = "TANGEM", + manufactureDate = Date(1671494400000), + signature = byteArrayOf(), + ), + issuer = CardDTO.Issuer( + name = "Start2Coin", + publicKey = byteArrayOf(3, 86, -25, -61, 55, 99, 41, -33, -82, 115, -120, -33, 22, -107, 103, 3, -122, 16, 60, -110, 72, 106, -121, 100, 79, -87, -27, 18, -55, -49, 78, -110, -2), + ), + settings = CardDTO.Settings( + securityDelay = 15000, + maxWalletsCount = 1, + isSettingAccessCodeAllowed = false, + isSettingPasscodeAllowed = false, + isResettingUserCodesAllowed = true, + isLinkedTerminalEnabled = true, + isBackupAllowed = false, + supportedEncryptionModes = listOf(EncryptionMode.Strong, EncryptionMode.Fast, EncryptionMode.None), + isFilesAllowed = false, + isHDWalletAllowed = false, + isKeysImportAllowed = false, + ), + userSettings = CardDTO.UserSettings(isUserCodeRecoveryAllowed = true), + linkedTerminalStatus = CardDTO.LinkedTerminalStatus.None, + isAccessCodeSet = false, + isPasscodeSet = false, + supportedCurves = listOf(EllipticCurve.Secp256k1), + wallets = listOf( + CardDTO.Wallet( + publicKey = byteArrayOf(2, 106, 7, -77, -109, 39, 3, 80, 99, 31, 50, -40, -113, -81, -76, -21, 123, -60, 0, -121, -56, 126, 2, 123, 111, 80, 47, -37, 40, 119, -22, 33, 32), + chainCode = byteArrayOf(), + curve = EllipticCurve.Secp256k1, + settings = CardWallet.Settings(isPermanent = true), + totalSignedHashes = 1, + remainingSignatures = 999999, + index = 0, + hasBackup = false, + derivedKeys = emptyMap(), + extendedPublicKey = null, + isImported = false, + ), + ), + attestation = Attestation( + cardKeyAttestation = Attestation.Status.Verified, + walletKeysAttestation = Attestation.Status.Skipped, + firmwareAttestation = Attestation.Status.Skipped, + cardUniquenessAttestation = Attestation.Status.Skipped, + ), + backupStatus = CardDTO.BackupStatus.NoBackup, + ) + + override val scanResponse = ScanResponse( + card = cardDto, + productType = ProductType.Start2Coin, + walletData = WalletData(blockchain = "BTC", token = null), + secondTwinPublicKey = null, + derivedKeys = emptyMap(), + primaryCard = null, + ) + + override val derivationTaskResponse = DerivationTaskResponse(entries = emptyMap()) + + override val extendedPublicKey + get() = error("Available only for wallet+?") + + override val successResponse = SuccessResponse(cardId = "1198724260000000") + + override val createProductWalletTaskResponse = CreateProductWalletTaskResponse( + card = cardDto, + derivedKeys = emptyMap(), + primaryCard = null, + ) + + override val importWalletResponse: CreateProductWalletTaskResponse + get() = error("Available only for Wallet 2") + + override val createFirstTwinResponse: CreateWalletResponse + get() = error("Available only for Twin") + + override val createSecondTwinResponse: CreateWalletResponse + get() = error("Available only for Twin") + + override val finalizeTwinResponse: ScanResponse + get() = error("Available only for Twin") +} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/domain/sdk/mocks/content/SingleCurrencyMockContent.kt b/app/src/main/java/com/tangem/tap/domain/sdk/mocks/content/SingleCurrencyMockContent.kt new file mode 100644 index 0000000000..e6d159d584 --- /dev/null +++ b/app/src/main/java/com/tangem/tap/domain/sdk/mocks/content/SingleCurrencyMockContent.kt @@ -0,0 +1,112 @@ +package com.tangem.tap.domain.sdk.mocks.content + +import com.tangem.common.SuccessResponse +import com.tangem.common.card.* +import com.tangem.domain.models.scan.CardDTO +import com.tangem.domain.models.scan.ProductType +import com.tangem.domain.models.scan.ScanResponse +import com.tangem.operations.attestation.Attestation +import com.tangem.operations.derivation.DerivationTaskResponse +import com.tangem.operations.wallet.CreateWalletResponse +import com.tangem.sdk.api.CreateProductWalletTaskResponse +import com.tangem.tap.domain.sdk.mocks.MockContent +import java.util.Date + +// Single-currency card (XLM/ed25519, pre-4.0 firmware) → isMultiwalletAllowed false → WalletConnect hidden. +object SingleCurrencyMockContent : MockContent { + + override val cardDto = CardDTO( + cardId = "0052000000000000", + batchId = "0052", + cardPublicKey = byteArrayOf(2, 102, 3, -106, -14, -87, -118, 120, 10, 93, 17, 55, 26, -44, 5, 115, 88, 35, 49, -88, -69, 116, 0, -72, -27, 57, 50, -55, 80, -16, 39, -70, 119), + firmwareVersion = CardDTO.FirmwareVersion( + major = 3, + minor = 5, + patch = 0, + type = FirmwareVersion.FirmwareType.Release, + ), + manufacturer = CardDTO.Manufacturer( + name = "TANGEM", + manufactureDate = Date(1649635200000), + signature = byteArrayOf(), + ), + issuer = CardDTO.Issuer( + name = "TANGEM AG", + publicKey = byteArrayOf(3, 86, -25, -61, 55, 99, 41, -33, -82, 115, -120, -33, 22, -107, 103, 3, -122, 16, 60, -110, 72, 106, -121, 100, 79, -87, -27, 18, -55, -49, 78, -110, -2), + ), + settings = CardDTO.Settings( + securityDelay = 15000, + maxWalletsCount = 1, + isSettingAccessCodeAllowed = false, + isSettingPasscodeAllowed = false, + isResettingUserCodesAllowed = true, + isLinkedTerminalEnabled = true, + isBackupAllowed = false, + supportedEncryptionModes = listOf(EncryptionMode.Strong, EncryptionMode.Fast, EncryptionMode.None), + isFilesAllowed = false, + isHDWalletAllowed = false, + isKeysImportAllowed = false, + ), + userSettings = CardDTO.UserSettings(isUserCodeRecoveryAllowed = false), + linkedTerminalStatus = CardDTO.LinkedTerminalStatus.None, + isAccessCodeSet = false, + isPasscodeSet = false, + supportedCurves = listOf(EllipticCurve.Ed25519), + wallets = listOf( + CardDTO.Wallet( + publicKey = byteArrayOf(-65, -53, -62, -12, -57, -32, -38, -9, -128, -52, -83, -61, 73, 39, 41, 15, -74, -97, 38, 52, -101, 63, 74, -56, -20, 15, 57, -127, 114, -93, -17, -109), + chainCode = byteArrayOf(), + curve = EllipticCurve.Ed25519, + settings = CardWallet.Settings(isPermanent = false), + totalSignedHashes = 1, + remainingSignatures = null, + index = 0, + hasBackup = false, + derivedKeys = emptyMap(), + extendedPublicKey = null, + isImported = false, + ), + ), + attestation = Attestation( + cardKeyAttestation = Attestation.Status.Verified, + walletKeysAttestation = Attestation.Status.Skipped, + firmwareAttestation = Attestation.Status.Skipped, + cardUniquenessAttestation = Attestation.Status.Skipped, + ), + backupStatus = CardDTO.BackupStatus.NoBackup, + ) + + override val scanResponse = ScanResponse( + card = cardDto, + productType = ProductType.Wallet, + walletData = WalletData(blockchain = "XLM", token = null), + secondTwinPublicKey = null, + derivedKeys = emptyMap(), + primaryCard = null, + ) + + override val derivationTaskResponse = DerivationTaskResponse(entries = emptyMap()) + + override val extendedPublicKey + get() = error("Available only for wallet+?") + + override val successResponse = SuccessResponse(cardId = "0052000000000000") + + override val createProductWalletTaskResponse = CreateProductWalletTaskResponse( + card = cardDto, + derivedKeys = emptyMap(), + primaryCard = null, + ) + + override val importWalletResponse: CreateProductWalletTaskResponse + get() = error("Available only for Wallet 2") + + override val createFirstTwinResponse: CreateWalletResponse + get() = error("Available only for Twin") + + override val createSecondTwinResponse: CreateWalletResponse + get() = error("Available only for Twin") + + override val finalizeTwinResponse: ScanResponse + get() = error("Available only for Twin") +} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/domain/sdk/mocks/content/V3MockContent.kt b/app/src/main/java/com/tangem/tap/domain/sdk/mocks/content/V3MockContent.kt new file mode 100644 index 0000000000..4fe7912d33 --- /dev/null +++ b/app/src/main/java/com/tangem/tap/domain/sdk/mocks/content/V3MockContent.kt @@ -0,0 +1,112 @@ +package com.tangem.tap.domain.sdk.mocks.content + +import com.tangem.common.SuccessResponse +import com.tangem.common.card.* +import com.tangem.domain.models.scan.CardDTO +import com.tangem.domain.models.scan.ProductType +import com.tangem.domain.models.scan.ScanResponse +import com.tangem.operations.attestation.Attestation +import com.tangem.operations.derivation.DerivationTaskResponse +import com.tangem.operations.wallet.CreateWalletResponse +import com.tangem.sdk.api.CreateProductWalletTaskResponse +import com.tangem.tap.domain.sdk.mocks.MockContent +import java.util.Date + +// v3 multicurrency card: single secp256k1 wallet on pre-4.0 firmware → isMultiwalletAllowed via the secp branch. +object V3MockContent : MockContent { + + override val cardDto = CardDTO( + cardId = "0045000000000000", + batchId = "0045", + cardPublicKey = byteArrayOf(2, 102, 3, -106, -14, -87, -118, 120, 10, 93, 17, 55, 26, -44, 5, 115, 88, 35, 49, -88, -69, 116, 0, -72, -27, 57, 50, -55, 80, -16, 39, -70, 119), + firmwareVersion = CardDTO.FirmwareVersion( + major = 3, + minor = 5, + patch = 0, + type = FirmwareVersion.FirmwareType.Release, + ), + manufacturer = CardDTO.Manufacturer( + name = "TANGEM", + manufactureDate = Date(1649635200000), + signature = byteArrayOf(), + ), + issuer = CardDTO.Issuer( + name = "TANGEM AG", + publicKey = byteArrayOf(3, 86, -25, -61, 55, 99, 41, -33, -82, 115, -120, -33, 22, -107, 103, 3, -122, 16, 60, -110, 72, 106, -121, 100, 79, -87, -27, 18, -55, -49, 78, -110, -2), + ), + settings = CardDTO.Settings( + securityDelay = 15000, + maxWalletsCount = 1, + isSettingAccessCodeAllowed = false, + isSettingPasscodeAllowed = false, + isResettingUserCodesAllowed = true, + isLinkedTerminalEnabled = true, + isBackupAllowed = false, + supportedEncryptionModes = listOf(EncryptionMode.Strong, EncryptionMode.Fast, EncryptionMode.None), + isFilesAllowed = false, + isHDWalletAllowed = false, + isKeysImportAllowed = false, + ), + userSettings = CardDTO.UserSettings(isUserCodeRecoveryAllowed = false), + linkedTerminalStatus = CardDTO.LinkedTerminalStatus.None, + isAccessCodeSet = false, + isPasscodeSet = false, + supportedCurves = listOf(EllipticCurve.Secp256k1), + wallets = listOf( + CardDTO.Wallet( + publicKey = byteArrayOf(2, -27, -117, 23, 68, -3, 21, -109, 18, -67, -107, -42, -44, -16, -127, -53, 46, -109, -46, -51, 89, 119, 79, 111, 78, 62, -125, 72, 109, 8, 45, 59, 117), + chainCode = byteArrayOf(), + curve = EllipticCurve.Secp256k1, + settings = CardWallet.Settings(isPermanent = false), + totalSignedHashes = 1, + remainingSignatures = null, + index = 0, + hasBackup = false, + derivedKeys = emptyMap(), + extendedPublicKey = null, + isImported = false, + ), + ), + attestation = Attestation( + cardKeyAttestation = Attestation.Status.Verified, + walletKeysAttestation = Attestation.Status.Skipped, + firmwareAttestation = Attestation.Status.Skipped, + cardUniquenessAttestation = Attestation.Status.Skipped, + ), + backupStatus = CardDTO.BackupStatus.NoBackup, + ) + + override val scanResponse = ScanResponse( + card = cardDto, + productType = ProductType.Wallet, + walletData = WalletData(blockchain = "BTC", token = null), + secondTwinPublicKey = null, + derivedKeys = emptyMap(), + primaryCard = null, + ) + + override val derivationTaskResponse = DerivationTaskResponse(entries = emptyMap()) + + override val extendedPublicKey + get() = error("Available only for wallet+?") + + override val successResponse = SuccessResponse(cardId = "0045000000000000") + + override val createProductWalletTaskResponse = CreateProductWalletTaskResponse( + card = cardDto, + derivedKeys = emptyMap(), + primaryCard = null, + ) + + override val importWalletResponse: CreateProductWalletTaskResponse + get() = error("Available only for Wallet 2") + + override val createFirstTwinResponse: CreateWalletResponse + get() = error("Available only for Twin") + + override val createSecondTwinResponse: CreateWalletResponse + get() = error("Available only for Twin") + + override val finalizeTwinResponse: ScanResponse + get() = error("Available only for Twin") +} \ No newline at end of file