Updated on 2026-08-14
This commit is contained in:
commit
e0033cef82
1726 changed files with 83681 additions and 14949 deletions
|
|
@ -3,7 +3,6 @@ plugins {
|
|||
alias(deps.plugins.kotlin.android)
|
||||
alias(deps.plugins.kotlin.kapt)
|
||||
alias(deps.plugins.hilt.android)
|
||||
alias(deps.plugins.ksp)
|
||||
id("configuration")
|
||||
}
|
||||
|
||||
|
|
@ -12,19 +11,23 @@ android {
|
|||
}
|
||||
|
||||
dependencies {
|
||||
/** DI */
|
||||
|
||||
// region DI
|
||||
implementation(deps.hilt.android)
|
||||
kapt(deps.hilt.kapt)
|
||||
// endregion
|
||||
|
||||
/** Other libraries */
|
||||
// region Kotlin
|
||||
implementation(deps.kotlin.coroutines)
|
||||
// endregion
|
||||
|
||||
/** Core modules */
|
||||
// region Other libraries
|
||||
implementation(deps.amplitude.experiment)
|
||||
// endregion
|
||||
|
||||
// region Core modules
|
||||
implementation(projects.core.analytics.models)
|
||||
implementation(projects.core.datasource)
|
||||
implementation(projects.core.utils)
|
||||
|
||||
implementation(projects.domain.models)
|
||||
|
||||
/** Amplitude experiment */
|
||||
implementation(deps.amplitude.experiment)
|
||||
// endregion
|
||||
}
|
||||
|
|
@ -3,28 +3,39 @@ plugins {
|
|||
alias(deps.plugins.kotlin.kapt)
|
||||
id("configuration")
|
||||
}
|
||||
|
||||
dependencies {
|
||||
|
||||
/** DI */
|
||||
// region DI
|
||||
implementation(deps.hilt.core)
|
||||
kapt(deps.hilt.kapt)
|
||||
// endregion
|
||||
|
||||
/** Analytics - Models */
|
||||
api(projects.core.analytics.models)
|
||||
|
||||
/** Domain */
|
||||
implementation(projects.domain.analytics)
|
||||
implementation(projects.domain.models)
|
||||
|
||||
/** Other */
|
||||
// region Kotlin
|
||||
implementation(deps.kotlin.coroutines)
|
||||
// endregion
|
||||
|
||||
/** Core shouldn't depend on core, but in case with utils and logging its necessary */
|
||||
// region Tangem
|
||||
implementation(tangemDeps.card.core) // for calculating user id hash
|
||||
// endregion
|
||||
|
||||
// region Core modules
|
||||
api(projects.core.analytics.models)
|
||||
// Core shouldn't depend on core, but with utils and logging it's necessary.
|
||||
implementation(projects.core.utils)
|
||||
// endregion
|
||||
|
||||
/** For calculating user id hash */
|
||||
implementation(tangemDeps.card.core)
|
||||
// region Domain
|
||||
api(projects.domain.analytics)
|
||||
// endregion
|
||||
|
||||
/** Tests */
|
||||
testImplementation(projects.test.core)
|
||||
// region Domain models
|
||||
api(projects.domain.models)
|
||||
// endregion
|
||||
|
||||
// region Tests
|
||||
testImplementation(deps.test.junit5)
|
||||
testImplementation(deps.test.mockk)
|
||||
testImplementation(deps.test.truth)
|
||||
// endregion
|
||||
}
|
||||
|
|
@ -86,8 +86,6 @@ sealed class MainScreenAnalyticsEvent(
|
|||
|
||||
class BuyScreenOpened : MainScreenAnalyticsEvent(event = "Buy Screen Opened")
|
||||
|
||||
class SwapScreenOpened : MainScreenAnalyticsEvent(event = "Swap Screen Opened")
|
||||
|
||||
class SellScreenOpened : MainScreenAnalyticsEvent(event = "Sell Screen Opened")
|
||||
|
||||
data class BuyTokenClicked(val currencySymbol: String) : MainScreenAnalyticsEvent(
|
||||
|
|
@ -100,21 +98,6 @@ sealed class MainScreenAnalyticsEvent(
|
|||
params = mapOf(TOKEN_PARAM to currencySymbol),
|
||||
)
|
||||
|
||||
data class SwapTokenClicked(val currencySymbol: String) : MainScreenAnalyticsEvent(
|
||||
event = "Swap Token Clicked",
|
||||
params = mapOf(TOKEN_PARAM to currencySymbol),
|
||||
)
|
||||
|
||||
data class ReceiveTokenClicked(val currencySymbol: String) : MainScreenAnalyticsEvent(
|
||||
event = "Receive Token Clicked",
|
||||
params = mapOf(TOKEN_PARAM to currencySymbol),
|
||||
)
|
||||
|
||||
data class RemoveTokenClicked(val currencySymbol: String) : MainScreenAnalyticsEvent(
|
||||
event = "Remove Button Clicked",
|
||||
params = mapOf(TOKEN_PARAM to currencySymbol),
|
||||
)
|
||||
|
||||
data class ButtonClose(val source: AnalyticsParam.ScreensSources) : MainScreenAnalyticsEvent(
|
||||
event = "Button - Close",
|
||||
params = mapOf(AnalyticsParam.SOURCE to source.value),
|
||||
|
|
|
|||
|
|
@ -1,11 +1,7 @@
|
|||
package com.tangem.core.analytics.models.event
|
||||
|
||||
import com.tangem.core.analytics.models.AnalyticsEvent
|
||||
import com.tangem.core.analytics.models.AnalyticsParam.Key.SEARCHED
|
||||
import com.tangem.core.analytics.models.AnalyticsParam.Key.SOURCE
|
||||
import com.tangem.core.analytics.models.AnalyticsParam.Key.TOKEN_PARAM
|
||||
import com.tangem.core.analytics.models.AnalyticsParam.Key.TYPE
|
||||
import com.tangem.core.analytics.models.AnalyticsParam.ScreensSources
|
||||
|
||||
/**
|
||||
[REDACTED_AUTHOR]
|
||||
|
|
@ -15,19 +11,6 @@ sealed class SwapAnalyticsEvent(
|
|||
params: Map<String, String> = emptyMap(),
|
||||
) : AnalyticsEvent("Swap", event, params) {
|
||||
|
||||
data class TokenSelected(
|
||||
val token: String,
|
||||
val source: ScreensSources,
|
||||
val isSearched: Boolean,
|
||||
) : SwapAnalyticsEvent(
|
||||
event = "Token Selected",
|
||||
params = mapOf(
|
||||
TOKEN_PARAM to token,
|
||||
SOURCE to source.value,
|
||||
SEARCHED to if (isSearched) "True" else "False",
|
||||
),
|
||||
)
|
||||
|
||||
class FilterProvider(filterType: String) : SwapAnalyticsEvent(
|
||||
event = "Filter Provider",
|
||||
params = mapOf(TYPE to filterType),
|
||||
|
|
|
|||
70
core/config-toggles/CLAUDE.md
Normal file
70
core/config-toggles/CLAUDE.md
Normal file
|
|
@ -0,0 +1,70 @@
|
|||
# core/config-toggles
|
||||
|
||||
Feature toggles (and the related excluded-blockchains toggles). Toggles gate
|
||||
features by app version; the JSON config is the source of truth and the
|
||||
`FeatureToggles` enum is generated from it at build time.
|
||||
|
||||
## How it works
|
||||
|
||||
- **Config:** `src/main/assets/configs/feature_toggles_config.json` — a JSON array
|
||||
of `{ "name": <STRING>, "version": <STRING> }` (`ConfigToggle`).
|
||||
- The **convention plugin** generates the `FeatureToggles` enum (one entry per
|
||||
`name`) at build time. Reference it as `FeatureToggles.<NAME>`.
|
||||
- **Entry point:** `FeatureTogglesManager.isFeatureEnabled(FeatureToggles.X)`.
|
||||
- `ProdFeatureTogglesManager` (release): a toggle is enabled when the app
|
||||
version `>=` its `version`.
|
||||
- `DevFeatureTogglesManager` (tester builds, `BuildConfig.TESTER_MENU_ENABLED`):
|
||||
runtime-toggleable via the Tester Menu.
|
||||
- **`version` semantics:**
|
||||
- `"undefined"` (`DISABLED_FEATURE_TOGGLE_VERSION`) → OFF in prod; can only be
|
||||
flipped ON via the Tester Menu / dev builds. Use this while a feature is in
|
||||
development.
|
||||
- `"X.Y"` (e.g. `5.40`) → ON in prod from that app version onward
|
||||
(`currentVersion >= localVersion`, see `VersionAvailabilityContract`).
|
||||
|
||||
## Naming convention (ENFORCED by a test)
|
||||
|
||||
- A toggle `name` MUST match `^(AND|TWI)_\d+(?:_[A-Z0-9]+)+$` — start with the
|
||||
Jira ticket id (`AND_<id>` for Android tickets, `TWI_<id>` for idea tickets),
|
||||
then an `UPPER_SNAKE_CASE` suffix. Example: `AND_15901_STORIES_CONTAINER_ENABLED`.
|
||||
- Enforced by `FeatureTogglesNamingConventionTest`. Legacy toggles that predate
|
||||
the rule are whitelisted in its `EXCLUDED_TOGGLES_LIST` — do **not** add new
|
||||
names there without an explicit reason.
|
||||
- The Kotlin interface property stays human-readable **without** the ticket id:
|
||||
`isStoriesContainerEnabled`.
|
||||
|
||||
## Per-feature toggles & how to add one
|
||||
|
||||
Each feature owns its toggles — feature code reads them through its own
|
||||
interface, never `FeatureTogglesManager` directly:
|
||||
|
||||
- `api/`: `XxxFeatureToggles` interface — `val isYyyEnabled: Boolean`.
|
||||
- `impl/`: `DefaultXxxFeatureToggles(featureTogglesManager)` exposes each toggle as
|
||||
a **getter-backed property**, not a stored value — so it is re-evaluated on every
|
||||
read (required for runtime toggling via the Tester Menu):
|
||||
|
||||
```kotlin
|
||||
override val isYyyEnabled: Boolean
|
||||
get() = featureTogglesManager.isFeatureEnabled(FeatureToggles.AND_<id>_YYY)
|
||||
```
|
||||
|
||||
Never `val isYyyEnabled = featureTogglesManager.isFeatureEnabled(...)` (evaluated
|
||||
once at construction).
|
||||
- DI: a `@Provides @Singleton` in the feature's Hilt module returning the interface.
|
||||
|
||||
To add a toggle:
|
||||
|
||||
1. Add `{ "name": "AND_<id>_FOO_ENABLED", "version": "undefined" }` to the config
|
||||
JSON (the enum is regenerated at build).
|
||||
2. Add `val isFooEnabled` to the feature's `XxxFeatureToggles` and map it in
|
||||
`DefaultXxxFeatureToggles` (create the interface/impl/DI provider if the
|
||||
feature has none yet).
|
||||
3. Gate code on `xxxFeatureToggles.isFooEnabled`.
|
||||
|
||||
## Removing (cleanup)
|
||||
|
||||
When a toggle ships at 100%, set its `version` to the release and run the
|
||||
`cleanup-feature-toggles` skill — it removes the JSON entry, the interface/impl
|
||||
members, inlines `true`, and drops dead branches. Mark code that must be deleted
|
||||
together with a toggle using `@RemoveWithToggle("AND_<id>_FOO_ENABLED")`
|
||||
(`com.tangem.utils.annotations.RemoveWithToggle`); the cleanup skill picks it up.
|
||||
|
|
@ -63,21 +63,32 @@ tasks.withType<Detekt>().configureEach {
|
|||
exclude { it.file.absolutePath.contains("/build/generated/") }
|
||||
}
|
||||
dependencies {
|
||||
/** DI */
|
||||
|
||||
// region DI
|
||||
implementation(deps.hilt.android)
|
||||
kapt(deps.hilt.kapt)
|
||||
// endregion
|
||||
|
||||
/** Local storages */
|
||||
// region Kotlin
|
||||
implementation(deps.kotlin.coroutines)
|
||||
// endregion
|
||||
|
||||
// region AndroidX
|
||||
implementation(deps.androidx.annotation)
|
||||
implementation(deps.androidx.datastore)
|
||||
// endregion
|
||||
|
||||
/** Other libraries */
|
||||
// region Other libraries
|
||||
implementation(deps.moshi)
|
||||
implementation(deps.moshi.kotlin)
|
||||
ksp(deps.moshi.kotlin.codegen)
|
||||
// endregion
|
||||
|
||||
/** Core modules */
|
||||
// region Core modules
|
||||
implementation(projects.core.datasource)
|
||||
implementation(projects.core.utils)
|
||||
// endregion
|
||||
|
||||
// region Tests
|
||||
testImplementation(projects.test.core)
|
||||
// endregion
|
||||
}
|
||||
|
|
@ -1,4 +1,12 @@
|
|||
[
|
||||
{
|
||||
"name": "AND_15901_STORIES_CONTAINER_ENABLED",
|
||||
"version": "undefined"
|
||||
},
|
||||
{
|
||||
"name": "TWI_1322_FORCE_UPDATE_ENABLED",
|
||||
"version": "undefined"
|
||||
},
|
||||
{
|
||||
"name": "NEW_CARD_SCANNING_ENABLED",
|
||||
"version": "undefined"
|
||||
|
|
@ -8,21 +16,13 @@
|
|||
"version": "undefined"
|
||||
},
|
||||
{
|
||||
"name": "STAKING_ETH_ENABLED",
|
||||
"version": "5.39"
|
||||
},
|
||||
{
|
||||
"name": "USEDESK_ENABLED",
|
||||
"name": "TWI_485_USEDESK_ENABLED",
|
||||
"version": "undefined"
|
||||
},
|
||||
{
|
||||
"name": "APP_REDESIGN_ENABLED",
|
||||
"version": "6.0"
|
||||
},
|
||||
{
|
||||
"name": "GASLESS_APPROVAL_ENABLED",
|
||||
"version": "5.37"
|
||||
},
|
||||
{
|
||||
"name": "DYNAMIC_ADDRESSES_ENABLED",
|
||||
"version": "5.39"
|
||||
|
|
@ -47,10 +47,6 @@
|
|||
"name": "HEDERA_ERC20_ENABLED",
|
||||
"version": "5.37"
|
||||
},
|
||||
{
|
||||
"name": "ADD_AND_MANAGE_TOKENS_ENABLED",
|
||||
"version": "5.38"
|
||||
},
|
||||
{
|
||||
"name": "WALLET_CONNECT_BITCOIN_ENABLED",
|
||||
"version": "undefined"
|
||||
|
|
@ -71,10 +67,6 @@
|
|||
"name": "AND_15120_SWAP_INTEGRATED_APPROVE",
|
||||
"version": "6.0"
|
||||
},
|
||||
{
|
||||
"name": "SWAP_AB_ENABLED",
|
||||
"version": "5.39"
|
||||
},
|
||||
{
|
||||
"name": "TWI_1403_PUSH_NOTIFICATION_SETTINGS_ENABLED",
|
||||
"version": "undefined"
|
||||
|
|
@ -91,10 +83,6 @@
|
|||
"name": "TWI_1377_MANAGE_FUNDS",
|
||||
"version": "6.0"
|
||||
},
|
||||
{
|
||||
"name": "AND_15009_SWAP_PROVIDER_FILTER_ENABLED",
|
||||
"version": "5.39"
|
||||
},
|
||||
{
|
||||
"name": "AND_15101_TANGEM_PAY_HOT_WALLET_ONBOARDING",
|
||||
"version": "5.39"
|
||||
|
|
@ -103,18 +91,6 @@
|
|||
"name": "AND_15402_ADI_MAIN_SCREEN_DEFAULT_ENABLED",
|
||||
"version": "5.39"
|
||||
},
|
||||
{
|
||||
"name": "AND_15103_SWAP_RATE_EXPERIENCE_ENABLED",
|
||||
"version": "5.39"
|
||||
},
|
||||
{
|
||||
"name": "AND_15122_SWAP_PREDEFINED_BUTTONS_ENABLED",
|
||||
"version": "5.39"
|
||||
},
|
||||
{
|
||||
"name": "AND_15154_YIELD_PROMO_ENABLED",
|
||||
"version": "5.39.2"
|
||||
},
|
||||
{
|
||||
"name": "TWI_1512_HIDE_STORIES_FOR_REFERRAL_ENABLED",
|
||||
"version": "5.39"
|
||||
|
|
@ -143,10 +119,18 @@
|
|||
"name": "AND_15741_VISA_PAY_REMOVE_ACCOUNT",
|
||||
"version": "undefined"
|
||||
},
|
||||
{
|
||||
"name": "AND_16041_VISA_TIERS_PLUS_PLAN",
|
||||
"version": "undefined"
|
||||
},
|
||||
{
|
||||
"name": "TWI_83_ADDRESS_BOOK_ENABLED",
|
||||
"version": "undefined"
|
||||
},
|
||||
{
|
||||
"name": "AND_15632_GASLESS_YIELD_WITHDRAW_ENABLED",
|
||||
"version": "undefined"
|
||||
},
|
||||
{
|
||||
"name": "AND_15489_EXPRESS_SHARE_BUTTON_ENABLED",
|
||||
"version": "6.0"
|
||||
|
|
@ -170,5 +154,29 @@
|
|||
{
|
||||
"name": "AND_16148_SOLANA_UNSTAKE_VALIDATION_ENABLED",
|
||||
"version": "6.0"
|
||||
},
|
||||
{
|
||||
"name": "TWI_1522_MARKETING_BANNERS_ENABLED",
|
||||
"version": "undefined"
|
||||
},
|
||||
{
|
||||
"name": "TWI_1469_FOR_YOU_ENABLED",
|
||||
"version": "undefined"
|
||||
},
|
||||
{
|
||||
"name": "TWI_1367_HIGH_FEE_WARNING_ENABLED",
|
||||
"version": "undefined"
|
||||
},
|
||||
{
|
||||
"name": "TWI_1638_VA_MVP0_ENABLED",
|
||||
"version": "6.1"
|
||||
},
|
||||
{
|
||||
"name": "TWI_1637_CASHBACK_AND_REACTIVATION_CAMPAIGNS_ENABLED",
|
||||
"version": "undefined"
|
||||
},
|
||||
{
|
||||
"name": "AND_16080_TRON_DEX_SWAP_ENABLED",
|
||||
"version": "undefined"
|
||||
}
|
||||
]
|
||||
|
|
|
|||
|
|
@ -39,18 +39,13 @@ internal class FeatureTogglesNamingConventionTest {
|
|||
/** Toggles created before the AND_/TWI_ naming convention. Do NOT add new entries. */
|
||||
val EXCLUDED_TOGGLES_LIST = setOf(
|
||||
"ADDRESS_SYNC_ENABLED",
|
||||
"ADD_AND_MANAGE_TOKENS_ENABLED",
|
||||
"APP_REDESIGN_ENABLED",
|
||||
"ASSETS_DISCOVERY_ENABLED",
|
||||
"DYNAMIC_ADDRESSES_ENABLED",
|
||||
"GASLESS_APPROVAL_ENABLED",
|
||||
"HEDERA_ERC20_ENABLED",
|
||||
"NEW_CARD_SCANNING_ENABLED",
|
||||
"SOLANA_SCALED_UI_AMOUNT_ENABLED",
|
||||
"SOLANA_TX_HISTORY_ENABLED",
|
||||
"STAKING_ETH_ENABLED",
|
||||
"SWAP_AB_ENABLED",
|
||||
"USEDESK_ENABLED",
|
||||
"VIRTUAL_ACCOUNTS_ENABLED",
|
||||
"VISA_ONBOARDING_ENABLED",
|
||||
"WALLET_CONNECT_BITCOIN_ENABLED",
|
||||
|
|
|
|||
|
|
@ -60,70 +60,80 @@ androidComponents {
|
|||
}
|
||||
dependencies {
|
||||
|
||||
/** Project */
|
||||
implementation(projects.core.analytics)
|
||||
implementation(projects.core.utils)
|
||||
implementation(projects.core.res)
|
||||
implementation(projects.domain.appTheme.models)
|
||||
implementation(projects.domain.core)
|
||||
implementation(projects.domain.tokens.models)
|
||||
implementation(projects.domain.wallets.models)
|
||||
implementation(projects.domain.balanceHiding.models)
|
||||
implementation(projects.domain.txhistory.models)
|
||||
implementation(projects.domain.staking.models)
|
||||
implementation(projects.domain.onramp.models)
|
||||
implementation(projects.domain.models)
|
||||
implementation(projects.domain.nft.models)
|
||||
implementation(projects.domain.walletConnect.models)
|
||||
implementation(projects.domain.yieldSupply.models)
|
||||
implementation(projects.domain.visa.models)
|
||||
|
||||
/** Tangem libraries */
|
||||
implementation(tangemDeps.blockchain)
|
||||
implementation(tangemDeps.card.core)
|
||||
|
||||
/** DI */
|
||||
// region DI
|
||||
implementation(deps.hilt.android)
|
||||
kapt(deps.hilt.kapt)
|
||||
// endregion
|
||||
|
||||
/** Coroutines */
|
||||
implementation(deps.kotlin.coroutines)
|
||||
implementation(deps.kotlin.coroutines.rx2)
|
||||
// region Kotlin
|
||||
api(deps.kotlin.coroutines)
|
||||
api(deps.kotlin.datetime)
|
||||
api(deps.kotlin.serialization)
|
||||
// endregion
|
||||
|
||||
/** Logging */
|
||||
// region AndroidX
|
||||
implementation(deps.androidx.core)
|
||||
api(deps.androidx.datastore)
|
||||
// endregion
|
||||
|
||||
/** Network */
|
||||
// region Network
|
||||
api(deps.moshi)
|
||||
implementation(deps.moshi.kotlin)
|
||||
implementation(deps.moshi.adapters)
|
||||
implementation(deps.moshi.adapters.ext)
|
||||
implementation(deps.moshi.kotlin)
|
||||
ksp(deps.moshi.kotlin.codegen)
|
||||
api(deps.okHttp)
|
||||
implementation(deps.okHttp.prettyLogging)
|
||||
implementation(deps.okio)
|
||||
api(deps.retrofit)
|
||||
implementation(deps.retrofit.moshi)
|
||||
ksp(deps.moshi.kotlin.codegen)
|
||||
api(deps.retrofit.moshi)
|
||||
kaptForObfuscatingVariants(deps.retrofit.response.type.keeper)
|
||||
// endregion
|
||||
|
||||
/** Time */
|
||||
implementation(deps.jodatime)
|
||||
// region Room
|
||||
api(deps.room.runtime)
|
||||
api(deps.room.ktx)
|
||||
ksp(deps.room.compiler)
|
||||
// endregion
|
||||
|
||||
/** Security */
|
||||
implementation(deps.spongecastle.core)
|
||||
// region Other libraries
|
||||
api(deps.jodatime)
|
||||
runtimeOnly(deps.spongecastle.core)
|
||||
// endregion
|
||||
|
||||
/** Chucker */
|
||||
// region Chucker
|
||||
debugImplementation(deps.chucker)
|
||||
mockedImplementation(deps.chucker)
|
||||
externalImplementation(deps.chuckerStub)
|
||||
internalImplementation(deps.chuckerStub)
|
||||
releaseImplementation(deps.chuckerStub)
|
||||
// endregion
|
||||
|
||||
/** Local storages */
|
||||
api(deps.androidx.datastore)
|
||||
implementation(deps.room.runtime)
|
||||
implementation(deps.room.ktx)
|
||||
ksp(deps.room.compiler)
|
||||
// region Tangem
|
||||
api(tangemDeps.blockchain)
|
||||
api(tangemDeps.card.core)
|
||||
// endregion
|
||||
|
||||
// region Core modules
|
||||
api(projects.core.analytics)
|
||||
implementation(projects.core.analytics.models)
|
||||
api(projects.core.utils)
|
||||
implementation(projects.core.res)
|
||||
// endregion
|
||||
|
||||
// region Domain models
|
||||
api(projects.domain.models)
|
||||
api(projects.domain.nft.models)
|
||||
api(projects.domain.onramp.models)
|
||||
api(projects.domain.staking.models)
|
||||
api(projects.domain.txhistory.models)
|
||||
api(projects.domain.visa.models)
|
||||
api(projects.domain.walletConnect.models)
|
||||
api(projects.domain.wallets.models)
|
||||
api(projects.domain.yieldSupply.models)
|
||||
// endregion
|
||||
|
||||
// region Tests
|
||||
testImplementation(projects.test.core)
|
||||
// endregion
|
||||
}
|
||||
|
|
@ -2,7 +2,7 @@
|
|||
"formatVersion": 1,
|
||||
"database": {
|
||||
"version": 1,
|
||||
"identityHash": "442ac578743a8b624777711cf49c77e2",
|
||||
"identityHash": "1ddc01e929ea21940e7f1e9e35810691",
|
||||
"entities": [
|
||||
{
|
||||
"tableName": "express_provider",
|
||||
|
|
@ -81,7 +81,7 @@
|
|||
},
|
||||
{
|
||||
"tableName": "express_exchange",
|
||||
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`tx_id` TEXT NOT NULL, `owner_address` TEXT NOT NULL, `provider_id` TEXT NOT NULL, `from_address` TEXT, `payin_address` TEXT NOT NULL, `payin_extra_id` TEXT, `payout_address` TEXT NOT NULL, `refund_address` TEXT, `refund_extra_id` TEXT, `rate_type` TEXT NOT NULL, `status` TEXT NOT NULL, `external_tx_id` TEXT, `external_tx_url` TEXT, `payin_hash` TEXT, `payout_hash` TEXT, `refund_network` TEXT, `refund_contract_address` TEXT, `created_at` TEXT NOT NULL, `updated_at` TEXT NOT NULL, `pay_till` TEXT, `average_duration` INTEGER, `from_contract_address` TEXT NOT NULL, `from_network` TEXT NOT NULL, `from_decimals` INTEGER NOT NULL, `from_amount` TEXT NOT NULL, `from_actual_amount` TEXT, `to_contract_address` TEXT NOT NULL, `to_network` TEXT NOT NULL, `to_decimals` INTEGER NOT NULL, `to_amount` TEXT NOT NULL, `to_actual_amount` TEXT, PRIMARY KEY(`tx_id`))",
|
||||
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`tx_id` TEXT NOT NULL, `provider_id` TEXT NOT NULL, `from_address` TEXT NOT NULL, `payin_address` TEXT NOT NULL, `payin_extra_id` TEXT, `payout_address` TEXT NOT NULL, `refund_address` TEXT, `refund_extra_id` TEXT, `rate_type` TEXT NOT NULL, `status` TEXT NOT NULL, `external_tx_id` TEXT, `external_tx_url` TEXT, `payin_hash` TEXT, `payout_hash` TEXT, `refund_network` TEXT, `refund_contract_address` TEXT, `created_at` TEXT NOT NULL, `updated_at` TEXT NOT NULL, `pay_till` TEXT, `average_duration` INTEGER, `from_contract_address` TEXT NOT NULL, `from_network` TEXT NOT NULL, `from_decimals` INTEGER NOT NULL, `from_amount` TEXT NOT NULL, `from_actual_amount` TEXT, `to_contract_address` TEXT NOT NULL, `to_network` TEXT NOT NULL, `to_decimals` INTEGER NOT NULL, `to_amount` TEXT NOT NULL, `to_actual_amount` TEXT, PRIMARY KEY(`tx_id`))",
|
||||
"fields": [
|
||||
{
|
||||
"fieldPath": "txId",
|
||||
|
|
@ -89,12 +89,6 @@
|
|||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "ownerAddress",
|
||||
"columnName": "owner_address",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "providerId",
|
||||
"columnName": "provider_id",
|
||||
|
|
@ -104,7 +98,8 @@
|
|||
{
|
||||
"fieldPath": "fromAddress",
|
||||
"columnName": "from_address",
|
||||
"affinity": "TEXT"
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "payinAddress",
|
||||
|
|
@ -264,33 +259,34 @@
|
|||
},
|
||||
"indices": [
|
||||
{
|
||||
"name": "index_express_exchange_owner_address_from_network_from_contract_address_created_at",
|
||||
"name": "index_express_exchange_from_address_from_network_from_contract_address_created_at",
|
||||
"unique": false,
|
||||
"columnNames": [
|
||||
"owner_address",
|
||||
"from_address",
|
||||
"from_network",
|
||||
"from_contract_address",
|
||||
"created_at"
|
||||
],
|
||||
"orders": [],
|
||||
"createSql": "CREATE INDEX IF NOT EXISTS `index_express_exchange_owner_address_from_network_from_contract_address_created_at` ON `${TABLE_NAME}` (`owner_address`, `from_network`, `from_contract_address`, `created_at`)"
|
||||
"createSql": "CREATE INDEX IF NOT EXISTS `index_express_exchange_from_address_from_network_from_contract_address_created_at` ON `${TABLE_NAME}` (`from_address`, `from_network`, `from_contract_address`, `created_at`)"
|
||||
},
|
||||
{
|
||||
"name": "index_express_exchange_to_network_to_contract_address_created_at",
|
||||
"name": "index_express_exchange_payout_address_to_network_to_contract_address_created_at",
|
||||
"unique": false,
|
||||
"columnNames": [
|
||||
"payout_address",
|
||||
"to_network",
|
||||
"to_contract_address",
|
||||
"created_at"
|
||||
],
|
||||
"orders": [],
|
||||
"createSql": "CREATE INDEX IF NOT EXISTS `index_express_exchange_to_network_to_contract_address_created_at` ON `${TABLE_NAME}` (`to_network`, `to_contract_address`, `created_at`)"
|
||||
"createSql": "CREATE INDEX IF NOT EXISTS `index_express_exchange_payout_address_to_network_to_contract_address_created_at` ON `${TABLE_NAME}` (`payout_address`, `to_network`, `to_contract_address`, `created_at`)"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"tableName": "express_onramp",
|
||||
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`tx_id` TEXT NOT NULL, `owner_address` TEXT NOT NULL, `provider_id` TEXT NOT NULL, `payout_address` TEXT NOT NULL, `status` TEXT NOT NULL, `fail_reason` TEXT, `external_tx_id` TEXT, `external_tx_url` TEXT, `payout_hash` TEXT, `created_at` TEXT NOT NULL, `updated_at` TEXT NOT NULL, `from_currency_code` TEXT NOT NULL, `from_amount` TEXT NOT NULL, `from_precision` INTEGER NOT NULL, `payment_method` TEXT NOT NULL, `country_code` TEXT NOT NULL, `to_contract_address` TEXT NOT NULL, `to_network` TEXT NOT NULL, `to_decimals` INTEGER NOT NULL, `to_amount` TEXT, `to_actual_amount` TEXT, PRIMARY KEY(`tx_id`))",
|
||||
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`tx_id` TEXT NOT NULL, `provider_id` TEXT NOT NULL, `payout_address` TEXT NOT NULL, `status` TEXT NOT NULL, `fail_reason` TEXT, `external_tx_id` TEXT, `external_tx_url` TEXT, `payout_hash` TEXT, `created_at` TEXT NOT NULL, `updated_at` TEXT NOT NULL, `from_currency_code` TEXT NOT NULL, `from_amount` TEXT NOT NULL, `from_precision` INTEGER NOT NULL, `payment_method` TEXT NOT NULL, `country_code` TEXT NOT NULL, `to_contract_address` TEXT NOT NULL, `to_network` TEXT NOT NULL, `to_decimals` INTEGER NOT NULL, `to_amount` TEXT, `to_actual_amount` TEXT, PRIMARY KEY(`tx_id`))",
|
||||
"fields": [
|
||||
{
|
||||
"fieldPath": "txId",
|
||||
|
|
@ -298,12 +294,6 @@
|
|||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "ownerAddress",
|
||||
"columnName": "owner_address",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "providerId",
|
||||
"columnName": "provider_id",
|
||||
|
|
@ -421,16 +411,16 @@
|
|||
},
|
||||
"indices": [
|
||||
{
|
||||
"name": "index_express_onramp_owner_address_to_network_to_contract_address_created_at",
|
||||
"name": "index_express_onramp_payout_address_to_network_to_contract_address_created_at",
|
||||
"unique": false,
|
||||
"columnNames": [
|
||||
"owner_address",
|
||||
"payout_address",
|
||||
"to_network",
|
||||
"to_contract_address",
|
||||
"created_at"
|
||||
],
|
||||
"orders": [],
|
||||
"createSql": "CREATE INDEX IF NOT EXISTS `index_express_onramp_owner_address_to_network_to_contract_address_created_at` ON `${TABLE_NAME}` (`owner_address`, `to_network`, `to_contract_address`, `created_at`)"
|
||||
"createSql": "CREATE INDEX IF NOT EXISTS `index_express_onramp_payout_address_to_network_to_contract_address_created_at` ON `${TABLE_NAME}` (`payout_address`, `to_network`, `to_contract_address`, `created_at`)"
|
||||
}
|
||||
]
|
||||
},
|
||||
|
|
@ -474,11 +464,194 @@
|
|||
"address"
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"tableName": "onramp_country",
|
||||
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`code` TEXT NOT NULL, `name` TEXT NOT NULL, `image` TEXT NOT NULL, `alpha3` TEXT NOT NULL, `continent` TEXT NOT NULL, `onramp_available` INTEGER NOT NULL, `currency_name` TEXT NOT NULL, `currency_code` TEXT NOT NULL, `currency_image` TEXT, `currency_precision` INTEGER NOT NULL, `currency_unit` TEXT NOT NULL, PRIMARY KEY(`code`))",
|
||||
"fields": [
|
||||
{
|
||||
"fieldPath": "code",
|
||||
"columnName": "code",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "name",
|
||||
"columnName": "name",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "image",
|
||||
"columnName": "image",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "alpha3",
|
||||
"columnName": "alpha3",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "continent",
|
||||
"columnName": "continent",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "isOnrampAvailable",
|
||||
"columnName": "onramp_available",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "defaultCurrency.name",
|
||||
"columnName": "currency_name",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "defaultCurrency.code",
|
||||
"columnName": "currency_code",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "defaultCurrency.image",
|
||||
"columnName": "currency_image",
|
||||
"affinity": "TEXT"
|
||||
},
|
||||
{
|
||||
"fieldPath": "defaultCurrency.precision",
|
||||
"columnName": "currency_precision",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "defaultCurrency.unit",
|
||||
"columnName": "currency_unit",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
}
|
||||
],
|
||||
"primaryKey": {
|
||||
"autoGenerate": false,
|
||||
"columnNames": [
|
||||
"code"
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"tableName": "token_info",
|
||||
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`network_id` TEXT NOT NULL, `contract_address` TEXT NOT NULL, `coin_id` TEXT NOT NULL, `name` TEXT NOT NULL, `symbol` TEXT NOT NULL, `decimals` INTEGER NOT NULL, `updated_at` INTEGER NOT NULL, PRIMARY KEY(`network_id`, `contract_address`))",
|
||||
"fields": [
|
||||
{
|
||||
"fieldPath": "networkId",
|
||||
"columnName": "network_id",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "contractAddress",
|
||||
"columnName": "contract_address",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "coinId",
|
||||
"columnName": "coin_id",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "name",
|
||||
"columnName": "name",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "symbol",
|
||||
"columnName": "symbol",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "decimals",
|
||||
"columnName": "decimals",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "updatedAt",
|
||||
"columnName": "updated_at",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
}
|
||||
],
|
||||
"primaryKey": {
|
||||
"autoGenerate": false,
|
||||
"columnNames": [
|
||||
"network_id",
|
||||
"contract_address"
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"tableName": "history_index",
|
||||
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`type` TEXT NOT NULL, `entity_id` TEXT NOT NULL, `address` TEXT NOT NULL, `sort_time_millis` INTEGER NOT NULL, PRIMARY KEY(`type`, `entity_id`, `address`))",
|
||||
"fields": [
|
||||
{
|
||||
"fieldPath": "type",
|
||||
"columnName": "type",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "entityId",
|
||||
"columnName": "entity_id",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "address",
|
||||
"columnName": "address",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "sortTimeMillis",
|
||||
"columnName": "sort_time_millis",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
}
|
||||
],
|
||||
"primaryKey": {
|
||||
"autoGenerate": false,
|
||||
"columnNames": [
|
||||
"type",
|
||||
"entity_id",
|
||||
"address"
|
||||
]
|
||||
},
|
||||
"indices": [
|
||||
{
|
||||
"name": "index_history_index_address_sort_time_millis_entity_id",
|
||||
"unique": false,
|
||||
"columnNames": [
|
||||
"address",
|
||||
"sort_time_millis",
|
||||
"entity_id"
|
||||
],
|
||||
"orders": [],
|
||||
"createSql": "CREATE INDEX IF NOT EXISTS `index_history_index_address_sort_time_millis_entity_id` ON `${TABLE_NAME}` (`address`, `sort_time_millis`, `entity_id`)"
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"setupQueries": [
|
||||
"CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)",
|
||||
"INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, '442ac578743a8b624777711cf49c77e2')"
|
||||
"INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, '1ddc01e929ea21940e7f1e9e35810691')"
|
||||
]
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,25 @@
|
|||
package com.tangem.datasource.api.addressbook
|
||||
|
||||
import com.tangem.datasource.api.addressbook.models.SyncAddressBooksRequest
|
||||
import com.tangem.datasource.api.addressbook.models.SyncAddressBooksResponse
|
||||
import com.tangem.datasource.api.addressbook.models.UpdateAddressBookRequest
|
||||
import com.tangem.datasource.api.addressbook.models.UpdateAddressBookResponse
|
||||
import com.tangem.datasource.api.common.response.ApiResponse
|
||||
import retrofit2.http.Body
|
||||
import retrofit2.http.Header
|
||||
import retrofit2.http.PUT
|
||||
import retrofit2.http.POST
|
||||
import retrofit2.http.Path
|
||||
|
||||
interface AddressBookApi {
|
||||
|
||||
@POST("v1/address-books/sync")
|
||||
suspend fun syncAddressBooks(@Body body: SyncAddressBooksRequest): ApiResponse<SyncAddressBooksResponse>
|
||||
|
||||
@PUT("v1/address-books/{walletId}")
|
||||
suspend fun updateAddressBook(
|
||||
@Path("walletId") walletId: String,
|
||||
@Header("If-Match") eTag: String?,
|
||||
@Body body: UpdateAddressBookRequest,
|
||||
): ApiResponse<UpdateAddressBookResponse>
|
||||
}
|
||||
|
|
@ -0,0 +1,22 @@
|
|||
package com.tangem.datasource.api.addressbook.models
|
||||
|
||||
import com.squareup.moshi.Json
|
||||
import com.squareup.moshi.JsonClass
|
||||
|
||||
/**
|
||||
* Request body for `POST /address-books/sync`.
|
||||
*
|
||||
* Each [Wallet.etag] is optional: when it matches the backend's etag, that wallet is omitted from the
|
||||
* response and the local copy is kept.
|
||||
*/
|
||||
@JsonClass(generateAdapter = true)
|
||||
data class SyncAddressBooksRequest(
|
||||
@Json(name = "wallets") val wallets: List<Wallet>,
|
||||
) {
|
||||
|
||||
@JsonClass(generateAdapter = true)
|
||||
data class Wallet(
|
||||
@Json(name = "walletId") val walletId: String,
|
||||
@Json(name = "etag") val etag: String? = null,
|
||||
)
|
||||
}
|
||||
|
|
@ -0,0 +1,27 @@
|
|||
package com.tangem.datasource.api.addressbook.models
|
||||
|
||||
import com.squareup.moshi.Json
|
||||
import com.squareup.moshi.JsonClass
|
||||
|
||||
/**
|
||||
* Response body for `POST /address-books/sync`.
|
||||
*
|
||||
* [items] contains only the wallets whose backend etag differs from the one sent in the request; wallets
|
||||
* with a matching etag are omitted and their local copy must be kept.
|
||||
*/
|
||||
@JsonClass(generateAdapter = true)
|
||||
data class SyncAddressBooksResponse(
|
||||
@Json(name = "items") val items: List<Item>,
|
||||
) {
|
||||
|
||||
@JsonClass(generateAdapter = true)
|
||||
data class Item(
|
||||
@Json(name = "walletId") val walletId: String,
|
||||
@Json(name = "etag") val etag: String,
|
||||
@Json(name = "version") val version: String,
|
||||
@Json(name = "updatedAt") val updatedAt: String,
|
||||
@Json(name = "nonce") val nonce: String,
|
||||
@Json(name = "ciphertext") val ciphertext: String,
|
||||
@Json(name = "authTag") val authTag: String,
|
||||
)
|
||||
}
|
||||
|
|
@ -0,0 +1,13 @@
|
|||
package com.tangem.datasource.api.addressbook.models
|
||||
|
||||
import com.squareup.moshi.Json
|
||||
import com.squareup.moshi.JsonClass
|
||||
|
||||
/** Request body for `PUT /address-books/{walletId}`. */
|
||||
@JsonClass(generateAdapter = true)
|
||||
data class UpdateAddressBookRequest(
|
||||
@Json(name = "version") val version: String,
|
||||
@Json(name = "nonce") val nonce: String,
|
||||
@Json(name = "ciphertext") val ciphertext: String,
|
||||
@Json(name = "authTag") val authTag: String,
|
||||
)
|
||||
|
|
@ -0,0 +1,12 @@
|
|||
package com.tangem.datasource.api.addressbook.models
|
||||
|
||||
import com.squareup.moshi.Json
|
||||
import com.squareup.moshi.JsonClass
|
||||
|
||||
/** Response body for `PUT /address-books/{walletId}`. */
|
||||
@JsonClass(generateAdapter = true)
|
||||
data class UpdateAddressBookResponse(
|
||||
@Json(name = "walletId") val walletId: String,
|
||||
@Json(name = "etag") val etag: String,
|
||||
@Json(name = "updatedAt") val updatedAt: String,
|
||||
)
|
||||
|
|
@ -4,6 +4,7 @@ import com.tangem.datasource.api.auth.models.request.AuthApiRequest
|
|||
import com.tangem.datasource.api.auth.models.request.NonceApiRequest
|
||||
import com.tangem.datasource.api.auth.models.request.RefreshApiRequest
|
||||
import com.tangem.datasource.api.auth.models.request.RegisterApiRequest
|
||||
import com.tangem.datasource.api.auth.models.request.WalletRegistrationRequest
|
||||
import com.tangem.datasource.api.auth.models.response.NonceApiResponse
|
||||
import com.tangem.datasource.api.auth.models.response.TokenApiResponse
|
||||
import com.tangem.datasource.api.common.response.ApiResponse
|
||||
|
|
@ -20,7 +21,7 @@ interface AuthApi {
|
|||
*
|
||||
* Generates a nonce bound to the device public key for the device registration flow.
|
||||
*/
|
||||
@POST("api/v1/auth/nonce/device")
|
||||
@POST("api/authentication/v1/mobile/nonce/device")
|
||||
suspend fun requestDeviceNonce(@Body request: NonceApiRequest): ApiResponse<NonceApiResponse>
|
||||
|
||||
/**
|
||||
|
|
@ -29,16 +30,15 @@ interface AuthApi {
|
|||
* Registers a new device using its hardware-backed public key and issues the initial
|
||||
* session token pair. Called once per app install.
|
||||
*/
|
||||
@POST("api/v1/auth/register")
|
||||
@RequiresDpopProof
|
||||
suspend fun register(@Body request: RegisterApiRequest): ApiResponse<TokenApiResponse>
|
||||
@POST("api/authentication/v1/mobile/register")
|
||||
suspend fun registerDevice(@Body request: RegisterApiRequest): ApiResponse<TokenApiResponse>
|
||||
|
||||
/**
|
||||
* Request authentication nonce.
|
||||
*
|
||||
* Generates a nonce bound to the device public key for the authentication flow.
|
||||
*/
|
||||
@POST("api/v1/auth/nonce/auth")
|
||||
@POST("api/authentication/v1/mobile/nonce/auth")
|
||||
suspend fun requestAuthNonce(@Body request: NonceApiRequest): ApiResponse<NonceApiResponse>
|
||||
|
||||
/**
|
||||
|
|
@ -48,7 +48,7 @@ interface AuthApi {
|
|||
* JWT access token with bound `walletIds[]` and risk tier. All subsequent auth after
|
||||
* registration uses this endpoint.
|
||||
*/
|
||||
@POST("api/v1/auth/authenticate")
|
||||
@POST("api/authentication/v1/mobile/authenticate")
|
||||
suspend fun authenticate(@Body request: AuthApiRequest): ApiResponse<TokenApiResponse>
|
||||
|
||||
/**
|
||||
|
|
@ -58,7 +58,26 @@ interface AuthApi {
|
|||
* with family-based reuse detection — replaying a consumed token revokes the entire token
|
||||
* family (SR-8). Sender-constraint is verified via the DPoP-proof header (`cnf.jkt`).
|
||||
*/
|
||||
@POST("api/v1/auth/refresh")
|
||||
@POST("api/authentication/v1/mobile/token/refresh")
|
||||
@RequiresDpopProof
|
||||
suspend fun refresh(@Body request: RefreshApiRequest): ApiResponse<TokenApiResponse>
|
||||
|
||||
/**
|
||||
* Request wallet registration nonce.
|
||||
*
|
||||
* Generates a nonce bound to the device public key for the wallet registration flow.
|
||||
*/
|
||||
@POST("api/authentication/v1/mobile/nonce/wallet")
|
||||
suspend fun requestWalletNonce(@Body request: NonceApiRequest): ApiResponse<NonceApiResponse>
|
||||
|
||||
/**
|
||||
* Register a wallet.
|
||||
*
|
||||
* Binds a new wallet to an already-registered device. When a card signature is provided the
|
||||
* wallet is bound as COLD (card-backed); otherwise it is registered as a MOBILE (hot) wallet.
|
||||
* Returns refreshed session tokens reflecting the updated wallet list.
|
||||
*/
|
||||
@POST("api/authentication/v1/mobile/wallet/register")
|
||||
@RequiresDpopProof
|
||||
suspend fun registerWallet(@Body request: WalletRegistrationRequest): ApiResponse<TokenApiResponse>
|
||||
}
|
||||
|
|
@ -10,17 +10,17 @@ import com.squareup.moshi.JsonClass
|
|||
@JsonClass(generateAdapter = true)
|
||||
data class DeviceMetadata(
|
||||
/** Device hardware model (e.g. `iPhone 15 Pro`). */
|
||||
@Json(name = "deviceModel") val deviceModel: String?,
|
||||
@Json(name = "deviceModel") val deviceModel: String,
|
||||
/** Operating system (`android` / `ios`). */
|
||||
@Json(name = "os") val os: String,
|
||||
/** OS version string (e.g. `17.4.1`). */
|
||||
@Json(name = "osVersion") val osVersion: String?,
|
||||
@Json(name = "osVersion") val osVersion: String,
|
||||
/** Application version (e.g. `5.8.0`). */
|
||||
@Json(name = "appVersion") val appVersion: String?,
|
||||
@Json(name = "appVersion") val appVersion: String,
|
||||
/** User-Agent header (e.g. `Tangem/5.8.0 (iPhone; iOS 17.4.1; Scale/3.00)`). */
|
||||
@Json(name = "userAgent") val userAgent: String?,
|
||||
@Json(name = "userAgent") val userAgent: String,
|
||||
/** Client locale (e.g. `en-US`). */
|
||||
@Json(name = "locale") val locale: String?,
|
||||
@Json(name = "locale") val locale: String,
|
||||
/** Client timezone (e.g. `Europe/Moscow`). */
|
||||
@Json(name = "timezone") val timezone: String?,
|
||||
@Json(name = "timezone") val timezone: String,
|
||||
)
|
||||
|
|
@ -6,7 +6,7 @@ import com.squareup.moshi.JsonClass
|
|||
/**
|
||||
* Registration request — registers a new device and establishes initial trust.
|
||||
*
|
||||
* Posted to `POST /api/v1/auth/register`; on success the server returns
|
||||
* On success the server returns
|
||||
* [com.tangem.datasource.api.auth.models.response.TokenApiResponse] (the initial session token pair).
|
||||
*/
|
||||
@JsonClass(generateAdapter = true)
|
||||
|
|
@ -22,7 +22,7 @@ data class RegisterApiRequest(
|
|||
data class RegisterPayload(
|
||||
/** Base64-encoded EC public key of the device. */
|
||||
@Json(name = "devicePublicKey") val devicePublicKey: String,
|
||||
/** Deciphered nonce value from the `/api/v1/auth/nonce/device` endpoint. */
|
||||
/** Deciphered nonce value from the device nonce endpoint. */
|
||||
@Json(name = "nonce") val nonce: String,
|
||||
/** Platform attestation token (Play Integrity / App Attest). Optional; backend accepts `null`. */
|
||||
@Json(name = "attestationToken") val attestationToken: String?,
|
||||
|
|
|
|||
|
|
@ -0,0 +1,46 @@
|
|||
package com.tangem.datasource.api.auth.models.request
|
||||
|
||||
import com.squareup.moshi.Json
|
||||
import com.squareup.moshi.JsonClass
|
||||
|
||||
/**
|
||||
* Wallet registration request — binds a new wallet to an already-registered device.
|
||||
*
|
||||
* When [cardSignature] (and the accompanying [cardSignatureSalt] / [walletStatus]) is provided the
|
||||
* wallet is bound as COLD (card-backed); otherwise it is registered as a MOBILE (hot) wallet.
|
||||
* Mirrors the `WalletRegistrationRequest` schema in the backend OpenAPI contract.
|
||||
*/
|
||||
@JsonClass(generateAdapter = true)
|
||||
data class WalletRegistrationRequest(
|
||||
/** Deciphered nonce value from the wallet nonce endpoint. */
|
||||
@Json(name = "nonce") val nonce: String,
|
||||
/**
|
||||
* Wallet identifier — Base64-encoded
|
||||
* `HMAC-SHA256(key = SHA-256(walletPublicKey), data = "UserWalletID")`.
|
||||
*/
|
||||
@Json(name = "walletId") val walletId: String,
|
||||
/**
|
||||
* Base64-encoded secp256k1 RSV signature (65 bytes) over `sha256(nonce || walletSignatureSalt)`.
|
||||
* The server recovers `walletPublicKey` from this signature.
|
||||
*/
|
||||
@Json(name = "walletSignature") val walletSignature: String,
|
||||
/** Base64-encoded salt used in the wallet signature hash. */
|
||||
@Json(name = "walletSignatureSalt") val walletSignatureSalt: String,
|
||||
/**
|
||||
* Base64-encoded secp256k1 RSV signature (65 bytes) over
|
||||
* `sha256(walletPublicKey || nonce || cardSignatureSalt || walletStatus)`. Required for
|
||||
* cold-wallet registration; `null` for mobile (hot) wallets.
|
||||
*/
|
||||
@Json(name = "cardSignature") val cardSignature: String?,
|
||||
/** Base64-encoded salt used in the card signature hash. Required for cold-wallet registration. */
|
||||
@Json(name = "cardSignatureSalt") val cardSignatureSalt: String?,
|
||||
/**
|
||||
* Base64-encoded single byte describing wallet provenance on the card
|
||||
* (`0x82` = generated on card, `0xC2` = SEED imported). Required for cold-wallet registration.
|
||||
*/
|
||||
@Json(name = "walletStatus") val walletStatus: String?,
|
||||
/** Platform attestation token (Play Integrity / App Attest). */
|
||||
@Json(name = "attestationToken") val attestationToken: String?,
|
||||
/** Client-reported device metadata. */
|
||||
@Json(name = "metadata") val metadata: DeviceMetadata,
|
||||
)
|
||||
|
|
@ -43,6 +43,6 @@ internal class Auth : ApiConfig() {
|
|||
private companion object {
|
||||
|
||||
private const val DEV_BASE_URL = "[REDACTED_ENV_URL]"
|
||||
private const val PROD_BASE_URL = "https://authentication.tangem.org/"
|
||||
private const val PROD_BASE_URL = "https://api.tangem.org/"
|
||||
}
|
||||
}
|
||||
|
|
@ -55,6 +55,7 @@ internal sealed class TangemPay(
|
|||
"version" to ProviderSuspend { appInfoProvider.appVersion },
|
||||
"platform" to ProviderSuspend { "Android" },
|
||||
"X-API-KEY" to ProviderSuspend { getBffStaticToken(apiEnvironment) },
|
||||
"X-Device-Scale" to ProviderSuspend { appInfoProvider.deviceScale.toString() },
|
||||
)
|
||||
|
||||
private fun getBffStaticToken(apiEnvironment: ApiEnvironment): String {
|
||||
|
|
|
|||
|
|
@ -79,11 +79,8 @@ data class ExchangeItemResponse(
|
|||
val createdAt: String,
|
||||
|
||||
/** Transaction last-update timestamp in ISO-8601 format */
|
||||
// todo txHistory uncomment
|
||||
/*
|
||||
@Json(name = "updatedAt")
|
||||
val updatedAt: String,
|
||||
*/
|
||||
|
||||
/** Pay-in expiration timestamp in ISO-8601 format */
|
||||
@Json(name = "payTill")
|
||||
|
|
|
|||
|
|
@ -0,0 +1,21 @@
|
|||
package com.tangem.datasource.api.gasless
|
||||
|
||||
import com.tangem.datasource.api.common.response.ApiResponse
|
||||
import com.tangem.datasource.api.gasless.models.GaslessBatchTransactionRequest
|
||||
import com.tangem.datasource.api.gasless.models.GaslessServiceResponse
|
||||
import com.tangem.datasource.api.gasless.models.GaslessSignedTransactionResultDTO
|
||||
import com.tangem.datasource.api.gasless.models.GaslessTransactionRequest
|
||||
import retrofit2.http.Body
|
||||
import retrofit2.http.POST
|
||||
|
||||
interface GaslessTxServiceApiV2 {
|
||||
@POST("api/v2/transaction/sign")
|
||||
suspend fun signGaslessTransaction(
|
||||
@Body transaction: GaslessTransactionRequest,
|
||||
): ApiResponse<GaslessServiceResponse<GaslessSignedTransactionResultDTO>>
|
||||
|
||||
@POST("api/v2/transaction/batch-sign")
|
||||
suspend fun signGaslessBatchTransaction(
|
||||
@Body transaction: GaslessBatchTransactionRequest,
|
||||
): ApiResponse<GaslessServiceResponse<GaslessSignedTransactionResultDTO>>
|
||||
}
|
||||
|
|
@ -0,0 +1,41 @@
|
|||
package com.tangem.datasource.api.gasless.models
|
||||
|
||||
import com.squareup.moshi.Json
|
||||
import com.squareup.moshi.JsonClass
|
||||
|
||||
/**
|
||||
* Request body for gasless batch transaction submission (v2 `POST /api/v2/transaction/batch-sign`).
|
||||
* Represents a batch of transactions with fee delegation metadata.
|
||||
*
|
||||
* The top-level payload field is `gaslessTransaction` (shared shape with single sign — see
|
||||
* gasless-service `BatchSignRequestDto`), carrying `transactions[]`, `fee`, `nonce`.
|
||||
*/
|
||||
@JsonClass(generateAdapter = true)
|
||||
data class GaslessBatchTransactionRequest(
|
||||
@Json(name = "gaslessTransaction")
|
||||
val gaslessTransaction: GaslessBatchTransactionDataDTO,
|
||||
|
||||
@Json(name = "signature")
|
||||
val signature: String,
|
||||
|
||||
@Json(name = "userAddress")
|
||||
val userAddress: String,
|
||||
|
||||
@Json(name = "chainId")
|
||||
val chainId: Int,
|
||||
|
||||
@Json(name = "eip7702auth")
|
||||
val eip7702Auth: Eip7702AuthorizationDTO? = null,
|
||||
)
|
||||
|
||||
@JsonClass(generateAdapter = true)
|
||||
data class GaslessBatchTransactionDataDTO(
|
||||
@Json(name = "transactions")
|
||||
val transactions: List<TransactionData>,
|
||||
|
||||
@Json(name = "fee")
|
||||
val fee: FeeData,
|
||||
|
||||
@Json(name = "nonce")
|
||||
val nonce: String,
|
||||
)
|
||||
|
|
@ -45,6 +45,9 @@ data class TransactionData(
|
|||
@Json(name = "value")
|
||||
val value: String,
|
||||
|
||||
@Json(name = "gasLimit")
|
||||
val gasLimit: String? = null,
|
||||
|
||||
@Json(name = "data")
|
||||
val data: String,
|
||||
)
|
||||
|
|
|
|||
|
|
@ -0,0 +1,11 @@
|
|||
package com.tangem.datasource.api.marketing.models
|
||||
|
||||
import com.squareup.moshi.Json
|
||||
import com.squareup.moshi.JsonClass
|
||||
|
||||
/** Cached campaigns response plus its ETag, persisted per [CampaignDto.type] for revalidation. */
|
||||
@JsonClass(generateAdapter = true)
|
||||
data class MarketingCampaignsCacheEntry(
|
||||
@Json(name = "eTag") val eTag: String?,
|
||||
@Json(name = "response") val response: MarketingCampaignsResponse,
|
||||
)
|
||||
|
|
@ -0,0 +1,42 @@
|
|||
package com.tangem.datasource.api.marketing.models
|
||||
|
||||
import com.squareup.moshi.Json
|
||||
import com.squareup.moshi.JsonClass
|
||||
import java.math.BigDecimal
|
||||
|
||||
@JsonClass(generateAdapter = true)
|
||||
data class MarketingCampaignsResponse(
|
||||
@Json(name = "campaigns") val campaigns: List<CampaignDto>,
|
||||
)
|
||||
|
||||
@JsonClass(generateAdapter = true)
|
||||
data class CampaignDto(
|
||||
@Json(name = "id") val id: Int,
|
||||
@Json(name = "type") val type: String,
|
||||
@Json(name = "priority") val priority: Int,
|
||||
@Json(name = "startAt") val startAt: String? = null,
|
||||
@Json(name = "endAt") val endAt: String? = null,
|
||||
@Json(name = "minAmount") val minAmount: BigDecimal? = null,
|
||||
@Json(name = "maxAmount") val maxAmount: BigDecimal? = null,
|
||||
@Json(name = "providerIds") val providerIds: List<String>? = null,
|
||||
@Json(name = "tokens") val tokens: List<CampaignTokenDto>? = null,
|
||||
@Json(name = "banner") val banner: BannerDto,
|
||||
)
|
||||
|
||||
@JsonClass(generateAdapter = true)
|
||||
data class CampaignTokenDto(
|
||||
@Json(name = "networkId") val networkId: String? = null,
|
||||
@Json(name = "contractAddress") val contractAddress: String? = null,
|
||||
@Json(name = "id") val id: String? = null,
|
||||
)
|
||||
|
||||
@JsonClass(generateAdapter = true)
|
||||
data class BannerDto(
|
||||
@Json(name = "uiType") val uiType: String,
|
||||
@Json(name = "text") val text: String? = null,
|
||||
@Json(name = "icon") val icon: String? = null,
|
||||
@Json(name = "iconAlign") val iconAlign: String? = null,
|
||||
@Json(name = "bgColor") val bgColor: String? = null,
|
||||
@Json(name = "deeplink") val deeplink: String? = null,
|
||||
@Json(name = "dismissible") val isDismissible: Boolean = false,
|
||||
)
|
||||
|
|
@ -44,11 +44,8 @@ data class OnrampItemResponse(
|
|||
val createdAt: String,
|
||||
|
||||
/** Transaction last-update timestamp in ISO-8601 format */
|
||||
// todo txHistory uncomment
|
||||
/*
|
||||
@Json(name = "updatedAt")
|
||||
val updatedAt: String,
|
||||
*/
|
||||
// endregion
|
||||
|
||||
// region fromAsset (fiat) info
|
||||
|
|
|
|||
|
|
@ -17,12 +17,39 @@ interface TangemPayApi {
|
|||
@Query("limit") limit: Int = TX_HISTORY_PAGING_DEFAULT_LIMIT,
|
||||
): ApiResponse<TangemPayTxHistoryResponse>
|
||||
|
||||
@GET("v1/customer/transactions/{transaction_id}")
|
||||
suspend fun getCustomerTransaction(
|
||||
@Header("Authorization") authHeader: String,
|
||||
@Path("transaction_id") transactionId: String,
|
||||
): ApiResponse<TangemPayTransactionResponse>
|
||||
|
||||
@GET("v1/customer/kyc")
|
||||
suspend fun getKycAccess(@Header("Authorization") authHeader: String): ApiResponse<KycAccessInfoResponse>
|
||||
|
||||
@GET("v1/customer/me")
|
||||
suspend fun getCustomerMe(@Header("Authorization") authHeader: String): ApiResponse<CustomerMeResponse>
|
||||
|
||||
@GET("v1/customer/tariff-plan/transitions")
|
||||
suspend fun getTariffPlanTransitions(
|
||||
@Header("Authorization") authHeader: String,
|
||||
): ApiResponse<TariffPlanTransitionsResponse>
|
||||
|
||||
@POST("v1/customer/tariff-plan/pending-transition")
|
||||
suspend fun setPendingTariffPlanTransition(
|
||||
@Header("Authorization") authHeader: String,
|
||||
@Body body: SetPendingTariffPlanTransitionRequest,
|
||||
): ApiResponse<Any>
|
||||
|
||||
@POST("v1/customer/tariff-plan/pending-transition/cancel")
|
||||
suspend fun cancelPendingTariffPlanTransition(@Header("Authorization") authHeader: String): ApiResponse<Any>
|
||||
|
||||
/** Fiat bank requisites for the Virtual Account on-ramp (VA MVP0, TWI-1638). */
|
||||
@GET("v1/account/bank-credentials/{product_instance_id}")
|
||||
suspend fun getBankCredentials(
|
||||
@Header("Authorization") authHeader: String,
|
||||
@Path("product_instance_id") productInstanceId: String,
|
||||
): ApiResponse<BankCredentialsResponse>
|
||||
|
||||
@GET("v1/customer/wallets/{customer_wallet_id}")
|
||||
suspend fun checkCustomerWalletId(
|
||||
@Path("customer_wallet_id") customerWalletId: String,
|
||||
|
|
@ -40,6 +67,12 @@ interface TangemPayApi {
|
|||
@GET("v1/eligibility/channels")
|
||||
suspend fun getEligibilityChannels(): ApiResponse<TangemPayEligibilityChannels>
|
||||
|
||||
/** Eligibility channels fetched with the user (customer-wallet) token (VA MVP0, TWI-1638). */
|
||||
@GET("v1/eligibility/channels")
|
||||
suspend fun getUserEligibilityChannels(
|
||||
@Header("Authorization") authHeader: String,
|
||||
): ApiResponse<TangemPayEligibilityChannels>
|
||||
|
||||
@GET("v1/order/{order_id}")
|
||||
suspend fun getOrder(
|
||||
@Header("Authorization") authHeader: String,
|
||||
|
|
@ -64,6 +97,19 @@ interface TangemPayApi {
|
|||
@Body body: OrderRequest,
|
||||
): ApiResponse<OrderResponse>
|
||||
|
||||
// TODO: Doston: [REDACTED_TASK_KEY] Unify with method above
|
||||
@POST("v1/order")
|
||||
suspend fun createVirtualAccountOrder(
|
||||
@Header("Authorization") authHeader: String,
|
||||
@Body body: VirtualAccountOrderRequest,
|
||||
): ApiResponse<OrderResponse>
|
||||
|
||||
@POST("v1/order/{order_id}/cancel")
|
||||
suspend fun cancelOrder(
|
||||
@Header("Authorization") authHeader: String,
|
||||
@Path("order_id") orderId: String,
|
||||
): ApiResponse<Any>
|
||||
|
||||
/** Customer offers — used to gate the issue-additional-card flow. */
|
||||
@GET("v1/customer/offers")
|
||||
suspend fun getCustomerOffers(@Header("Authorization") authHeader: String): ApiResponse<CustomerOffersResponse>
|
||||
|
|
|
|||
|
|
@ -11,7 +11,9 @@ data class OrderRequest(
|
|||
@JsonClass(generateAdapter = true)
|
||||
data class Data(
|
||||
@Json(name = "customer_wallet_address") val customerWalletAddress: String,
|
||||
@Json(name = "specification_name") val specificationName: String = "SP_000004",
|
||||
@Json(name = "type") val type: String = "CARD_ISSUE_VIRTUAL_RAIN_KYC",
|
||||
@Json(name = "specification_name") val specificationName: String?,
|
||||
@Json(name = "type") val type: String,
|
||||
@Json(name = "target_tariff_plan_id") val targetTariffPlanId: String? = null,
|
||||
@Json(name = "tariff_plan_transition_type") val tariffPlanTransitionType: String? = null,
|
||||
)
|
||||
}
|
||||
|
|
@ -0,0 +1,9 @@
|
|||
package com.tangem.datasource.api.pay.models.request
|
||||
|
||||
import com.squareup.moshi.Json
|
||||
import com.squareup.moshi.JsonClass
|
||||
|
||||
@JsonClass(generateAdapter = true)
|
||||
data class SetPendingTariffPlanTransitionRequest(
|
||||
@Json(name = "pending_tariff_plan_id") val pendingTariffPlanId: String,
|
||||
)
|
||||
|
|
@ -0,0 +1,23 @@
|
|||
package com.tangem.datasource.api.pay.models.request
|
||||
|
||||
import com.squareup.moshi.Json
|
||||
import com.squareup.moshi.JsonClass
|
||||
|
||||
/**
|
||||
* Request body for creating a Virtual Account on-ramp order (VA MVP0, TWI-1638).
|
||||
*
|
||||
* `wallet_address` is the customer's managing (collateral-managing) wallet address; `payment_account_address`
|
||||
* is the existing collateral address. Distinct from the card-issue [OrderRequest] contract.
|
||||
*/
|
||||
@JsonClass(generateAdapter = true)
|
||||
data class VirtualAccountOrderRequest(
|
||||
@Json(name = "data") val data: Data,
|
||||
@Json(name = "idempotency_key") val idempotencyKey: String,
|
||||
) {
|
||||
@JsonClass(generateAdapter = true)
|
||||
data class Data(
|
||||
@Json(name = "deposit_address") val depositAddress: String,
|
||||
@Json(name = "type") val type: String = "ACCOUNT_ISSUE_VIRTUAL_RAIN",
|
||||
@Json(name = "specification_name") val specificationName: String = "SP_000006",
|
||||
)
|
||||
}
|
||||
|
|
@ -0,0 +1,24 @@
|
|||
package com.tangem.datasource.api.pay.models.response
|
||||
|
||||
import com.squareup.moshi.Json
|
||||
import com.squareup.moshi.JsonClass
|
||||
|
||||
/**
|
||||
* Response of `bff-v2/v1/account/bank-credentials/{product_instance_id}` — fiat bank requisites for the
|
||||
* Virtual Account on-ramp (VA MVP0, TWI-1638).
|
||||
*/
|
||||
@JsonClass(generateAdapter = true)
|
||||
data class BankCredentialsResponse(
|
||||
@Json(name = "result") val result: Result?,
|
||||
) {
|
||||
@JsonClass(generateAdapter = true)
|
||||
data class Result(
|
||||
@Json(name = "type") val type: String?,
|
||||
@Json(name = "beneficiary_name") val beneficiaryName: String?,
|
||||
@Json(name = "beneficiary_address") val beneficiaryAddress: String?,
|
||||
@Json(name = "beneficiary_bank_name") val beneficiaryBankName: String?,
|
||||
@Json(name = "beneficiary_bank_address") val beneficiaryBankAddress: String?,
|
||||
@Json(name = "account_number") val accountNumber: String?,
|
||||
@Json(name = "routing_number") val routingNumber: String?,
|
||||
)
|
||||
}
|
||||
|
|
@ -21,13 +21,56 @@ data class CustomerMeResponse(
|
|||
@Json(name = "balance") val balance: BalanceResponse?,
|
||||
@Json(name = "product_instances") val productInstances: List<ProductInstance>,
|
||||
@Json(name = "cards") val cards: List<Card>,
|
||||
@Json(name = "customer_tariff_plan") val customerTariffPlan: CustomerTariffPlan? = null,
|
||||
)
|
||||
|
||||
@JsonClass(generateAdapter = true)
|
||||
data class CustomerTariffPlan(
|
||||
@Json(name = "status") val status: String?,
|
||||
@Json(name = "next_billing_at") val nextBillingAt: String?,
|
||||
@Json(name = "pending_transition_at") val pendingTransitionAt: String?,
|
||||
@Json(name = "tariff_plan") val tariffPlan: TariffPlan?,
|
||||
@Json(name = "pending_tariff_plan") val pendingTariffPlan: TariffPlan?,
|
||||
)
|
||||
|
||||
@JsonClass(generateAdapter = true)
|
||||
data class TariffPlan(
|
||||
@Json(name = "id") val id: String?,
|
||||
@Json(name = "type") val type: String?,
|
||||
@Json(name = "name") val name: String?,
|
||||
@Json(name = "description_items") val descriptionItems: List<DescriptionItem>?,
|
||||
@Json(name = "images") val images: List<Image>? = null,
|
||||
@Json(name = "fees") val fees: List<Fee>? = null,
|
||||
)
|
||||
|
||||
@JsonClass(generateAdapter = true)
|
||||
data class Fee(
|
||||
@Json(name = "type") val type: String?,
|
||||
@Json(name = "amount") val amount: BigDecimal?,
|
||||
@Json(name = "currency") val currency: String?,
|
||||
@Json(name = "description") val description: String?,
|
||||
@Json(name = "period") val period: String?,
|
||||
)
|
||||
|
||||
@JsonClass(generateAdapter = true)
|
||||
data class Image(
|
||||
@Json(name = "type") val type: String?,
|
||||
@Json(name = "url") val url: String?,
|
||||
)
|
||||
|
||||
@JsonClass(generateAdapter = true)
|
||||
data class DescriptionItem(
|
||||
@Json(name = "type") val type: String?,
|
||||
@Json(name = "order") val order: Int?,
|
||||
@Json(name = "title") val title: String?,
|
||||
@Json(name = "body") val body: String?,
|
||||
)
|
||||
|
||||
@JsonClass(generateAdapter = true)
|
||||
data class ProductInstance(
|
||||
@Json(name = "id") val id: String,
|
||||
@Json(name = "cid") val cid: String?,
|
||||
@Json(name = "card_id") val cardId: String,
|
||||
@Json(name = "card_id") val cardId: String?,
|
||||
@Json(name = "card_wallet_address") val cardWalletAddress: String?,
|
||||
@Json(name = "status") val status: Status,
|
||||
@Json(name = "updated_at") val updatedAt: String,
|
||||
|
|
@ -35,7 +78,17 @@ data class CustomerMeResponse(
|
|||
@Json(name = "display_name") val displayName: String?,
|
||||
@Json(name = "actual_card_limit") val actualCardLimit: CardLimit?,
|
||||
@Json(name = "admin_card_limit") val adminCardLimit: CardLimit?,
|
||||
@Json(name = "product_specification_data_type") val specificationDataType: SpecificationDataType,
|
||||
) {
|
||||
@JsonClass(generateAdapter = false)
|
||||
enum class SpecificationDataType {
|
||||
@Json(name = "ACCOUNT")
|
||||
ACCOUNT,
|
||||
|
||||
@Json(name = "CARD")
|
||||
CARD,
|
||||
}
|
||||
|
||||
@JsonClass(generateAdapter = false)
|
||||
enum class Status {
|
||||
@Json(name = "NEW")
|
||||
|
|
|
|||
|
|
@ -27,6 +27,7 @@ data class OrderResponse(
|
|||
@Json(name = "emboss_name") val embossName: String?,
|
||||
@Json(name = "product_instance_id") val productInstanceId: String?,
|
||||
@Json(name = "payment_account_id") val paymentAccountId: String?,
|
||||
@Json(name = "target_tariff_plan_id") val targetTariffPlanId: String?,
|
||||
@Json(name = "transaction_hash") val transactionHash: String?,
|
||||
)
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,10 @@
|
|||
package com.tangem.datasource.api.pay.models.response
|
||||
|
||||
import com.squareup.moshi.Json
|
||||
import com.squareup.moshi.JsonClass
|
||||
|
||||
/** Response of `GET v1/customer/transactions/{transaction_id}` — a single transaction by its id. */
|
||||
@JsonClass(generateAdapter = true)
|
||||
data class TangemPayTransactionResponse(
|
||||
@Json(name = "result") val result: TangemPayTxHistoryResponse.Transaction,
|
||||
)
|
||||
|
|
@ -44,6 +44,8 @@ data class TangemPayTxHistoryResponse(
|
|||
@Json(name = "enriched_merchant_category") val enrichedMerchantCategory: String? = null,
|
||||
@Json(name = "card_id") val cardId: String? = null,
|
||||
@Json(name = "card_type") val cardType: String? = null,
|
||||
@Json(name = "card_display_name") val cardDisplayName: String? = null,
|
||||
@Json(name = "card_number_end") val cardNumberEnd: String? = null,
|
||||
@Json(name = "status") val status: String,
|
||||
@Json(name = "declined_reason") val declinedReason: String? = null,
|
||||
@Json(name = "authorized_at") val authorizedAt: DateTime,
|
||||
|
|
|
|||
|
|
@ -0,0 +1,15 @@
|
|||
package com.tangem.datasource.api.pay.models.response
|
||||
|
||||
import com.squareup.moshi.Json
|
||||
import com.squareup.moshi.JsonClass
|
||||
|
||||
@JsonClass(generateAdapter = true)
|
||||
data class TariffPlanTransitionsResponse(
|
||||
@Json(name = "result") val result: List<TariffPlanTransitionResponse>?,
|
||||
)
|
||||
|
||||
@JsonClass(generateAdapter = true)
|
||||
data class TariffPlanTransitionResponse(
|
||||
@Json(name = "type") val type: String?,
|
||||
@Json(name = "tariff_plan") val tariffPlan: CustomerMeResponse.TariffPlan?,
|
||||
)
|
||||
|
|
@ -0,0 +1,18 @@
|
|||
package com.tangem.datasource.api.promotion.models
|
||||
|
||||
import com.squareup.moshi.Json
|
||||
import com.squareup.moshi.JsonClass
|
||||
|
||||
@JsonClass(generateAdapter = true)
|
||||
data class CreatePromotionRegistrationBody(
|
||||
@Json(name = "campaignId") val campaignId: String,
|
||||
@Json(name = "walletIds") val walletIds: List<String>,
|
||||
@Json(name = "tokenReward") val tokenReward: TokenRewardDto,
|
||||
) {
|
||||
|
||||
@JsonClass(generateAdapter = true)
|
||||
data class TokenRewardDto(
|
||||
@Json(name = "tokenAddress") val tokenAddress: String,
|
||||
@Json(name = "networkId") val networkId: String,
|
||||
)
|
||||
}
|
||||
|
|
@ -0,0 +1,19 @@
|
|||
package com.tangem.datasource.api.promotion.models
|
||||
|
||||
import com.squareup.moshi.Json
|
||||
import com.squareup.moshi.JsonClass
|
||||
|
||||
@JsonClass(generateAdapter = true)
|
||||
data class PromotionRegistrationResponse(
|
||||
@Json(name = "status") val status: String,
|
||||
@Json(name = "message") val message: String?,
|
||||
@Json(name = "data") val data: RegistrationData,
|
||||
) {
|
||||
|
||||
@JsonClass(generateAdapter = true)
|
||||
data class RegistrationData(
|
||||
@Json(name = "campaignId") val campaignId: String,
|
||||
@Json(name = "registeredAt") val registeredAt: String?,
|
||||
@Json(name = "tokenReward") val tokenReward: CreatePromotionRegistrationBody.TokenRewardDto,
|
||||
)
|
||||
}
|
||||
|
|
@ -1,6 +1,9 @@
|
|||
package com.tangem.datasource.api.tangemTech
|
||||
|
||||
import com.tangem.datasource.api.common.response.ApiResponse
|
||||
import com.tangem.datasource.api.promotion.models.CreatePromotionRegistrationBody
|
||||
import com.tangem.datasource.api.promotion.models.PromotionRegistrationResponse
|
||||
import com.tangem.datasource.api.marketing.models.MarketingCampaignsResponse
|
||||
import com.tangem.datasource.api.promotion.models.PromotionsResponse
|
||||
import com.tangem.datasource.api.promotion.models.YieldBoostStatusResponse
|
||||
import com.tangem.datasource.api.stories.models.StoryContentResponse
|
||||
|
|
@ -46,22 +49,25 @@ interface TangemTechApi {
|
|||
@GET("v1/geo")
|
||||
suspend fun getUserCountryCode(): GeoResponse
|
||||
|
||||
@GET("v1/application/versions")
|
||||
suspend fun getApplicationVersions(): ApiResponse<ApplicationVersionsResponse>
|
||||
|
||||
@PUT("/v1/wallets/{walletId}/tokens")
|
||||
suspend fun saveTokens(
|
||||
@Path(value = "walletId") userId: String,
|
||||
@Body userTokens: UserTokensResponse,
|
||||
): ApiResponse<Unit>
|
||||
|
||||
@GET("/v1/wallets/{wallet_id}/notification-preferences")
|
||||
@GET("/api/v1/notification-preferences/{wallet_id}")
|
||||
suspend fun getPushNotificationPreferences(
|
||||
@Path("wallet_id") walletId: String,
|
||||
): ApiResponse<PushNotificationPreferencesResponse>
|
||||
|
||||
@PUT("/v1/wallets/{wallet_id}/notification-preferences")
|
||||
@PUT("/api/v1/notification-preferences/{wallet_id}")
|
||||
suspend fun updatePushNotificationPreferences(
|
||||
@Path("wallet_id") walletId: String,
|
||||
@Body body: PushNotificationPreferencesBody,
|
||||
): ApiResponse<Unit>
|
||||
): ApiResponse<PushNotificationPreferencesResponse>
|
||||
|
||||
// region Referral
|
||||
/** Returns referral status by [walletId] */
|
||||
|
|
@ -120,7 +126,7 @@ interface TangemTechApi {
|
|||
@GET("v1/stories/{story_id}")
|
||||
suspend fun getStoryById(@Path("story_id") storyId: String): ApiResponse<StoryContentResponse>
|
||||
|
||||
// region yield-boost promo
|
||||
// region promotions
|
||||
@GET("/v2/promotion")
|
||||
suspend fun getPromotions(
|
||||
@Query("walletId") walletId: String,
|
||||
|
|
@ -130,6 +136,11 @@ interface TangemTechApi {
|
|||
@Suppress("FunctionSignature", "TrailingCommaOnDeclarationSite")
|
||||
@GET("/v2/promotion/yield-apr-boost/status")
|
||||
suspend fun getYieldBoostStatus(@Query("walletId") walletId: String): ApiResponse<YieldBoostStatusResponse>
|
||||
|
||||
@POST("/v2/promotion/registrations")
|
||||
suspend fun createPromotionRegistration(
|
||||
@Body body: CreatePromotionRegistrationBody,
|
||||
): ApiResponse<PromotionRegistrationResponse>
|
||||
// endregion
|
||||
|
||||
// region push notifications
|
||||
|
|
@ -235,4 +246,18 @@ interface TangemTechApi {
|
|||
@GET("v1/earn/networks")
|
||||
suspend fun getEarnNetworks(@Query("type") type: String? = null): ApiResponse<EarnNetworkListResponse>
|
||||
// endregion
|
||||
|
||||
// region marketing
|
||||
@GET("api/v1/marketing/campaigns")
|
||||
suspend fun getMarketingCampaigns(
|
||||
@Query("type") type: String,
|
||||
@Query("language") language: String? = null,
|
||||
@Query("fromNetwork") fromNetwork: String? = null,
|
||||
@Query("fromContractAddress") fromContractAddress: String? = null,
|
||||
@Query("toNetwork") toNetwork: String? = null,
|
||||
@Query("toContractAddress") toContractAddress: String? = null,
|
||||
@Query("fromFiat") fromFiat: String? = null,
|
||||
@Header("If-None-Match") eTag: String? = null,
|
||||
): ApiResponse<MarketingCampaignsResponse>
|
||||
// endregion
|
||||
}
|
||||
|
|
@ -0,0 +1,31 @@
|
|||
package com.tangem.datasource.api.tangemTech.models
|
||||
|
||||
import com.squareup.moshi.Json
|
||||
import com.squareup.moshi.JsonClass
|
||||
|
||||
/**
|
||||
* Response of `GET v1/application/versions`. All fields are nullable — the backend may omit any, in
|
||||
* which case the corresponding check is skipped.
|
||||
*
|
||||
* @property minSupportedVersion app version threshold for a mandatory update: if
|
||||
* `installedVersion <= minSupportedVersion` the app must force-update (or show "update your OS"
|
||||
* when [minSupportedOSVersion] is not met). Inclusive. E.g. "5.30".
|
||||
* @property minSupportedOSVersion minimal device OS version required to install the update for the
|
||||
* [minSupportedVersion] case: if `deviceOsVersion < minSupportedOSVersion` the device can't update
|
||||
* and the "OS too old" screen is shown. Exclusive.
|
||||
* @property criticalVersion app version threshold for a critical mandatory update: if
|
||||
* `installedVersion <= criticalVersion` the app must force-update (or permanently "brick" when
|
||||
* [criticalOSVersion] is not met). Inclusive.
|
||||
* @property criticalOSVersion minimal device OS version required to install the critical update:
|
||||
* if `deviceOsVersion < criticalOSVersion` the app is bricked (update impossible). Exclusive.
|
||||
* @property latestVersion latest available app version: if `installedVersion < latestVersion`
|
||||
* an optional update is offered. Exclusive. E.g. "5.40".
|
||||
*/
|
||||
@JsonClass(generateAdapter = true)
|
||||
data class ApplicationVersionsResponse(
|
||||
@Json(name = "minSupportedVersion") val minSupportedVersion: String?,
|
||||
@Json(name = "minSupportedOSVersion") val minSupportedOSVersion: String?,
|
||||
@Json(name = "criticalVersion") val criticalVersion: String?,
|
||||
@Json(name = "criticalOSVersion") val criticalOSVersion: String?,
|
||||
@Json(name = "latestVersion") val latestVersion: String?,
|
||||
)
|
||||
|
|
@ -1,10 +0,0 @@
|
|||
package com.tangem.datasource.api.tangemTech.models
|
||||
|
||||
import com.squareup.moshi.Json
|
||||
import com.squareup.moshi.JsonClass
|
||||
|
||||
@JsonClass(generateAdapter = true)
|
||||
data class PushNotificationPreferenceState(
|
||||
@Json(name = "isEnabled") val isEnabled: Boolean,
|
||||
@Json(name = "isVisible") val isVisible: Boolean,
|
||||
)
|
||||
|
|
@ -5,10 +5,10 @@ import com.squareup.moshi.JsonClass
|
|||
|
||||
@JsonClass(generateAdapter = true)
|
||||
data class PushNotificationPreferencesBody(
|
||||
@Json(name = "transactionAlerts")
|
||||
val areTransactionAlertsEnabled: Boolean,
|
||||
@Json(name = "offersUpdates")
|
||||
val areOffersUpdatesEnabled: Boolean,
|
||||
@Json(name = "priceAlerts")
|
||||
@Json(name = "transactionEventsEnabled")
|
||||
val areTransactionEventsEnabled: Boolean,
|
||||
@Json(name = "offerUpdatesEnabled")
|
||||
val areOfferUpdatesEnabled: Boolean,
|
||||
@Json(name = "priceAlertsEnabled")
|
||||
val arePriceAlertsEnabled: Boolean,
|
||||
)
|
||||
|
|
@ -5,7 +5,7 @@ import com.squareup.moshi.JsonClass
|
|||
|
||||
@JsonClass(generateAdapter = true)
|
||||
data class PushNotificationPreferencesResponse(
|
||||
@Json(name = "transactionAlerts") val transactionAlerts: PushNotificationPreferenceState,
|
||||
@Json(name = "offersUpdates") val offersUpdates: PushNotificationPreferenceState,
|
||||
@Json(name = "priceAlerts") val priceAlerts: PushNotificationPreferenceState,
|
||||
@Json(name = "transactionEventsEnabled") val areTransactionEventsEnabled: Boolean,
|
||||
@Json(name = "offerUpdatesEnabled") val areOfferUpdatesEnabled: Boolean,
|
||||
@Json(name = "priceAlertsEnabled") val arePriceAlertsEnabled: Boolean,
|
||||
)
|
||||
|
|
@ -1,6 +1,7 @@
|
|||
package com.tangem.datasource.di
|
||||
|
||||
import com.tangem.datasource.BuildConfig
|
||||
import com.tangem.datasource.api.addressbook.AddressBookApi
|
||||
import com.tangem.datasource.api.auth.AuthApi
|
||||
import com.tangem.datasource.api.common.blockaid.BlockAidApi
|
||||
import com.tangem.datasource.api.surveysparrow.SurveySparrowApi
|
||||
|
|
@ -18,6 +19,7 @@ import com.tangem.datasource.api.news.NewsApi
|
|||
import com.tangem.datasource.api.onramp.OnrampApi
|
||||
import com.tangem.datasource.api.ethpool.P2PEthPoolApi
|
||||
import com.tangem.datasource.api.gasless.GaslessTxServiceApi
|
||||
import com.tangem.datasource.api.gasless.GaslessTxServiceApiV2
|
||||
import com.tangem.datasource.api.pay.TangemPayApi
|
||||
import com.tangem.datasource.api.pay.TangemPayAuthApi
|
||||
import com.tangem.datasource.api.stakekit.StakeKitApi
|
||||
|
|
@ -117,6 +119,16 @@ internal object NetworkModule {
|
|||
)
|
||||
}
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
fun provideAddressBookApi(retrofitApiBuilder: RetrofitApiBuilder): AddressBookApi {
|
||||
return retrofitApiBuilder.build(
|
||||
apiConfigId = ApiConfig.ID.TangemTech,
|
||||
applyTimeoutAnnotations = false,
|
||||
sessionAuth = false,
|
||||
)
|
||||
}
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
fun provideYieldSupplyApi(retrofitApiBuilder: RetrofitApiBuilder): YieldSupplyApi {
|
||||
|
|
@ -252,4 +264,20 @@ internal object NetworkModule {
|
|||
),
|
||||
)
|
||||
}
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
fun provideGaslessTxServiceApiV2(retrofitApiBuilder: RetrofitApiBuilder): GaslessTxServiceApiV2 {
|
||||
return retrofitApiBuilder.build(
|
||||
apiConfigId = ApiConfig.ID.GaslessTxService,
|
||||
applyTimeoutAnnotations = false,
|
||||
sessionAuth = false,
|
||||
timeouts = Timeouts(
|
||||
callTimeoutSeconds = TIMEOUT_60_SECONDS,
|
||||
connectTimeoutSeconds = TIMEOUT_60_SECONDS,
|
||||
readTimeoutSeconds = TIMEOUT_60_SECONDS,
|
||||
writeTimeoutSeconds = TIMEOUT_60_SECONDS,
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,32 @@
|
|||
package com.tangem.datasource.di
|
||||
|
||||
import com.tangem.datasource.api.promotion.models.PromotionsResponse
|
||||
import com.tangem.datasource.api.tangemTech.TangemTechApi
|
||||
import com.tangem.datasource.local.datastore.RuntimeSharedStore
|
||||
import com.tangem.datasource.local.promotion.DefaultPromotionsSupplier
|
||||
import com.tangem.datasource.local.promotion.PromotionsSupplier
|
||||
import com.tangem.domain.models.wallet.UserWalletId
|
||||
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
|
||||
import dagger.Module
|
||||
import dagger.Provides
|
||||
import dagger.hilt.InstallIn
|
||||
import dagger.hilt.components.SingletonComponent
|
||||
import javax.inject.Singleton
|
||||
|
||||
@Module
|
||||
@InstallIn(SingletonComponent::class)
|
||||
object PromotionModule {
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
fun providePromotionsSupplier(
|
||||
tangemApi: TangemTechApi,
|
||||
dispatchers: CoroutineDispatcherProvider,
|
||||
): PromotionsSupplier {
|
||||
return DefaultPromotionsSupplier(
|
||||
tangemApi = tangemApi,
|
||||
store = RuntimeSharedStore<Map<UserWalletId, PromotionsResponse>>(),
|
||||
dispatchers = dispatchers,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -1,14 +1,27 @@
|
|||
package com.tangem.datasource.di
|
||||
|
||||
import android.content.Context
|
||||
import androidx.datastore.core.DataStoreFactory
|
||||
import androidx.datastore.core.handlers.ReplaceFileCorruptionHandler
|
||||
import androidx.datastore.dataStoreFile
|
||||
import com.tangem.datasource.local.datastore.RuntimeDataStore
|
||||
import com.tangem.datasource.local.txhistory.DefaultTxHistoryItemsStore
|
||||
import com.tangem.datasource.local.txhistory.TxHistoryItemsStore
|
||||
import com.tangem.datasource.local.visa.DefaultTangemPayTxHistoryItemsStore
|
||||
import com.tangem.datasource.local.visa.TangemPayTxHistoryItemsStore
|
||||
import com.tangem.datasource.local.visa.entity.TangemPayTxHistoryItemDM
|
||||
import com.tangem.datasource.local.visa.entity.TangemPayTxHistoryItemToDMConverter
|
||||
import com.tangem.datasource.local.visa.entity.TangemPayTxHistoryItemToDomainConverter
|
||||
import com.tangem.datasource.utils.KotlinxDataStoreSerializer
|
||||
import com.tangem.utils.coroutines.AppCoroutineScope
|
||||
import dagger.Module
|
||||
import dagger.Provides
|
||||
import dagger.hilt.InstallIn
|
||||
import dagger.hilt.android.qualifiers.ApplicationContext
|
||||
import dagger.hilt.components.SingletonComponent
|
||||
import kotlinx.serialization.builtins.ListSerializer
|
||||
import kotlinx.serialization.builtins.MapSerializer
|
||||
import kotlinx.serialization.builtins.serializer
|
||||
import javax.inject.Singleton
|
||||
|
||||
@Module
|
||||
|
|
@ -25,9 +38,34 @@ internal object TxHistoryItemsStoreModule {
|
|||
|
||||
@Provides
|
||||
@Singleton
|
||||
fun provideTangemPayTxHistoryItemsStore(): TangemPayTxHistoryItemsStore {
|
||||
fun provideTangemPayTxHistoryItemsStore(
|
||||
@ApplicationContext context: Context,
|
||||
appScope: AppCoroutineScope,
|
||||
toDMConverter: TangemPayTxHistoryItemToDMConverter,
|
||||
toDomainConverter: TangemPayTxHistoryItemToDomainConverter,
|
||||
): TangemPayTxHistoryItemsStore {
|
||||
return DefaultTangemPayTxHistoryItemsStore(
|
||||
dataStore = RuntimeDataStore(),
|
||||
dataStore = DataStoreFactory.create(
|
||||
serializer = KotlinxDataStoreSerializer(
|
||||
defaultValue = emptyMap(),
|
||||
serializer = MapSerializer(
|
||||
keySerializer = String.serializer(),
|
||||
valueSerializer = MapSerializer(
|
||||
keySerializer = String.serializer(),
|
||||
valueSerializer = ListSerializer(TangemPayTxHistoryItemDM.serializer()),
|
||||
),
|
||||
),
|
||||
// TangemPayTxHistoryItemDM.Collateral declares a `type` field that would clash with the
|
||||
// default kotlinx polymorphic class discriminator ("type"), so persist sealed subtypes
|
||||
// under a non-conflicting discriminator key.
|
||||
json = KotlinxDataStoreSerializer.jsonBuilder { classDiscriminator = "__type" },
|
||||
),
|
||||
corruptionHandler = ReplaceFileCorruptionHandler { emptyMap() },
|
||||
produceFile = { context.dataStoreFile(fileName = "tangem_pay_tx_history") },
|
||||
scope = appScope,
|
||||
),
|
||||
toDMConverter = toDMConverter,
|
||||
toDomainConverter = toDomainConverter,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -5,6 +5,8 @@ import androidx.room.Room
|
|||
import com.tangem.datasource.local.txhistory.db.TxHistoryDatabase
|
||||
import com.tangem.datasource.local.txhistory.db.dao.ExpressHistoryDao
|
||||
import com.tangem.datasource.local.txhistory.db.dao.ExpressSyncStateDao
|
||||
import com.tangem.datasource.local.txhistory.db.dao.HistoryIndexDao
|
||||
import com.tangem.datasource.local.txhistory.db.dao.TokenInfoDao
|
||||
import dagger.Module
|
||||
import dagger.Provides
|
||||
import dagger.hilt.InstallIn
|
||||
|
|
@ -37,5 +39,11 @@ internal interface TxHistoryModule {
|
|||
|
||||
@Provides
|
||||
fun provideSyncStateDao(database: TxHistoryDatabase): ExpressSyncStateDao = database.syncStateDao()
|
||||
|
||||
@Provides
|
||||
fun provideTokenInfoDao(database: TxHistoryDatabase): TokenInfoDao = database.tokenInfoDao()
|
||||
|
||||
@Provides
|
||||
fun provideHistoryIndexDao(database: TxHistoryDatabase): HistoryIndexDao = database.historyIndexDao()
|
||||
}
|
||||
}
|
||||
|
|
@ -128,8 +128,13 @@ internal class RetrofitApiBuilder @Inject constructor(
|
|||
// feature toggle is OFF we skip installing the hooks entirely (avoids wiring up DPoP
|
||||
// header generation and 401 retry logic on builds where auth isn't live yet).
|
||||
if (condition && isBackendAuthEnabled.get()) {
|
||||
addInterceptor(sessionAuthInterceptor.get())
|
||||
authenticator(sessionAuthenticator.get())
|
||||
// Resolve the providers lazily, at request time, instead of eagerly here. The session
|
||||
// authenticator depends (via SessionTokenRefresher) back on AuthApi, so calling `.get()`
|
||||
// while AuthApi is still being built would recurse into provideAuthApi → applySessionAuth
|
||||
// → `.get()` → … and overflow the stack. Deferring `.get()` to the first HTTP call lets
|
||||
// AuthApi finish constructing (and get cached) first, breaking the cycle.
|
||||
addInterceptor(Interceptor { chain -> sessionAuthInterceptor.get().intercept(chain) })
|
||||
authenticator { route, response -> sessionAuthenticator.get().authenticate(route, response) }
|
||||
}
|
||||
|
||||
return this
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
package com.tangem.datasource.local.appsflyer
|
||||
|
||||
import com.tangem.domain.wallets.models.AppsFlyerConversionData
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
|
||||
interface AppsFlyerStore {
|
||||
|
||||
|
|
@ -14,20 +15,11 @@ interface AppsFlyerStore {
|
|||
|
||||
suspend fun storeUIDIfAbsent(value: String)
|
||||
|
||||
suspend fun getDeeplink(source: AppsFlyerDeeplinkSource): String?
|
||||
fun observeNavigationDeeplink(): Flow<String?>
|
||||
|
||||
suspend fun storeDeeplink(source: AppsFlyerDeeplinkSource, deeplink: String)
|
||||
suspend fun getNavigationDeeplink(): String?
|
||||
|
||||
suspend fun clearDeeplink(source: AppsFlyerDeeplinkSource)
|
||||
}
|
||||
suspend fun storeNavigationDeeplink(deepLinkValue: String)
|
||||
|
||||
enum class AppsFlyerDeeplinkSource {
|
||||
TangemPayHotWalletOnboarding,
|
||||
Referral,
|
||||
;
|
||||
|
||||
fun toStoreKey() = when (this) {
|
||||
TangemPayHotWalletOnboarding -> "tangem_pay_hot_wallet_onboarding"
|
||||
Referral -> "referral"
|
||||
}
|
||||
suspend fun clearNavigationDeeplink()
|
||||
}
|
||||
|
|
@ -9,6 +9,9 @@ import com.tangem.datasource.local.preferences.utils.getSyncOrNull
|
|||
import com.tangem.datasource.local.preferences.utils.storeObject
|
||||
import com.tangem.domain.wallets.models.AppsFlyerConversionData
|
||||
import com.tangem.utils.logging.TangemLogger
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.distinctUntilChanged
|
||||
import kotlinx.coroutines.flow.map
|
||||
|
||||
internal class DefaultAppsFlyerStore(
|
||||
private val appPreferencesStore: AppPreferencesStore,
|
||||
|
|
@ -56,24 +59,28 @@ internal class DefaultAppsFlyerStore(
|
|||
}
|
||||
}
|
||||
|
||||
override suspend fun getDeeplink(source: AppsFlyerDeeplinkSource): String? =
|
||||
appPreferencesStore.getSyncOrNull(stringPreferencesKey(source.toStoreKey()))
|
||||
override fun observeNavigationDeeplink(): Flow<String?> = appPreferencesStore.data
|
||||
.map { preferences -> preferences[NAVIGATION_DEEPLINK_KEY] }
|
||||
.distinctUntilChanged()
|
||||
|
||||
override suspend fun storeDeeplink(source: AppsFlyerDeeplinkSource, deeplink: String) {
|
||||
override suspend fun getNavigationDeeplink(): String? = appPreferencesStore.getSyncOrNull(NAVIGATION_DEEPLINK_KEY)
|
||||
|
||||
override suspend fun storeNavigationDeeplink(deepLinkValue: String) {
|
||||
appPreferencesStore.editData { preferences ->
|
||||
preferences[stringPreferencesKey(source.toStoreKey())] = deeplink
|
||||
preferences[NAVIGATION_DEEPLINK_KEY] = deepLinkValue
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun clearDeeplink(source: AppsFlyerDeeplinkSource) {
|
||||
override suspend fun clearNavigationDeeplink() {
|
||||
appPreferencesStore.editData { preferences ->
|
||||
preferences.remove(stringPreferencesKey(source.toStoreKey()))
|
||||
preferences.remove(NAVIGATION_DEEPLINK_KEY)
|
||||
}
|
||||
}
|
||||
|
||||
private companion object {
|
||||
val UID_KEY = stringPreferencesKey("APPS_FLYER_UID")
|
||||
val CONVERSION_DATA_KEY = stringPreferencesKey("APPS_FLYER_CONVERSION_DATA")
|
||||
val NAVIGATION_DEEPLINK_KEY = stringPreferencesKey("appsflyer_navigation_deeplink")
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -7,13 +7,13 @@ import com.tangem.datasource.local.txhistory.db.entity.express.ExpressOnrampEnti
|
|||
|
||||
/**
|
||||
* Maps API history items into their persisted [androidx.room.Entity] representations.
|
||||
*
|
||||
* @param ownerAddress address the history was requested for. Stored as the query key.
|
||||
*/
|
||||
fun ExchangeItemResponse.toEntity(ownerAddress: String): ExpressExchangeEntity {
|
||||
fun ExchangeItemResponse.toEntity(): ExpressExchangeEntity? {
|
||||
// Items with no fromAddress (very old app versions didn't send it) can't be found by the outgoing-swap
|
||||
// lookup, which keys on from_address — drop them. Such items are effectively nonexistent nowadays.
|
||||
if (fromAddress == null) return null
|
||||
return ExpressExchangeEntity(
|
||||
txId = txId,
|
||||
ownerAddress = ownerAddress,
|
||||
providerId = providerId,
|
||||
fromAddress = fromAddress,
|
||||
payinAddress = payinAddress,
|
||||
|
|
@ -30,7 +30,7 @@ fun ExchangeItemResponse.toEntity(ownerAddress: String): ExpressExchangeEntity {
|
|||
refundNetwork = refundNetwork,
|
||||
refundContractAddress = refundContractAddress,
|
||||
createdAt = createdAt,
|
||||
updatedAt = ""/*updatedAt*/, // todo txHistory uncomment
|
||||
updatedAt = updatedAt,
|
||||
payTill = payTill,
|
||||
averageDuration = averageDuration,
|
||||
from = ExpressExchangeEntity.AssetEmbedded(
|
||||
|
|
@ -50,10 +50,9 @@ fun ExchangeItemResponse.toEntity(ownerAddress: String): ExpressExchangeEntity {
|
|||
)
|
||||
}
|
||||
|
||||
fun OnrampItemResponse.toEntity(ownerAddress: String): ExpressOnrampEntity {
|
||||
fun OnrampItemResponse.toEntity(): ExpressOnrampEntity {
|
||||
return ExpressOnrampEntity(
|
||||
txId = txId,
|
||||
ownerAddress = ownerAddress,
|
||||
providerId = providerId,
|
||||
payoutAddress = payoutAddress,
|
||||
status = status,
|
||||
|
|
@ -62,7 +61,7 @@ fun OnrampItemResponse.toEntity(ownerAddress: String): ExpressOnrampEntity {
|
|||
externalTxUrl = externalTxUrl,
|
||||
payoutHash = payoutHash,
|
||||
createdAt = createdAt,
|
||||
updatedAt = ""/*updatedAt*/, // todo txHistory uncomment,
|
||||
updatedAt = updatedAt,
|
||||
fromCurrencyCode = fromCurrencyCode,
|
||||
fromAmount = fromAmount,
|
||||
fromPrecision = fromPrecision,
|
||||
|
|
|
|||
|
|
@ -0,0 +1,23 @@
|
|||
package com.tangem.datasource.local.converter
|
||||
|
||||
import com.tangem.datasource.api.onramp.models.response.model.OnrampCountryDTO
|
||||
import com.tangem.datasource.local.txhistory.db.entity.express.OnrampCountryEntity
|
||||
|
||||
/** Maps an [OnrampCountryDTO] API response into its persisted [OnrampCountryEntity]. */
|
||||
fun OnrampCountryDTO.toEntity(): OnrampCountryEntity {
|
||||
return OnrampCountryEntity(
|
||||
code = code,
|
||||
name = name,
|
||||
image = image,
|
||||
alpha3 = alpha3,
|
||||
continent = continent,
|
||||
isOnrampAvailable = onrampAvailable,
|
||||
defaultCurrency = OnrampCountryEntity.CurrencyEmbedded(
|
||||
name = defaultCurrency.name,
|
||||
code = defaultCurrency.code,
|
||||
image = defaultCurrency.image,
|
||||
precision = defaultCurrency.precision,
|
||||
unit = defaultCurrency.unit ?: defaultCurrency.code,
|
||||
),
|
||||
)
|
||||
}
|
||||
|
|
@ -33,6 +33,16 @@ object PreferencesKeys {
|
|||
|
||||
val APP_LAUNCH_COUNT_KEY by lazy { intPreferencesKey(name = "launchCount") }
|
||||
|
||||
val LAST_OPTIONAL_UPDATE_SHOWN_VERSION_KEY by lazy {
|
||||
stringPreferencesKey(name = "lastOptionalUpdateShownVersion")
|
||||
}
|
||||
|
||||
val LAST_OPTIONAL_UPDATE_SHOWN_AT_KEY by lazy { longPreferencesKey(name = "lastOptionalUpdateShownAt") }
|
||||
|
||||
val CACHED_APP_VERSIONS_KEY by lazy { stringPreferencesKey(name = "cachedApplicationVersions") }
|
||||
|
||||
val CACHED_APP_VERSIONS_AT_KEY by lazy { longPreferencesKey(name = "cachedApplicationVersionsAt") }
|
||||
|
||||
val SHOW_RATING_DIALOG_AT_LAUNCH_COUNT_KEY by lazy { intPreferencesKey(name = "showRatingDialogAtLaunchCount") }
|
||||
|
||||
val FUNDS_FOUND_DATE_KEY by lazy { longPreferencesKey(name = "fundsFoundDate") }
|
||||
|
|
@ -41,6 +51,8 @@ object PreferencesKeys {
|
|||
|
||||
val USED_CARDS_INFO_KEY by lazy { stringPreferencesKey(name = "usedCardsInfo_v2") }
|
||||
|
||||
val USEDESK_CLIENT_ID_KEY by lazy { stringPreferencesKey(name = "usedeskClientId") }
|
||||
|
||||
val APP_THEME_MODE_KEY by lazy { stringPreferencesKey(name = "appThemeMode") }
|
||||
|
||||
val SELECTED_APP_CURRENCY_KEY by lazy { stringPreferencesKey(name = "selectedAppCurrency") }
|
||||
|
|
@ -105,6 +117,9 @@ object PreferencesKeys {
|
|||
|
||||
val IS_DEVICE_REGISTERED_KEY by lazy { booleanPreferencesKey(name = "isDeviceRegistered") }
|
||||
|
||||
/** Base64 `UserWalletId`s already registered with the Tangem Auth Service (`/auth/wallet`). */
|
||||
val REGISTERED_WALLET_IDS_KEY by lazy { stringSetPreferencesKey(name = "registeredWalletIds") }
|
||||
|
||||
val WAS_LOG_FILE_CLEARED by lazy { booleanPreferencesKey(name = "wasLogFileCleared") }
|
||||
|
||||
val SEED_FIRST_NOTIFICATION_SHOW_TIME by lazy { longPreferencesKey("seedFirstNotificationTime") }
|
||||
|
|
@ -129,6 +144,8 @@ object PreferencesKeys {
|
|||
|
||||
val PENDING_ASSETS_DISCOVERY_KEY by lazy { stringPreferencesKey(name = "pendingAssetsDiscovery") }
|
||||
|
||||
val PROMO_ENROLLMENTS_KEY by lazy { stringPreferencesKey(name = "promoEnrollments") }
|
||||
|
||||
// region Notifications
|
||||
val NOTIFICATIONS_APPLICATION_ID_KEY by lazy { stringPreferencesKey(name = "notificationsApplicationId") }
|
||||
|
||||
|
|
@ -146,6 +163,10 @@ object PreferencesKeys {
|
|||
)
|
||||
}
|
||||
|
||||
val PUSH_NOTIFICATION_FIRST_ACTIVATION_DONE_WALLET_IDS_KEY by lazy {
|
||||
stringSetPreferencesKey(name = "pushNotificationFirstActivationDoneWalletIds")
|
||||
}
|
||||
|
||||
val TRON_NETWORK_FEE_NOTIFICATION_SHOW_COUNT_KEY by lazy {
|
||||
intPreferencesKey(name = "tronNetworkFeeNotificationShowCount")
|
||||
}
|
||||
|
|
@ -154,7 +175,7 @@ object PreferencesKeys {
|
|||
val TANGEM_PAY_ACTIVE_WITHDRAW_ORDERS_KEY by lazy {
|
||||
stringPreferencesKey(name = "tangemPayActiveWithdrawOrdersKey")
|
||||
}
|
||||
val TANGEM_PAY_ELIGIBILITY_KEY by lazy { stringSetPreferencesKey(name = "tangemPayEligibilityList") }
|
||||
val TANGEM_PAY_ELIGIBILITY_KEY by lazy { stringSetPreferencesKey(name = "tangemPayEligibilityListV2") }
|
||||
|
||||
fun getShouldShowNotificationKey(key: String) = booleanPreferencesKey("showShowNotificationUM_$key")
|
||||
// endregion
|
||||
|
|
@ -189,6 +210,9 @@ object PreferencesKeys {
|
|||
fun getTangemPayOrderIdKey(customerWalletAddress: String) =
|
||||
stringPreferencesKey("tangem_pay_order_id_key_$customerWalletAddress")
|
||||
|
||||
fun getTangemPayVirtualAccountOrderIdKey(customerWalletAddress: String) =
|
||||
stringPreferencesKey("tangem_pay_va_order_id_key_$customerWalletAddress")
|
||||
|
||||
fun getTangemPayCustomerWalletAddressKey(userWalletId: UserWalletId) =
|
||||
stringPreferencesKey("tangem_pay_customer_wallet_address_key_${userWalletId.stringValue}")
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,27 @@
|
|||
package com.tangem.datasource.local.promotion
|
||||
|
||||
import com.tangem.datasource.api.common.response.getOrThrow
|
||||
import com.tangem.datasource.api.promotion.models.PromotionsResponse
|
||||
import com.tangem.datasource.api.tangemTech.TangemTechApi
|
||||
import com.tangem.datasource.local.datastore.RuntimeSharedStore
|
||||
import com.tangem.domain.models.wallet.UserWalletId
|
||||
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
|
||||
import kotlinx.coroutines.withContext
|
||||
|
||||
internal class DefaultPromotionsSupplier(
|
||||
private val tangemApi: TangemTechApi,
|
||||
private val store: RuntimeSharedStore<Map<UserWalletId, PromotionsResponse>>,
|
||||
private val dispatchers: CoroutineDispatcherProvider,
|
||||
) : PromotionsSupplier {
|
||||
|
||||
override suspend fun getPromotions(userWalletId: UserWalletId, forceRefresh: Boolean): PromotionsResponse {
|
||||
if (!forceRefresh) {
|
||||
store.getSyncOrNull()?.get(userWalletId)?.let { return it }
|
||||
}
|
||||
val fresh = withContext(dispatchers.io) {
|
||||
tangemApi.getPromotions(walletId = userWalletId.stringValue).getOrThrow()
|
||||
}
|
||||
store.update(emptyMap()) { it + (userWalletId to fresh) }
|
||||
return fresh
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,15 @@
|
|||
package com.tangem.datasource.local.promotion
|
||||
|
||||
import com.tangem.datasource.api.promotion.models.PromotionsResponse
|
||||
import com.tangem.domain.models.wallet.UserWalletId
|
||||
|
||||
/**
|
||||
* Shared cache-first fetch of GET /v2/promotion. Keeps one in-memory entry per [UserWalletId]:
|
||||
* a non-forced call returns the cached response when present, otherwise it fetches. A fetch failure
|
||||
* propagates to the caller (no stale-cache fallback), so the caller decides how to handle it.
|
||||
*/
|
||||
interface PromotionsSupplier {
|
||||
|
||||
@Throws(Exception::class)
|
||||
suspend fun getPromotions(userWalletId: UserWalletId, forceRefresh: Boolean = false): PromotionsResponse
|
||||
}
|
||||
|
|
@ -4,10 +4,15 @@ import androidx.room.Database
|
|||
import androidx.room.RoomDatabase
|
||||
import com.tangem.datasource.local.txhistory.db.dao.ExpressHistoryDao
|
||||
import com.tangem.datasource.local.txhistory.db.dao.ExpressSyncStateDao
|
||||
import com.tangem.datasource.local.txhistory.db.dao.HistoryIndexDao
|
||||
import com.tangem.datasource.local.txhistory.db.dao.TokenInfoDao
|
||||
import com.tangem.datasource.local.txhistory.db.entity.HistoryIndexEntity
|
||||
import com.tangem.datasource.local.txhistory.db.entity.express.ExpressSyncStateEntity
|
||||
import com.tangem.datasource.local.txhistory.db.entity.express.ExpressExchangeEntity
|
||||
import com.tangem.datasource.local.txhistory.db.entity.express.ExpressOnrampEntity
|
||||
import com.tangem.datasource.local.txhistory.db.entity.express.ExpressProviderEntity
|
||||
import com.tangem.datasource.local.txhistory.db.entity.express.OnrampCountryEntity
|
||||
import com.tangem.datasource.local.txhistory.db.entity.express.TokenInfoEntity
|
||||
|
||||
@Database(
|
||||
version = 1,
|
||||
|
|
@ -16,6 +21,9 @@ import com.tangem.datasource.local.txhistory.db.entity.express.ExpressProviderEn
|
|||
ExpressExchangeEntity::class,
|
||||
ExpressOnrampEntity::class,
|
||||
ExpressSyncStateEntity::class,
|
||||
OnrampCountryEntity::class,
|
||||
TokenInfoEntity::class,
|
||||
HistoryIndexEntity::class,
|
||||
],
|
||||
)
|
||||
abstract class TxHistoryDatabase : RoomDatabase() {
|
||||
|
|
@ -23,4 +31,8 @@ abstract class TxHistoryDatabase : RoomDatabase() {
|
|||
abstract fun expressHistoryDao(): ExpressHistoryDao
|
||||
|
||||
abstract fun syncStateDao(): ExpressSyncStateDao
|
||||
|
||||
abstract fun tokenInfoDao(): TokenInfoDao
|
||||
|
||||
abstract fun historyIndexDao(): HistoryIndexDao
|
||||
}
|
||||
|
|
@ -8,6 +8,7 @@ import androidx.room.Query
|
|||
import com.tangem.datasource.local.txhistory.db.entity.express.ExpressExchangeEntity
|
||||
import com.tangem.datasource.local.txhistory.db.entity.express.ExpressOnrampEntity
|
||||
import com.tangem.datasource.local.txhistory.db.entity.express.ExpressProviderEntity
|
||||
import com.tangem.datasource.local.txhistory.db.entity.express.OnrampCountryEntity
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
|
||||
@Dao
|
||||
|
|
@ -22,15 +23,22 @@ interface ExpressHistoryDao {
|
|||
@Insert(onConflict = OnConflictStrategy.REPLACE)
|
||||
suspend fun upsertOnramps(items: List<ExpressOnrampEntity>)
|
||||
|
||||
@Insert(onConflict = OnConflictStrategy.REPLACE)
|
||||
suspend fun upsertCountries(items: List<OnrampCountryEntity>)
|
||||
|
||||
/**
|
||||
* All persisted providers keyed by [ExpressProviderEntity.id]
|
||||
*/
|
||||
@Query("SELECT * FROM express_provider")
|
||||
fun getProvidersById(): Flow<Map<@MapColumn(columnName = "id") String, ExpressProviderEntity>>
|
||||
|
||||
/** All persisted onramp countries keyed by [OnrampCountryEntity.code]. */
|
||||
@Query("SELECT * FROM onramp_country")
|
||||
fun getCountriesByCode(): Flow<Map<@MapColumn(columnName = "code") String, OnrampCountryEntity>>
|
||||
|
||||
/**
|
||||
* Outgoing swaps: the viewed currency is the swap's `from` side, so the row is stored under this
|
||||
* address ([ExpressExchangeEntity.ownerAddress] == fromAddress). Join to on-chain by `payin_hash`.
|
||||
* Outgoing swaps: the viewed currency is the swap's `from` side, so the row is looked up by its `from_address`.
|
||||
* Join to on-chain by `payin_hash`.
|
||||
*
|
||||
|
||||
* loading the whole table; [activeStatuses] keeps in-progress deals visible even outside the window.
|
||||
|
|
@ -38,7 +46,7 @@ interface ExpressHistoryDao {
|
|||
@Query(
|
||||
"""
|
||||
SELECT * FROM express_exchange
|
||||
WHERE owner_address = :ownerAddress
|
||||
WHERE from_address = :fromAddress
|
||||
AND from_network = :network
|
||||
AND from_contract_address = :contract
|
||||
AND (created_at >= :fromCreatedAtIso OR status IN (:activeStatuses))
|
||||
|
|
@ -46,7 +54,7 @@ interface ExpressHistoryDao {
|
|||
""",
|
||||
)
|
||||
fun observeOutgoingSwaps(
|
||||
ownerAddress: String,
|
||||
fromAddress: String,
|
||||
network: String,
|
||||
contract: String,
|
||||
fromCreatedAtIso: String,
|
||||
|
|
@ -54,20 +62,21 @@ interface ExpressHistoryDao {
|
|||
): Flow<List<ExpressExchangeEntity>>
|
||||
|
||||
/**
|
||||
* Incoming swaps: the viewed currency is the swap's `to` side. Such a deal was initiated from a
|
||||
* different coin, so the row is stored under that coin's `owner_address` — hence this query is
|
||||
* cross-owner, matched by the `to` asset. Join to on-chain by `payout_hash`.
|
||||
* Incoming swaps: the viewed currency is the swap's `to` side, so the row is looked up by its `payout_address`
|
||||
* (where the target assets landed = this currency's address). Join to on-chain by `payout_hash`.
|
||||
*/
|
||||
@Query(
|
||||
"""
|
||||
SELECT * FROM express_exchange
|
||||
WHERE to_network = :network
|
||||
WHERE payout_address = :payoutAddress
|
||||
AND to_network = :network
|
||||
AND to_contract_address = :contract
|
||||
AND (created_at >= :fromCreatedAtIso OR status IN (:activeStatuses))
|
||||
ORDER BY created_at DESC
|
||||
""",
|
||||
)
|
||||
fun observeIncomingSwaps(
|
||||
payoutAddress: String,
|
||||
network: String,
|
||||
contract: String,
|
||||
fromCreatedAtIso: String,
|
||||
|
|
@ -75,12 +84,12 @@ interface ExpressHistoryDao {
|
|||
): Flow<List<ExpressExchangeEntity>>
|
||||
|
||||
/**
|
||||
* Onramp is always incoming: [ExpressOnrampEntity.ownerAddress] == payoutAddress. Join by `payout_hash`.
|
||||
* Onramp is always incoming, looked up by its `payout_address`. Join by `payout_hash`.
|
||||
*/
|
||||
@Query(
|
||||
"""
|
||||
SELECT * FROM express_onramp
|
||||
WHERE owner_address = :ownerAddress
|
||||
WHERE payout_address = :payoutAddress
|
||||
AND to_network = :network
|
||||
AND to_contract_address = :contract
|
||||
AND (created_at >= :fromCreatedAtIso OR status IN (:activeStatuses))
|
||||
|
|
@ -88,7 +97,7 @@ interface ExpressHistoryDao {
|
|||
""",
|
||||
)
|
||||
fun observeIncomingOnramps(
|
||||
ownerAddress: String,
|
||||
payoutAddress: String,
|
||||
network: String,
|
||||
contract: String,
|
||||
fromCreatedAtIso: String,
|
||||
|
|
|
|||
|
|
@ -0,0 +1,74 @@
|
|||
package com.tangem.datasource.local.txhistory.db.dao
|
||||
|
||||
import androidx.room.Dao
|
||||
import androidx.room.Insert
|
||||
import androidx.room.OnConflictStrategy
|
||||
import androidx.room.Query
|
||||
import com.tangem.datasource.local.txhistory.db.entity.HistoryIndexEntity
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
|
||||
@Dao
|
||||
interface HistoryIndexDao {
|
||||
|
||||
@Insert(onConflict = OnConflictStrategy.REPLACE)
|
||||
suspend fun upsert(items: List<HistoryIndexEntity>)
|
||||
|
||||
@Insert(onConflict = OnConflictStrategy.REPLACE)
|
||||
suspend fun upsert(item: HistoryIndexEntity)
|
||||
|
||||
/**
|
||||
* One page of the unified timeline for [addresses] (usually one, but some token-details screens span several),
|
||||
* newest first, **one row per operation**. An operation indexed under several of the queried [addresses] (e.g. a
|
||||
* swap under both its from- and payout-address) is collapsed via `GROUP BY (type, entity_id)`, keeping its
|
||||
* most-recent occurrence (SQLite bare-column rule under a single `MAX`). This both de-duplicates the timeline and
|
||||
* makes the keyset key (`sort_time_millis`, `entity_id`) unique, so rows are neither skipped nor duplicated across
|
||||
* page boundaries even when several addresses are queried.
|
||||
*
|
||||
* The cursor is the (`sort_time_millis`, `entity_id`) of the last (oldest) row of the previous page — pass both
|
||||
* [cursorSortTimeMillis] and [cursorEntityId], or `null` for the first page.
|
||||
*/
|
||||
@Query(
|
||||
"""
|
||||
SELECT type, entity_id, address, MAX(sort_time_millis) AS sort_time_millis FROM history_index
|
||||
WHERE address IN (:addresses)
|
||||
GROUP BY type, entity_id
|
||||
HAVING (
|
||||
:cursorSortTimeMillis IS NULL
|
||||
OR MAX(sort_time_millis) < :cursorSortTimeMillis
|
||||
OR (MAX(sort_time_millis) = :cursorSortTimeMillis AND entity_id < :cursorEntityId)
|
||||
)
|
||||
ORDER BY sort_time_millis DESC, entity_id DESC
|
||||
LIMIT :limit
|
||||
""",
|
||||
)
|
||||
fun observePage(
|
||||
addresses: List<String>,
|
||||
cursorSortTimeMillis: Long?,
|
||||
cursorEntityId: String?,
|
||||
limit: Int,
|
||||
): Flow<List<HistoryIndexEntity>>
|
||||
|
||||
fun observePage(addresses: List<String>, cursor: Cursor?, limit: Int): Flow<List<HistoryIndexEntity>> = observePage(
|
||||
addresses = addresses,
|
||||
cursorSortTimeMillis = cursor?.sortTimeMillis,
|
||||
cursorEntityId = cursor?.entityId,
|
||||
limit = limit,
|
||||
)
|
||||
|
||||
/**
|
||||
* Keyset cursor for [observePage]: the (sortTimeMillis, entityId) of the last (oldest) row of a page. Build it from
|
||||
* the previous page's last row to fetch the next page; a `null` cursor requests the first page.
|
||||
*/
|
||||
data class Cursor(
|
||||
val sortTimeMillis: Long,
|
||||
val entityId: String,
|
||||
) {
|
||||
|
||||
companion object {
|
||||
fun from(lastRow: HistoryIndexEntity): Cursor = Cursor(
|
||||
sortTimeMillis = lastRow.sortTimeMillis,
|
||||
entityId = lastRow.entityId,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,33 @@
|
|||
package com.tangem.datasource.local.txhistory.db.dao
|
||||
|
||||
import androidx.room.Dao
|
||||
import androidx.room.Insert
|
||||
import androidx.room.OnConflictStrategy
|
||||
import androidx.room.Query
|
||||
import com.tangem.datasource.local.txhistory.db.entity.express.TokenInfoEntity
|
||||
|
||||
@Dao
|
||||
interface TokenInfoDao {
|
||||
|
||||
@Insert(onConflict = OnConflictStrategy.REPLACE)
|
||||
suspend fun upsert(items: List<TokenInfoEntity>)
|
||||
|
||||
/**
|
||||
* Cached tokens for the given networks/contracts. Filters each column independently, so the result is a
|
||||
* cross-product superset of the `(networkId, contractAddress)` pairs — the caller must match exact pairs.
|
||||
* Contract match is case-insensitive; pass [minUpdatedAt] = `now - ttl` to drop stale rows.
|
||||
*/
|
||||
@Query(
|
||||
"""
|
||||
SELECT * FROM token_info
|
||||
WHERE network_id IN (:networkIds)
|
||||
AND contract_address COLLATE NOCASE IN (:contractAddresses)
|
||||
AND updated_at >= :minUpdatedAt
|
||||
""",
|
||||
)
|
||||
suspend fun getCached(
|
||||
networkIds: Collection<String>,
|
||||
contractAddresses: Collection<String>,
|
||||
minUpdatedAt: Long = 0,
|
||||
): List<TokenInfoEntity>
|
||||
}
|
||||
|
|
@ -0,0 +1,42 @@
|
|||
package com.tangem.datasource.local.txhistory.db.entity
|
||||
|
||||
import androidx.room.ColumnInfo
|
||||
import androidx.room.Entity
|
||||
import androidx.room.Index
|
||||
|
||||
/**
|
||||
* Unified pagination index over the local history sources.
|
||||
*/
|
||||
@Entity(
|
||||
tableName = "history_index",
|
||||
// A single row (type + entity_id) may be shown under more than one address (e.g. a swap between two owned tokens
|
||||
// appears under both), so the address is part of the identity.
|
||||
primaryKeys = ["type", "entity_id", "address"],
|
||||
indices = [
|
||||
// Newest-first cursor scan within an address: address filter + sort-time ordering + entity_id tie-break.
|
||||
Index(value = ["address", "sort_time_millis", "entity_id"]),
|
||||
],
|
||||
)
|
||||
data class HistoryIndexEntity(
|
||||
|
||||
@ColumnInfo(name = "type")
|
||||
val type: String,
|
||||
|
||||
/** ID of the row in its own table. */
|
||||
@ColumnInfo(name = "entity_id")
|
||||
val entityId: String,
|
||||
|
||||
/** Address the row is loaded under on the token-details screen. */
|
||||
@ColumnInfo(name = "address")
|
||||
val address: String,
|
||||
|
||||
/** Time the unified timeline is sorted by (newest first). */
|
||||
@ColumnInfo(name = "sort_time_millis")
|
||||
val sortTimeMillis: Long,
|
||||
) {
|
||||
|
||||
enum class Type(val value: String) {
|
||||
EXCHANGE(value = "EXCHANGE"),
|
||||
ONRAMP(value = "ONRAMP"),
|
||||
}
|
||||
}
|
||||
|
|
@ -10,11 +10,10 @@ import androidx.room.*
|
|||
@Entity(
|
||||
tableName = "express_exchange",
|
||||
indices = [
|
||||
// Outgoing swaps lookup (observeOutgoingSwaps): owner + from-asset equality, created_at range/sort.
|
||||
Index(value = ["owner_address", "from_network", "from_contract_address", "created_at"]),
|
||||
// Incoming (cross-owner) swaps lookup (observeIncomingSwaps): to-asset equality, created_at range/sort.
|
||||
// No owner filter here, so to_contract_address in the index is what keeps a popular to-network selective.
|
||||
Index(value = ["to_network", "to_contract_address", "created_at"]),
|
||||
// Outgoing swaps lookup (observeOutgoingSwaps): from-address + from-asset equality, created_at range/sort.
|
||||
Index(value = ["from_address", "from_network", "from_contract_address", "created_at"]),
|
||||
// Incoming swaps lookup (observeIncomingSwaps): payout-address + to-asset equality, created_at range/sort.
|
||||
Index(value = ["payout_address", "to_network", "to_contract_address", "created_at"]),
|
||||
],
|
||||
)
|
||||
data class ExpressExchangeEntity(
|
||||
|
|
@ -23,21 +22,16 @@ data class ExpressExchangeEntity(
|
|||
@ColumnInfo(name = "tx_id")
|
||||
val txId: String,
|
||||
|
||||
/**
|
||||
* Address used to query the history. For exchange it matches [fromAddress].
|
||||
*/
|
||||
@ColumnInfo(name = "owner_address")
|
||||
val ownerAddress: String,
|
||||
|
||||
@ColumnInfo(name = "provider_id")
|
||||
val providerId: String,
|
||||
|
||||
/**
|
||||
* Address from which the `from` assets were taken for the exchange. Optional because the very first
|
||||
* app versions did not send it; for newer versions it can be considered effectively mandatory.
|
||||
* Address from which the `from` assets were taken — the key outgoing swaps are looked up by. The API may omit it
|
||||
* (the very first app versions did not send it), but such items are filtered out before persisting, so the stored
|
||||
* value is always present.
|
||||
*/
|
||||
@ColumnInfo(name = "from_address")
|
||||
val fromAddress: String?,
|
||||
val fromAddress: String,
|
||||
|
||||
/** Address to which the source assets were transferred for the exchange */
|
||||
@ColumnInfo(name = "payin_address")
|
||||
|
|
|
|||
|
|
@ -10,8 +10,8 @@ import androidx.room.*
|
|||
@Entity(
|
||||
tableName = "express_onramp",
|
||||
indices = [
|
||||
// Incoming onramp lookup (observeIncomingOnramps): owner + to-asset equality, created_at range/sort.
|
||||
Index(value = ["owner_address", "to_network", "to_contract_address", "created_at"]),
|
||||
// Incoming onramp lookup (observeIncomingOnramps): payout-address + to-asset equality, created_at range/sort.
|
||||
Index(value = ["payout_address", "to_network", "to_contract_address", "created_at"]),
|
||||
],
|
||||
)
|
||||
data class ExpressOnrampEntity(
|
||||
|
|
@ -20,16 +20,10 @@ data class ExpressOnrampEntity(
|
|||
@ColumnInfo(name = "tx_id")
|
||||
val txId: String,
|
||||
|
||||
/**
|
||||
* Address used to query the history. For onramp it matches [payoutAddress].
|
||||
*/
|
||||
@ColumnInfo(name = "owner_address")
|
||||
val ownerAddress: String,
|
||||
|
||||
@ColumnInfo(name = "provider_id")
|
||||
val providerId: String,
|
||||
|
||||
/** Address that received the target assets */
|
||||
/** Address that received the target assets — the key incoming onramps are looked up by. */
|
||||
@ColumnInfo(name = "payout_address")
|
||||
val payoutAddress: String,
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,52 @@
|
|||
package com.tangem.datasource.local.txhistory.db.entity.express
|
||||
|
||||
import androidx.room.ColumnInfo
|
||||
import androidx.room.Embedded
|
||||
import androidx.room.Entity
|
||||
import androidx.room.PrimaryKey
|
||||
|
||||
/** Persisted onramp country, matched to a transaction by [code] == [ExpressOnrampEntity.countryCode]. */
|
||||
@Entity(tableName = "onramp_country")
|
||||
data class OnrampCountryEntity(
|
||||
|
||||
@PrimaryKey
|
||||
@ColumnInfo(name = "code")
|
||||
val code: String,
|
||||
|
||||
@ColumnInfo(name = "name")
|
||||
val name: String,
|
||||
|
||||
@ColumnInfo(name = "image")
|
||||
val image: String,
|
||||
|
||||
@ColumnInfo(name = "alpha3")
|
||||
val alpha3: String,
|
||||
|
||||
@ColumnInfo(name = "continent")
|
||||
val continent: String,
|
||||
|
||||
@ColumnInfo(name = "onramp_available")
|
||||
val isOnrampAvailable: Boolean,
|
||||
|
||||
@Embedded(prefix = "currency_")
|
||||
val defaultCurrency: CurrencyEmbedded,
|
||||
) {
|
||||
|
||||
data class CurrencyEmbedded(
|
||||
|
||||
@ColumnInfo(name = "name")
|
||||
val name: String,
|
||||
|
||||
@ColumnInfo(name = "code")
|
||||
val code: String,
|
||||
|
||||
@ColumnInfo(name = "image")
|
||||
val image: String?,
|
||||
|
||||
@ColumnInfo(name = "precision")
|
||||
val precision: Int,
|
||||
|
||||
@ColumnInfo(name = "unit")
|
||||
val unit: String,
|
||||
)
|
||||
}
|
||||
|
|
@ -0,0 +1,37 @@
|
|||
package com.tangem.datasource.local.txhistory.db.entity.express
|
||||
|
||||
import androidx.room.ColumnInfo
|
||||
import androidx.room.Entity
|
||||
|
||||
/**
|
||||
* Cached token data fetched from [TangemTechApi.getCoins], keyed by network id + contract address.
|
||||
*/
|
||||
@Entity(
|
||||
tableName = "token_info",
|
||||
primaryKeys = ["network_id", "contract_address"],
|
||||
)
|
||||
data class TokenInfoEntity(
|
||||
|
||||
@ColumnInfo(name = "network_id")
|
||||
val networkId: String,
|
||||
|
||||
@ColumnInfo(name = "contract_address")
|
||||
val contractAddress: String,
|
||||
|
||||
/** Coin id from the backend (used as the token's raw currency id and for the icon URL). */
|
||||
@ColumnInfo(name = "coin_id")
|
||||
val coinId: String,
|
||||
|
||||
@ColumnInfo(name = "name")
|
||||
val name: String,
|
||||
|
||||
@ColumnInfo(name = "symbol")
|
||||
val symbol: String,
|
||||
|
||||
@ColumnInfo(name = "decimals")
|
||||
val decimals: Int,
|
||||
|
||||
/** Last refresh timestamp in epoch milliseconds, used to evict stale entries. */
|
||||
@ColumnInfo(name = "updated_at")
|
||||
val updatedAt: Long,
|
||||
)
|
||||
|
|
@ -10,15 +10,15 @@ internal class DefaultTangemPayCloseCardStore(
|
|||
private val prefs: AppPreferencesStore,
|
||||
) : TangemPayCloseCardStore {
|
||||
|
||||
override suspend fun setCloseOrderId(cardId: String, orderId: String?) {
|
||||
if (orderId == null) {
|
||||
prefs.edit { it.remove(getCloseKey(cardId)) }
|
||||
} else {
|
||||
prefs.store(
|
||||
key = getCloseKey(cardId),
|
||||
value = orderId,
|
||||
)
|
||||
}
|
||||
override suspend fun storeCloseOrderId(cardId: String, orderId: String) {
|
||||
prefs.store(
|
||||
key = getCloseKey(cardId),
|
||||
value = orderId,
|
||||
)
|
||||
}
|
||||
|
||||
override suspend fun removeCloseOrderId(cardId: String) {
|
||||
prefs.edit { it.remove(getCloseKey(cardId)) }
|
||||
}
|
||||
|
||||
override suspend fun getOrderId(cardId: String): String? {
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
package com.tangem.datasource.local.visa
|
||||
|
||||
import androidx.datastore.preferences.core.edit
|
||||
import androidx.datastore.preferences.core.stringPreferencesKey
|
||||
import com.tangem.datasource.local.datastore.RuntimeDataStore
|
||||
import com.tangem.datasource.local.preferences.AppPreferencesStore
|
||||
|
|
@ -31,6 +32,10 @@ internal class DefaultTangemPayReissueCardStore(
|
|||
)
|
||||
}
|
||||
|
||||
override suspend fun removeReissueOrderId(cardId: String) {
|
||||
prefs.edit { it.remove(getReissueKey(cardId)) }
|
||||
}
|
||||
|
||||
override suspend fun getOrderId(cardId: String): String? {
|
||||
return prefs.getSyncOrNull(key = getReissueKey(cardId))
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,23 +1,37 @@
|
|||
package com.tangem.datasource.local.visa
|
||||
|
||||
import com.tangem.datasource.local.datastore.core.StringKeyDataStore
|
||||
import com.tangem.datasource.local.datastore.core.StringKeyDataStoreDecorator
|
||||
import androidx.datastore.core.DataStore
|
||||
import com.tangem.datasource.local.visa.entity.TangemPayTxHistoryItemDM
|
||||
import com.tangem.datasource.local.visa.entity.TangemPayTxHistoryItemToDMConverter
|
||||
import com.tangem.datasource.local.visa.entity.TangemPayTxHistoryItemToDomainConverter
|
||||
import com.tangem.domain.visa.model.TangemPayTxHistoryItem
|
||||
import kotlinx.coroutines.flow.first
|
||||
|
||||
internal typealias StoredTangemPayTxHistory = Map<String, Map<String, List<TangemPayTxHistoryItemDM>>>
|
||||
|
||||
internal class DefaultTangemPayTxHistoryItemsStore(
|
||||
dataStore: StringKeyDataStore<Map<String, List<TangemPayTxHistoryItem>>>,
|
||||
) : TangemPayTxHistoryItemsStore,
|
||||
StringKeyDataStoreDecorator<String, Map<String, List<TangemPayTxHistoryItem>>>(dataStore) {
|
||||
override fun provideStringKey(key: String): String = key
|
||||
private val dataStore: DataStore<StoredTangemPayTxHistory>,
|
||||
private val toDMConverter: TangemPayTxHistoryItemToDMConverter,
|
||||
private val toDomainConverter: TangemPayTxHistoryItemToDomainConverter,
|
||||
) : TangemPayTxHistoryItemsStore {
|
||||
|
||||
override suspend fun getSyncOrNull(key: String, cursor: String): List<TangemPayTxHistoryItem>? {
|
||||
val storedValue = getSyncOrNull(key)
|
||||
return storedValue?.get(cursor)
|
||||
return dataStore.data.first()[key]?.get(cursor)?.let { toDomainConverter.convertList(it) }
|
||||
}
|
||||
|
||||
override suspend fun store(key: String, cursor: String, value: List<TangemPayTxHistoryItem>) {
|
||||
val oldValue = getSyncOrNull(key).orEmpty()
|
||||
val newValue = oldValue.toMutableMap().apply { put(cursor, value) }
|
||||
store(key, newValue)
|
||||
val page = toDMConverter.convertList(value)
|
||||
dataStore.updateData { stored ->
|
||||
val walletPages = stored[key].orEmpty()
|
||||
stored + (key to walletPages + (cursor to page))
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun remove(key: String) {
|
||||
remove(keys = listOf(key))
|
||||
}
|
||||
|
||||
override suspend fun remove(keys: List<String>) {
|
||||
dataStore.updateData { stored -> stored - keys.toSet() }
|
||||
}
|
||||
}
|
||||
|
|
@ -2,7 +2,9 @@ package com.tangem.datasource.local.visa
|
|||
|
||||
interface TangemPayCloseCardStore {
|
||||
|
||||
suspend fun setCloseOrderId(cardId: String, orderId: String?)
|
||||
suspend fun storeCloseOrderId(cardId: String, orderId: String)
|
||||
|
||||
suspend fun removeCloseOrderId(cardId: String)
|
||||
|
||||
suspend fun getOrderId(cardId: String): String?
|
||||
}
|
||||
|
|
@ -11,5 +11,7 @@ interface TangemPayReissueCardStore {
|
|||
|
||||
suspend fun storeReissueOrderId(cardId: String, orderId: String)
|
||||
|
||||
suspend fun removeReissueOrderId(cardId: String)
|
||||
|
||||
suspend fun getOrderId(cardId: String): String?
|
||||
}
|
||||
|
|
@ -23,6 +23,12 @@ interface TangemPayStorage {
|
|||
|
||||
suspend fun clearOrderId(customerWalletAddress: String)
|
||||
|
||||
suspend fun storeVirtualAccountOrderId(customerWalletAddress: String, vaOrderId: String)
|
||||
|
||||
suspend fun getVirtualAccountOrderId(customerWalletAddress: String): String?
|
||||
|
||||
suspend fun clearVirtualAccountOrderId(customerWalletAddress: String)
|
||||
|
||||
suspend fun getAddToWalletDone(customerWalletAddress: String): Boolean
|
||||
|
||||
suspend fun storeAddToWalletDone(customerWalletAddress: String, isDone: Boolean)
|
||||
|
|
|
|||
|
|
@ -8,5 +8,7 @@ interface TangemPayTxHistoryItemsStore {
|
|||
|
||||
suspend fun remove(key: String)
|
||||
|
||||
suspend fun remove(keys: List<String>)
|
||||
|
||||
suspend fun store(key: String, cursor: String, value: List<TangemPayTxHistoryItem>)
|
||||
}
|
||||
|
|
@ -0,0 +1,102 @@
|
|||
package com.tangem.datasource.local.visa.entity
|
||||
|
||||
import com.tangem.domain.models.serialization.SerializedBigDecimal
|
||||
import com.tangem.domain.models.serialization.SerializedCurrency
|
||||
import com.tangem.domain.models.serialization.SerializedDateTime
|
||||
import kotlinx.serialization.SerialName
|
||||
import kotlinx.serialization.Serializable
|
||||
|
||||
@Serializable
|
||||
internal sealed class TangemPayTxHistoryItemDM {
|
||||
|
||||
abstract val id: String
|
||||
abstract val date: SerializedDateTime
|
||||
abstract val amount: SerializedBigDecimal
|
||||
abstract val currency: SerializedCurrency
|
||||
abstract val jsonRepresentation: String
|
||||
|
||||
@Serializable
|
||||
@SerialName("spend")
|
||||
data class Spend(
|
||||
@SerialName("id") override val id: String,
|
||||
@SerialName("json_representation") override val jsonRepresentation: String,
|
||||
@SerialName("date") override val date: SerializedDateTime,
|
||||
@SerialName("amount") override val amount: SerializedBigDecimal,
|
||||
@SerialName("currency") override val currency: SerializedCurrency,
|
||||
@SerialName("authorized_amount") val authorizedAmount: SerializedBigDecimal,
|
||||
@SerialName("local_amount") val localAmount: SerializedBigDecimal?,
|
||||
@SerialName("local_currency") val localCurrency: SerializedCurrency?,
|
||||
@SerialName("enriched_merchant_name") val enrichedMerchantName: String?,
|
||||
@SerialName("merchant_name") val merchantName: String,
|
||||
@SerialName("enriched_merchant_category") val enrichedMerchantCategory: String?,
|
||||
@SerialName("merchant_category_code") val merchantCategoryCode: String?,
|
||||
@SerialName("merchant_category") val merchantCategory: String?,
|
||||
@SerialName("status") val status: Status,
|
||||
@SerialName("enriched_merchant_icon_url") val enrichedMerchantIconUrl: String?,
|
||||
@SerialName("declined_reason") val declinedReason: String?,
|
||||
) : TangemPayTxHistoryItemDM()
|
||||
|
||||
@Serializable
|
||||
@SerialName("payment")
|
||||
data class Payment(
|
||||
@SerialName("id") override val id: String,
|
||||
@SerialName("json_representation") override val jsonRepresentation: String,
|
||||
@SerialName("date") override val date: SerializedDateTime,
|
||||
@SerialName("amount") override val amount: SerializedBigDecimal,
|
||||
@SerialName("currency") override val currency: SerializedCurrency,
|
||||
@SerialName("transaction_hash") val transactionHash: String?,
|
||||
) : TangemPayTxHistoryItemDM()
|
||||
|
||||
@Serializable
|
||||
@SerialName("fee")
|
||||
data class Fee(
|
||||
@SerialName("id") override val id: String,
|
||||
@SerialName("json_representation") override val jsonRepresentation: String,
|
||||
@SerialName("date") override val date: SerializedDateTime,
|
||||
@SerialName("amount") override val amount: SerializedBigDecimal,
|
||||
@SerialName("currency") override val currency: SerializedCurrency,
|
||||
@SerialName("description") val description: String?,
|
||||
) : TangemPayTxHistoryItemDM()
|
||||
|
||||
@Serializable
|
||||
@SerialName("collateral")
|
||||
data class Collateral(
|
||||
@SerialName("id") override val id: String,
|
||||
@SerialName("json_representation") override val jsonRepresentation: String,
|
||||
@SerialName("date") override val date: SerializedDateTime,
|
||||
@SerialName("amount") override val amount: SerializedBigDecimal,
|
||||
@SerialName("currency") override val currency: SerializedCurrency,
|
||||
@SerialName("transaction_hash") val transactionHash: String,
|
||||
@SerialName("type") val type: Type,
|
||||
) : TangemPayTxHistoryItemDM()
|
||||
|
||||
@Serializable
|
||||
enum class Type {
|
||||
@SerialName("deposit")
|
||||
Deposit,
|
||||
|
||||
@SerialName("withdrawal")
|
||||
Withdrawal,
|
||||
}
|
||||
|
||||
@Serializable
|
||||
enum class Status {
|
||||
@SerialName("pending")
|
||||
PENDING,
|
||||
|
||||
@SerialName("reserved")
|
||||
RESERVED,
|
||||
|
||||
@SerialName("completed")
|
||||
COMPLETED,
|
||||
|
||||
@SerialName("declined")
|
||||
DECLINED,
|
||||
|
||||
@SerialName("reversed")
|
||||
REVERSED,
|
||||
|
||||
@SerialName("unknown")
|
||||
UNKNOWN,
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,133 @@
|
|||
package com.tangem.datasource.local.visa.entity
|
||||
|
||||
import com.tangem.domain.visa.model.TangemPayTxHistoryItem
|
||||
import com.tangem.utils.converter.Converter
|
||||
import javax.inject.Inject
|
||||
|
||||
internal class TangemPayTxHistoryItemToDMConverter @Inject constructor() :
|
||||
Converter<TangemPayTxHistoryItem, TangemPayTxHistoryItemDM> {
|
||||
|
||||
override fun convert(value: TangemPayTxHistoryItem): TangemPayTxHistoryItemDM = when (value) {
|
||||
is TangemPayTxHistoryItem.Spend -> TangemPayTxHistoryItemDM.Spend(
|
||||
id = value.id,
|
||||
jsonRepresentation = value.jsonRepresentation,
|
||||
date = value.date,
|
||||
amount = value.amount,
|
||||
currency = value.currency,
|
||||
authorizedAmount = value.authorizedAmount,
|
||||
localAmount = value.localAmount,
|
||||
localCurrency = value.localCurrency,
|
||||
enrichedMerchantName = value.enrichedMerchantName,
|
||||
merchantName = value.merchantName,
|
||||
enrichedMerchantCategory = value.enrichedMerchantCategory,
|
||||
merchantCategoryCode = value.merchantCategoryCode,
|
||||
merchantCategory = value.merchantCategory,
|
||||
status = value.status.toDM(),
|
||||
enrichedMerchantIconUrl = value.enrichedMerchantIconUrl,
|
||||
declinedReason = value.declinedReason,
|
||||
)
|
||||
is TangemPayTxHistoryItem.Payment -> TangemPayTxHistoryItemDM.Payment(
|
||||
id = value.id,
|
||||
jsonRepresentation = value.jsonRepresentation,
|
||||
date = value.date,
|
||||
amount = value.amount,
|
||||
currency = value.currency,
|
||||
transactionHash = value.transactionHash,
|
||||
)
|
||||
is TangemPayTxHistoryItem.Fee -> TangemPayTxHistoryItemDM.Fee(
|
||||
id = value.id,
|
||||
jsonRepresentation = value.jsonRepresentation,
|
||||
date = value.date,
|
||||
amount = value.amount,
|
||||
currency = value.currency,
|
||||
description = value.description,
|
||||
)
|
||||
is TangemPayTxHistoryItem.Collateral -> TangemPayTxHistoryItemDM.Collateral(
|
||||
id = value.id,
|
||||
jsonRepresentation = value.jsonRepresentation,
|
||||
date = value.date,
|
||||
amount = value.amount,
|
||||
currency = value.currency,
|
||||
transactionHash = value.transactionHash,
|
||||
type = value.type.toDM(),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
internal class TangemPayTxHistoryItemToDomainConverter @Inject constructor() :
|
||||
Converter<TangemPayTxHistoryItemDM, TangemPayTxHistoryItem> {
|
||||
|
||||
override fun convert(value: TangemPayTxHistoryItemDM): TangemPayTxHistoryItem = when (value) {
|
||||
is TangemPayTxHistoryItemDM.Spend -> TangemPayTxHistoryItem.Spend(
|
||||
id = value.id,
|
||||
jsonRepresentation = value.jsonRepresentation,
|
||||
date = value.date,
|
||||
amount = value.amount,
|
||||
currency = value.currency,
|
||||
authorizedAmount = value.authorizedAmount,
|
||||
localAmount = value.localAmount,
|
||||
localCurrency = value.localCurrency,
|
||||
enrichedMerchantName = value.enrichedMerchantName,
|
||||
merchantName = value.merchantName,
|
||||
enrichedMerchantCategory = value.enrichedMerchantCategory,
|
||||
merchantCategoryCode = value.merchantCategoryCode,
|
||||
merchantCategory = value.merchantCategory,
|
||||
status = value.status.toDomain(),
|
||||
enrichedMerchantIconUrl = value.enrichedMerchantIconUrl,
|
||||
declinedReason = value.declinedReason,
|
||||
)
|
||||
is TangemPayTxHistoryItemDM.Payment -> TangemPayTxHistoryItem.Payment(
|
||||
id = value.id,
|
||||
jsonRepresentation = value.jsonRepresentation,
|
||||
date = value.date,
|
||||
amount = value.amount,
|
||||
currency = value.currency,
|
||||
transactionHash = value.transactionHash,
|
||||
)
|
||||
is TangemPayTxHistoryItemDM.Fee -> TangemPayTxHistoryItem.Fee(
|
||||
id = value.id,
|
||||
jsonRepresentation = value.jsonRepresentation,
|
||||
date = value.date,
|
||||
amount = value.amount,
|
||||
currency = value.currency,
|
||||
description = value.description,
|
||||
)
|
||||
is TangemPayTxHistoryItemDM.Collateral -> TangemPayTxHistoryItem.Collateral(
|
||||
id = value.id,
|
||||
jsonRepresentation = value.jsonRepresentation,
|
||||
date = value.date,
|
||||
amount = value.amount,
|
||||
currency = value.currency,
|
||||
transactionHash = value.transactionHash,
|
||||
type = value.type.toDomain(),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private fun TangemPayTxHistoryItem.Status.toDM(): TangemPayTxHistoryItemDM.Status = when (this) {
|
||||
TangemPayTxHistoryItem.Status.PENDING -> TangemPayTxHistoryItemDM.Status.PENDING
|
||||
TangemPayTxHistoryItem.Status.RESERVED -> TangemPayTxHistoryItemDM.Status.RESERVED
|
||||
TangemPayTxHistoryItem.Status.COMPLETED -> TangemPayTxHistoryItemDM.Status.COMPLETED
|
||||
TangemPayTxHistoryItem.Status.DECLINED -> TangemPayTxHistoryItemDM.Status.DECLINED
|
||||
TangemPayTxHistoryItem.Status.REVERSED -> TangemPayTxHistoryItemDM.Status.REVERSED
|
||||
TangemPayTxHistoryItem.Status.UNKNOWN -> TangemPayTxHistoryItemDM.Status.UNKNOWN
|
||||
}
|
||||
|
||||
private fun TangemPayTxHistoryItemDM.Status.toDomain(): TangemPayTxHistoryItem.Status = when (this) {
|
||||
TangemPayTxHistoryItemDM.Status.PENDING -> TangemPayTxHistoryItem.Status.PENDING
|
||||
TangemPayTxHistoryItemDM.Status.RESERVED -> TangemPayTxHistoryItem.Status.RESERVED
|
||||
TangemPayTxHistoryItemDM.Status.COMPLETED -> TangemPayTxHistoryItem.Status.COMPLETED
|
||||
TangemPayTxHistoryItemDM.Status.DECLINED -> TangemPayTxHistoryItem.Status.DECLINED
|
||||
TangemPayTxHistoryItemDM.Status.REVERSED -> TangemPayTxHistoryItem.Status.REVERSED
|
||||
TangemPayTxHistoryItemDM.Status.UNKNOWN -> TangemPayTxHistoryItem.Status.UNKNOWN
|
||||
}
|
||||
|
||||
private fun TangemPayTxHistoryItem.Type.toDM(): TangemPayTxHistoryItemDM.Type = when (this) {
|
||||
TangemPayTxHistoryItem.Type.Deposit -> TangemPayTxHistoryItemDM.Type.Deposit
|
||||
TangemPayTxHistoryItem.Type.Withdrawal -> TangemPayTxHistoryItemDM.Type.Withdrawal
|
||||
}
|
||||
|
||||
private fun TangemPayTxHistoryItemDM.Type.toDomain(): TangemPayTxHistoryItem.Type = when (this) {
|
||||
TangemPayTxHistoryItemDM.Type.Deposit -> TangemPayTxHistoryItem.Type.Deposit
|
||||
TangemPayTxHistoryItemDM.Type.Withdrawal -> TangemPayTxHistoryItem.Type.Withdrawal
|
||||
}
|
||||
|
|
@ -47,6 +47,7 @@ class WireMockRedirectInterceptor : Interceptor {
|
|||
"deep-index.moralis.io",
|
||||
"solana-gateway.moralis.io",
|
||||
"api.etherscan.io",
|
||||
"eth-blockbook.nownodes.io",
|
||||
)
|
||||
|
||||
/**
|
||||
|
|
|
|||
|
|
@ -0,0 +1,119 @@
|
|||
package com.tangem.datasource.local.promotion
|
||||
|
||||
import com.google.common.truth.Truth.assertThat
|
||||
import com.tangem.datasource.api.common.response.ApiResponse
|
||||
import com.tangem.datasource.api.promotion.models.PromotionsResponse
|
||||
import com.tangem.datasource.api.tangemTech.TangemTechApi
|
||||
import com.tangem.datasource.local.datastore.RuntimeSharedStore
|
||||
import com.tangem.domain.models.wallet.UserWalletId
|
||||
import com.tangem.utils.coroutines.TestingCoroutineDispatcherProvider
|
||||
import io.mockk.clearMocks
|
||||
import io.mockk.coEvery
|
||||
import io.mockk.coVerify
|
||||
import io.mockk.mockk
|
||||
import kotlinx.coroutines.test.runTest
|
||||
import org.junit.jupiter.api.BeforeEach
|
||||
import org.junit.jupiter.api.Test
|
||||
import org.junit.jupiter.api.TestInstance
|
||||
import java.io.IOException
|
||||
|
||||
@TestInstance(TestInstance.Lifecycle.PER_CLASS)
|
||||
internal class DefaultPromotionsSupplierTest {
|
||||
|
||||
private val tangemApi: TangemTechApi = mockk()
|
||||
|
||||
private fun newSupplier() = DefaultPromotionsSupplier(
|
||||
tangemApi = tangemApi,
|
||||
store = RuntimeSharedStore(),
|
||||
dispatchers = TestingCoroutineDispatcherProvider(),
|
||||
)
|
||||
|
||||
private val userWalletId = UserWalletId("abcdef012345")
|
||||
private val response = PromotionsResponse(promotions = emptyList())
|
||||
private val response2 = PromotionsResponse(
|
||||
promotions = listOf(
|
||||
PromotionsResponse.PromotionDto(name = "dummy", all = null),
|
||||
),
|
||||
)
|
||||
|
||||
@BeforeEach
|
||||
fun setUp() {
|
||||
clearMocks(tangemApi)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `GIVEN empty cache WHEN getPromotions THEN fetches and returns response`() = runTest {
|
||||
// Arrange
|
||||
coEvery { tangemApi.getPromotions(any(), any()) } returns ApiResponse.Success(response)
|
||||
val supplier = newSupplier()
|
||||
|
||||
// Act
|
||||
val result = supplier.getPromotions(userWalletId)
|
||||
|
||||
// Assert
|
||||
assertThat(result).isEqualTo(response)
|
||||
coVerify(exactly = 1) { tangemApi.getPromotions(userWalletId.stringValue, any()) }
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `GIVEN cached value and no refresh WHEN getPromotions THEN returns cache without api`() = runTest {
|
||||
// Arrange
|
||||
coEvery { tangemApi.getPromotions(any(), any()) } returns ApiResponse.Success(response)
|
||||
val supplier = newSupplier()
|
||||
supplier.getPromotions(userWalletId)
|
||||
clearMocks(tangemApi)
|
||||
|
||||
// Act
|
||||
val result = supplier.getPromotions(userWalletId, forceRefresh = false)
|
||||
|
||||
// Assert
|
||||
assertThat(result).isEqualTo(response)
|
||||
coVerify(exactly = 0) { tangemApi.getPromotions(any(), any()) }
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `GIVEN cached value WHEN getPromotions forceRefresh THEN hits api again and rebinds value`() = runTest {
|
||||
// Arrange
|
||||
coEvery { tangemApi.getPromotions(any(), any()) } returnsMany listOf(
|
||||
ApiResponse.Success(response),
|
||||
ApiResponse.Success(response2),
|
||||
)
|
||||
val supplier = newSupplier()
|
||||
supplier.getPromotions(userWalletId)
|
||||
|
||||
// Act
|
||||
val result = supplier.getPromotions(userWalletId, forceRefresh = true)
|
||||
|
||||
// Assert
|
||||
assertThat(result).isEqualTo(response2)
|
||||
coVerify(exactly = 2) { tangemApi.getPromotions(any(), any()) }
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `GIVEN fetch fails and cache present WHEN getPromotions forceRefresh THEN rethrows`() = runTest {
|
||||
// Arrange
|
||||
coEvery { tangemApi.getPromotions(any(), any()) } returns ApiResponse.Success(response)
|
||||
val supplier = newSupplier()
|
||||
supplier.getPromotions(userWalletId)
|
||||
coEvery { tangemApi.getPromotions(any(), any()) } throws IOException("boom")
|
||||
|
||||
// Act
|
||||
val error = runCatching { supplier.getPromotions(userWalletId, forceRefresh = true) }.exceptionOrNull()
|
||||
|
||||
// Assert
|
||||
assertThat(error).isInstanceOf(IOException::class.java)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `GIVEN fetch fails and empty cache WHEN getPromotions THEN rethrows`() = runTest {
|
||||
// Arrange
|
||||
coEvery { tangemApi.getPromotions(any(), any()) } throws IOException("boom")
|
||||
val supplier = newSupplier()
|
||||
|
||||
// Act
|
||||
val error = runCatching { supplier.getPromotions(userWalletId) }.exceptionOrNull()
|
||||
|
||||
// Assert
|
||||
assertThat(error).isInstanceOf(IOException::class.java)
|
||||
}
|
||||
}
|
||||
|
|
@ -70,6 +70,7 @@ internal class ProdApiConfigsManagerTest {
|
|||
every { appInfoProvider.osVersion } returns "Android 16"
|
||||
every { appInfoProvider.language } returns Locale.getDefault().toLanguageTag()
|
||||
every { appInfoProvider.device } returns "${Build.MANUFACTURER} ${Build.MODEL}"
|
||||
every { appInfoProvider.deviceScale } returns DEVICE_SCALE
|
||||
|
||||
manager = ProdApiConfigsManager(apiConfigs = createApiConfigs())
|
||||
}
|
||||
|
|
@ -170,7 +171,7 @@ internal class ProdApiConfigsManagerTest {
|
|||
expected = ApiEnvironmentConfig(
|
||||
environment = environment,
|
||||
baseUrl = when (environment) {
|
||||
ApiEnvironment.PROD -> "https://authentication.tangem.org/"
|
||||
ApiEnvironment.PROD -> "https://api.tangem.org/"
|
||||
else -> "[REDACTED_ENV_URL]"
|
||||
},
|
||||
headers = emptyMap(),
|
||||
|
|
@ -298,6 +299,7 @@ internal class ProdApiConfigsManagerTest {
|
|||
"version" to ProviderSuspend { VERSION_NAME },
|
||||
"platform" to ProviderSuspend { "Android" },
|
||||
"X-API-KEY" to ProviderSuspend { TANGEM_PAY_BFF_KEY_DEV },
|
||||
"X-Device-Scale" to ProviderSuspend { DEVICE_SCALE.toString() },
|
||||
),
|
||||
),
|
||||
)
|
||||
|
|
@ -313,6 +315,7 @@ internal class ProdApiConfigsManagerTest {
|
|||
"version" to ProviderSuspend { VERSION_NAME },
|
||||
"platform" to ProviderSuspend { "Android" },
|
||||
"X-API-KEY" to ProviderSuspend { TANGEM_PAY_BFF_KEY_DEV },
|
||||
"X-Device-Scale" to ProviderSuspend { DEVICE_SCALE.toString() },
|
||||
),
|
||||
),
|
||||
)
|
||||
|
|
@ -457,6 +460,7 @@ internal class ProdApiConfigsManagerTest {
|
|||
private companion object {
|
||||
|
||||
const val VERSION_NAME = "debug"
|
||||
const val DEVICE_SCALE = 3f
|
||||
const val EXPRESS_SESSION_ID = "express_session_id"
|
||||
const val STAKE_KIT_API_KEY = "stake_kit_api_key"
|
||||
const val P2P_API_KEY = "p2p_api_key"
|
||||
|
|
|
|||
|
|
@ -0,0 +1,75 @@
|
|||
package com.tangem.datasource.api.marketing
|
||||
|
||||
import com.google.common.truth.Truth.assertThat
|
||||
import com.squareup.moshi.Moshi
|
||||
import com.tangem.datasource.api.common.adapter.BigDecimalAdapter
|
||||
import com.tangem.datasource.api.marketing.models.MarketingCampaignsResponse
|
||||
import org.junit.jupiter.api.Test
|
||||
import java.math.BigDecimal
|
||||
|
||||
internal class MarketingCampaignsResponseTest {
|
||||
|
||||
private val moshi = Moshi.Builder().add(BigDecimalAdapter()).build()
|
||||
private val adapter = moshi.adapter(MarketingCampaignsResponse::class.java)
|
||||
|
||||
@Test
|
||||
fun `GIVEN swap response json WHEN parsed THEN fields mapped`() {
|
||||
// Arrange
|
||||
val json = """
|
||||
{"campaigns":[{"id":12,"type":"swap","priority":1,"minAmount":50,"maxAmount":300,
|
||||
"providerIds":["provider1"],
|
||||
"banner":{"uiType":"linked_to_provider","text":"Cashback 4 U","icon":"https://x/star.webp",
|
||||
"bgColor":"#FF0011","deeplink":"https://tangem.com","dismissible":true}}]}
|
||||
""".trimIndent()
|
||||
|
||||
// Act
|
||||
val result = adapter.fromJson(json)!!
|
||||
|
||||
// Assert
|
||||
val campaign = result.campaigns.single()
|
||||
assertThat(campaign.id).isEqualTo(12)
|
||||
assertThat(campaign.type).isEqualTo("swap")
|
||||
assertThat(campaign.minAmount).isEqualTo(BigDecimal(50))
|
||||
assertThat(campaign.providerIds).containsExactly("provider1")
|
||||
assertThat(campaign.banner.uiType).isEqualTo("linked_to_provider")
|
||||
assertThat(campaign.banner.isDismissible).isTrue()
|
||||
assertThat(campaign.tokens).isNull()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `GIVEN token_details response WHEN parsed THEN network targets mapped`() {
|
||||
// Arrange
|
||||
val json = """
|
||||
{"campaigns":[{"id":12,"type":"token_details","priority":1,
|
||||
"tokens":[{"networkId":"ethereum","contractAddress":"0xA0b8"}],
|
||||
"banner":{"uiType":"standalone","dismissible":false}}]}
|
||||
""".trimIndent()
|
||||
|
||||
// Act
|
||||
val campaign = adapter.fromJson(json)!!.campaigns.single()
|
||||
|
||||
// Assert
|
||||
val token = campaign.tokens!!.single()
|
||||
assertThat(token.networkId).isEqualTo("ethereum")
|
||||
assertThat(token.contractAddress).isEqualTo("0xA0b8")
|
||||
assertThat(token.id).isNull()
|
||||
assertThat(campaign.minAmount).isNull()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `GIVEN markets response WHEN parsed THEN coingecko ids mapped`() {
|
||||
// Arrange
|
||||
val json = """
|
||||
{"campaigns":[{"id":12,"type":"markets_token","priority":1,
|
||||
"tokens":[{"id":"1696501400"},{"id":"3296501412"}],
|
||||
"banner":{"uiType":"standalone","dismissible":true}}]}
|
||||
""".trimIndent()
|
||||
|
||||
// Act
|
||||
val tokens = adapter.fromJson(json)!!.campaigns.single().tokens!!
|
||||
|
||||
// Assert
|
||||
assertThat(tokens.map { it.id }).containsExactly("1696501400", "3296501412")
|
||||
assertThat(tokens.all { it.networkId == null }).isTrue()
|
||||
}
|
||||
}
|
||||
|
|
@ -15,11 +15,10 @@ internal class ExpressHistoryConverterTest {
|
|||
val item = createExchangeItem()
|
||||
|
||||
// WHEN
|
||||
val entity = item.toEntity(ownerAddress = OWNER_ADDRESS)
|
||||
val entity = requireNotNull(item.toEntity())
|
||||
|
||||
// THEN
|
||||
Truth.assertThat(entity.txId).isEqualTo(item.txId)
|
||||
Truth.assertThat(entity.ownerAddress).isEqualTo(OWNER_ADDRESS)
|
||||
Truth.assertThat(entity.providerId).isEqualTo(item.providerId)
|
||||
Truth.assertThat(entity.fromAddress).isEqualTo(item.fromAddress)
|
||||
Truth.assertThat(entity.payinAddress).isEqualTo(item.payinAddress)
|
||||
|
|
@ -35,8 +34,7 @@ internal class ExpressHistoryConverterTest {
|
|||
Truth.assertThat(entity.refundNetwork).isEqualTo(item.refundNetwork)
|
||||
Truth.assertThat(entity.refundContractAddress).isEqualTo(item.refundContractAddress)
|
||||
Truth.assertThat(entity.createdAt).isEqualTo(item.createdAt)
|
||||
// todo txHistory uncomment
|
||||
// Truth.assertThat(entity.updatedAt).isEqualTo(item.updatedAt)
|
||||
Truth.assertThat(entity.updatedAt).isEqualTo(item.updatedAt)
|
||||
Truth.assertThat(entity.payTill).isEqualTo(item.payTill)
|
||||
Truth.assertThat(entity.averageDuration).isEqualTo(item.averageDuration)
|
||||
}
|
||||
|
|
@ -47,7 +45,7 @@ internal class ExpressHistoryConverterTest {
|
|||
val item = createExchangeItem(status = "finished")
|
||||
|
||||
// WHEN
|
||||
val entity = item.toEntity(ownerAddress = OWNER_ADDRESS)
|
||||
val entity = requireNotNull(item.toEntity())
|
||||
|
||||
// THEN
|
||||
Truth.assertThat(entity.status).isEqualTo("finished")
|
||||
|
|
@ -59,7 +57,7 @@ internal class ExpressHistoryConverterTest {
|
|||
val item = createExchangeItem()
|
||||
|
||||
// WHEN
|
||||
val entity = item.toEntity(ownerAddress = OWNER_ADDRESS)
|
||||
val entity = requireNotNull(item.toEntity())
|
||||
|
||||
// THEN
|
||||
Truth.assertThat(entity.from.contractAddress).isEqualTo(item.fromContractAddress)
|
||||
|
|
@ -95,7 +93,7 @@ internal class ExpressHistoryConverterTest {
|
|||
)
|
||||
|
||||
// WHEN
|
||||
val entity = item.toEntity(ownerAddress = OWNER_ADDRESS)
|
||||
val entity = requireNotNull(item.toEntity())
|
||||
|
||||
// THEN
|
||||
Truth.assertThat(entity.payinExtraId).isNull()
|
||||
|
|
@ -112,17 +110,28 @@ internal class ExpressHistoryConverterTest {
|
|||
Truth.assertThat(entity.to.actualAmount).isNull()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `GIVEN exchange item with null fromAddress WHEN toEntity THEN returns null`() {
|
||||
// GIVEN
|
||||
val item = createExchangeItem().copy(fromAddress = null)
|
||||
|
||||
// WHEN
|
||||
val entity = item.toEntity()
|
||||
|
||||
// THEN
|
||||
Truth.assertThat(entity).isNull()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `GIVEN onramp item WHEN toEntity THEN all transaction fields are mapped`() {
|
||||
// GIVEN
|
||||
val item = createOnrampItem()
|
||||
|
||||
// WHEN
|
||||
val entity = item.toEntity(ownerAddress = OWNER_ADDRESS)
|
||||
val entity = item.toEntity()
|
||||
|
||||
// THEN
|
||||
Truth.assertThat(entity.txId).isEqualTo(item.txId)
|
||||
Truth.assertThat(entity.ownerAddress).isEqualTo(OWNER_ADDRESS)
|
||||
Truth.assertThat(entity.providerId).isEqualTo(item.providerId)
|
||||
Truth.assertThat(entity.payoutAddress).isEqualTo(item.payoutAddress)
|
||||
Truth.assertThat(entity.failReason).isEqualTo(item.failReason)
|
||||
|
|
@ -145,7 +154,7 @@ internal class ExpressHistoryConverterTest {
|
|||
val item = createOnrampItem(status = "waiting-for-payment")
|
||||
|
||||
// WHEN
|
||||
val entity = item.toEntity(ownerAddress = OWNER_ADDRESS)
|
||||
val entity = item.toEntity()
|
||||
|
||||
// THEN
|
||||
Truth.assertThat(entity.status).isEqualTo("waiting-for-payment")
|
||||
|
|
@ -157,7 +166,7 @@ internal class ExpressHistoryConverterTest {
|
|||
val item = createOnrampItem()
|
||||
|
||||
// WHEN
|
||||
val entity = item.toEntity(ownerAddress = OWNER_ADDRESS)
|
||||
val entity = item.toEntity()
|
||||
|
||||
// THEN
|
||||
Truth.assertThat(entity.to.contractAddress).isEqualTo(item.toContractAddress)
|
||||
|
|
@ -180,7 +189,7 @@ internal class ExpressHistoryConverterTest {
|
|||
)
|
||||
|
||||
// WHEN
|
||||
val entity = item.toEntity(ownerAddress = OWNER_ADDRESS)
|
||||
val entity = item.toEntity()
|
||||
|
||||
// THEN
|
||||
Truth.assertThat(entity.failReason).isNull()
|
||||
|
|
@ -223,8 +232,7 @@ internal class ExpressHistoryConverterTest {
|
|||
refundNetwork = refundNetwork,
|
||||
refundContractAddress = refundContractAddress,
|
||||
createdAt = "2026-06-01T00:00:00Z",
|
||||
// todo txHistory uncomment
|
||||
// updatedAt = "2026-06-01T00:05:00Z",
|
||||
updatedAt = "2026-06-01T00:05:00Z",
|
||||
payTill = payTill,
|
||||
averageDuration = averageDuration,
|
||||
fromContractAddress = "0xfromContract",
|
||||
|
|
@ -256,8 +264,7 @@ internal class ExpressHistoryConverterTest {
|
|||
externalTxUrl = externalTxUrl,
|
||||
payoutHash = payoutHash,
|
||||
createdAt = "2026-06-01T00:00:00Z",
|
||||
// todo txHistory uncomment
|
||||
// updatedAt = "2026-06-01T00:05:00Z",
|
||||
updatedAt = "2026-06-01T00:05:00Z",
|
||||
fromCurrencyCode = "USD",
|
||||
fromAmount = "100.0",
|
||||
fromPrecision = 2,
|
||||
|
|
@ -269,8 +276,4 @@ internal class ExpressHistoryConverterTest {
|
|||
paymentMethod = "card",
|
||||
countryCode = "US",
|
||||
)
|
||||
|
||||
private companion object {
|
||||
const val OWNER_ADDRESS = "0xowner"
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,88 @@
|
|||
package com.tangem.datasource.local.visa
|
||||
|
||||
import androidx.datastore.preferences.core.emptyPreferences
|
||||
import com.google.common.truth.Truth.assertThat
|
||||
import com.squareup.moshi.Moshi
|
||||
import com.tangem.datasource.local.datastore.RuntimeDataStore
|
||||
import com.tangem.datasource.local.preferences.AppPreferencesStore
|
||||
import com.tangem.domain.models.pay.TangemPayReissueCardFee
|
||||
import com.tangem.test.core.datastore.MockStateDataStore
|
||||
import com.tangem.utils.coroutines.TestingCoroutineDispatcherProvider
|
||||
import io.mockk.mockk
|
||||
import kotlinx.coroutines.runBlocking
|
||||
import kotlinx.coroutines.test.runTest
|
||||
import org.junit.jupiter.api.BeforeEach
|
||||
import org.junit.jupiter.api.Test
|
||||
import org.junit.jupiter.api.TestInstance
|
||||
|
||||
/**
|
||||
* Tests for [DefaultTangemPayReissueCardStore], focused on the reissue order-id lifecycle.
|
||||
*
|
||||
* Uses a real [AppPreferencesStore] backed by an in-memory [MockStateDataStore] so the preferences
|
||||
* round-trip (store / read / remove) is exercised end-to-end. The remove path backs the [REDACTED_TASK_KEY] fix:
|
||||
* a terminal reissue order must be forgotten so the payment-account refresh stops re-polling
|
||||
* `GET /order/{id}` for it.
|
||||
*/
|
||||
@TestInstance(TestInstance.Lifecycle.PER_CLASS)
|
||||
internal class DefaultTangemPayReissueCardStoreTest {
|
||||
|
||||
private val dataStore = MockStateDataStore(default = emptyPreferences())
|
||||
private val prefs = AppPreferencesStore(
|
||||
moshi = Moshi.Builder().build(),
|
||||
dispatchers = TestingCoroutineDispatcherProvider(),
|
||||
preferencesDataStore = dataStore,
|
||||
)
|
||||
private val feeStore: RuntimeDataStore<TangemPayReissueCardFee> = mockk(relaxed = true)
|
||||
|
||||
private val store = DefaultTangemPayReissueCardStore(feeStore = feeStore, prefs = prefs)
|
||||
|
||||
@BeforeEach
|
||||
fun resetStore() {
|
||||
runBlocking { dataStore.updateData { emptyPreferences() } }
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `GIVEN order id stored WHEN getOrderId THEN returns stored id`() = runTest {
|
||||
// Arrange
|
||||
store.storeReissueOrderId(CARD_ID, ORDER_ID)
|
||||
|
||||
// Act
|
||||
val result = store.getOrderId(CARD_ID)
|
||||
|
||||
// Assert
|
||||
assertThat(result).isEqualTo(ORDER_ID)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `GIVEN order id stored WHEN removeReissueOrderId THEN order id is cleared`() = runTest {
|
||||
// Arrange
|
||||
store.storeReissueOrderId(CARD_ID, ORDER_ID)
|
||||
|
||||
// Act
|
||||
store.removeReissueOrderId(CARD_ID)
|
||||
|
||||
// Assert
|
||||
assertThat(store.getOrderId(CARD_ID)).isNull()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `GIVEN clearing one card WHEN another card has an order THEN the other is untouched`() = runTest {
|
||||
// Arrange
|
||||
store.storeReissueOrderId(CARD_ID, ORDER_ID)
|
||||
store.storeReissueOrderId(OTHER_CARD_ID, OTHER_ORDER_ID)
|
||||
|
||||
// Act
|
||||
store.removeReissueOrderId(CARD_ID)
|
||||
|
||||
// Assert
|
||||
assertThat(store.getOrderId(CARD_ID)).isNull()
|
||||
assertThat(store.getOrderId(OTHER_CARD_ID)).isEqualTo(OTHER_ORDER_ID)
|
||||
}
|
||||
|
||||
private companion object {
|
||||
const val CARD_ID = "card-1"
|
||||
const val OTHER_CARD_ID = "card-2"
|
||||
const val ORDER_ID = "reissue-order-1"
|
||||
const val OTHER_ORDER_ID = "reissue-order-2"
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,119 @@
|
|||
package com.tangem.datasource.local.visa
|
||||
|
||||
import com.google.common.truth.Truth.assertThat
|
||||
import com.tangem.datasource.local.visa.entity.TangemPayTxHistoryItemToDMConverter
|
||||
import com.tangem.datasource.local.visa.entity.TangemPayTxHistoryItemToDomainConverter
|
||||
import com.tangem.domain.visa.model.TangemPayTxHistoryItem
|
||||
import com.tangem.test.core.datastore.MockStateDataStore
|
||||
import kotlinx.coroutines.test.runTest
|
||||
import org.joda.time.DateTime
|
||||
import org.junit.jupiter.api.BeforeEach
|
||||
import org.junit.jupiter.api.Test
|
||||
import org.junit.jupiter.api.TestInstance
|
||||
import java.math.BigDecimal
|
||||
import java.util.Currency
|
||||
|
||||
@TestInstance(TestInstance.Lifecycle.PER_CLASS)
|
||||
internal class DefaultTangemPayTxHistoryItemsStoreTest {
|
||||
|
||||
private lateinit var store: DefaultTangemPayTxHistoryItemsStore
|
||||
|
||||
@BeforeEach
|
||||
fun setup() {
|
||||
store = DefaultTangemPayTxHistoryItemsStore(
|
||||
dataStore = MockStateDataStore(default = emptyMap()),
|
||||
toDMConverter = TangemPayTxHistoryItemToDMConverter(),
|
||||
toDomainConverter = TangemPayTxHistoryItemToDomainConverter(),
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `GIVEN empty store WHEN getSyncOrNull THEN returns null`() = runTest {
|
||||
val result = store.getSyncOrNull(key = WALLET_A, cursor = CURSOR_1)
|
||||
|
||||
assertThat(result).isNull()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `GIVEN stored page WHEN getSyncOrNull with same key and cursor THEN returns page`() = runTest {
|
||||
// Arrange
|
||||
val page = listOf(payment("1"), payment("2"))
|
||||
store.store(key = WALLET_A, cursor = CURSOR_1, value = page)
|
||||
|
||||
// Act
|
||||
val result = store.getSyncOrNull(key = WALLET_A, cursor = CURSOR_1)
|
||||
|
||||
// Assert
|
||||
assertThat(result).isEqualTo(page)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `GIVEN page stored under one cursor WHEN getSyncOrNull with another cursor THEN returns null`() = runTest {
|
||||
// Arrange
|
||||
store.store(key = WALLET_A, cursor = CURSOR_1, value = listOf(payment("1")))
|
||||
|
||||
// Act
|
||||
val result = store.getSyncOrNull(key = WALLET_A, cursor = CURSOR_2)
|
||||
|
||||
// Assert
|
||||
assertThat(result).isNull()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `GIVEN two cursors stored for one wallet WHEN getSyncOrNull THEN both pages are kept`() = runTest {
|
||||
// Arrange
|
||||
val page1 = listOf(payment("1"))
|
||||
val page2 = listOf(payment("2"))
|
||||
store.store(key = WALLET_A, cursor = CURSOR_1, value = page1)
|
||||
store.store(key = WALLET_A, cursor = CURSOR_2, value = page2)
|
||||
|
||||
// Assert
|
||||
assertThat(store.getSyncOrNull(WALLET_A, CURSOR_1)).isEqualTo(page1)
|
||||
assertThat(store.getSyncOrNull(WALLET_A, CURSOR_2)).isEqualTo(page2)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `GIVEN entries for two wallets WHEN remove one wallet THEN only that wallet is cleared`() = runTest {
|
||||
// Arrange
|
||||
store.store(key = WALLET_A, cursor = CURSOR_1, value = listOf(payment("a")))
|
||||
store.store(key = WALLET_B, cursor = CURSOR_1, value = listOf(payment("b")))
|
||||
|
||||
// Act
|
||||
store.remove(WALLET_A)
|
||||
|
||||
// Assert
|
||||
assertThat(store.getSyncOrNull(WALLET_A, CURSOR_1)).isNull()
|
||||
assertThat(store.getSyncOrNull(WALLET_B, CURSOR_1)).isEqualTo(listOf(payment("b")))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `GIVEN entries for two wallets WHEN remove both in one call THEN both are cleared`() = runTest {
|
||||
// Arrange
|
||||
store.store(key = WALLET_A, cursor = CURSOR_1, value = listOf(payment("a")))
|
||||
store.store(key = WALLET_B, cursor = CURSOR_1, value = listOf(payment("b")))
|
||||
|
||||
// Act
|
||||
store.remove(listOf(WALLET_A, WALLET_B))
|
||||
|
||||
// Assert
|
||||
assertThat(store.getSyncOrNull(WALLET_A, CURSOR_1)).isNull()
|
||||
assertThat(store.getSyncOrNull(WALLET_B, CURSOR_1)).isNull()
|
||||
}
|
||||
|
||||
private fun payment(id: String) = TangemPayTxHistoryItem.Payment(
|
||||
id = id,
|
||||
jsonRepresentation = "{}",
|
||||
date = DateTime(DATE_MILLIS),
|
||||
amount = BigDecimal("1.00"),
|
||||
currency = Currency.getInstance("USD"),
|
||||
transactionHash = null,
|
||||
)
|
||||
|
||||
private companion object {
|
||||
const val WALLET_A = "wallet-a"
|
||||
const val WALLET_B = "wallet-b"
|
||||
const val CURSOR_1 = "cursor-1"
|
||||
const val CURSOR_2 = "cursor-2"
|
||||
const val DATE_MILLIS = 1_700_000_000_000L
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,122 @@
|
|||
package com.tangem.datasource.local.visa
|
||||
|
||||
import com.google.common.truth.Truth.assertThat
|
||||
import com.tangem.datasource.local.visa.entity.TangemPayTxHistoryItemDM
|
||||
import com.tangem.datasource.utils.KotlinxDataStoreSerializer
|
||||
import kotlinx.serialization.builtins.ListSerializer
|
||||
import kotlinx.serialization.builtins.MapSerializer
|
||||
import kotlinx.serialization.builtins.serializer
|
||||
import org.joda.time.DateTime
|
||||
import org.junit.jupiter.api.Test
|
||||
import org.junit.jupiter.api.TestInstance
|
||||
import java.math.BigDecimal
|
||||
import java.util.Currency
|
||||
|
||||
@TestInstance(TestInstance.Lifecycle.PER_CLASS)
|
||||
internal class TangemPayTxHistoryStoreSerializationTest {
|
||||
|
||||
private val json = KotlinxDataStoreSerializer.jsonBuilder { classDiscriminator = "__type" }
|
||||
|
||||
private val serializer = MapSerializer(
|
||||
keySerializer = String.serializer(),
|
||||
valueSerializer = MapSerializer(
|
||||
keySerializer = String.serializer(),
|
||||
valueSerializer = ListSerializer(TangemPayTxHistoryItemDM.serializer()),
|
||||
),
|
||||
)
|
||||
|
||||
@Test
|
||||
fun `GIVEN all tx history item types WHEN serialized and deserialized THEN value is preserved`() {
|
||||
// Arrange
|
||||
val original: Map<String, Map<String, List<TangemPayTxHistoryItemDM>>> = mapOf(
|
||||
"wallet-1" to mapOf(
|
||||
"initial_cursor_key" to listOf(spend(), payment(), fee(), collateral()),
|
||||
),
|
||||
)
|
||||
|
||||
// Act
|
||||
val restored = json.decodeFromString(serializer, json.encodeToString(serializer, original))
|
||||
|
||||
// Assert
|
||||
assertThat(restored).isEqualTo(original)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `GIVEN Collateral with type field WHEN serialized and deserialized THEN discriminator does not clash`() {
|
||||
// Arrange
|
||||
val original: Map<String, Map<String, List<TangemPayTxHistoryItemDM>>> = mapOf(
|
||||
"wallet-1" to mapOf("cursor" to listOf(collateral())),
|
||||
)
|
||||
|
||||
// Act
|
||||
val restored = json.decodeFromString(serializer, json.encodeToString(serializer, original))
|
||||
|
||||
// Assert
|
||||
assertThat(restored).isEqualTo(original)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `GIVEN tx history item WHEN serialized THEN uses stable SerialName under non-clashing discriminator`() {
|
||||
// Arrange
|
||||
val original: Map<String, Map<String, List<TangemPayTxHistoryItemDM>>> = mapOf(
|
||||
"wallet-1" to mapOf("cursor" to listOf(collateral())),
|
||||
)
|
||||
|
||||
// Act
|
||||
val encoded = json.encodeToString(serializer, original)
|
||||
|
||||
// Assert
|
||||
assertThat(encoded).contains("\"__type\":\"collateral\"")
|
||||
}
|
||||
|
||||
private fun spend() = TangemPayTxHistoryItemDM.Spend(
|
||||
id = "spend-1",
|
||||
jsonRepresentation = "{}",
|
||||
date = DateTime(DATE_MILLIS),
|
||||
amount = BigDecimal("12.34"),
|
||||
currency = Currency.getInstance("USD"),
|
||||
authorizedAmount = BigDecimal("12.34"),
|
||||
localAmount = BigDecimal("11.00"),
|
||||
localCurrency = Currency.getInstance("EUR"),
|
||||
enrichedMerchantName = "Coffee Co",
|
||||
merchantName = "COFFEE CO",
|
||||
enrichedMerchantCategory = "Food",
|
||||
merchantCategoryCode = "5814",
|
||||
merchantCategory = "restaurants",
|
||||
status = TangemPayTxHistoryItemDM.Status.COMPLETED,
|
||||
enrichedMerchantIconUrl = "https://example.com/icon.png",
|
||||
declinedReason = null,
|
||||
)
|
||||
|
||||
private fun payment() = TangemPayTxHistoryItemDM.Payment(
|
||||
id = "payment-1",
|
||||
jsonRepresentation = "{}",
|
||||
date = DateTime(DATE_MILLIS),
|
||||
amount = BigDecimal("100.00"),
|
||||
currency = Currency.getInstance("USD"),
|
||||
transactionHash = "0xabc",
|
||||
)
|
||||
|
||||
private fun fee() = TangemPayTxHistoryItemDM.Fee(
|
||||
id = "fee-1",
|
||||
jsonRepresentation = "{}",
|
||||
date = DateTime(DATE_MILLIS),
|
||||
amount = BigDecimal("0.50"),
|
||||
currency = Currency.getInstance("USD"),
|
||||
description = "network fee",
|
||||
)
|
||||
|
||||
private fun collateral() = TangemPayTxHistoryItemDM.Collateral(
|
||||
id = "collateral-1",
|
||||
jsonRepresentation = "{}",
|
||||
date = DateTime(DATE_MILLIS),
|
||||
amount = BigDecimal("250.00"),
|
||||
currency = Currency.getInstance("USD"),
|
||||
transactionHash = "0xdef",
|
||||
type = TangemPayTxHistoryItemDM.Type.Deposit,
|
||||
)
|
||||
|
||||
private companion object {
|
||||
const val DATE_MILLIS = 1_700_000_000_000L
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,103 @@
|
|||
package com.tangem.datasource.local.visa.entity
|
||||
|
||||
import com.google.common.truth.Truth.assertThat
|
||||
import com.tangem.domain.visa.model.TangemPayTxHistoryItem
|
||||
import com.tangem.test.core.ProvideTestModels
|
||||
import org.joda.time.DateTime
|
||||
import org.junit.jupiter.api.TestInstance
|
||||
import org.junit.jupiter.params.ParameterizedTest
|
||||
import org.junit.jupiter.params.provider.EnumSource
|
||||
import java.math.BigDecimal
|
||||
import java.util.Currency
|
||||
|
||||
@TestInstance(TestInstance.Lifecycle.PER_CLASS)
|
||||
internal class TangemPayTxHistoryItemDMConverterTest {
|
||||
|
||||
private val toDMConverter = TangemPayTxHistoryItemToDMConverter()
|
||||
private val toDomainConverter = TangemPayTxHistoryItemToDomainConverter()
|
||||
|
||||
@ParameterizedTest
|
||||
@ProvideTestModels
|
||||
fun `GIVEN every item subtype WHEN converted to DM and back THEN all fields preserved`(
|
||||
item: TangemPayTxHistoryItem,
|
||||
) {
|
||||
assertRoundTrip(item)
|
||||
}
|
||||
|
||||
@ParameterizedTest
|
||||
@EnumSource(TangemPayTxHistoryItem.Status::class)
|
||||
fun `GIVEN every Spend status WHEN converted to DM and back THEN status preserved`(
|
||||
status: TangemPayTxHistoryItem.Status,
|
||||
) {
|
||||
assertRoundTrip(spend().copy(status = status))
|
||||
}
|
||||
|
||||
@ParameterizedTest
|
||||
@EnumSource(TangemPayTxHistoryItem.Type::class)
|
||||
fun `GIVEN every Collateral type WHEN converted to DM and back THEN type preserved`(
|
||||
type: TangemPayTxHistoryItem.Type,
|
||||
) {
|
||||
assertRoundTrip(collateral().copy(type = type))
|
||||
}
|
||||
|
||||
private fun assertRoundTrip(item: TangemPayTxHistoryItem) {
|
||||
// Act
|
||||
val restored = toDomainConverter.convert(toDMConverter.convert(item))
|
||||
|
||||
// Assert
|
||||
assertThat(restored).isEqualTo(item)
|
||||
}
|
||||
|
||||
private fun provideTestModels() = listOf(spend(), payment(), fee(), collateral())
|
||||
|
||||
private fun spend() = TangemPayTxHistoryItem.Spend(
|
||||
id = "spend-1",
|
||||
jsonRepresentation = "{}",
|
||||
date = DateTime(DATE_MILLIS),
|
||||
amount = BigDecimal("12.34"),
|
||||
currency = Currency.getInstance("USD"),
|
||||
authorizedAmount = BigDecimal("12.34"),
|
||||
localAmount = BigDecimal("11.00"),
|
||||
localCurrency = Currency.getInstance("EUR"),
|
||||
enrichedMerchantName = "Coffee Co",
|
||||
merchantName = "COFFEE CO",
|
||||
enrichedMerchantCategory = "Food",
|
||||
merchantCategoryCode = "5814",
|
||||
merchantCategory = "restaurants",
|
||||
status = TangemPayTxHistoryItem.Status.COMPLETED,
|
||||
enrichedMerchantIconUrl = "https://example.com/icon.png",
|
||||
declinedReason = null,
|
||||
)
|
||||
|
||||
private fun payment() = TangemPayTxHistoryItem.Payment(
|
||||
id = "payment-1",
|
||||
jsonRepresentation = "{}",
|
||||
date = DateTime(DATE_MILLIS),
|
||||
amount = BigDecimal("100.00"),
|
||||
currency = Currency.getInstance("USD"),
|
||||
transactionHash = "0xabc",
|
||||
)
|
||||
|
||||
private fun fee() = TangemPayTxHistoryItem.Fee(
|
||||
id = "fee-1",
|
||||
jsonRepresentation = "{}",
|
||||
date = DateTime(DATE_MILLIS),
|
||||
amount = BigDecimal("0.50"),
|
||||
currency = Currency.getInstance("USD"),
|
||||
description = "network fee",
|
||||
)
|
||||
|
||||
private fun collateral() = TangemPayTxHistoryItem.Collateral(
|
||||
id = "collateral-1",
|
||||
jsonRepresentation = "{}",
|
||||
date = DateTime(DATE_MILLIS),
|
||||
amount = BigDecimal("250.00"),
|
||||
currency = Currency.getInstance("USD"),
|
||||
transactionHash = "0xdef",
|
||||
type = TangemPayTxHistoryItem.Type.Deposit,
|
||||
)
|
||||
|
||||
private companion object {
|
||||
const val DATE_MILLIS = 1_700_000_000_000L
|
||||
}
|
||||
}
|
||||
|
|
@ -10,12 +10,25 @@ android {
|
|||
}
|
||||
|
||||
dependencies {
|
||||
api(projects.core.utils)
|
||||
|
||||
api(deps.decompose)
|
||||
api(deps.androidx.appCompat)
|
||||
implementation(deps.kotlin.coroutines)
|
||||
|
||||
// region DI
|
||||
implementation(deps.hilt.core)
|
||||
kapt(deps.hilt.kapt)
|
||||
// endregion
|
||||
|
||||
// region Kotlin
|
||||
api(deps.kotlin.coroutines)
|
||||
// endregion
|
||||
|
||||
// region AndroidX
|
||||
api(deps.androidx.appCompat)
|
||||
// endregion
|
||||
|
||||
// region Other libraries
|
||||
api(deps.decompose)
|
||||
// endregion
|
||||
|
||||
// region Core modules
|
||||
api(projects.core.utils)
|
||||
// endregion
|
||||
}
|
||||
|
|
@ -9,8 +9,13 @@ android {
|
|||
}
|
||||
|
||||
dependencies {
|
||||
api(projects.core.error)
|
||||
|
||||
implementation(tangemDeps.card.core)
|
||||
implementation(tangemDeps.blockchain)
|
||||
// region Tangem
|
||||
api(tangemDeps.blockchain)
|
||||
api(tangemDeps.card.core)
|
||||
// endregion
|
||||
|
||||
// region Core modules
|
||||
api(projects.core.error)
|
||||
// endregion
|
||||
}
|
||||
|
|
@ -1,8 +1,6 @@
|
|||
plugins {
|
||||
alias(deps.plugins.android.library)
|
||||
alias(deps.plugins.kotlin.android)
|
||||
alias(deps.plugins.kotlin.kapt)
|
||||
alias(deps.plugins.hilt.android)
|
||||
id("configuration")
|
||||
}
|
||||
|
||||
|
|
@ -11,10 +9,16 @@ android {
|
|||
}
|
||||
|
||||
dependencies {
|
||||
|
||||
// region Core modules
|
||||
implementation(projects.core.utils)
|
||||
// endregion
|
||||
|
||||
// region DI
|
||||
implementation(deps.hilt.android)
|
||||
kapt(deps.hilt.kapt)
|
||||
// endregion
|
||||
|
||||
implementation(deps.material)
|
||||
// region AndroidX
|
||||
implementation(deps.androidx.core)
|
||||
// endregion
|
||||
}
|
||||
|
|
@ -0,0 +1,6 @@
|
|||
package com.tangem.core.navigation.url
|
||||
|
||||
interface AppStoreOpener {
|
||||
|
||||
fun openStorePage()
|
||||
}
|
||||
|
|
@ -0,0 +1,50 @@
|
|||
package com.tangem.core.navigation.url
|
||||
|
||||
import android.content.ActivityNotFoundException
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import androidx.core.net.toUri
|
||||
import com.tangem.utils.buildConfig.AppConfigurationProvider
|
||||
import com.tangem.utils.logging.TangemLogger
|
||||
import dagger.hilt.android.qualifiers.ApplicationContext
|
||||
import javax.inject.Inject
|
||||
|
||||
class DefaultAppStoreOpener @Inject constructor(
|
||||
@ApplicationContext private val context: Context,
|
||||
private val appConfigurationProvider: AppConfigurationProvider,
|
||||
) : AppStoreOpener {
|
||||
|
||||
override fun openStorePage() {
|
||||
val storeUri: String
|
||||
val webUrl: String
|
||||
if (appConfigurationProvider.isHuawei()) {
|
||||
storeUri = "$HUAWEI_STORE_SCHEME$STORE_PACKAGE_NAME"
|
||||
webUrl = "$HUAWEI_WEB_URL$STORE_PACKAGE_NAME"
|
||||
} else {
|
||||
storeUri = "$GOOGLE_STORE_SCHEME$STORE_PACKAGE_NAME"
|
||||
webUrl = "$GOOGLE_WEB_URL$STORE_PACKAGE_NAME"
|
||||
}
|
||||
|
||||
openUri(storeUri) || openUri(webUrl)
|
||||
}
|
||||
|
||||
private fun openUri(uri: String): Boolean {
|
||||
return try {
|
||||
context.startActivity(
|
||||
Intent(Intent.ACTION_VIEW, uri.toUri()).addFlags(Intent.FLAG_ACTIVITY_NEW_TASK),
|
||||
)
|
||||
true
|
||||
} catch (e: ActivityNotFoundException) {
|
||||
TangemLogger.e("Unable to open store uri: $uri", e)
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
private companion object {
|
||||
const val STORE_PACKAGE_NAME = "com.tangem.wallet"
|
||||
const val GOOGLE_STORE_SCHEME = "market://details?id="
|
||||
const val GOOGLE_WEB_URL = "https://play.google.com/store/apps/details?id="
|
||||
const val HUAWEI_STORE_SCHEME = "appmarket://details?id="
|
||||
const val HUAWEI_WEB_URL = "https://appgallery.huawei.com/app/"
|
||||
}
|
||||
}
|
||||
|
|
@ -4,7 +4,7 @@ plugins {
|
|||
}
|
||||
|
||||
dependencies {
|
||||
// region Coroutines
|
||||
implementation(deps.kotlin.coroutines)
|
||||
// region Kotlin
|
||||
api(deps.kotlin.coroutines)
|
||||
// endregion
|
||||
}
|
||||
|
|
@ -10,12 +10,17 @@ android {
|
|||
|
||||
dependencies {
|
||||
|
||||
implementation(projects.core.utils)
|
||||
// region AndroidX
|
||||
api(deps.androidx.annotation)
|
||||
// endregion
|
||||
|
||||
|
||||
// region Firebase libraries
|
||||
// region Firebase
|
||||
implementation(platform(deps.firebase.bom))
|
||||
implementation(deps.firebase.analytics)
|
||||
implementation(deps.firebase.crashlytics)
|
||||
// endregion
|
||||
|
||||
// region Core modules
|
||||
implementation(projects.core.utils)
|
||||
// endregion
|
||||
}
|
||||
|
|
@ -256,7 +256,7 @@
|
|||
<string name="common_action_failed">%s falló</string>
|
||||
<string name="common_activate">Activar</string>
|
||||
<string name="common_add">Agregar</string>
|
||||
<string name="common_add_funds">Agregar fondos</string>
|
||||
<string name="common_add_funds">Depositar</string>
|
||||
<string name="common_add_to_portfolio">Añadir al portafolio</string>
|
||||
<string name="common_add_token">Agregar token</string>
|
||||
<string name="common_add_tokens">Añada tokens</string>
|
||||
|
|
@ -347,8 +347,9 @@
|
|||
<string name="common_from">De</string>
|
||||
<string name="common_from_wallet_name">De %s</string>
|
||||
<string name="common_generate_addresses">Sincronizar direcciones</string>
|
||||
<string name="common_get">Comprar</string>
|
||||
<string name="common_get_started">Comenzar</string>
|
||||
<string name="common_get_token">Obtener token</string>
|
||||
<string name="common_get_token">Comprar token</string>
|
||||
<string name="common_go_to_provider">Ir al proveedor</string>
|
||||
<string name="common_go_to_token">Ir al token</string>
|
||||
<string name="common_got_it">Entendido</string>
|
||||
|
|
@ -1241,6 +1242,8 @@
|
|||
<string name="onramp_title_available_from">Disponible desde</string>
|
||||
<string name="onramp_title_available_up_to">Disponible hasta</string>
|
||||
<string name="onramp_title_you_get">Obtiene</string>
|
||||
<string name="onramp_token_is_not_supported_banner_subtitle">Este token no está soportado. Por favor, elija otro token para comprar.</string>
|
||||
<string name="onramp_token_is_not_supported_banner_title">%s no está soportado</string>
|
||||
<string name="onramp_tos_external_providers">El servicio es proporcionado por un proveedor externo. \nTangem no es responsable.</string>
|
||||
<string name="onramp_transaction_status_footer_text">Puede comprobar el estado de la transacción desde la página detallada del token</string>
|
||||
<string name="onramp_up_to_rate">Hasta</string>
|
||||
|
|
@ -2505,7 +2508,7 @@
|
|||
<string name="yield_module_approve_sheet_fee_note">Se le descontará la comisión y se volverán a suministrar sus activos.</string>
|
||||
<string name="yield_module_approve_sheet_subtitle">Para seguir generando rendimiento, se requiere aprobación.</string>
|
||||
<string name="yield_module_approve_sheet_title">Confirmar aprobación</string>
|
||||
<string name="yield_module_average_apy">APY promedio %1$s%%</string>
|
||||
<string name="yield_module_average_apy">APY actual %1$s%%</string>
|
||||
<string name="yield_module_balance_info_sheet_subtitle">Sus fondos se suministran actualmente al protocolo Aave, pero puede gestionarlos en cualquier momento.</string>
|
||||
<string name="yield_module_balance_info_sheet_title">Su %s está depositado en Aave</string>
|
||||
<string name="yield_module_chart_loading_error">No se puede cargar el gráfico...</string>
|
||||
|
|
|
|||
|
|
@ -2011,6 +2011,7 @@
|
|||
<string name="tangempay_reissue_card_insufficient_funds_subtitle">Внесите USDC на счёт, чтобы покрыть комиссию</string>
|
||||
<string name="tangempay_reissue_card_insufficient_funds_title">Невозможно покрыть комиссию</string>
|
||||
<string name="tangempay_reissue_card_title">Перевыпустить карту?</string>
|
||||
<string name="tangempay_remove_account">Удалить аккаунт</string>
|
||||
<string name="tangempay_service_unavailable_description">Мы устраняем техническую проблему. Пожалуйста, попробуйте позже.</string>
|
||||
<string name="tangempay_service_unavailable_title">Сервис временно недоступен</string>
|
||||
<string name="tangempay_service_unreachable_try_later">Не можем показать данные карты, но оплаты продолжают работать.</string>
|
||||
|
|
@ -2520,7 +2521,7 @@
|
|||
<string name="yield_module_approve_sheet_fee_note">Комиссия будет списана, и ваши активы снова начнут приносить доход.</string>
|
||||
<string name="yield_module_approve_sheet_subtitle">Чтобы продолжить зарабатывать, нужно выдать разрешение.</string>
|
||||
<string name="yield_module_approve_sheet_title">Подтвердить разрешение</string>
|
||||
<string name="yield_module_average_apy">Средний APY %1$s%%</string>
|
||||
<string name="yield_module_average_apy">Текущий APY %1$s%%</string>
|
||||
<string name="yield_module_balance_info_sheet_subtitle">Ваши средства в данный момент размещены в протоколе Aave, но вы можете воспользоваться ими в любое время.</string>
|
||||
<string name="yield_module_balance_info_sheet_title">Ваш %s внесён в Aave</string>
|
||||
<string name="yield_module_chart_loading_error">Невозможно загрузить график</string>
|
||||
|
|
|
|||
|
|
@ -928,6 +928,7 @@
|
|||
<string name="markets_portfolio_block_subtitle">In your portfolio</string>
|
||||
<string name="markets_portfolio_block_title">Your portfolio</string>
|
||||
<string name="markets_portfolio_block_token_unsupported">**Token not supported**. This token is currently not supported in the wallet</string>
|
||||
<string name="markets_portfolio_eligible_block_title">Other eligible tokens</string>
|
||||
<string name="markets_pulse_common_title">Market Pulse</string>
|
||||
<string name="markets_quick_actions">Quick actions</string>
|
||||
<string name="markets_search_clear_all_hints">Clear all</string>
|
||||
|
|
@ -2624,7 +2625,7 @@
|
|||
<string name="yield_module_approve_sheet_fee_note">The fee will be deducted, and your assets will be resupplied.</string>
|
||||
<string name="yield_module_approve_sheet_subtitle">To continue generating yield, approval is required.</string>
|
||||
<string name="yield_module_approve_sheet_title">Confirm approval</string>
|
||||
<string name="yield_module_average_apy">Average APY %1$s%%</string>
|
||||
<string name="yield_module_average_apy">Current APY %1$s%%</string>
|
||||
<string name="yield_module_balance_info_sheet_subtitle">Your funds are currently supplied to the Aave protocol, but you can manage them at any time.</string>
|
||||
<string name="yield_module_balance_info_sheet_title">Your %s is supplied to Aave</string>
|
||||
<string name="yield_module_chart_loading_error">Unable to load chart...</string>
|
||||
|
|
|
|||
|
|
@ -1,3 +1,4 @@
|
|||
import java.io.ByteArrayOutputStream
|
||||
import java.security.MessageDigest
|
||||
|
||||
plugins {
|
||||
|
|
@ -61,15 +62,15 @@ abstract class VerifyDesignTokensTask : DefaultTask() {
|
|||
|
||||
require(actual == expected) {
|
||||
"Design tokens are out of date!\n" +
|
||||
" ds-tokens hash: $actual\n" +
|
||||
" generated hash: $expected\n" +
|
||||
" computed from ds-tokens: $actual\n" +
|
||||
" committed .tokens-hash: $expected\n" +
|
||||
"Run the token generator: cd core/ui/token-gen && npm run build"
|
||||
}
|
||||
|
||||
stampFile.get().asFile.writeText(actual)
|
||||
}
|
||||
|
||||
private fun hashTreeHex(root: java.io.File, extension: String): String {
|
||||
private fun hashTreeHex(root: File, extension: String): String {
|
||||
val digest = MessageDigest.getInstance("SHA-256")
|
||||
val files = root.walkTopDown()
|
||||
.filter { it.isFile && it.extension == extension }
|
||||
|
|
@ -79,12 +80,27 @@ abstract class VerifyDesignTokensTask : DefaultTask() {
|
|||
for (file in files) {
|
||||
digest.update(file.relativeTo(root).invariantSeparatorsPath.toByteArray())
|
||||
digest.update(nul)
|
||||
digest.update(file.readBytes())
|
||||
digest.update(file.readBytes().stripCr())
|
||||
digest.update(nul)
|
||||
}
|
||||
return digest.digest()
|
||||
.joinToString("") { b: Byte -> b.toInt().and(0xFF).toString(16).padStart(2, '0') }
|
||||
}
|
||||
|
||||
/**
|
||||
* Strips CR (0x0D) bytes so the token hash ignores CRLF vs LF line endings. The ds-tokens
|
||||
* submodule is not covered by this repo's .gitattributes, so its .json/.svg sources may be
|
||||
* checked out with CRLF on some platforms; without this the verification is non-deterministic.
|
||||
* Removes lone CRs too — safe for UTF-8 sources, where 0x0D never appears inside a
|
||||
* multi-byte sequence. Must stay byte-for-byte identical to stripCr() in token-gen/hash-util.mjs.
|
||||
*/
|
||||
private fun ByteArray.stripCr(): ByteArray {
|
||||
val cr: Byte = 0x0D
|
||||
if (none { it == cr }) return this
|
||||
val out = ByteArrayOutputStream(size)
|
||||
for (b in this) if (b != cr) out.write(b.toInt())
|
||||
return out.toByteArray()
|
||||
}
|
||||
}
|
||||
android {
|
||||
namespace = "com.tangem.core.ui"
|
||||
|
|
@ -109,51 +125,53 @@ tasks.named("preBuild") {
|
|||
}
|
||||
|
||||
dependencies {
|
||||
/** Project - Domain */
|
||||
implementation(projects.domain.appTheme.models)
|
||||
implementation(projects.domain.express.models)
|
||||
implementation(projects.domain.models)
|
||||
implementation(projects.domain.tokens.models)
|
||||
|
||||
/** Project - Core */
|
||||
implementation(projects.core.res)
|
||||
implementation(projects.core.utils)
|
||||
api(projects.core.decompose)
|
||||
implementation(projects.core.error)
|
||||
// region DI
|
||||
implementation(deps.hilt.android)
|
||||
// endregion
|
||||
|
||||
/** AndroidX libraries */
|
||||
implementation(deps.androidx.fragment.ktx)
|
||||
// region Kotlin
|
||||
api(deps.kotlin.coroutines)
|
||||
api(deps.kotlin.immutable.collections)
|
||||
api(deps.kotlin.serialization.core)
|
||||
// endregion
|
||||
|
||||
// region Compose
|
||||
implementation(deps.compose.accompanist.permission)
|
||||
api(deps.compose.accompanist.systemUiController)
|
||||
api(deps.compose.coil)
|
||||
implementation(deps.compose.constraintLayout)
|
||||
api(deps.compose.foundation)
|
||||
api(deps.compose.material3)
|
||||
api(deps.compose.paging)
|
||||
api(deps.compose.reorderable)
|
||||
api(deps.compose.shimmer)
|
||||
implementation(deps.compose.ui.tooling)
|
||||
implementation(deps.compose.ui.utils)
|
||||
// endregion
|
||||
|
||||
// region AndroidX
|
||||
api(deps.androidx.activity)
|
||||
implementation(deps.androidx.activity.compose)
|
||||
implementation(deps.androidx.annotation)
|
||||
implementation(deps.androidx.appCompat)
|
||||
implementation(deps.androidx.core)
|
||||
implementation(deps.androidx.core.ktx)
|
||||
implementation(deps.androidx.paging.runtime)
|
||||
implementation(deps.lifecycle.runtime.ktx)
|
||||
implementation(deps.androidx.palette)
|
||||
api(deps.androidx.palette)
|
||||
implementation(deps.androidx.savedState)
|
||||
implementation(deps.androidx.windowManager) {
|
||||
exclude(
|
||||
deps.kotlin.coroutines.android.get().module.group,
|
||||
deps.kotlin.coroutines.android.get().module.name
|
||||
deps.kotlin.coroutines.android.get().module.name,
|
||||
)
|
||||
}
|
||||
api(deps.lifecycle.compose)
|
||||
api(deps.lifecycle.runtime.ktx)
|
||||
// endregion
|
||||
|
||||
/** Compose */
|
||||
implementation(deps.compose.constraintLayout)
|
||||
implementation(deps.compose.foundation)
|
||||
implementation(deps.compose.material3)
|
||||
implementation(deps.compose.paging)
|
||||
implementation(deps.compose.ui.tooling)
|
||||
implementation(deps.compose.ui.utils)
|
||||
implementation(deps.compose.coil)
|
||||
implementation(deps.compose.navigation)
|
||||
implementation(deps.compose.navigation.hilt)
|
||||
api(deps.compose.reorderable)
|
||||
|
||||
/** Other libraries */
|
||||
implementation(deps.compose.accompanist.systemUiController)
|
||||
implementation(deps.compose.accompanist.permission)
|
||||
implementation(deps.material)
|
||||
implementation(deps.compose.shimmer)
|
||||
implementation(deps.kotlin.immutable.collections)
|
||||
implementation(deps.zxing.qrCore)
|
||||
api(deps.jodatime)
|
||||
implementation(deps.markdown)
|
||||
// region Other libraries
|
||||
api(deps.arrow.core)
|
||||
api(deps.haze) {
|
||||
exclude(module = "activity-compose")
|
||||
exclude(module = "activity")
|
||||
|
|
@ -164,9 +182,28 @@ dependencies {
|
|||
exclude(module = "activity")
|
||||
exclude(module = "activity-ktx")
|
||||
}
|
||||
api(deps.jodatime)
|
||||
api(deps.markdown)
|
||||
implementation(deps.material)
|
||||
implementation(deps.zxing.qrCore)
|
||||
// endregion
|
||||
|
||||
/** Tests */
|
||||
// region Core modules
|
||||
api(projects.core.decompose)
|
||||
api(projects.core.error)
|
||||
implementation(projects.core.res)
|
||||
implementation(projects.core.utils)
|
||||
// endregion
|
||||
|
||||
// region Domain models
|
||||
api(projects.domain.appTheme.models)
|
||||
api(projects.domain.express.models)
|
||||
api(projects.domain.models)
|
||||
// endregion
|
||||
|
||||
// region Tests
|
||||
testImplementation(deps.test.junit5)
|
||||
testImplementation(deps.test.mockk)
|
||||
testImplementation(deps.test.truth)
|
||||
testImplementation(deps.test.junit5)
|
||||
// endregion
|
||||
}
|
||||
|
|
@ -1 +1 @@
|
|||
Subproject commit 42aac70c1d2d0d636470fa476703cab353010bba
|
||||
Subproject commit bfd2053b1f6d1ffc051d537e41e6bc386a34ad5f
|
||||
|
|
@ -32,7 +32,15 @@ import com.tangem.core.ui.res.TangemThemePreview
|
|||
import com.tangem.core.ui.res.TangemThemePreviewRedesign
|
||||
|
||||
enum class AccountIconSize {
|
||||
Default, Large, Medium, Small, ExtraSmall, RedesignedDefault, RedesignExtraSmall
|
||||
Default,
|
||||
Large,
|
||||
Medium,
|
||||
Small,
|
||||
ExtraSmall,
|
||||
RedesignedDefault,
|
||||
RedesignExtraSmall,
|
||||
ContactLarge,
|
||||
ContactDefault,
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -132,6 +140,8 @@ fun AccountCharIcon(char: Char, color: Color, size: AccountIconSize, modifier: M
|
|||
AccountIconSize.ExtraSmall -> TangemTheme.typography.caption1
|
||||
AccountIconSize.RedesignedDefault -> TangemTheme.typography2.headingSemibold28
|
||||
AccountIconSize.RedesignExtraSmall -> TangemTheme.typography2.captionMedium11
|
||||
AccountIconSize.ContactLarge -> TangemTheme.typography3.heading.medium
|
||||
AccountIconSize.ContactDefault -> TangemTheme.typography3.body.medium
|
||||
}
|
||||
|
||||
val textSize by animateFloatAsState(
|
||||
|
|
@ -166,6 +176,8 @@ private fun AccountIconSize.iconSizeInDp(): Dp = when (this) {
|
|||
AccountIconSize.ExtraSmall -> 8.dp
|
||||
AccountIconSize.RedesignedDefault -> 20.dp
|
||||
AccountIconSize.RedesignExtraSmall -> 8.dp
|
||||
AccountIconSize.ContactLarge -> 32.dp
|
||||
AccountIconSize.ContactDefault -> 20.dp
|
||||
}
|
||||
|
||||
fun AccountIconSize.toBoxSize(): Dp = when (this) {
|
||||
|
|
@ -176,6 +188,8 @@ fun AccountIconSize.toBoxSize(): Dp = when (this) {
|
|||
AccountIconSize.ExtraSmall -> 14.dp
|
||||
AccountIconSize.RedesignedDefault -> 40.dp
|
||||
AccountIconSize.RedesignExtraSmall -> 16.dp
|
||||
AccountIconSize.ContactLarge -> 80.dp
|
||||
AccountIconSize.ContactDefault -> 40.dp
|
||||
}
|
||||
|
||||
private fun AccountIconSize.boxShapeSizeInDp(): Dp = when (this) {
|
||||
|
|
@ -186,6 +200,8 @@ private fun AccountIconSize.boxShapeSizeInDp(): Dp = when (this) {
|
|||
AccountIconSize.ExtraSmall -> 4.dp
|
||||
AccountIconSize.RedesignedDefault -> 12.dp
|
||||
AccountIconSize.RedesignExtraSmall -> 6.dp
|
||||
AccountIconSize.ContactLarge -> 80.dp
|
||||
AccountIconSize.ContactDefault -> 100.dp
|
||||
}
|
||||
|
||||
@Preview(showBackground = true)
|
||||
|
|
@ -228,7 +244,9 @@ private fun Sample() {
|
|||
AccountIconSize.Small -> AccountIconSize.ExtraSmall
|
||||
AccountIconSize.ExtraSmall -> AccountIconSize.RedesignedDefault
|
||||
AccountIconSize.RedesignedDefault -> AccountIconSize.RedesignExtraSmall
|
||||
AccountIconSize.RedesignExtraSmall -> AccountIconSize.Default
|
||||
AccountIconSize.RedesignExtraSmall -> AccountIconSize.ContactLarge
|
||||
AccountIconSize.ContactLarge -> AccountIconSize.ContactDefault
|
||||
AccountIconSize.ContactDefault -> AccountIconSize.Default
|
||||
}
|
||||
}) { Text("Change") }
|
||||
|
||||
|
|
|
|||
|
|
@ -36,12 +36,10 @@ import com.tangem.core.ui.components.bottomsheets.sheet.TangemBottomSheetDraggab
|
|||
import com.tangem.core.ui.components.sheetscaffold.TangemSheetState
|
||||
import com.tangem.core.ui.components.sheetscaffold.TangemSheetValue
|
||||
import com.tangem.core.ui.components.sheetscaffold.rememberSheetState
|
||||
import com.tangem.core.ui.res.LocalBottomSheetAlwaysVisible
|
||||
import com.tangem.core.ui.res.LocalWindowSize
|
||||
import com.tangem.core.ui.res.TangemTheme
|
||||
import com.tangem.core.ui.res.TangemThemePreviewRedesign
|
||||
import com.tangem.core.ui.res.*
|
||||
import com.tangem.core.ui.test.BaseBottomSheetTestTags
|
||||
import com.tangem.core.ui.utils.WindowInsetsZero
|
||||
import dev.chrisbanes.haze.rememberHazeState
|
||||
|
||||
/**
|
||||
* Extra bottom inset that scrollable content under a [BasicBottomSheet] should reserve so it
|
||||
|
|
@ -235,46 +233,48 @@ inline fun <reified T : TangemBottomSheetConfigContent> BasicBottomSheet(
|
|||
}
|
||||
|
||||
val bsContent: @Composable ColumnScope.() -> Unit = {
|
||||
val contentModifier = when (type) {
|
||||
Default -> Modifier
|
||||
.clip(
|
||||
RoundedCornerShape(
|
||||
topStart = TangemTheme.dimens2.x8,
|
||||
topEnd = TangemTheme.dimens2.x8,
|
||||
),
|
||||
)
|
||||
Modal -> Modifier
|
||||
.padding(
|
||||
start = TangemTheme.dimens2.x2,
|
||||
end = TangemTheme.dimens2.x2,
|
||||
bottom = bottomBarHeight,
|
||||
)
|
||||
.clip(RoundedCornerShape(TangemTheme.dimens2.x8))
|
||||
}
|
||||
|
||||
Column(
|
||||
modifier = contentModifier
|
||||
.background(containerColor)
|
||||
.heightIn(max = maxHeight)
|
||||
.testTag(BaseBottomSheetTestTags.CONTAINER),
|
||||
) {
|
||||
Box(modifier = Modifier.fillMaxWidth()) {
|
||||
title(model)
|
||||
}
|
||||
Box(modifier = Modifier.fillMaxWidth()) {
|
||||
if (footer != null) {
|
||||
FooterOverlay(
|
||||
measuredFooterHeight = footerHeightDp,
|
||||
onMeasureFooter = { newHeight ->
|
||||
if (newHeight != footerHeightDp) {
|
||||
footerHeightDp = newHeight
|
||||
}
|
||||
},
|
||||
footer = { footer(model) },
|
||||
content = { content(model) },
|
||||
CompositionLocalProvider(LocalHazeState provides rememberHazeState()) {
|
||||
val contentModifier = when (type) {
|
||||
Default -> Modifier
|
||||
.clip(
|
||||
RoundedCornerShape(
|
||||
topStart = TangemTheme.dimens2.x8,
|
||||
topEnd = TangemTheme.dimens2.x8,
|
||||
),
|
||||
)
|
||||
} else {
|
||||
content(model)
|
||||
Modal -> Modifier
|
||||
.padding(
|
||||
start = TangemTheme.dimens2.x2,
|
||||
end = TangemTheme.dimens2.x2,
|
||||
bottom = bottomBarHeight,
|
||||
)
|
||||
.clip(RoundedCornerShape(TangemTheme.dimens2.x8))
|
||||
}
|
||||
|
||||
Column(
|
||||
modifier = contentModifier
|
||||
.background(containerColor)
|
||||
.heightIn(max = maxHeight)
|
||||
.testTag(BaseBottomSheetTestTags.CONTAINER),
|
||||
) {
|
||||
Box(modifier = Modifier.fillMaxWidth()) {
|
||||
title(model)
|
||||
}
|
||||
Box(modifier = Modifier.fillMaxWidth()) {
|
||||
if (footer != null) {
|
||||
FooterOverlay(
|
||||
measuredFooterHeight = footerHeightDp,
|
||||
onMeasureFooter = { newHeight ->
|
||||
if (newHeight != footerHeightDp) {
|
||||
footerHeightDp = newHeight
|
||||
}
|
||||
},
|
||||
footer = { footer(model) },
|
||||
content = { content(model) },
|
||||
)
|
||||
} else {
|
||||
content(model)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -49,11 +49,11 @@ data class MessageBottomSheetUM(
|
|||
var backgroundType: BackgroundType = BackgroundType.Unspecified,
|
||||
) : Element {
|
||||
enum class Type {
|
||||
Unspecified, Accent, Informative, Attention, Warning,
|
||||
Unspecified, Accent, Informative, Attention, Warning, Success,
|
||||
}
|
||||
|
||||
enum class BackgroundType {
|
||||
Unspecified, SameAsTint, Accent, Informative, Attention, Warning,
|
||||
Unspecified, SameAsTint, Accent, Informative, Attention, Warning, Success,
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -214,6 +214,7 @@ private fun BottomSheetVector(vector: MessageBottomSheetUM.Vector, modifier: Mod
|
|||
MessageBottomSheetUM.Vector.Type.Informative -> TangemTheme.colors3.icon.status.info
|
||||
MessageBottomSheetUM.Vector.Type.Attention -> TangemTheme.colors3.icon.status.warning
|
||||
MessageBottomSheetUM.Vector.Type.Warning -> TangemTheme.colors3.icon.status.error
|
||||
MessageBottomSheetUM.Vector.Type.Success -> TangemTheme.colors3.icon.status.success
|
||||
}
|
||||
|
||||
val backgroundColor = when (vector.backgroundType) {
|
||||
|
|
@ -223,6 +224,7 @@ private fun BottomSheetVector(vector: MessageBottomSheetUM.Vector, modifier: Mod
|
|||
MessageBottomSheetUM.Vector.BackgroundType.Informative -> TangemTheme.colors3.bg.status.infoSubtle
|
||||
MessageBottomSheetUM.Vector.BackgroundType.Attention -> TangemTheme.colors3.bg.status.warningSubtle
|
||||
MessageBottomSheetUM.Vector.BackgroundType.Warning -> TangemTheme.colors3.bg.status.errorSubtle
|
||||
MessageBottomSheetUM.Vector.BackgroundType.Success -> TangemTheme.colors3.bg.status.successSubtle
|
||||
}
|
||||
|
||||
Box(
|
||||
|
|
|
|||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Add a link
Reference in a new issue