Updated on 2026-08-14
This commit is contained in:
commit
19df6fefae
194 changed files with 5193 additions and 1945 deletions
|
|
@ -113,6 +113,7 @@ dependencies {
|
|||
implementation(projects.domain.account)
|
||||
implementation(projects.domain.account.status)
|
||||
implementation(projects.domain.addressBook)
|
||||
implementation(projects.domain.appsflyer)
|
||||
implementation(projects.domain.models)
|
||||
implementation(projects.domain.core)
|
||||
api(projects.domain.common)
|
||||
|
|
|
|||
|
|
@ -0,0 +1,98 @@
|
|||
package com.tangem.scenarios
|
||||
|
||||
import com.tangem.common.BaseTestCase
|
||||
import com.tangem.common.constants.TestConstants.WAIT_UNTIL_TIMEOUT_LONG
|
||||
import com.tangem.common.constants.TestConstants.WAIT_UNTIL_TIMEOUT_VERY_LONG
|
||||
import com.tangem.common.extensions.clickWithAssertion
|
||||
import com.tangem.common.extensions.isDisplayedSafely
|
||||
import com.tangem.screens.*
|
||||
import com.tangem.tap.domain.sdk.mocks.MockContent
|
||||
import com.tangem.tap.domain.sdk.mocks.MockProvider
|
||||
import io.qameta.allure.kotlin.Allure.step
|
||||
|
||||
/** 'Add Wallet' scans a card immediately (no type chooser), so [mockContent] must be set before the click. */
|
||||
fun BaseTestCase.addNewCardWallet(mockContent: MockContent) {
|
||||
step("Click 'More' button on TopBar") {
|
||||
onMainScreenTopBar { moreButton.clickWithAssertion() }
|
||||
}
|
||||
MockProvider.setMocks(mockContent)
|
||||
step("Click on 'Add Wallet' button (scans a new hardware wallet)") {
|
||||
onDetailsScreen { addWalletButton.clickWithAssertion() }
|
||||
}
|
||||
// Gate on the top-bar More button, not the container — the bottom Markets sheet can leave the container un-"displayed".
|
||||
step("Assert 'Main' screen is displayed with the new wallet") {
|
||||
composeTestRule.waitUntil(timeoutMillis = WAIT_UNTIL_TIMEOUT_VERY_LONG) {
|
||||
runCatching { onMainScreenTopBar { moreButton.assertIsDisplayed() } }.isSuccess
|
||||
}
|
||||
}
|
||||
// The added card is the newest pager page; its "Synchronize addresses" prompt is off-screen until swiped to.
|
||||
step("Synchronize the new card wallet's addresses") {
|
||||
composeTestRule.waitUntil(timeoutMillis = WAIT_UNTIL_TIMEOUT_LONG) {
|
||||
var shown = false
|
||||
onMainScreen { shown = synchronizeAddressesButton.isDisplayedSafely() }
|
||||
if (!shown) onMainScreen { swipeToAdjacentWallet(toPrevious = false) }
|
||||
shown
|
||||
}
|
||||
// Let the pager fling settle — a click mid-animation is eaten by the button's clickableSingle debounce.
|
||||
waitForIdle()
|
||||
onMainScreen { synchronizeAddressesButton.clickWithAssertion() }
|
||||
// The prompt clears once the card's addresses are derived (re-scan + reload over many, some failing, RPCs).
|
||||
composeTestRule.waitUntil(timeoutMillis = WAIT_UNTIL_TIMEOUT_VERY_LONG) {
|
||||
var generated = false
|
||||
onMainScreen { generated = !synchronizeAddressesButton.isDisplayedSafely() }
|
||||
generated
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun BaseTestCase.clickDisplayedTokenOnMain(tokenName: String) {
|
||||
step("Click on token '$tokenName' on the visible wallet") {
|
||||
onMainScreen { clickDisplayedToken(tokenName) }
|
||||
}
|
||||
}
|
||||
|
||||
fun BaseTestCase.switchToPreviousWallet() {
|
||||
step("Swipe wallet card to the previous wallet") {
|
||||
onMainScreen { swipeToAdjacentWallet(toPrevious = true) }
|
||||
}
|
||||
}
|
||||
|
||||
/** Picks a [token] the recipient [walletName] already holds, via the wallet tab. */
|
||||
fun BaseTestCase.selectReceiveTokenOnWallet(token: String, walletName: String) {
|
||||
step("Click on 'Choose token' button") {
|
||||
onSwapTokenScreen { chooseTokenButton.performClick() }
|
||||
}
|
||||
step("Select wallet tab '$walletName'") {
|
||||
onBuyTokenScreen { walletTab(walletName).performClick() }
|
||||
}
|
||||
step("Click on token with name '$token'") {
|
||||
onBuyTokenScreen { tokenWithTitle(token).performClick() }
|
||||
}
|
||||
}
|
||||
|
||||
/** Adds [token] to [recipientWalletName] which lacks it, via market search. */
|
||||
fun BaseTestCase.addMissingReceiveTokenToWallet(token: String, recipientWalletName: String) {
|
||||
step("Click on 'Choose token' button") {
|
||||
onSwapTokenScreen { chooseTokenButton.performClick() }
|
||||
}
|
||||
step("Type '$token' in search field") {
|
||||
onSwapSelectTokenScreen {
|
||||
searchBarBlock.performClick()
|
||||
searchBarBlock.performTextInput(token)
|
||||
}
|
||||
}
|
||||
step("Click on market token '$token'") {
|
||||
composeTestRule.waitUntil(timeoutMillis = WAIT_UNTIL_TIMEOUT_LONG) {
|
||||
runCatching { onSwapSelectTokenScreen { marketsTokenWithName(token).performClick() } }.isSuccess
|
||||
}
|
||||
}
|
||||
// The 'Add token' sheet pre-selects the recipient (the only wallet missing the token, since the source already holds it).
|
||||
step("Assert recipient wallet '$recipientWalletName' is selected") {
|
||||
composeTestRule.waitUntil(timeoutMillis = WAIT_UNTIL_TIMEOUT_LONG) {
|
||||
runCatching { onAddToPortfolioScreen { walletName(recipientWalletName).assertIsDisplayed() } }.isSuccess
|
||||
}
|
||||
}
|
||||
step("Click on 'Add' button") {
|
||||
onAddToPortfolioScreen { addButton.performClick() }
|
||||
}
|
||||
}
|
||||
|
|
@ -490,11 +490,12 @@ fun BaseTestCase.confirmSwapByHolding(accessCode: String? = null) {
|
|||
}
|
||||
}
|
||||
|
||||
/** Holds the last BASE_BUTTON to confirm a transfer; the caller asserts the outcome (transfer mode has no in-progress marker to wait on). */
|
||||
// Caller asserts the outcome — transfer mode has no in-progress marker to wait on.
|
||||
fun BaseTestCase.holdToConfirmTransfer() {
|
||||
val buttons = composeTestRule.onAllNodes(hasTestTag(BaseButtonTestTags.BUTTON))
|
||||
val confirmButton = buttons[buttons.fetchSemanticsNodes().lastIndex]
|
||||
confirmButton.performTouchInput { longClick(durationMillis = HOLD_DURATION_MS) }
|
||||
composeTestRule.onNode(
|
||||
hasTestTag(BaseButtonTestTags.BUTTON) and
|
||||
hasText(getResourceString(CoreUiR.string.swapping_transfer_action)),
|
||||
).performTouchInput { longClick(durationMillis = HOLD_DURATION_MS) }
|
||||
waitForIdle()
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,27 @@
|
|||
package com.tangem.screens
|
||||
|
||||
import androidx.compose.ui.test.SemanticsNodeInteractionsProvider
|
||||
import androidx.compose.ui.test.hasClickAction
|
||||
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 AddToPortfolioPageObject(semanticsProvider: SemanticsNodeInteractionsProvider) :
|
||||
ComposeScreen<AddToPortfolioPageObject>(semanticsProvider = semanticsProvider) {
|
||||
|
||||
fun walletName(walletName: String): KNode = child {
|
||||
hasText(walletName)
|
||||
useUnmergedTree = true
|
||||
}
|
||||
|
||||
val addButton: KNode = child {
|
||||
hasText(getResourceString(R.string.common_add))
|
||||
hasClickAction()
|
||||
}
|
||||
}
|
||||
|
||||
internal fun BaseTestCase.onAddToPortfolioScreen(function: AddToPortfolioPageObject.() -> Unit) =
|
||||
onComposeScreen(composeTestRule, function)
|
||||
|
|
@ -15,6 +15,7 @@ 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.hasText as withText
|
||||
|
||||
class BuyTokenPageObject(semanticsProvider: SemanticsNodeInteractionsProvider) :
|
||||
ComposeScreen<BuyTokenPageObject>(semanticsProvider = semanticsProvider) {
|
||||
|
|
@ -48,6 +49,19 @@ class BuyTokenPageObject(semanticsProvider: SemanticsNodeInteractionsProvider) :
|
|||
useUnmergedTree = true
|
||||
}
|
||||
}
|
||||
|
||||
fun walletTab(walletName: String): KNode = child {
|
||||
hasTestTag(BuyTokenScreenTestTags.WALLET_TAB)
|
||||
hasAnyDescendant(withText(walletName))
|
||||
useUnmergedTree = true
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalTestApi::class)
|
||||
fun tokenWithTitle(tokenTitle: String): LazyListItemNode = lazyList.childWith<LazyListItemNode> {
|
||||
hasTestTag(BuyTokenScreenTestTags.LAZY_LIST_ITEM)
|
||||
hasText(tokenTitle)
|
||||
useUnmergedTree = true
|
||||
}
|
||||
}
|
||||
|
||||
internal fun BaseTestCase.onBuyTokenScreen(function: BuyTokenPageObject.() -> Unit) =
|
||||
|
|
|
|||
|
|
@ -32,6 +32,10 @@ class DetailsPageObject(semanticsProvider: SemanticsNodeInteractionsProvider) :
|
|||
useUnmergedTree = true
|
||||
}
|
||||
|
||||
val addWalletButton: KNode = child {
|
||||
hasTestTag(DetailsScreenTestTags.ADD_WALLET_BUTTON)
|
||||
}
|
||||
|
||||
val buyTangemButton: KNode = child {
|
||||
hasTestTag(DetailsScreenTestTags.SCREEN_ITEM)
|
||||
hasText(getResourceString(R.string.details_buy_wallet))
|
||||
|
|
|
|||
|
|
@ -111,6 +111,28 @@ class MainScreenPageObject(private val semanticsProvider: SemanticsNodeInteracti
|
|||
)
|
||||
}
|
||||
|
||||
// Wallet pager keeps the adjacent page composed (beyondViewportPageCount=1), so the token is mounted on two pages — click the displayed copy.
|
||||
fun clickDisplayedToken(tokenName: String) {
|
||||
val matcher = withTestTag(MainScreenTestTags.TOKEN_LIST_ITEM) and hasAnyDescendant(withText(tokenName))
|
||||
val nodes = semanticsProvider.onAllNodes(matcher, useUnmergedTree = true)
|
||||
for (i in 0 until nodes.fetchSemanticsNodes().size) {
|
||||
if (runCatching { nodes[i].assertIsDisplayed(); nodes[i].performClick() }.isSuccess) return
|
||||
}
|
||||
error("Token '$tokenName' is not displayed on the current wallet page")
|
||||
}
|
||||
|
||||
// Adjacent pager pages stay mounted; swipe the wallet card that's actually on-screen.
|
||||
fun swipeToAdjacentWallet(toPrevious: Boolean) {
|
||||
val nodes = semanticsProvider.onAllNodes(withTestTag(MainScreenTestTags.WALLET_LIST_ITEM))
|
||||
for (i in 0 until nodes.fetchSemanticsNodes().size) {
|
||||
val swiped = runCatching {
|
||||
nodes[i].assertIsDisplayed()
|
||||
nodes[i].performTouchInput { if (toPrevious) swipeRight() else swipeLeft() }
|
||||
}.isSuccess
|
||||
if (swiped) return
|
||||
}
|
||||
}
|
||||
|
||||
val restoringProgressText: KNode = child {
|
||||
hasTestTag(MainScreenTestTags.SYNC_PROGRESS_TEXT)
|
||||
useUnmergedTree = true
|
||||
|
|
|
|||
|
|
@ -5,14 +5,18 @@ import com.tangem.common.extensions.clickWithAssertion
|
|||
import com.tangem.common.extensions.extractText
|
||||
import com.tangem.common.constants.TestConstants.QUOTES_API_SCENARIO
|
||||
import com.tangem.common.constants.TestConstants.SVS_SEED_PHRASE_12
|
||||
import com.tangem.common.constants.TestConstants.TANGEM_PAY_ELIGIBILITY_SCENARIO
|
||||
import com.tangem.common.constants.TestConstants.USER_TOKENS_API_SCENARIO
|
||||
import com.tangem.common.constants.TestConstants.WAIT_UNTIL_TIMEOUT_LONG
|
||||
import com.tangem.common.constants.TestConstants.WAIT_UNTIL_TIMEOUT_VERY_LONG
|
||||
import com.tangem.common.utils.resetWireMockScenarioState
|
||||
import com.tangem.common.utils.setWireMockScenarioState
|
||||
import com.tangem.core.ui.R as CoreUiR
|
||||
import com.tangem.scenarios.*
|
||||
import com.tangem.screens.*
|
||||
import com.tangem.screens.tangempay.*
|
||||
import com.tangem.tap.domain.sdk.mocks.content.Wallet2WithDerivationsMockContent
|
||||
import com.tangem.tap.domain.sdk.mocks.content.WalletMockContent
|
||||
import dagger.hilt.android.testing.HiltAndroidTest
|
||||
import io.github.kakaocup.kakao.common.utilities.getResourceString
|
||||
import io.qameta.allure.kotlin.Allure.step
|
||||
|
|
@ -808,6 +812,333 @@ class AppTransfersTest : BaseTestCase() {
|
|||
}
|
||||
}
|
||||
|
||||
@AllureId("10005")
|
||||
@DisplayName("App transfers: Tron network fee")
|
||||
@Test
|
||||
fun tronNetworkFeeTest() {
|
||||
val token = "Tron"
|
||||
val amount = "0.001"
|
||||
val userTokensState = "TwoAccountsSameTron"
|
||||
val networksProvidersScenario = "networks_providers"
|
||||
val appTransfersNetworksState = "AppTransfersNetworks"
|
||||
|
||||
setupHooks(
|
||||
additionalBeforeAppLaunchSection = {
|
||||
setWireMockScenarioState(storiesScenario, storiesErrorState)
|
||||
// networks_providers configures SDK RPC hosts at launch — must be set before the activity starts.
|
||||
setWireMockScenarioState(networksProvidersScenario, appTransfersNetworksState)
|
||||
},
|
||||
additionalAfterSection = {
|
||||
resetWireMockScenarioState(storiesScenario)
|
||||
resetWireMockScenarioState(USER_TOKENS_API_SCENARIO)
|
||||
resetWireMockScenarioState(networksProvidersScenario)
|
||||
resetWireMockScenarioState(QUOTES_API_SCENARIO)
|
||||
}
|
||||
).run {
|
||||
step("Set WireMock scenario: '$USER_TOKENS_API_SCENARIO' to state: '$userTokensState'") {
|
||||
setWireMockScenarioState(scenarioName = USER_TOKENS_API_SCENARIO, state = userTokensState)
|
||||
}
|
||||
// Non-zero prices keep total fiat > 0 so the empty-wallet banner doesn't push the account list under the Markets sheet.
|
||||
step("Set WireMock scenario: '$QUOTES_API_SCENARIO' to state: '$appTransfersNetworksState'") {
|
||||
setWireMockScenarioState(scenarioName = QUOTES_API_SCENARIO, state = appTransfersNetworksState)
|
||||
}
|
||||
|
||||
step("Open Swap in Transfer mode for '$token'") { openSwapInTransferMode(token) }
|
||||
step("Enter amount '$amount'") { inputAmount(amount) }
|
||||
step("Assert Transfer mode is ready") { assertTransferReady() }
|
||||
step("Assert network fee is displayed") { waitForFeeDisplayed() }
|
||||
}
|
||||
}
|
||||
|
||||
// blockchain SDK TonProvidersBuilder drops public providers, so TON has no provider in the mocked build.
|
||||
@Ignore("[REDACTED_JIRA]")
|
||||
@AllureId("10012")
|
||||
@DisplayName("App transfers: TON network fee")
|
||||
@Test
|
||||
fun tonNetworkFeeTest() {
|
||||
// The SDK names TON's coin "Gram" (Blockchain.TON.getCoinName), so the portfolio row shows "Gram", not "Toncoin".
|
||||
val token = "Gram"
|
||||
val amount = "0.001"
|
||||
val userTokensState = "TwoAccountsSameTON"
|
||||
val networksProvidersScenario = "networks_providers"
|
||||
val appTransfersNetworksState = "AppTransfersNetworks"
|
||||
|
||||
setupHooks(
|
||||
additionalBeforeAppLaunchSection = {
|
||||
setWireMockScenarioState(storiesScenario, storiesErrorState)
|
||||
// networks_providers configures SDK RPC hosts at launch — must be set before the activity starts.
|
||||
setWireMockScenarioState(networksProvidersScenario, appTransfersNetworksState)
|
||||
},
|
||||
additionalAfterSection = {
|
||||
resetWireMockScenarioState(storiesScenario)
|
||||
resetWireMockScenarioState(USER_TOKENS_API_SCENARIO)
|
||||
resetWireMockScenarioState(networksProvidersScenario)
|
||||
resetWireMockScenarioState(QUOTES_API_SCENARIO)
|
||||
}
|
||||
).run {
|
||||
step("Set WireMock scenario: '$USER_TOKENS_API_SCENARIO' to state: '$userTokensState'") {
|
||||
setWireMockScenarioState(scenarioName = USER_TOKENS_API_SCENARIO, state = userTokensState)
|
||||
}
|
||||
// Non-zero prices keep total fiat > 0 so the empty-wallet banner doesn't push the account list under the Markets sheet.
|
||||
step("Set WireMock scenario: '$QUOTES_API_SCENARIO' to state: '$appTransfersNetworksState'") {
|
||||
setWireMockScenarioState(scenarioName = QUOTES_API_SCENARIO, state = appTransfersNetworksState)
|
||||
}
|
||||
|
||||
step("Open Swap in Transfer mode for '$token'") { openSwapInTransferMode(token) }
|
||||
step("Enter amount '$amount'") { inputAmount(amount) }
|
||||
step("Assert Transfer mode is ready") { assertTransferReady() }
|
||||
step("Assert network fee is displayed") { waitForFeeDisplayed() }
|
||||
}
|
||||
}
|
||||
|
||||
@AllureId("10013")
|
||||
@DisplayName("App transfers: Cosmos network fee")
|
||||
@Test
|
||||
fun cosmosNetworkFeeTest() {
|
||||
val token = "Cosmos"
|
||||
val amount = "0.001"
|
||||
val userTokensState = "TwoAccountsSameCosmos"
|
||||
val networksProvidersScenario = "networks_providers"
|
||||
val appTransfersNetworksState = "AppTransfersNetworks"
|
||||
|
||||
setupHooks(
|
||||
additionalBeforeAppLaunchSection = {
|
||||
setWireMockScenarioState(storiesScenario, storiesErrorState)
|
||||
// networks_providers configures SDK RPC hosts at launch — must be set before the activity starts.
|
||||
setWireMockScenarioState(networksProvidersScenario, appTransfersNetworksState)
|
||||
},
|
||||
additionalAfterSection = {
|
||||
resetWireMockScenarioState(storiesScenario)
|
||||
resetWireMockScenarioState(USER_TOKENS_API_SCENARIO)
|
||||
resetWireMockScenarioState(networksProvidersScenario)
|
||||
resetWireMockScenarioState(QUOTES_API_SCENARIO)
|
||||
}
|
||||
).run {
|
||||
step("Set WireMock scenario: '$USER_TOKENS_API_SCENARIO' to state: '$userTokensState'") {
|
||||
setWireMockScenarioState(scenarioName = USER_TOKENS_API_SCENARIO, state = userTokensState)
|
||||
}
|
||||
// Non-zero prices keep total fiat > 0 so the empty-wallet banner doesn't push the account list under the Markets sheet.
|
||||
step("Set WireMock scenario: '$QUOTES_API_SCENARIO' to state: '$appTransfersNetworksState'") {
|
||||
setWireMockScenarioState(scenarioName = QUOTES_API_SCENARIO, state = appTransfersNetworksState)
|
||||
}
|
||||
|
||||
step("Open Swap in Transfer mode for '$token'") { openSwapInTransferMode(token) }
|
||||
step("Enter amount '$amount'") { inputAmount(amount) }
|
||||
step("Assert Transfer mode is ready") { assertTransferReady() }
|
||||
step("Assert network fee is displayed") { waitForFeeDisplayed() }
|
||||
}
|
||||
}
|
||||
|
||||
@AllureId("10015")
|
||||
@DisplayName("App transfers: Aptos network fee")
|
||||
@Test
|
||||
fun aptosNetworkFeeTest() {
|
||||
val token = "Aptos"
|
||||
val amount = "0.001"
|
||||
val userTokensState = "TwoAccountsSameAptos"
|
||||
val networksProvidersScenario = "networks_providers"
|
||||
val appTransfersNetworksState = "AppTransfersNetworks"
|
||||
|
||||
setupHooks(
|
||||
additionalBeforeAppLaunchSection = {
|
||||
setWireMockScenarioState(storiesScenario, storiesErrorState)
|
||||
// networks_providers configures SDK RPC hosts at launch — must be set before the activity starts.
|
||||
setWireMockScenarioState(networksProvidersScenario, appTransfersNetworksState)
|
||||
},
|
||||
additionalAfterSection = {
|
||||
resetWireMockScenarioState(storiesScenario)
|
||||
resetWireMockScenarioState(USER_TOKENS_API_SCENARIO)
|
||||
resetWireMockScenarioState(networksProvidersScenario)
|
||||
resetWireMockScenarioState(QUOTES_API_SCENARIO)
|
||||
}
|
||||
).run {
|
||||
step("Set WireMock scenario: '$USER_TOKENS_API_SCENARIO' to state: '$userTokensState'") {
|
||||
setWireMockScenarioState(scenarioName = USER_TOKENS_API_SCENARIO, state = userTokensState)
|
||||
}
|
||||
// Non-zero prices keep total fiat > 0 so the empty-wallet banner doesn't push the account list under the Markets sheet.
|
||||
step("Set WireMock scenario: '$QUOTES_API_SCENARIO' to state: '$appTransfersNetworksState'") {
|
||||
setWireMockScenarioState(scenarioName = QUOTES_API_SCENARIO, state = appTransfersNetworksState)
|
||||
}
|
||||
|
||||
step("Open Swap in Transfer mode for '$token'") { openSwapInTransferMode(token) }
|
||||
step("Enter amount '$amount'") { inputAmount(amount) }
|
||||
step("Assert Transfer mode is ready") { assertTransferReady() }
|
||||
step("Assert network fee is displayed") { waitForFeeDisplayed() }
|
||||
}
|
||||
}
|
||||
|
||||
@AllureId("9856")
|
||||
@DisplayName("App transfers: Transfer mode is available from a Tangem Pay account")
|
||||
@Test
|
||||
fun transferModeAvailableFromTangemPayAccountTest() {
|
||||
val token = "USDC"
|
||||
val receiveAccountName = "Main account"
|
||||
val eligibilityState = "PaeraCustomer"
|
||||
val balanceScenario = "tangem_pay_balance_update"
|
||||
val balanceInitialState = "InitialBalance"
|
||||
val historyScenario = "tangem_pay_transaction_history"
|
||||
val historyInitialState = "InitialEmpty"
|
||||
val userTokensState = "TangemPayTransferUsdc"
|
||||
|
||||
setupHooks(
|
||||
additionalBeforeAppLaunchSection = { setWireMockScenarioState(storiesScenario, storiesErrorState) },
|
||||
additionalAfterSection = {
|
||||
resetWireMockScenarioState(storiesScenario)
|
||||
resetWireMockScenarioState(TANGEM_PAY_ELIGIBILITY_SCENARIO)
|
||||
resetWireMockScenarioState(balanceScenario)
|
||||
resetWireMockScenarioState(historyScenario)
|
||||
resetWireMockScenarioState(USER_TOKENS_API_SCENARIO)
|
||||
}
|
||||
).run {
|
||||
step("Set WireMock scenario: '$TANGEM_PAY_ELIGIBILITY_SCENARIO' to state: '$eligibilityState'") {
|
||||
setWireMockScenarioState(scenarioName = TANGEM_PAY_ELIGIBILITY_SCENARIO, state = eligibilityState)
|
||||
}
|
||||
step("Set WireMock scenario: '$balanceScenario' to state: '$balanceInitialState'") {
|
||||
setWireMockScenarioState(scenarioName = balanceScenario, state = balanceInitialState)
|
||||
}
|
||||
step("Set WireMock scenario: '$historyScenario' to state: '$historyInitialState'") {
|
||||
setWireMockScenarioState(scenarioName = historyScenario, state = historyInitialState)
|
||||
}
|
||||
step("Set WireMock scenario: '$USER_TOKENS_API_SCENARIO' to state: '$userTokensState'") {
|
||||
setWireMockScenarioState(scenarioName = USER_TOKENS_API_SCENARIO, state = userTokensState)
|
||||
}
|
||||
|
||||
step("Open Tangem Pay") { openTangemPay() }
|
||||
step("Click on 'Withdraw' button") {
|
||||
onTangemPayMainScreen { withdrawButton.clickWithAssertion() }
|
||||
}
|
||||
step("Acknowledge withdrawal note sheet") {
|
||||
onTangemPayWithdrawNoteSheet {
|
||||
title.assertIsDisplayed()
|
||||
gotItButton.clickWithAssertion()
|
||||
}
|
||||
}
|
||||
step("Choose identical receive token '$token' from '$receiveAccountName'") {
|
||||
chooseIdenticalReceiveToken(tokenName = token, receiveAccountName = receiveAccountName)
|
||||
}
|
||||
// Withdraw-entry swap keeps recalculating — use flakySafely rather than assertTransferReady's waitUntil.
|
||||
step("Assert Transfer mode is ready") {
|
||||
flakySafely(WAIT_UNTIL_TIMEOUT_VERY_LONG) {
|
||||
onSwapTokenScreen { transferTitle.assertIsDisplayed() }
|
||||
}
|
||||
onSwapTokenScreen { providersBlock.assertIsNotDisplayed() }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@AllureId("9995")
|
||||
@DisplayName("App transfers: transfer between different wallets reaches 'Transfer in progress' screen")
|
||||
@Test
|
||||
fun transferBetweenDifferentWalletsReachesFinishTest() {
|
||||
val token = "Ethereum"
|
||||
val amount = "0.001"
|
||||
val secondWalletName = "Wallet 2"
|
||||
val userTokensState = "EthereumWithSecondToken"
|
||||
|
||||
setupHooks(
|
||||
additionalBeforeAppLaunchSection = { setWireMockScenarioState(storiesScenario, storiesErrorState) },
|
||||
additionalAfterSection = {
|
||||
resetWireMockScenarioState(storiesScenario)
|
||||
resetWireMockScenarioState(USER_TOKENS_API_SCENARIO)
|
||||
resetWireMockScenarioState(ethCallScenario)
|
||||
resetWireMockScenarioState(ethBalanceScenario)
|
||||
}
|
||||
).run {
|
||||
step("Set WireMock scenario: '$USER_TOKENS_API_SCENARIO' to state: '$userTokensState'") {
|
||||
setWireMockScenarioState(scenarioName = USER_TOKENS_API_SCENARIO, state = userTokensState)
|
||||
}
|
||||
step("Set WireMock scenario: '$ethCallScenario' to state: '$started'") {
|
||||
setWireMockScenarioState(scenarioName = ethCallScenario, state = started)
|
||||
}
|
||||
step("Set WireMock scenario: '$ethBalanceScenario' to state: '$started'") {
|
||||
setWireMockScenarioState(scenarioName = ethBalanceScenario, state = started)
|
||||
}
|
||||
|
||||
step("Open 'Main' screen with existing hot wallet") {
|
||||
openMainScreenWithExistingHotWallet(SVS_SEED_PHRASE_12)
|
||||
}
|
||||
step("Generate missing addresses") { generateMissingHotWalletAddresses() }
|
||||
step("Wait for addresses to be generated") { waitForAddressesGenerated() }
|
||||
step("Add a second card wallet '$secondWalletName'") {
|
||||
addNewCardWallet(WalletMockContent)
|
||||
}
|
||||
step("Switch back to the hot wallet") { switchToPreviousWallet() }
|
||||
step("Click on token with name: '$token'") { clickDisplayedTokenOnMain(token) }
|
||||
step("Open 'Swap' screen") {
|
||||
openSwapScreen(from = SwapEntryPoint.TokenDetails, storiesExist = false)
|
||||
}
|
||||
step("Select identical receive token '$token' on '$secondWalletName'") {
|
||||
selectReceiveTokenOnWallet(token = token, walletName = secondWalletName)
|
||||
}
|
||||
step("Enter amount '$amount'") { inputAmount(amount) }
|
||||
step("Assert Transfer mode is ready") { assertTransferReady() }
|
||||
step("Assert network fee is displayed") { waitForFeeDisplayed() }
|
||||
step("Hold to confirm the transfer") { holdToConfirmTransfer() }
|
||||
step("Assert 'Transfer in progress' screen is displayed") {
|
||||
flakySafely(WAIT_UNTIL_TIMEOUT_LONG) {
|
||||
onSwapSuccessScreen { transferInProgressTitle.assertIsDisplayed() }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@AllureId("9996")
|
||||
@DisplayName("App transfers: adding a missing token to the recipient wallet enables Transfer")
|
||||
@Test
|
||||
fun addMissingTokenToRecipientWalletEnablesTransferTest() {
|
||||
val token = "Ethereum"
|
||||
val bitcoinToken = "Bitcoin"
|
||||
val recipientWalletName = "Wallet"
|
||||
val recipientWithoutEthereumState = "RecipientWithoutEthereum"
|
||||
val ethereumWithSecondTokenState = "EthereumWithSecondToken"
|
||||
|
||||
setupHooks(
|
||||
additionalBeforeAppLaunchSection = { setWireMockScenarioState(storiesScenario, storiesErrorState) },
|
||||
additionalAfterSection = {
|
||||
resetWireMockScenarioState(storiesScenario)
|
||||
resetWireMockScenarioState(USER_TOKENS_API_SCENARIO)
|
||||
resetWireMockScenarioState(ethCallScenario)
|
||||
resetWireMockScenarioState(ethBalanceScenario)
|
||||
}
|
||||
).run {
|
||||
step("Set WireMock scenario: '$USER_TOKENS_API_SCENARIO' to state: '$recipientWithoutEthereumState'") {
|
||||
setWireMockScenarioState(scenarioName = USER_TOKENS_API_SCENARIO, state = recipientWithoutEthereumState)
|
||||
}
|
||||
step("Set WireMock scenario: '$ethCallScenario' to state: '$started'") {
|
||||
setWireMockScenarioState(scenarioName = ethCallScenario, state = started)
|
||||
}
|
||||
step("Set WireMock scenario: '$ethBalanceScenario' to state: '$started'") {
|
||||
setWireMockScenarioState(scenarioName = ethBalanceScenario, state = started)
|
||||
}
|
||||
|
||||
step("Open 'Main' screen with existing hot wallet") {
|
||||
openMainScreenWithExistingHotWallet(SVS_SEED_PHRASE_12)
|
||||
}
|
||||
step("Generate missing addresses") { generateMissingHotWalletAddresses() }
|
||||
step("Wait for addresses to be generated") { waitForAddressesGenerated() }
|
||||
step("Assert token '$bitcoinToken' is displayed") {
|
||||
flakySafely(WAIT_UNTIL_TIMEOUT_LONG) {
|
||||
onMainScreen { tokenWithTitleAndAddress(bitcoinToken).assertIsDisplayed() }
|
||||
}
|
||||
}
|
||||
// Switch the user-tokens mock so the second wallet loads with Ethereum while the recipient stays Ethereum-less.
|
||||
step("Set WireMock scenario: '$USER_TOKENS_API_SCENARIO' to state: '$ethereumWithSecondTokenState'") {
|
||||
setWireMockScenarioState(scenarioName = USER_TOKENS_API_SCENARIO, state = ethereumWithSecondTokenState)
|
||||
}
|
||||
step("Add a second card wallet") {
|
||||
addNewCardWallet(WalletMockContent)
|
||||
}
|
||||
step("Click on token with name: '$token'") { clickDisplayedTokenOnMain(token) }
|
||||
step("Open 'Swap' screen") {
|
||||
openSwapScreen(from = SwapEntryPoint.TokenDetails, storiesExist = false)
|
||||
}
|
||||
step("Add missing token '$token' to recipient wallet '$recipientWalletName'") {
|
||||
addMissingReceiveTokenToWallet(token = token, recipientWalletName = recipientWalletName)
|
||||
}
|
||||
step("Assert Transfer mode is ready") { assertTransferReady() }
|
||||
}
|
||||
}
|
||||
|
||||
// [REDACTED_TASK_KEY]: transfer mode never runs tx validation, so the destination rent-exemption notification never shows.
|
||||
@Ignore("[REDACTED_JIRA]")
|
||||
@AllureId("9852")
|
||||
|
|
|
|||
|
|
@ -1 +1 @@
|
|||
Subproject commit a7b32c766817076c6346156390c135a3dae1b6ce
|
||||
Subproject commit f983c6defd0b2240eb6f134aefe30f7e93696795
|
||||
|
|
@ -4,13 +4,10 @@ import com.appsflyer.deeplink.DeepLink
|
|||
import com.tangem.datasource.local.appsflyer.AppsFlyerDeeplinkSource
|
||||
import com.tangem.datasource.local.appsflyer.AppsFlyerStore
|
||||
import com.tangem.domain.wallets.models.AppsFlyerConversionData
|
||||
import com.tangem.feature.referral.domain.SetShouldShowMobileWalletPromoUseCase
|
||||
import com.tangem.utils.coroutines.AppCoroutineScope
|
||||
import com.tangem.utils.logging.TangemLogger
|
||||
import kotlinx.coroutines.CompletableDeferred
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.sync.Mutex
|
||||
import kotlinx.coroutines.sync.withLock
|
||||
import javax.inject.Inject
|
||||
import javax.inject.Singleton
|
||||
import kotlin.contracts.ExperimentalContracts
|
||||
|
|
@ -19,11 +16,9 @@ import kotlin.contracts.contract
|
|||
@Singleton
|
||||
class AppsFlyerReferralParamsHandler @Inject constructor(
|
||||
private val appsFlyerStore: AppsFlyerStore,
|
||||
private val setShouldShowMobileWalletPromoUseCase: SetShouldShowMobileWalletPromoUseCase,
|
||||
private val coroutineScope: AppCoroutineScope,
|
||||
) {
|
||||
|
||||
private val mutex = Mutex()
|
||||
private val deepLinkDeferred = CompletableDeferred<String?>()
|
||||
|
||||
fun handle(params: Map<String?, Any?>) {
|
||||
|
|
@ -48,15 +43,17 @@ class AppsFlyerReferralParamsHandler @Inject constructor(
|
|||
}
|
||||
|
||||
suspend fun waitForDeeplink(deeplinkSource: AppsFlyerDeeplinkSource): String? {
|
||||
val deeplinkFromCache = appsFlyerStore.getDeeplink(deeplinkSource)
|
||||
return if (deeplinkFromCache == null) {
|
||||
val value = when (deeplinkSource) {
|
||||
AppsFlyerDeeplinkSource.TangemPayHotWalletOnboarding -> TANGEM_PAY_HOT_WALLET_ONBOARDING_DEEP_LINK_VALUE
|
||||
}
|
||||
deepLinkDeferred.await().takeIf { it == value }
|
||||
} else {
|
||||
deeplinkFromCache
|
||||
appsFlyerStore.getDeeplink(deeplinkSource)?.let { return it }
|
||||
|
||||
val expectedValue = when (deeplinkSource) {
|
||||
AppsFlyerDeeplinkSource.TangemPayHotWalletOnboarding -> TANGEM_PAY_HOT_WALLET_ONBOARDING_DEEP_LINK_VALUE
|
||||
AppsFlyerDeeplinkSource.Referral -> REFERRAL_DEEP_LINK_VALUE
|
||||
}
|
||||
val resolvedValue = deepLinkDeferred.await().takeIf { it == expectedValue }
|
||||
|
||||
// The deep link may have been persisted to the store while we were awaiting (e.g. from
|
||||
// conversion-data handling, which stores the deep link but doesn't complete the deferred).
|
||||
return resolvedValue ?: appsFlyerStore.getDeeplink(deeplinkSource)
|
||||
}
|
||||
|
||||
private fun handle(deepLinkValue: String?, deepLinkSub1: String?, deepLinkSub2: String?) {
|
||||
|
|
@ -79,6 +76,10 @@ class AppsFlyerReferralParamsHandler @Inject constructor(
|
|||
@Suppress("NullableToStringCall")
|
||||
TangemLogger.i("refcode=$deepLinkSub1\ncampaign=$deepLinkSub2")
|
||||
|
||||
coroutineScope.launch {
|
||||
appsFlyerStore.storeDeeplink(AppsFlyerDeeplinkSource.Referral, REFERRAL_DEEP_LINK_VALUE)
|
||||
}
|
||||
|
||||
if (!isValidParam(deepLinkSub1)) {
|
||||
TangemLogger.e("Deeplink conversion data is invalid")
|
||||
return
|
||||
|
|
@ -98,13 +99,9 @@ class AppsFlyerReferralParamsHandler @Inject constructor(
|
|||
|
||||
private fun storeConversionData(refcode: String, campaign: String?) {
|
||||
coroutineScope.launch {
|
||||
mutex.withLock {
|
||||
setShouldShowMobileWalletPromoUseCase(true)
|
||||
.onLeft { TangemLogger.e("Error", it) }
|
||||
appsFlyerStore.storeIfAbsent(
|
||||
value = AppsFlyerConversionData(refcode = refcode, campaign = campaign),
|
||||
)
|
||||
}
|
||||
appsFlyerStore.storeIfAbsent(
|
||||
value = AppsFlyerConversionData(refcode = refcode, campaign = campaign),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -35,12 +35,15 @@ internal interface CardSdkModule {
|
|||
sdkRepository: CardSdkConfigRepository,
|
||||
@ApplicationContext context: Context,
|
||||
): CardArtworksProvider {
|
||||
// Use internal storage (always mounted) instead of external files dir. External
|
||||
// storage can be transiently unavailable/unmounted or cleared after this singleton
|
||||
// is constructed, leaving the directory missing when the SDK later writes to it —
|
||||
// ArtworksStorage.store() opens a FileOutputStream without re-creating the parent,
|
||||
// which crashes with ENOENT. Artwork is only a cache, so internal storage is fine.
|
||||
val artworksDirectory = File(context.filesDir, "card_artworks").apply { mkdirs() }
|
||||
return CardArtworksProvider(
|
||||
tangemApiBaseUrlProvider = { sdkRepository.sdk.config.tangemApiBaseUrl },
|
||||
artworksDirectory = File(
|
||||
context.getExternalFilesDir(null) ?: context.filesDir,
|
||||
"card_artworks",
|
||||
).apply { mkdirs() },
|
||||
artworksDirectory = artworksDirectory,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -132,6 +132,10 @@ object WalletMockContent : MockContent {
|
|||
publicKey = byteArrayOf(2, -40, 115, -15, 80, 104, 100, -29, 80, 29, 112, -35, -54, 64, -98, 45, 115, 108, 93, 113, -71, 89, 17, 72, 98, 21, 126, 82, -50, 50, -12, -87, -62),
|
||||
chainCode = byteArrayOf(-27, 56, 57, 54, 27, -40, 65, 109, 119, -54, -94, 88, -29, -115, -45, -112, -31, 57, 53, 56, 118, 87, 34, -16, -64, -45, 105, 77, 91, -106, -92, 41),
|
||||
),
|
||||
DerivationPath("m/44'/118'/0'/0/0") to ExtendedPublicKey( // Cosmos
|
||||
publicKey = byteArrayOf(2, -40, 115, -15, 80, 104, 100, -29, 80, 29, 112, -35, -54, 64, -98, 45, 115, 108, 93, 113, -71, 89, 17, 72, 98, 21, 126, 82, -50, 50, -12, -87, -62),
|
||||
chainCode = byteArrayOf(-27, 56, 57, 54, 27, -40, 65, 109, 119, -54, -94, 88, -29, -115, -45, -112, -31, 57, 53, 56, 118, 87, 34, -16, -64, -45, 105, 77, 91, -106, -92, 41),
|
||||
),
|
||||
DerivationPath("m/1852'/1815'/0'/0/0") to ExtendedPublicKey(
|
||||
publicKey = byteArrayOf(2, -40, 115, -15, 80, 104, 100, -29, 80, 29, 112, -35, -54, 64, -98, 45, 115, 108, 93, 113, -71, 89, 17, 72, 98, 21, 126, 82, -50, 50, -12, -87, -62),
|
||||
chainCode = byteArrayOf(-27, 56, 57, 54, 27, -40, 65, 109, 119, -54, -94, 88, -29, -115, -45, -112, -31, 57, 53, 56, 118, 87, 34, -16, -64, -45, 105, 77, 91, -106, -92, 41),
|
||||
|
|
@ -181,6 +185,22 @@ object WalletMockContent : MockContent {
|
|||
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(-81, 15, 125, 28, -115, -22, 87, 81, 87, -123, -25, -74, 86, 2, 1, 110, -115, 65, -110, 63, -64, 83, -93, -97, -104, 123, 12, -26, 94, 27, 84, -6),
|
||||
),
|
||||
DerivationPath("m/44'/607'/0'/0/0") to ExtendedPublicKey( // TON (account 1)
|
||||
publicKey = byteArrayOf(30, -109, -39, 33, -94, -73, 121, 50, -75, 86, 102, -2, -74, -23, 63, -3, 79, -82, -103, 106, 82, -86, -107, -63, -46, 104, 7, 18, -41, 15, -87, -43),
|
||||
chainCode = byteArrayOf(15, 61, -29, 22, 30, 45, -51, -60, 5, 62, -87, -35, 54, -97, -5, -44, -54, -107, -14, -119, -3, 92, 91, 75, -66, 26, 112, 83, 122, -25, -64, 40),
|
||||
),
|
||||
DerivationPath("m/44'/607'/1'/0/0") to ExtendedPublicKey( // TON (account 2)
|
||||
publicKey = byteArrayOf(-51, 62, 97, 25, 83, 75, -79, 23, 6, -42, -94, 45, 91, -66, 57, -80, -75, -39, 19, -88, 95, -124, 50, 39, 114, -118, 27, -122, 48, -69, 7, -111),
|
||||
chainCode = byteArrayOf(77, 63, 69, -114, -25, 105, -123, -42, -87, 107, 86, -43, 46, 92, -78, -107, -72, -81, -102, 45, 75, 97, -120, -10, 118, 27, -34, -50, -92, 3, 47, -126),
|
||||
),
|
||||
DerivationPath("m/44'/637'/0'/0'/0'") to ExtendedPublicKey( // Aptos (account 1)
|
||||
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(-81, 15, 125, 28, -115, -22, 87, 81, 87, -123, -25, -74, 86, 2, 1, 110, -115, 65, -110, 63, -64, 83, -93, -97, -104, 123, 12, -26, 94, 27, 84, -6),
|
||||
),
|
||||
DerivationPath("m/44'/637'/1'/0'/0'") to ExtendedPublicKey( // Aptos (account 2)
|
||||
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(-81, 15, 125, 28, -115, -22, 87, 81, 87, -123, -25, -74, 86, 2, 1, 110, -115, 65, -110, 63, -64, 83, -93, -97, -104, 123, 12, -26, 94, 27, 84, -6),
|
||||
),
|
||||
),
|
||||
extendedPublicKey = ExtendedPublicKey(
|
||||
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),
|
||||
|
|
@ -290,6 +310,20 @@ object WalletMockContent : MockContent {
|
|||
parentFingerprint = byteArrayOf(0, 0, 0, 0),
|
||||
childNumber = 0,
|
||||
),
|
||||
DerivationPath("m/44'/118'/0'/0/0") to ExtendedPublicKey( // Cosmos (account 1)
|
||||
publicKey = byteArrayOf(2, -40, 115, -15, 80, 104, 100, -29, 80, 29, 112, -35, -54, 64, -98, 45, 115, 108, 93, 113, -71, 89, 17, 72, 98, 21, 126, 82, -50, 50, -12, -87, -62),
|
||||
chainCode = byteArrayOf(-27, 56, 57, 54, 27, -40, 65, 109, 119, -54, -94, 88, -29, -115, -45, -112, -31, 57, 53, 56, 118, 87, 34, -16, -64, -45, 105, 77, 91, -106, -92, 41),
|
||||
depth = 0,
|
||||
parentFingerprint = byteArrayOf(0, 0, 0, 0),
|
||||
childNumber = 0,
|
||||
),
|
||||
DerivationPath("m/44'/118'/1'/0/0") to ExtendedPublicKey( // Cosmos (account 2)
|
||||
publicKey = byteArrayOf(2, -40, 115, -15, 80, 104, 100, -29, 80, 29, 112, -35, -54, 64, -98, 45, 115, 108, 93, 113, -71, 89, 17, 72, 98, 21, 126, 82, -50, 50, -12, -87, -62),
|
||||
chainCode = byteArrayOf(-27, 56, 57, 54, 27, -40, 65, 109, 119, -54, -94, 88, -29, -115, -45, -112, -31, 57, 53, 56, 118, 87, 34, -16, -64, -45, 105, 77, 91, -106, -92, 41),
|
||||
depth = 0,
|
||||
parentFingerprint = byteArrayOf(0, 0, 0, 0),
|
||||
childNumber = 0,
|
||||
),
|
||||
DerivationPath("m/44'/144'/0'/0/0") to ExtendedPublicKey( // XRP
|
||||
publicKey = byteArrayOf(2, -40, 115, -15, 80, 104, 100, -29, 80, 29, 112, -35, -54, 64, -98, 45, 115, 108, 93, 113, -71, 89, 17, 72, 98, 21, 126, 82, -50, 50, -12, -87, -62),
|
||||
chainCode = byteArrayOf(-27, 56, 57, 54, 27, -40, 65, 109, 119, -54, -94, 88, -29, -115, -45, -112, -31, 57, 53, 56, 118, 87, 34, -16, -64, -45, 105, 77, 91, -106, -92, 41),
|
||||
|
|
@ -424,6 +458,34 @@ object WalletMockContent : MockContent {
|
|||
parentFingerprint = byteArrayOf(0, 0, 0, 0),
|
||||
childNumber = 0,
|
||||
),
|
||||
DerivationPath("m/44'/607'/0'/0/0") to ExtendedPublicKey( // TON (account 1)
|
||||
publicKey = byteArrayOf(30, -109, -39, 33, -94, -73, 121, 50, -75, 86, 102, -2, -74, -23, 63, -3, 79, -82, -103, 106, 82, -86, -107, -63, -46, 104, 7, 18, -41, 15, -87, -43),
|
||||
chainCode = byteArrayOf(15, 61, -29, 22, 30, 45, -51, -60, 5, 62, -87, -35, 54, -97, -5, -44, -54, -107, -14, -119, -3, 92, 91, 75, -66, 26, 112, 83, 122, -25, -64, 40),
|
||||
depth = 0,
|
||||
parentFingerprint = byteArrayOf(0, 0, 0, 0),
|
||||
childNumber = 0,
|
||||
),
|
||||
DerivationPath("m/44'/607'/1'/0/0") to ExtendedPublicKey( // TON (account 2)
|
||||
publicKey = byteArrayOf(-51, 62, 97, 25, 83, 75, -79, 23, 6, -42, -94, 45, 91, -66, 57, -80, -75, -39, 19, -88, 95, -124, 50, 39, 114, -118, 27, -122, 48, -69, 7, -111),
|
||||
chainCode = byteArrayOf(77, 63, 69, -114, -25, 105, -123, -42, -87, 107, 86, -43, 46, 92, -78, -107, -72, -81, -102, 45, 75, 97, -120, -10, 118, 27, -34, -50, -92, 3, 47, -126),
|
||||
depth = 0,
|
||||
parentFingerprint = byteArrayOf(0, 0, 0, 0),
|
||||
childNumber = 0,
|
||||
),
|
||||
DerivationPath("m/44'/637'/0'/0'/0'") to ExtendedPublicKey( // Aptos (account 1)
|
||||
publicKey = byteArrayOf(2, 34, 6, 119, -106, 5, -119, 111, -22, 8, 23, -108, -72, -56, 6, 77, -17, -61, -101, -85, 16, 28, 18, 3, -3, -89, -81, -108, 48, -7, -86, -82),
|
||||
chainCode = byteArrayOf(-75, 55, 107, -106, -37, -81, -15, 72, -102, 94, 55, -39, 9, -112, 1, 90, -50, 103, 53, 120, -92, -36, -85, -39, -65, 1, 88, 46, 92, 104, -13, -109),
|
||||
depth = 0,
|
||||
parentFingerprint = byteArrayOf(0, 0, 0, 0),
|
||||
childNumber = 0,
|
||||
),
|
||||
DerivationPath("m/44'/637'/1'/0'/0'") to ExtendedPublicKey( // Aptos (account 2)
|
||||
publicKey = byteArrayOf(2, 34, 6, 119, -106, 5, -119, 111, -22, 8, 23, -108, -72, -56, 6, 77, -17, -61, -101, -85, 16, 28, 18, 3, -3, -89, -81, -108, 48, -7, -86, -82),
|
||||
chainCode = byteArrayOf(-75, 55, 107, -106, -37, -81, -15, 72, -102, 94, 55, -39, 9, -112, 1, 90, -50, 103, 53, 120, -92, -36, -85, -39, -65, 1, 88, 46, 92, 104, -13, -109),
|
||||
depth = 0,
|
||||
parentFingerprint = byteArrayOf(0, 0, 0, 0),
|
||||
childNumber = 0,
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
|
|
|
|||
|
|
@ -9,6 +9,7 @@ import com.tangem.common.services.secure.SecureStorage
|
|||
import com.tangem.core.analytics.api.AnalyticsEventHandler
|
||||
import com.tangem.core.analytics.utils.TrackingContextProxy
|
||||
import com.tangem.datasource.local.preferences.AppPreferencesStore
|
||||
import com.tangem.domain.appsflyer.usecase.ClearAppsFlyerDeeplinkUseCase
|
||||
import com.tangem.domain.common.wallets.UserWalletSelectedHandler
|
||||
import com.tangem.domain.common.wallets.UserWalletsListRepository
|
||||
import com.tangem.domain.hotwallet.repository.HotWalletRepository
|
||||
|
|
@ -17,7 +18,6 @@ import com.tangem.domain.visa.model.VisaActivationRemoteState
|
|||
import com.tangem.domain.visa.model.VisaCardActivationStatus
|
||||
import com.tangem.domain.wallets.hot.HotWalletAccessCodeAttemptsRepository
|
||||
import com.tangem.domain.wallets.hot.HotWalletPasswordRequester
|
||||
import com.tangem.feature.referral.domain.MobileWalletPromoRepository
|
||||
import com.tangem.hot.sdk.TangemHotSdk
|
||||
import com.tangem.sdk.storage.AndroidSecureStorage
|
||||
import com.tangem.sdk.storage.AndroidSecureStorageV2
|
||||
|
|
@ -57,7 +57,7 @@ internal object UserWalletsListRepositoryModule {
|
|||
trackingContextProxy: TrackingContextProxy,
|
||||
analyticsEventHandler: AnalyticsEventHandler,
|
||||
hotWalletRepository: HotWalletRepository,
|
||||
mobileWalletPromoRepository: MobileWalletPromoRepository,
|
||||
clearAppsFlyerDeeplinkUseCase: ClearAppsFlyerDeeplinkUseCase,
|
||||
userWalletSelectedHandler: Lazy<UserWalletSelectedHandler>,
|
||||
): UserWalletsListRepository {
|
||||
val moshi = buildMoshi()
|
||||
|
|
@ -109,7 +109,7 @@ internal object UserWalletsListRepositoryModule {
|
|||
trackingContextProxy = trackingContextProxy,
|
||||
analyticsEventHandler = analyticsEventHandler,
|
||||
hotWalletRepository = hotWalletRepository,
|
||||
mobileWalletPromoRepository = mobileWalletPromoRepository,
|
||||
clearAppsFlyerDeeplinkUseCase = clearAppsFlyerDeeplinkUseCase,
|
||||
userWalletSelectedHandler = userWalletSelectedHandler,
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -14,6 +14,8 @@ import com.tangem.core.analytics.utils.TrackingContextProxy
|
|||
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.domain.appsflyer.AppsFlyerDeeplinkSource
|
||||
import com.tangem.domain.appsflyer.usecase.ClearAppsFlyerDeeplinkUseCase
|
||||
import com.tangem.domain.common.wallets.UserWalletSelectedHandler
|
||||
import com.tangem.domain.common.wallets.UserWalletTransformAction
|
||||
import com.tangem.domain.common.wallets.UserWalletsListRepository
|
||||
|
|
@ -26,7 +28,6 @@ import com.tangem.domain.wallets.analytics.WalletSettingsAnalyticEvents
|
|||
import com.tangem.domain.wallets.builder.UserWalletIdBuilder
|
||||
import com.tangem.domain.wallets.hot.HotWalletAccessCodeAttemptsRepository
|
||||
import com.tangem.domain.wallets.hot.HotWalletPasswordRequester
|
||||
import com.tangem.feature.referral.domain.MobileWalletPromoRepository
|
||||
import com.tangem.hot.sdk.TangemHotSdk
|
||||
import com.tangem.hot.sdk.model.HotWalletId
|
||||
import com.tangem.sdk.api.TangemSdkManager
|
||||
|
|
@ -60,7 +61,7 @@ internal class DefaultUserWalletsListRepository(
|
|||
private val trackingContextProxy: TrackingContextProxy,
|
||||
private val analyticsEventHandler: AnalyticsEventHandler,
|
||||
private val hotWalletRepository: HotWalletRepository,
|
||||
private val mobileWalletPromoRepository: MobileWalletPromoRepository,
|
||||
private val clearAppsFlyerDeeplinkUseCase: ClearAppsFlyerDeeplinkUseCase,
|
||||
private val userWalletSelectedHandler: Lazy<UserWalletSelectedHandler>,
|
||||
) : UserWalletsListRepository {
|
||||
|
||||
|
|
@ -620,14 +621,13 @@ internal class DefaultUserWalletsListRepository(
|
|||
}
|
||||
|
||||
private suspend fun onFirstWalletCreated() {
|
||||
// reset flag (that is set from AF deeplink) after creating a new wallet
|
||||
mobileWalletPromoRepository.setShouldShowMobileWalletPromo(false)
|
||||
// reset the referral attribution (set from AF deeplink) after creating a new wallet
|
||||
clearAppsFlyerDeeplinkUseCase(AppsFlyerDeeplinkSource.Referral)
|
||||
}
|
||||
|
||||
private suspend fun onAllWalletsDeleted() {
|
||||
// reset flag (that is set from AF deeplink) after removing the last wallet
|
||||
mobileWalletPromoRepository.setShouldShowMobileWalletPromo(false)
|
||||
// wipe the Usedesk support-chat clientId so a fresh UUID is generated for the next wallet
|
||||
// reset the referral attribution (set from AF deeplink) after removing the last wallet
|
||||
clearAppsFlyerDeeplinkUseCase(AppsFlyerDeeplinkSource.Referral)
|
||||
appPreferencesStore.editData { it.remove(PreferencesKeys.USEDESK_CLIENT_ID_KEY) }
|
||||
}
|
||||
}
|
||||
|
|
@ -166,4 +166,5 @@ internal val Blockchain.moonPaySupportedCurrency: MoonPaySupportedCurrency?
|
|||
Adi, AdiTestnet -> null
|
||||
SeiEvm, SeiEvmTestnet -> null
|
||||
Monad, MonadTestnet -> null
|
||||
Gonka -> null
|
||||
}
|
||||
|
|
@ -43,7 +43,6 @@ import com.tangem.domain.onboarding.repository.OnboardingRepository
|
|||
import com.tangem.domain.settings.NeverRequestPermissionUseCase
|
||||
import com.tangem.domain.settings.NeverToInitiallyAskPermissionUseCase
|
||||
import com.tangem.domain.settings.ShouldInitiallyAskPermissionUseCase
|
||||
import com.tangem.feature.referral.domain.ShouldShowMobileWalletPromoUseCase
|
||||
import com.tangem.features.hotwallet.HotAccessCodeRequestComponent
|
||||
import com.tangem.features.hotwallet.accesscoderequest.proxy.HotWalletPasswordRequesterProxy
|
||||
import com.tangem.features.onboarding.v2.common.analytics.OnboardingEvent
|
||||
|
|
@ -69,6 +68,8 @@ import com.tangem.wallet.R
|
|||
import dagger.assisted.Assisted
|
||||
import dagger.assisted.AssistedFactory
|
||||
import dagger.assisted.AssistedInject
|
||||
import kotlinx.coroutines.async
|
||||
import kotlinx.coroutines.coroutineScope
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withTimeoutOrNull
|
||||
import kotlin.time.Duration.Companion.seconds
|
||||
|
|
@ -102,7 +103,6 @@ internal class DefaultRoutingComponent @AssistedInject constructor(
|
|||
private val shouldInitiallyAskPermissionUseCase: ShouldInitiallyAskPermissionUseCase,
|
||||
private val neverRequestPermissionUseCase: NeverRequestPermissionUseCase,
|
||||
private val featureTogglesManager: FeatureTogglesManager,
|
||||
private val shouldShowMobileWalletPromoUseCase: ShouldShowMobileWalletPromoUseCase,
|
||||
) : RoutingComponent,
|
||||
AppComponentContext by context,
|
||||
SnackbarHandler {
|
||||
|
|
@ -206,23 +206,8 @@ internal class DefaultRoutingComponent @AssistedInject constructor(
|
|||
}
|
||||
|
||||
private suspend fun navigateForEmptyWallets(): AppRoute {
|
||||
val isHotWalletOnboardingEnabled = featureTogglesManager.isFeatureEnabled(
|
||||
FeatureToggles.AND_15101_TANGEM_PAY_HOT_WALLET_ONBOARDING,
|
||||
)
|
||||
TangemLogger.i("[TangemPay][HWO] Feature toggle enabled=$isHotWalletOnboardingEnabled")
|
||||
val afterEmptyRoute: AppRoute = if (isHotWalletOnboardingEnabled) {
|
||||
val tangemPayHotWalletOnboardingDeepLink = withTimeoutOrNull(2.seconds) {
|
||||
appsFlyerReferralParamsHandler.waitForDeeplink(AppsFlyerDeeplinkSource.TangemPayHotWalletOnboarding)
|
||||
}
|
||||
TangemLogger.i("[TangemPay][HWO] Deep link present=${tangemPayHotWalletOnboardingDeepLink != null}")
|
||||
if (tangemPayHotWalletOnboardingDeepLink != null) {
|
||||
AppRoute.TangemPayHotWalletOnboarding
|
||||
} else {
|
||||
getDefaultRoute()
|
||||
}
|
||||
} else {
|
||||
getDefaultRoute()
|
||||
}
|
||||
val afterEmptyRoute = resolveAppsFlyerOnboardingRoute()
|
||||
?: AppRoute.Home(launchMode = launchMode)
|
||||
|
||||
val shouldAskPushPermission = shouldInitiallyAskPermissionUseCase(PUSH_PERMISSION).getOrNull()
|
||||
?: return afterEmptyRoute
|
||||
|
|
@ -242,16 +227,41 @@ internal class DefaultRoutingComponent @AssistedInject constructor(
|
|||
}
|
||||
}
|
||||
|
||||
private suspend fun getDefaultRoute(): AppRoute {
|
||||
val isHideStoriesForReferralEnabled = featureTogglesManager.isFeatureEnabled(
|
||||
private suspend fun resolveAppsFlyerOnboardingRoute(): AppRoute? = coroutineScope {
|
||||
val tangemPayRoute = async { resolveTangemPayHotWalletOnboardingRoute() }
|
||||
val referralRoute = async { resolveReferralRoute() }
|
||||
tangemPayRoute.await() ?: referralRoute.await()
|
||||
}
|
||||
|
||||
private suspend fun resolveTangemPayHotWalletOnboardingRoute(): AppRoute? {
|
||||
val isEnabled = featureTogglesManager.isFeatureEnabled(
|
||||
FeatureToggles.AND_15101_TANGEM_PAY_HOT_WALLET_ONBOARDING,
|
||||
)
|
||||
TangemLogger.i("[TangemPay][HWO] Feature toggle enabled=$isEnabled")
|
||||
if (!isEnabled) return null
|
||||
|
||||
val deepLink = awaitAppsFlyerDeeplink(AppsFlyerDeeplinkSource.TangemPayHotWalletOnboarding)
|
||||
TangemLogger.i("[TangemPay][HWO] Deep link present=${deepLink != null}")
|
||||
return if (deepLink != null) AppRoute.TangemPayHotWalletOnboarding else null
|
||||
}
|
||||
|
||||
private suspend fun resolveReferralRoute(): AppRoute? {
|
||||
val isEnabled = featureTogglesManager.isFeatureEnabled(
|
||||
FeatureToggles.TWI_1512_HIDE_STORIES_FOR_REFERRAL_ENABLED,
|
||||
)
|
||||
// Referral users skip the Home stories screen and land directly on the
|
||||
// mobile wallet creation flow.
|
||||
return if (isHideStoriesForReferralEnabled && shouldShowMobileWalletPromoUseCase()) {
|
||||
if (!isEnabled) return null
|
||||
|
||||
val referralDeepLink = awaitAppsFlyerDeeplink(AppsFlyerDeeplinkSource.Referral)
|
||||
return if (referralDeepLink != null) {
|
||||
AppRoute.CreateWalletStart(mode = AppRoute.CreateWalletStart.Mode.HotWallet)
|
||||
} else {
|
||||
AppRoute.Home(launchMode = launchMode)
|
||||
null
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun awaitAppsFlyerDeeplink(source: AppsFlyerDeeplinkSource): String? {
|
||||
return withTimeoutOrNull(2.seconds) {
|
||||
appsFlyerReferralParamsHandler.waitForDeeplink(source)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -5,9 +5,7 @@ import com.google.common.truth.Truth.assertThat
|
|||
import com.tangem.datasource.local.appsflyer.AppsFlyerDeeplinkSource
|
||||
import com.tangem.datasource.local.appsflyer.AppsFlyerStore
|
||||
import com.tangem.domain.wallets.models.AppsFlyerConversionData
|
||||
import com.tangem.feature.referral.domain.SetShouldShowMobileWalletPromoUseCase
|
||||
import com.tangem.test.core.ProvideTestModels
|
||||
import arrow.core.right
|
||||
import com.tangem.test.core.TestAppCoroutineScope
|
||||
import io.mockk.clearMocks
|
||||
import io.mockk.coEvery
|
||||
|
|
@ -28,13 +26,9 @@ import org.junit.jupiter.params.ParameterizedTest
|
|||
class AppsFlyerReferralParamsHandlerTest {
|
||||
|
||||
private val appsFlyerStore: AppsFlyerStore = mockk(relaxUnitFun = true)
|
||||
private val setShouldShowMobileWalletPromoUseCase: SetShouldShowMobileWalletPromoUseCase = mockk {
|
||||
coEvery { this@mockk.invoke(true) } returns Unit.right()
|
||||
}
|
||||
private val handler = AppsFlyerReferralParamsHandler(
|
||||
appsFlyerStore = appsFlyerStore,
|
||||
coroutineScope = TestAppCoroutineScope(),
|
||||
setShouldShowMobileWalletPromoUseCase = setShouldShowMobileWalletPromoUseCase,
|
||||
)
|
||||
|
||||
@AfterEach
|
||||
|
|
@ -175,7 +169,6 @@ class AppsFlyerReferralParamsHandlerTest {
|
|||
private val localHandler = AppsFlyerReferralParamsHandler(
|
||||
appsFlyerStore = localStore,
|
||||
coroutineScope = TestAppCoroutineScope(),
|
||||
setShouldShowMobileWalletPromoUseCase = mockk { coEvery { this@mockk.invoke(true) } returns Unit.right() },
|
||||
)
|
||||
|
||||
@Test
|
||||
|
|
@ -240,6 +233,93 @@ class AppsFlyerReferralParamsHandlerTest {
|
|||
}
|
||||
}
|
||||
|
||||
@Nested
|
||||
inner class WaitForReferralDeeplink {
|
||||
|
||||
private val localStore: AppsFlyerStore = mockk(relaxUnitFun = true)
|
||||
private val localHandler = AppsFlyerReferralParamsHandler(
|
||||
appsFlyerStore = localStore,
|
||||
coroutineScope = TestAppCoroutineScope(),
|
||||
)
|
||||
|
||||
@Test
|
||||
fun `GIVEN cached referral deeplink WHEN waitForDeeplink THEN returns cached value`() = runTest {
|
||||
// GIVEN
|
||||
coEvery { localStore.getDeeplink(AppsFlyerDeeplinkSource.Referral) } returns "referral"
|
||||
|
||||
// WHEN
|
||||
val result = localHandler.waitForDeeplink(AppsFlyerDeeplinkSource.Referral)
|
||||
|
||||
// THEN
|
||||
assertThat(result).isEqualTo("referral")
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `GIVEN no cache and referral deeplink WHEN handleDeeplink then waitForDeeplink THEN returns referral value`() =
|
||||
runTest {
|
||||
// GIVEN
|
||||
coEvery { localStore.getDeeplink(AppsFlyerDeeplinkSource.Referral) } returns null
|
||||
val deepLink = mockk<DeepLink> {
|
||||
every { deepLinkValue } returns "referral"
|
||||
every { getStringValue(any()) } returns SUCCESS_REFCODE
|
||||
}
|
||||
|
||||
// WHEN
|
||||
localHandler.handleDeeplink(deepLink)
|
||||
val result = localHandler.waitForDeeplink(AppsFlyerDeeplinkSource.Referral)
|
||||
|
||||
// THEN
|
||||
assertThat(result).isEqualTo("referral")
|
||||
coVerify { localStore.storeDeeplink(AppsFlyerDeeplinkSource.Referral, "referral") }
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `GIVEN no cache and non-referral deeplink WHEN handleDeeplink then waitForDeeplink THEN returns null`() =
|
||||
runTest {
|
||||
// GIVEN
|
||||
coEvery { localStore.getDeeplink(AppsFlyerDeeplinkSource.Referral) } returns null
|
||||
val deepLink = mockk<DeepLink> {
|
||||
every { deepLinkValue } returns "tpay_mobileonboard"
|
||||
every { getStringValue(any()) } returns null
|
||||
}
|
||||
|
||||
// WHEN
|
||||
localHandler.handleDeeplink(deepLink)
|
||||
val result = localHandler.waitForDeeplink(AppsFlyerDeeplinkSource.Referral)
|
||||
|
||||
// THEN
|
||||
assertThat(result).isNull()
|
||||
coVerify(inverse = true) { localStore.storeDeeplink(AppsFlyerDeeplinkSource.Referral, any()) }
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `GIVEN no cache WHEN handleNoDeeplink then waitForDeeplink THEN returns null`() = runTest {
|
||||
// GIVEN
|
||||
coEvery { localStore.getDeeplink(AppsFlyerDeeplinkSource.Referral) } returns null
|
||||
|
||||
// WHEN
|
||||
localHandler.handleNoDeeplink()
|
||||
val result = localHandler.waitForDeeplink(AppsFlyerDeeplinkSource.Referral)
|
||||
|
||||
// THEN
|
||||
assertThat(result).isNull()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `GIVEN deeplink stored during wait WHEN waitForDeeplink THEN returns stored value`() = runTest {
|
||||
// GIVEN cache is empty on the initial read but populated (e.g. from conversion-data
|
||||
// handling) by the time we re-check after awaiting the deferred
|
||||
coEvery { localStore.getDeeplink(AppsFlyerDeeplinkSource.Referral) } returns null andThen "referral"
|
||||
|
||||
// WHEN the deferred resolves without a matching value (UDL reported no deep link)
|
||||
localHandler.handleNoDeeplink()
|
||||
val result = localHandler.waitForDeeplink(AppsFlyerDeeplinkSource.Referral)
|
||||
|
||||
// THEN the value persisted during the wait is preferred
|
||||
assertThat(result).isEqualTo("referral")
|
||||
}
|
||||
}
|
||||
|
||||
private companion object Companion {
|
||||
const val SUCCESS_REFCODE = "valid_refcode"
|
||||
const val SUCCESS_CAMPAIGN = "valid_campaign"
|
||||
|
|
|
|||
|
|
@ -7,13 +7,13 @@ import com.tangem.common.test.domain.wallet.MockUserWalletFactory
|
|||
import com.tangem.core.analytics.api.AnalyticsEventHandler
|
||||
import com.tangem.core.analytics.utils.TrackingContextProxy
|
||||
import com.tangem.datasource.local.preferences.AppPreferencesStore
|
||||
import com.tangem.domain.appsflyer.usecase.ClearAppsFlyerDeeplinkUseCase
|
||||
import com.tangem.domain.common.wallets.UserWalletSelectedHandler
|
||||
import com.tangem.domain.hotwallet.repository.HotWalletRepository
|
||||
import com.tangem.domain.models.wallet.UserWallet
|
||||
import com.tangem.domain.models.wallet.UserWalletId
|
||||
import com.tangem.domain.wallets.hot.HotWalletAccessCodeAttemptsRepository
|
||||
import com.tangem.domain.wallets.hot.HotWalletPasswordRequester
|
||||
import com.tangem.feature.referral.domain.MobileWalletPromoRepository
|
||||
import com.tangem.hot.sdk.TangemHotSdk
|
||||
import com.tangem.sdk.api.TangemSdkManager
|
||||
import com.tangem.utils.Provider
|
||||
|
|
@ -45,7 +45,7 @@ internal class DefaultUserWalletsListRepositoryTest {
|
|||
private val trackingContextProxy: TrackingContextProxy = mockk(relaxed = true)
|
||||
private val analyticsEventHandler: AnalyticsEventHandler = mockk(relaxed = true)
|
||||
private val hotWalletRepository: HotWalletRepository = mockk(relaxed = true)
|
||||
private val mobileWalletPromoRepository: MobileWalletPromoRepository = mockk(relaxed = true)
|
||||
private val clearAppsFlyerDeeplinkUseCase: ClearAppsFlyerDeeplinkUseCase = mockk(relaxed = true)
|
||||
private val userWalletSelectedHandler: UserWalletSelectedHandler = mockk(relaxed = true)
|
||||
|
||||
private val walletA = MockUserWalletFactory.create().copy(walletId = UserWalletId("0011"), name = "Wallet A")
|
||||
|
|
@ -61,7 +61,7 @@ internal class DefaultUserWalletsListRepositoryTest {
|
|||
selectedUserWalletRepository,
|
||||
userWalletEncryptionKeysRepository,
|
||||
trackingContextProxy,
|
||||
mobileWalletPromoRepository,
|
||||
clearAppsFlyerDeeplinkUseCase,
|
||||
userWalletSelectedHandler,
|
||||
)
|
||||
|
||||
|
|
@ -82,7 +82,7 @@ internal class DefaultUserWalletsListRepositoryTest {
|
|||
trackingContextProxy = trackingContextProxy,
|
||||
analyticsEventHandler = analyticsEventHandler,
|
||||
hotWalletRepository = hotWalletRepository,
|
||||
mobileWalletPromoRepository = mobileWalletPromoRepository,
|
||||
clearAppsFlyerDeeplinkUseCase = clearAppsFlyerDeeplinkUseCase,
|
||||
userWalletSelectedHandler = Lazy { userWalletSelectedHandler },
|
||||
)
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue