Updated on 2026-08-14

This commit is contained in:
Tangem 2026-07-28 14:08:01 +03:00
commit 06a915a0f4
2186 changed files with 101417 additions and 36593 deletions

View file

@ -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
}

View file

@ -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
}

View file

@ -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),

View file

@ -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),

View 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.

View file

@ -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
}

View file

@ -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,49 +16,13 @@
"version": "undefined"
},
{
"name": "STAKING_ETH_ENABLED",
"version": "5.39"
},
{
"name": "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"
"name": "TWI_485_USEDESK_ENABLED",
"version": "6.1"
},
{
"name": "VIRTUAL_ACCOUNTS_ENABLED",
"version": "undefined"
},
{
"name": "ASSETS_DISCOVERY_ENABLED",
"version": "5.38"
},
{
"name": "SOLANA_TX_HISTORY_ENABLED",
"version": "5.39"
},
{
"name": "SOLANA_SCALED_UI_AMOUNT_ENABLED",
"version": "5.39"
},
{
"name": "HEDERA_ERC20_ENABLED",
"version": "5.37"
},
{
"name": "ADD_AND_MANAGE_TOKENS_ENABLED",
"version": "5.38"
},
{
"name": "WALLET_CONNECT_BITCOIN_ENABLED",
"version": "undefined"
@ -63,61 +35,13 @@
"name": "ADDRESS_SYNC_ENABLED",
"version": "undefined"
},
{
"name": "AND_15207_SWAP_SWITCH_TO_TRANSFER_ENABLED",
"version": "6.0"
},
{
"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"
"version": "6.1"
},
{
"name": "AND_15596_ONBOARDING_PUSH_NOTIFICATION_DOUBLE_ASK_AB_ENABLED",
"version": "6.0"
},
{
"name": "AND_15310_ADD_FUNDS_STAGE1",
"version": "5.39"
},
{
"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"
},
{
"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"
"name": "TWI_1403_ONBOARDING_PUSH_NOTIFICATION_DOUBLE_ASK_AB_ENABLED",
"version": "6.1"
},
{
"name": "AND_15438_BACKEND_AUTHENTICATION_ENABLED",
@ -125,40 +49,24 @@
},
{
"name": "AND_15482_SURVEYSPARROW_ENABLED",
"version": "undefined"
},
{
"name": "AND_15258_QUICK_TOP_UP_ENABLED",
"version": "6.0"
},
{
"name": "AND_15368_VISA_PAY_REDESIGN",
"version": "6.0"
},
{
"name": "AND_15364_VISA_PAY_CARD_CLOSE",
"version": "6.0"
"version": "6.1"
},
{
"name": "AND_15741_VISA_PAY_REMOVE_ACCOUNT",
"version": "undefined"
},
{
"name": "AND_16041_VISA_TIERS_PLUS_PLAN",
"version": "6.1"
},
{
"name": "TWI_83_ADDRESS_BOOK_ENABLED",
"version": "6.1"
},
{
"name": "AND_15632_GASLESS_YIELD_WITHDRAW_ENABLED",
"version": "undefined"
},
{
"name": "AND_15489_EXPRESS_SHARE_BUTTON_ENABLED",
"version": "6.0"
},
{
"name": "AND_15235_VISA_MULTIPLE_CARDS",
"version": "6.0"
},
{
"name": "AND_15715_SWAP_BEST_DEX_RATE_ENABLED",
"version": "6.0"
},
{
"name": "AND_15767_NEW_TX_HISTORY_ENABLED",
"version": "undefined"
@ -168,8 +76,40 @@
"version": "undefined"
},
{
"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": "6.1"
},
{
"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"
},
{
"name": "TWI_1192_TANGEM_PAY_CASHBACK_ENABLED",
"version": "undefined"
},
{
"name": "AND_15231_STAKING_REGION_UNAVAILABLE_ENABLED",
"version": "6.1"
},
{
"name": "AND_16204_POLYMARKET_ENABLED",
"version": "undefined"
},
{
"name": "TWI_1638_VA_MVP0_ENABLED",

View file

@ -39,18 +39,7 @@ 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",

View file

@ -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
}

View file

@ -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')"
]
}
}

View file

@ -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>
}

View file

@ -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,
)
}

View file

@ -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,
)
}

View file

@ -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,
)

View file

@ -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,
)

View file

@ -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>
}

View file

@ -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,
)

View file

@ -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?,

View file

@ -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,
)

View file

@ -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/"
}
}

View file

@ -3,6 +3,7 @@ package com.tangem.datasource.api.common.config
import com.tangem.datasource.BuildConfig
import com.tangem.datasource.local.config.environment.EnvironmentConfig
import com.tangem.utils.ProviderSuspend
import com.tangem.utils.SupportedLanguages
import com.tangem.utils.info.AppInfoProvider
internal sealed class TangemPay(
@ -55,6 +56,8 @@ 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() },
"Accept-Language" to ProviderSuspend { SupportedLanguages.getCurrentSupportedLanguageCode() },
)
private fun getBffStaticToken(apiEnvironment: ApiEnvironment): String {

View file

@ -78,12 +78,10 @@ data class ExchangeItemResponse(
@Json(name = "createdAt")
val createdAt: String,
/** Transaction last-update timestamp in ISO-8601 format */
// todo txHistory uncomment
/*
@Json(name = "updatedAt")
val updatedAt: String,
*/
// todo txHistory backend does not send `updatedAt` yet — uncomment once it does.
// /** Transaction last-update timestamp in ISO-8601 format */
// @Json(name = "updatedAt")
// val updatedAt: String,
/** Pay-in expiration timestamp in ISO-8601 format */
@Json(name = "payTill")

View file

@ -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>>
}

View file

@ -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,
)

View file

@ -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,
)

View file

@ -43,12 +43,10 @@ data class OnrampItemResponse(
@Json(name = "createdAt")
val createdAt: String,
/** Transaction last-update timestamp in ISO-8601 format */
// todo txHistory uncomment
/*
@Json(name = "updatedAt")
val updatedAt: String,
*/
// todo txHistory backend does not send `updatedAt` yet — uncomment once it does.
// /** Transaction last-update timestamp in ISO-8601 format */
// @Json(name = "updatedAt")
// val updatedAt: String,
// endregion
// region fromAsset (fiat) info

View file

@ -17,12 +17,32 @@ 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(
@ -84,6 +104,12 @@ interface TangemPayApi {
@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>
@ -161,4 +187,23 @@ interface TangemPayApi {
@Body body: UpdateCardRequest,
@Path("card_id") cardId: String,
): ApiResponse<UpdateCardDisplayNameResponse>
@GET("v1/customer/cashback/summary")
suspend fun getCashbackSummary(@Header("Authorization") authHeader: String): ApiResponse<CashbackSummaryResponse>
@GET("v1/customer/cashback/promotions")
suspend fun getCashbackPromotions(
@Header("Authorization") authHeader: String,
): ApiResponse<CashbackPromotionsResponse>
@GET("v1/customer/cashback/accruals/docs")
suspend fun getCashbackAccrualDocs(
@Header("Authorization") authHeader: String,
): ApiResponse<CashbackAccrualDocsResponse>
@GET("v1/customer/cashback/history")
suspend fun getCashbackHistory(
@Header("Authorization") authHeader: String,
@Query("months") months: Int,
): ApiResponse<CashbackHistoryResponse>
}

View file

@ -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,
)
}

View file

@ -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,
)

View file

@ -0,0 +1,20 @@
package com.tangem.datasource.api.pay.models.response
import com.squareup.moshi.Json
import com.squareup.moshi.JsonClass
/**
* Response from `GET v1/customer/cashback/accruals/docs` cashback program documents (always English).
*/
@JsonClass(generateAdapter = true)
data class CashbackAccrualDocsResponse(
@Json(name = "docs") val docs: List<Doc>?,
) {
@JsonClass(generateAdapter = true)
data class Doc(
@Json(name = "id") val id: String?,
@Json(name = "title") val title: String?,
@Json(name = "url") val url: String?,
)
}

View file

@ -0,0 +1,25 @@
package com.tangem.datasource.api.pay.models.response
import com.squareup.moshi.Json
import com.squareup.moshi.JsonClass
import java.math.BigDecimal
/**
* Response from `GET /v1/customer/cashback/history`.
*
* Confirmed cashback grouped by calendar month, ordered oldest to newest. Drives the monthly
* earnings histogram on the Cashback screen.
*/
@JsonClass(generateAdapter = true)
data class CashbackHistoryResponse(
@Json(name = "currency") val currency: String?,
@Json(name = "items") val items: List<Item>?,
) {
@JsonClass(generateAdapter = true)
data class Item(
@Json(name = "year") val year: Int,
@Json(name = "month") val month: Int,
@Json(name = "confirmed_amount") val confirmedAmount: BigDecimal?,
)
}

View file

@ -0,0 +1,41 @@
package com.tangem.datasource.api.pay.models.response
import com.squareup.moshi.Json
import com.squareup.moshi.JsonClass
import java.math.BigDecimal
/**
* Response from `GET v1/customer/cashback/promotions` cashback program configuration for the customer.
*/
@JsonClass(generateAdapter = true)
data class CashbackPromotionsResponse(
@Json(name = "cashback_on_cards") val cashbackOnCards: CashbackOnCards?,
@Json(name = "additional_cashback") val additionalCashback: List<AdditionalCashback>?,
) {
@JsonClass(generateAdapter = true)
data class CashbackOnCards(
@Json(name = "tiers") val tiers: List<CardTier>?,
@Json(name = "monthly_cap_amount") val monthlyCapAmount: BigDecimal?,
@Json(name = "monthly_cap_currency") val monthlyCapCurrency: String?,
)
@JsonClass(generateAdapter = true)
data class CardTier(
@Json(name = "tier") val tier: String?,
@Json(name = "label") val label: String?,
@Json(name = "scope") val scope: String?,
@Json(name = "min_transaction_amount") val minTransactionAmount: BigDecimal?,
@Json(name = "tier_monthly_cap_amount") val tierMonthlyCapAmount: BigDecimal?,
@Json(name = "promotion_id") val promotionId: String?,
)
@JsonClass(generateAdapter = true)
data class AdditionalCashback(
@Json(name = "id") val id: String?,
@Json(name = "name") val name: String?,
@Json(name = "description") val description: String?,
@Json(name = "is_permanent") val isPermanent: Boolean?,
@Json(name = "end_date") val endDate: String?,
)
}

View file

@ -0,0 +1,31 @@
package com.tangem.datasource.api.pay.models.response
import com.squareup.moshi.Json
import com.squareup.moshi.JsonClass
import java.math.BigDecimal
/**
* Response from `GET /v1/customer/cashback/summary`.
*
* When [cashbackProgramStatus] is not `enabled`, the amount/period fields may be omitted.
*/
@JsonClass(generateAdapter = true)
data class CashbackSummaryResponse(
@Json(name = "cashback_program_status") val cashbackProgramStatus: String,
@Json(name = "cashback_display_mode") val cashbackDisplayMode: String?,
@Json(name = "period") val period: Period?,
@Json(name = "confirmed_amount") val confirmedAmount: BigDecimal?,
@Json(name = "pending_amount") val pendingAmount: BigDecimal?,
@Json(name = "currency") val currency: String?,
@Json(name = "payout_currency") val payoutCurrency: String?,
@Json(name = "payout_network") val payoutNetwork: String?,
) {
@JsonClass(generateAdapter = true)
data class Period(
@Json(name = "year") val year: Int,
@Json(name = "month") val month: Int,
@Json(name = "payout_start_date") val payoutStartDate: String?,
@Json(name = "payout_end_date") val payoutEndDate: String?,
)
}

View file

@ -21,6 +21,51 @@ 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 = "source") val source: 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 = "program_name") val programName: 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)
@ -122,5 +167,6 @@ data class CustomerMeResponse(
@Json(name = "card_status") val cardStatus: String,
@Json(name = "card_number_end") val cardNumberEnd: String,
@Json(name = "is_pin_set") val isPinSet: Boolean?,
@Json(name = "images") val images: List<Image>?,
)
}

View file

@ -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?,
)

View file

@ -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,
)

View file

@ -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,

View file

@ -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?,
)

View file

@ -0,0 +1,26 @@
package com.tangem.datasource.api.polymarket
import com.tangem.datasource.api.common.response.ApiResponse
import com.tangem.datasource.api.polymarket.models.PolymarketEventsResponse
import retrofit2.http.GET
import retrofit2.http.Query
/**
* Polymarket predictions BFF Discovery API (`api/predictions/v1`).
*
* Provided in [com.tangem.datasource.di.NetworkModule] via the [ApiConfig.ID.Predictions] config.
*/
interface PolymarketApi {
/**
* Discovery feed: paginated prediction events, each with its top active markets.
*
* @param limit page size (BFF default 20)
* @param cursor keyset pagination cursor; `null` for the first page
*/
@GET("api/predictions/v1/events")
suspend fun getEvents(
@Query("limit") limit: Int,
@Query("cursor") cursor: String?,
): ApiResponse<PolymarketEventsResponse>
}

View file

@ -0,0 +1,45 @@
package com.tangem.datasource.api.polymarket.models
import com.squareup.moshi.Json
import com.squareup.moshi.JsonClass
/**
* Response of `GET /api/predictions/v1/events` (BFF `EventsPageResponse`).
*/
@JsonClass(generateAdapter = true)
data class PolymarketEventsResponse(
@Json(name = "events") val events: List<PolymarketEventDto>,
@Json(name = "cursor") val cursor: String?,
@Json(name = "hasNext") val hasNext: Boolean,
)
/** BFF `Event`. Only the fields consumed by the app are declared; unknown JSON is ignored. */
@JsonClass(generateAdapter = true)
data class PolymarketEventDto(
@Json(name = "eventId") val eventId: String,
@Json(name = "slug") val slug: String,
@Json(name = "title") val title: String,
@Json(name = "icon") val icon: String?,
@Json(name = "image") val image: String?,
@Json(name = "volume") val volume: Double?,
@Json(name = "totalMarketsCount") val totalMarketsCount: Int,
@Json(name = "markets") val markets: List<PolymarketMarketDto>,
)
/** BFF `Market` (reduced preview inside an [PolymarketEventDto]). */
@JsonClass(generateAdapter = true)
data class PolymarketMarketDto(
@Json(name = "id") val id: String,
@Json(name = "question") val question: String,
@Json(name = "icon") val icon: String?,
@Json(name = "volume") val volume: Double?,
@Json(name = "outcomes") val outcomes: List<PolymarketOutcomeDto>,
)
/** BFF `OutcomeSummary`. */
@JsonClass(generateAdapter = true)
data class PolymarketOutcomeDto(
@Json(name = "assetId") val assetId: String,
@Json(name = "label") val label: String,
@Json(name = "probability") val probability: Double?,
)

View file

@ -49,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] */

View file

@ -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?,
)

View file

@ -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,
)

View file

@ -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,
)

View file

@ -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,
)

View file

@ -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,8 +19,10 @@ 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.polymarket.PolymarketApi
import com.tangem.datasource.api.stakekit.StakeKitApi
import com.tangem.datasource.api.tangemTech.TangemTechApi
import com.tangem.datasource.api.tangemTech.YieldSupplyApi
@ -34,6 +37,7 @@ import dagger.hilt.InstallIn
import dagger.hilt.components.SingletonComponent
import javax.inject.Singleton
@Suppress("TooManyFunctions")
@Module
@InstallIn(SingletonComponent::class)
internal object NetworkModule {
@ -117,6 +121,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 {
@ -223,6 +237,17 @@ internal object NetworkModule {
)
}
@Provides
@Singleton
fun providePolymarketApi(retrofitApiBuilder: RetrofitApiBuilder): PolymarketApi {
// Polymarket BFF Discovery lives on the main Tangem gateway — reuse the TangemTech config.
return retrofitApiBuilder.build(
apiConfigId = ApiConfig.ID.TangemTech,
applyTimeoutAnnotations = false,
sessionAuth = false,
)
}
@Provides
@Singleton
fun provideAuthApi(retrofitApiBuilder: RetrofitApiBuilder): AuthApi {
@ -252,4 +277,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,
),
)
}
}

View file

@ -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,
)
}
}

View file

@ -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()
}
}

View file

@ -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

View file

@ -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()
}

View file

@ -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")
}
}

View file

@ -101,10 +101,6 @@ internal object GeneratedEnvironmentConfigConverter {
apiKey = GeneratedEnvironmentConfig.quiknodeMonadApiKey,
subdomain = GeneratedEnvironmentConfig.quiknodeMonadSubdomain,
),
quickNodeHederaCredentials = QuickNodeCredentials(
apiKey = GeneratedEnvironmentConfig.quiknodeHederaApiKey,
subdomain = GeneratedEnvironmentConfig.quiknodeHederaSubdomain,
),
infuraProjectId = GeneratedEnvironmentConfig.infuraProjectId,
tronGridApiKey = GeneratedEnvironmentConfig.tronGridApiKey,
nowNodeCredentials = NowNodeCredentials(apiKey = GeneratedEnvironmentConfig.nowNodesApiKey),

View file

@ -4,16 +4,21 @@ import com.tangem.datasource.api.express.models.response.ExchangeItemResponse
import com.tangem.datasource.api.onramp.models.response.OnrampItemResponse
import com.tangem.datasource.local.txhistory.db.entity.express.ExpressExchangeEntity
import com.tangem.datasource.local.txhistory.db.entity.express.ExpressOnrampEntity
import org.joda.time.DateTime
// todo txHistory backend does not send `updatedAt` yet — drop this placeholder and map the real
// ExchangeItemResponse.updatedAt / OnrampItemResponse.updatedAt once it does.
private fun updatedAtPlaceholder(): String = DateTime.now().toString()
/**
* 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 +35,7 @@ fun ExchangeItemResponse.toEntity(ownerAddress: String): ExpressExchangeEntity {
refundNetwork = refundNetwork,
refundContractAddress = refundContractAddress,
createdAt = createdAt,
updatedAt = ""/*updatedAt*/, // todo txHistory uncomment
updatedAt = updatedAtPlaceholder(),
payTill = payTill,
averageDuration = averageDuration,
from = ExpressExchangeEntity.AssetEmbedded(
@ -50,10 +55,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 +66,7 @@ fun OnrampItemResponse.toEntity(ownerAddress: String): ExpressOnrampEntity {
externalTxUrl = externalTxUrl,
payoutHash = payoutHash,
createdAt = createdAt,
updatedAt = ""/*updatedAt*/, // todo txHistory uncomment,
updatedAt = updatedAtPlaceholder(),
fromCurrencyCode = fromCurrencyCode,
fromAmount = fromAmount,
fromPrecision = fromPrecision,

View file

@ -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,
),
)
}

View file

@ -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") }
@ -146,6 +161,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")
}
@ -186,6 +205,9 @@ object PreferencesKeys {
fun getTangemPayAddToWalletKey(customerWalletAddress: String) =
booleanPreferencesKey("tangem_pay_add_to_wallet_done_key_$customerWalletAddress")
fun getTangemPayCashbackDeactivationDismissedKey(customerWalletAddress: String) =
booleanPreferencesKey("tangem_pay_cashback_deactivation_dismissed_key_$customerWalletAddress")
fun getTangemPayOrderIdKey(customerWalletAddress: String) =
stringPreferencesKey("tangem_pay_order_id_key_$customerWalletAddress")

View file

@ -0,0 +1,13 @@
package com.tangem.datasource.local.token
import com.tangem.datasource.local.datastore.RuntimeStateStore
import javax.inject.Inject
import javax.inject.Singleton
/**
* In-memory flag: the last P2P vaults request returned HTTP 451 (region unavailable).
* `true` staking is blocked in the region. Reset to `false` on a successful fetch.
*/
@Singleton
class P2PEthPoolRegionBlockedStore @Inject constructor() :
RuntimeStateStore<Boolean> by RuntimeStateStore(defaultValue = false)

View file

@ -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
}

View file

@ -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,

View file

@ -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,
)
}
}
}

View file

@ -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>
}

View file

@ -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"),
}
}

View file

@ -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")

View file

@ -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,

View file

@ -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,
)
}

View file

@ -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,
)

View file

@ -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? {

View file

@ -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))
}

View file

@ -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() }
}
}

View file

@ -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?
}

View file

@ -11,5 +11,7 @@ interface TangemPayReissueCardStore {
suspend fun storeReissueOrderId(cardId: String, orderId: String)
suspend fun removeReissueOrderId(cardId: String)
suspend fun getOrderId(cardId: String): String?
}

View file

@ -1,68 +0,0 @@
package com.tangem.datasource.local.visa
import com.tangem.domain.models.wallet.UserWalletId
import com.tangem.domain.pay.TangemPayWithdrawState
import com.tangem.domain.visa.model.TangemPayAuthTokens
@Suppress("TooManyFunctions")
interface TangemPayStorage {
suspend fun storeCustomerWalletAddress(userWalletId: UserWalletId, customerWalletAddress: String)
suspend fun getCustomerWalletAddress(userWalletId: UserWalletId): String?
suspend fun clearCustomerWalletAddress(userWalletId: UserWalletId)
suspend fun storeAuthTokens(customerWalletAddress: String, tokens: TangemPayAuthTokens)
suspend fun getAuthTokens(customerWalletAddress: String): TangemPayAuthTokens?
suspend fun clearAuthTokens(customerWalletAddress: String)
suspend fun storeOrderId(customerWalletAddress: String, orderId: String)
suspend fun getOrderId(customerWalletAddress: String): String?
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)
suspend fun storeCheckCustomerWalletResult(userWalletId: UserWalletId, isPaeraCustomer: Boolean)
suspend fun checkCustomerWalletResult(userWalletId: UserWalletId): Boolean?
/** Called after creating withdraw order, active order id */
suspend fun storeActiveWithdrawOrderId(userWalletId: UserWalletId, orderId: String)
/** Called after creating withdraw order, saves order data */
suspend fun storeWithdrawOrder(userWalletId: UserWalletId, data: TangemPayWithdrawState)
/** Returns single active order id. Once the order is completed, deletes id from storage.
* Only one active order allowed for a wallet */
suspend fun getActiveWithdrawOrderId(userWalletId: UserWalletId): String?
/** Returns all withdraw orders saved.
* Once we get tx hash for an order, it gets deleted from this storage */
suspend fun getWithdrawOrders(userWalletId: UserWalletId): List<TangemPayWithdrawState>?
/** Deletes active withdraw order. Called after order is completed */
suspend fun deleteActiveWithdrawOrder(userWalletId: UserWalletId)
/** Deletes withdraw order data. Called after getting its tx hash */
suspend fun deleteWithdrawOrder(userWalletId: UserWalletId, orderId: String)
suspend fun getHideMainOnboardingBanner(userWalletId: UserWalletId): Boolean
suspend fun storeHideOnboardingBanner(userWalletId: UserWalletId, hide: Boolean)
suspend fun storeTangemPayEligibility(eligibility: Set<String>)
suspend fun getTangemPayEligibility(): Set<String>
suspend fun clearAll(userWalletId: UserWalletId, customerWalletAddress: String)
suspend fun storeIsTangemPayDeactivated(userWalletId: UserWalletId)
suspend fun isTangemPayDeactivated(userWalletId: UserWalletId): Boolean
}

View file

@ -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>)
}

View file

@ -93,6 +93,13 @@ sealed interface PaymentAccountStatusValueDM {
@Json(name = "admin_daily_limit") val adminDailyLimit: SerializedBigDecimal?,
@Json(name = "frozen_state") val frozenState: String,
@Json(name = "last_digits") val lastDigits: String,
@Json(name = "images") val images: List<ImageDM>,
@Json(name = "state") val state: String,
)
@JsonClass(generateAdapter = true)
data class ImageDM(
@Json(name = "type") val type: String,
@Json(name = "url") val url: String,
)
}

View file

@ -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,
}
}

View file

@ -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
}

View file

@ -33,7 +33,15 @@ sealed class RequestHeader(vararg pairs: Pair<String, ProviderSuspend<String>>)
"platform" to ProviderSuspend { "android" },
"language" to ProviderSuspend { appInfoProvider.language.checkHeaderValueOrEmpty() },
"timezone" to ProviderSuspend {
TimeZone.getDefault().getDisplayName(false, TimeZone.SHORT).checkHeaderValueOrEmpty()
val timeZone = TimeZone.getDefault()
// TimeZone.getDisplayName may throw AssertionError on some devices due to an Android ICU bug
// ("No NameTypeIndex match for SHORT_STANDARD"), so fall back to the timezone id.
val displayName = try {
timeZone.getDisplayName(false, TimeZone.SHORT)
} catch (_: AssertionError) {
timeZone.id
}
displayName.checkHeaderValueOrEmpty()
},
"device" to ProviderSuspend { appInfoProvider.device.checkHeaderValueOrEmpty() },
)

View file

@ -2,10 +2,18 @@ package com.tangem.datasource.utils
import com.tangem.utils.logging.TangemLogger
import okhttp3.Interceptor
import okhttp3.Request
import okhttp3.Response
import okhttp3.ResponseBody.Companion.toResponseBody
/**
* OkHttp interceptor that redirects requests from wiremock.tests-d.com to a local WireMock instance.
*
* When a request hits the tangem-api-mocks catch-all (an unmocked endpoint HTTP 404 with the
* [MOCKED_ENDPOINT_NOT_CONFIGURED_MARKER] body), it folds the failing endpoint (method + URL) into the
* response body. That body becomes [com.tangem.datasource.api.common.response.ApiResponseError.HttpException.errorBody]
* (see ResponseExt), whose data-class `toString()` is what appears in a failing test's stack trace so the
* exact missing mapping is named instead of an opaque `HttpException(404, errorBody={"error":"not_found",})`.
*/
class WireMockRedirectInterceptor : Interceptor {
@ -17,25 +25,64 @@ class WireMockRedirectInterceptor : Interceptor {
val host = request.url.host
val sanitizedOverride = override.trimEnd('/')
if (host == WIREMOCK_REMOTE_HOST) {
val newUrl = url.replace(WIREMOCK_REMOTE_URL, sanitizedOverride)
TangemLogger.d("WireMockRedirect: $url -> $newUrl")
return chain.proceed(request.newBuilder().url(newUrl).build())
val redirectedRequest = when {
host == WIREMOCK_REMOTE_HOST -> {
val newUrl = url.replace(WIREMOCK_REMOTE_URL, sanitizedOverride)
TangemLogger.d("WireMockRedirect: $url -> $newUrl")
request.newBuilder().url(newUrl).build()
}
host in REDIRECTABLE_THIRD_PARTY_HOSTS -> {
val newUrl = url.replace("https://$host", "$sanitizedOverride/$host")
TangemLogger.d("WireMockRedirect (3p): $url -> $newUrl")
request.newBuilder().url(newUrl).build()
}
else -> request
}
if (host in REDIRECTABLE_THIRD_PARTY_HOSTS) {
val newUrl = url.replace("https://$host", "$sanitizedOverride/$host")
TangemLogger.d("WireMockRedirect (3p): $url -> $newUrl")
return chain.proceed(request.newBuilder().url(newUrl).build())
}
return chain.proceed(request)
val response = chain.proceed(redirectedRequest)
return response.withEndpointInMockNotConfiguredError(redirectedRequest)
}
/**
* If [this] is the tangem-api-mocks catch-all 404 ("Mocked endpoint not configured"), rewrite the error body
* to also carry the failing endpoint, so it surfaces in the resulting `HttpException.errorBody`. Non-404
* responses (incl. the POST JSON-RPC "unreachable" catch-all, which is handled gracefully and must not be
* altered) are returned untouched.
*/
private fun Response.withEndpointInMockNotConfiguredError(request: Request): Response {
if (code != HTTP_NOT_FOUND) return this
val original = runCatching { peekBody(PEEK_LIMIT_BYTES).string() }.getOrNull() ?: return this
if (!original.contains(MOCKED_ENDPOINT_NOT_CONFIGURED_MARKER)) return this
val endpoint = "${request.method} ${request.url}".jsonEscaped()
val enrichedJson = original.indexOf('{').let { open ->
if (open < 0) {
"""{"message":"$MOCKED_ENDPOINT_NOT_CONFIGURED_MARKER","endpoint":"$endpoint"}"""
} else {
original.substring(0, open + 1) + """"endpoint":"$endpoint",""" + original.substring(open + 1)
}
}
val contentType = body?.contentType()
body?.close()
return newBuilder().body(enrichedJson.toResponseBody(contentType)).build()
}
private fun String.jsonEscaped(): String = replace("\\", "\\\\").replace("\"", "\\\"")
companion object {
private const val WIREMOCK_REMOTE_HOST = "wiremock.tests-d.com"
private const val WIREMOCK_REMOTE_URL = "https://$WIREMOCK_REMOTE_HOST"
private const val HTTP_NOT_FOUND = 404
/** Bounded peek so a large real response is never fully buffered just to look for the marker. */
private const val PEEK_LIMIT_BYTES = 4_096L
/** Emitted by the tangem-api-mocks catch-all mapping for any endpoint without an explicit mapping. */
private const val MOCKED_ENDPOINT_NOT_CONFIGURED_MARKER = "Mocked endpoint not configured"
/**
* Upstream hosts that have no other override knob and should be funnelled into WireMock
* when [overriddenBaseUrl] is set. Each matched URL becomes `<override>/<host>/<original-path>`,
@ -47,6 +94,7 @@ class WireMockRedirectInterceptor : Interceptor {
"deep-index.moralis.io",
"solana-gateway.moralis.io",
"api.etherscan.io",
"eth-blockbook.nownodes.io",
)
/**

View file

@ -18,6 +18,7 @@ import com.tangem.datasource.api.auth.P2PEthPoolAuthProvider
import com.tangem.datasource.api.auth.StakeKitAuthProvider
import com.tangem.test.core.ProvideTestModels
import com.tangem.utils.ProviderSuspend
import com.tangem.utils.SupportedLanguages
import com.tangem.utils.info.AppInfoProvider
import io.mockk.clearMocks
import io.mockk.coEvery
@ -70,6 +71,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 +172,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 +300,8 @@ 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() },
"Accept-Language" to ProviderSuspend { SupportedLanguages.getCurrentSupportedLanguageCode() },
),
),
)
@ -313,6 +317,8 @@ 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() },
"Accept-Language" to ProviderSuspend { SupportedLanguages.getCurrentSupportedLanguageCode() },
),
),
)
@ -457,6 +463,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"

View file

@ -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)
@ -47,7 +46,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 +58,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 +94,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 +111,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 +155,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 +167,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 +190,7 @@ internal class ExpressHistoryConverterTest {
)
// WHEN
val entity = item.toEntity(ownerAddress = OWNER_ADDRESS)
val entity = item.toEntity()
// THEN
Truth.assertThat(entity.failReason).isNull()
@ -269,8 +279,4 @@ internal class ExpressHistoryConverterTest {
paymentMethod = "card",
countryCode = "US",
)
private companion object {
const val OWNER_ADDRESS = "0xowner"
}
}

View file

@ -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"
}
}

View file

@ -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
}
}

View file

@ -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
}
}

View file

@ -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
}
}

View file

@ -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
}

View file

@ -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
}

View file

@ -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
}

View file

@ -0,0 +1,6 @@
package com.tangem.core.navigation.url
interface AppStoreOpener {
fun openStorePage()
}

View file

@ -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/"
}
}

View file

@ -4,7 +4,7 @@ plugins {
}
dependencies {
// region Coroutines
implementation(deps.kotlin.coroutines)
// region Kotlin
api(deps.kotlin.coroutines)
// endregion
}

View file

@ -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
}

View file

@ -281,6 +281,7 @@
<string name="common_added">Hinzugefügt</string>
<string name="common_address">Vertragsadresse</string>
<string name="common_all">Alle</string>
<string name="common_all_accounts">Alle Konten</string>
<string name="common_allow">Erlauben</string>
<string name="common_amount">Betrag</string>
<string name="common_analytics">Analysen</string>
@ -290,6 +291,10 @@
<string name="common_approve">Genehmigen</string>
<string name="common_approved">Genehmigt</string>
<string name="common_approving">Genehmigen</string>
<plurals name="common_assets_count">
<item quantity="one">%d Vermögenswert</item>
<item quantity="other">%d Vermögenswerte</item>
</plurals>
<string name="common_attention">Achtung</string>
<string name="common_available_networks">Verfügbare Netzwerke</string>
<string name="common_backup">Sicherungskopie</string>
@ -325,6 +330,7 @@
<string name="common_create">Erstellen</string>
<string name="common_crypto_fiat_format">%1$s (%2$s)</string>
<string name="common_custom">Eigen</string>
<string name="common_day">Tag</string>
<plurals name="common_days">
<item quantity="one">%d tag</item>
<item quantity="other">%d tage</item>
@ -399,7 +405,22 @@
<item quantity="other">%dMinuten her</item>
</plurals>
<string name="common_month">Monat</string>
<string-array name="common_month_in">
<item>Januar</item>
<item>Februar</item>
<item>März</item>
<item>April</item>
<item>Mai</item>
<item>Juni</item>
<item>Juli</item>
<item>August</item>
<item>September</item>
<item>Oktober</item>
<item>November</item>
<item>Dezember</item>
</string-array>
<string name="common_more">Mehr</string>
<string name="common_negative">Negativ</string>
<string name="common_network">Netzwerk</string>
<string name="common_network_fee_title">Netzgebühr</string>
<string name="common_network_fee_warning_content">Der überwiesene Betrag wird um %1$s (%2$s) gekürzt, um die gewählte Gebührenhöhe zu decken.</string>
@ -407,6 +428,7 @@
<item quantity="one">%d Netzwerk</item>
<item quantity="other">%d Netzwerke</item>
</plurals>
<string name="common_neutral">Neutral</string>
<string name="common_new_address">Neue Adresse</string>
<string name="common_news">Nachrichten</string>
<string name="common_next">Weiter</string>
@ -414,6 +436,7 @@
<string name="common_no">Nein</string>
<string name="common_no_address">Keine Adresse</string>
<string name="common_no_results">Keine Ergebnisse gefunden</string>
<string name="common_none">Keine</string>
<string name="common_not_added">Nicht hinzugefügt</string>
<string name="common_not_available">Nicht verfügbar</string>
<string name="common_not_now">Nicht jetzt</string>
@ -427,6 +450,7 @@
<string name="common_other">Sonstiges</string>
<string name="common_passphrase">Passphrase</string>
<string name="common_paste">Einfügen</string>
<string name="common_positive">Positiv</string>
<string name="common_privacy_policy">Datenschutzrichtlinie</string>
<string name="common_range">%1$s-%2$s</string>
<string name="common_range_with_space">%1$s — %2$s</string>
@ -728,6 +752,7 @@
<string name="for_you_description">Portfolio prüfen und Verdienstmöglichkeiten erkunden</string>
<string name="for_you_earn_opportunities">Verdienstmöglichkeiten</string>
<string name="for_you_earn_opportunities_all_tokens_active">Alle Ihre Ressourcen sind im Einsatz entdecken Sie neue Möglichkeiten</string>
<string name="for_you_earn_opportunities_explore">Alle Token entdecken</string>
<string name="for_you_earn_opportunities_no_available_tokens">Erhalten Sie jährlich bis zu %1$s</string>
<string name="for_you_earn_opportunities_tokens_rewards">Maximale potenzielle Gewinne %1$s</string>
<string name="for_you_earn_per_year">%1$s/Jahr</string>
@ -741,6 +766,7 @@
<string name="force_update_button">Update</string>
<string name="force_update_os_description">Ihr Betriebssystem ist veraltet. Bitte aktualisieren Sie es, um die App weiterhin nutzen zu können.</string>
<string name="force_update_os_title">Aktualisieren Sie Ihr Betriebssystem</string>
<string name="force_update_required_action">App aktualisieren</string>
<string name="force_update_warning_message">Bitte aktualisiere die Anwendung auf die neueste Version, um eine einwandfreie Funktion zu gewährleisten.</string>
<string name="force_update_warning_title">Aktualisierung erforderlich</string>
<string name="gasless_not_enough_funds_to_cover_token_fee">Nicht genügend Mittel</string>
@ -1313,6 +1339,8 @@
<string name="organize_tokens_sort_by_balance">Nach Guthaben</string>
<string name="organize_tokens_title">Token organisieren</string>
<string name="organize_tokens_ungroup">Gruppe löschen</string>
<string name="prediction_account_subtitle">Guthaben aufladen und mit den Vorhersagen beginnen</string>
<string name="prediction_account_title">Prognosekonto</string>
<string name="promo_campaign_already_activated_subtitle">Berechtigte Cashback-Zahlungen werden wie folgt ausgezahlt:</string>
<string name="promo_campaign_already_activated_title">Sie sind bereits eingeschrieben in %1$s</string>
<string name="promo_campaign_eligible_tokens">Zulässige Token</string>
@ -1622,7 +1650,7 @@
<string name="staking_error_no_validators_message">Zurzeit sind keine Validierer verfügbar. Bitte versuche es später noch einmal.</string>
<string name="staking_error_no_validators_title">Staking nicht verfügbar</string>
<string name="staking_error_unavailable_region">Staking ist in Ihrer Region nicht verfügbar.</string>
<string name="staking_error_unavailable_region_description">Staking ist in Ihrer Region derzeit nicht verfügbar. Falls Sie ein VPN aktiviert haben, deaktivieren Sie es bitte.</string>
<string name="staking_error_unavailable_region_description">Staking ist in Deiner Region derzeit nicht verfügbar. Falls Du ein VPN aktiviert hast, deaktiviere es bitte.</string>
<string name="staking_give_permission_fee_footer">Das Netzwerk erhebt eine Token-Genehmigungsgebühr, um zu überprüfen, ob Du die Verwendung Deines Tokens für das Staking genehmigst.</string>
<string name="staking_legal">Indem Du die Staking-Funktionalität nutzt, stimmst Du den %1$s und %2$s des Anbieters zu.</string>
<string name="staking_locked">Gesperrt</string>
@ -1836,6 +1864,12 @@
<string name="tangem_pay_terms_fees_limits">Bedingungen, Gebühren &amp; Limits</string>
<string name="tangem_pay_terms_limits">Bedingungen und Einschränkungen</string>
<string name="tangem_pay_transaction_declined_notification_text">Die Bank hat diese Transaktionsanfrage abgelehnt.</string>
<string name="tangem_pay_transaction_details_cashback_below_min">Der Mindestbetrag für den Versand beträgt %1$s.</string>
<string name="tangem_pay_transaction_details_cashback_cap_reached">Monatslimit erreicht</string>
<string name="tangem_pay_transaction_details_cashback_mcc_excluded">MCC ausgeschlossen</string>
<string name="tangem_pay_transaction_details_cashback_none">Kein Cashback</string>
<string name="tangem_pay_transaction_details_cashback_refund">Rückgabe des Kaufs</string>
<string name="tangem_pay_transaction_details_cashback_region_excluded">Ausgeschlossene Region</string>
<string name="tangem_pay_transaction_details_category">Kategorie</string>
<string name="tangem_pay_transaction_details_mcc">MCC</string>
<string name="tangem_pay_transaction_fee_notification_text">Eine Gebühr wird gemäß den Servicetarifen erhoben</string>
@ -1945,16 +1979,25 @@
<string name="tangempay_cashback_additional_until">Bis %1$s</string>
<string name="tangempay_cashback_deactivated_description">Dies geschah aufgrund Ihres verdächtigen Verhaltens. Wenden Sie sich an den Support, um mehr zu erfahren.</string>
<string name="tangempay_cashback_deactivated_title">Cashback deaktiviert</string>
<string name="tangempay_cashback_deposit_banner">Der Cashback- %1$s -Betrag für %2$s wird bis zum %3$s</string>
<string name="tangempay_cashback_deposited_on">Wird eingezahlt am %1$s</string>
<string name="tangempay_cashback_details_cap">%1$s maximal pro Monat</string>
<string name="tangempay_cashback_details_eu_excluded">Kein Cashback für Einkäufe vor Ort bei Händlern in der EU</string>
<string name="tangempay_cashback_details_paid_in">Bezahlt in %1$s</string>
<string name="tangempay_cashback_details_tier">%1$s%% Bei allen Einkäufen mit Ihren „ %2$s “-Karten gilt ein Mindestumsatz von %3$s</string>
<string name="tangempay_cashback_earned_title">%1$s verdient in %2$s</string>
<string name="tangempay_cashback_empty_subtitle">Der gesammelte Betrag wird hier angezeigt</string>
<string name="tangempay_cashback_empty_title">Fangen Sie an, Geld auszugeben\nund Cashback zu verdienen</string>
<string name="tangempay_cashback_error_title">Die Seite konnte nicht geladen werden.\nZum Neuladen bitte antippen</string>
<string name="tangempay_cashback_rate_subtitle">Mit Ihrem „ %1$s “-Tarif</string>
<string name="tangempay_cashback_rate_title">Cashback %1$s%%</string>
<string name="tangempay_cashback_rate_title_up_to">Cashback bis zu %1$s%%</string>
<string name="tangempay_cashback_refund_banner">Wir haben eine Rückerstattung für einen Kauf erhalten, für den zuvor bereits Cashback gewährt worden war</string>
<string name="tangempay_cashback_title">Cashback</string>
<string name="tangempay_cashback_total_earned">%1$s insgesamt verdient</string>
<string name="tangempay_cashback_widget_title">%1$s Cashback in %2$s</string>
<string name="tangempay_change_pin_code">PIN-Code ändern</string>
<string name="tangempay_changing_plan">Deinen Tarif ändern</string>
<string name="tangempay_come_back_if_forget_pin">Kehren Sie zur App zurück, falls Sie ihn vergessen.</string>
<string name="tangempay_common_card">Karte</string>
<string name="tangempay_common_error_loading">Fehler beim Laden</string>
@ -2024,6 +2067,9 @@
<string name="tangempay_kyc_rejected_description_span">nicht verifizieren.</string>
<string name="tangempay_main_select_plan">Tarif auswählen</string>
<string name="tangempay_maximum_cards_issued_description">Du kannst bis zu 3 Karten haben. Lösche eine, um eine neue Karte hinzuzufügen.</string>
<string name="tangempay_maximum_cards_issued_for_plan_description">Upgrade-Plan für mehr Karten</string>
<string name="tangempay_maximum_cards_issued_for_plan_title">Das Kartenlimit für diesen Tarif wurde erreicht</string>
<string name="tangempay_maximum_cards_issued_for_plan_upgrade_btn">Upgrade-Plan</string>
<string name="tangempay_maximum_cards_issued_title">Maximale Anzahl ausgegebener Karten</string>
<string name="tangempay_newonboard_Q1_body">Ja — für die Nutzung einer regulierten Visa-Karte ist eine Identitätsprüfung Pflicht. Das KYC wird von Sumsub abgewickelt, dem Compliance-Partner.</string>
<string name="tangempay_newonboard_Q1_title">Muss ich meine Dokumente teilen?</string>
@ -2111,8 +2157,11 @@
<string name="tangempay_topup_swap_body">Laden Sie Ihr Konto mit einem beliebigen Token aus Ihrer Wallet auf</string>
<string name="tangempay_topup_swap_title">Aus Ihrer Tangem Wallet</string>
<string name="tangempay_usdc_on_polygon_network">USDC im Polygon</string>
<string name="tangempay_va_account_details">Kontodetails</string>
<string name="tangempay_va_available_to_deposit_day">Täglich zur Einzahlung verfügbar:</string>
<string name="tangempay_va_banking_details_error_description">Bitte versuche es später erneut. Sollte das Problem weiterhin bestehen, kontaktiere bitte unseren Support. Wir helfen Dir gerne bei der Lösung.</string>
<string name="tangempay_va_banking_details_error_title">Die Bankdaten konnten nicht geladen werden</string>
<string name="tangempay_va_limit_resetting_everyday">Das Limit wird jeden Tag zurückgesetzt</string>
<string name="tangempay_visa_benefits">Visavorteile</string>
<string name="tangempay_withdrawal_note_description">Gelder aus erstatteten Käufen werden nicht auf Ihr On-Chain-Guthaben zurückerstattet und stehen nicht für Abhebungen zur Verfügung, bleiben aber auf Ihrem Kartenguthaben für Einkäufe verfügbar</string>
<string name="tangempay_withdrawal_note_title">Bitte beachten Sie</string>
@ -2160,6 +2209,7 @@
<string name="token_summary_last_update_subtitle">Letzte Aktualisierung: %1$s</string>
<string name="token_summary_negative_outlook_title">Negativer Ausblick</string>
<string name="token_summary_neutral_outlook_title">Neutraler Ausblick</string>
<string name="token_summary_outlook_is_not_available">Outlook für dieses Token ist nicht verfügbar</string>
<string name="token_summary_positive_outlook_title">Positiver Ausblick</string>
<string name="token_summary_title">Token-Zusammenfassung</string>
<string name="token_swap_changelly_promotion_message">Tausche diesen Token gegen einen anderen zu %1$s Servicegebühren von Februar %2$s-%3$s.</string>
@ -2172,6 +2222,8 @@
<string name="tokens_list_unavailable_to_swap_source_header">Nicht zum Tausch verfügbar</string>
<string name="transaction_history_claiming_reward">Belohnung einfordern</string>
<string name="transaction_history_contract_address">Vertrag: %s</string>
<string name="transaction_history_detail_page_amount_signed_value" formatted="false">%1s %2s %3s</string>
<string name="transaction_history_detail_page_amount_value" formatted="false">%1s %2s</string>
<string name="transaction_history_disabling_yield_mode">Deaktivierung des Ertragsmodus</string>
<string name="transaction_history_earned_from_stake">Verdient aus dem Einsatz</string>
<string name="transaction_history_empty_transactions">Du hast noch keine Transaktionen</string>

View file

@ -718,10 +718,16 @@
<string name="feedback_subject_support_tangem">Comentarios en Tangem</string>
<string name="feedback_subject_tx_failed">No se puede completar una transacción</string>
<string name="feedback_token_description_error">Error de descripción de moneda</string>
<string name="force_update_banner_message">Actualice la aplicación a la última versión para garantizar su correcto funcionamiento</string>
<string name="force_update_banner_title">Se necesita actualizar</string>
<string name="force_update_action">Actualizar ahora</string>
<string name="force_update_banner_message">Actualice la aplicación Tangem para desbloquear nuevas funciones y disfrutar de las últimas mejoras.</string>
<string name="force_update_banner_title">Nueva versión disponible</string>
<string name="force_update_brick_description">Tangem no funciona con esta versión del sistema operativo. Sus fondos están a salvo y puede seguir utilizando su billetera en otro dispositivo compatible.</string>
<string name="force_update_brick_title">Actualización no disponible</string>
<string name="force_update_button">Actualizar</string>
<string name="force_update_warning_message">Por favor, actualice la aplicación a la última versión para garantizar su correcto funcionamiento.</string>
<string name="force_update_os_description">Su sistema operativo está desactualizado. Tangem seguirá funcionando por ahora, pero tendrá que actualizarlo para recibir futuras actualizaciones de la aplicación.</string>
<string name="force_update_os_title">Actualice su sistema operativo</string>
<string name="force_update_required_action">Actualizar la app</string>
<string name="force_update_warning_message">Actualice Tangem a la última versión para seguir utilizando la aplicación.</string>
<string name="force_update_warning_title">Es necesario actualizar</string>
<string name="gasless_not_enough_funds_to_cover_token_fee">Fondos insuficientes</string>
<string name="gasless_transaction_fee">Tarifa de transacción</string>
@ -903,6 +909,7 @@
<string name="markets_portfolio_block_subtitle">En su portafolio</string>
<string name="markets_portfolio_block_title">Su portafolio</string>
<string name="markets_portfolio_block_token_unsupported">**Token no soportado**. Este token no está soportado actualmente en la billetera.</string>
<string name="markets_portfolio_eligible_block_title">Otros tokens elegibles</string>
<string name="markets_pulse_common_title">Análisis del Mercado</string>
<string name="markets_quick_actions">Acciones rápidas</string>
<string name="markets_search_clear_all_hints">Borrar todo</string>
@ -1587,6 +1594,7 @@
<string name="staking_error_no_validators_message">No hay validadores disponibles en este momento. Por favor, inténtelo de nuevo más tarde.</string>
<string name="staking_error_no_validators_title">Staking no disponible</string>
<string name="staking_error_unavailable_region">Staking no está disponible en tu región.</string>
<string name="staking_error_unavailable_region_description">Actualmente, el staking no está disponible en su región. Si tiene activada una VPN, pruebe a desactivarla.</string>
<string name="staking_give_permission_fee_footer">La red cobrará una tarifa de aprobación de token para verificar que usted está autorizando el uso de su token para el staking.</string>
<string name="staking_legal">Al utilizar la función de staking, usted acepta %1$s y %2$s del proveedor</string>
<string name="staking_locked">Bloqueado</string>
@ -1696,6 +1704,8 @@
<string name="story_web3_title">Compatible con Web 3.0</string>
<string name="sui_not_enough_coin_for_fee_description">Se requiere una transacción entrante de al menos %1$s para proceder</string>
<string name="sui_not_enough_coin_for_fee_title">Fondos insuficientes</string>
<string name="support_chat_screen_title">Chat de soporte</string>
<string name="support_chat_share_logs_button">Adjuntar logs de la app</string>
<string name="support_chat_swap_prefilled_message">Datos de la operación SWAP:\nDe: %1$s %2$s\nPara: %3$s %4$s\nPor %5$s - %6$s</string>
<string name="support_selector_view_chat_button">Abrir chat</string>
<string name="support_selector_view_email_button">Abrir correo</string>

View file

@ -654,9 +654,15 @@
<string name="feedback_subject_support_tangem">Commentaires sur Tangem</string>
<string name="feedback_subject_tx_failed">Impossible d\'effectuer une transaction</string>
<string name="feedback_token_description_error">Erreur de description de la pièce</string>
<string name="force_update_action">Mettre à jour maintenant</string>
<string name="force_update_banner_message">Mettez l\'application à jour pour assurer son bon fonctionnement</string>
<string name="force_update_banner_title">Mise à jour nécessaire</string>
<string name="force_update_brick_description">Tangem ne peut pas fonctionner sur cette version de système d\'exploitation. Vos fonds sont en sécurité, et vous pouvez continuer à utiliser votre wallet sur un autre appareil compatible.</string>
<string name="force_update_brick_title">Mise à jour indisponible</string>
<string name="force_update_button">Mettre à jour</string>
<string name="force_update_os_description">Votre système d\'exploitation est obsolète. Tangem continuera de fonctionner pour l\'instant, mais vous devrez le mettre à jour pour recevoir les futures mises à jour de l\'application.</string>
<string name="force_update_os_title">Mettez à jour votre système d\'exploitation</string>
<string name="force_update_required_action">Mettre à jour l\'application</string>
<string name="force_update_warning_message">Veuillez mettre à jour l\'application pour assurer son bon fonctionnement</string>
<string name="force_update_warning_title">Mise à jour requise</string>
<string name="gasless_not_enough_funds_to_cover_token_fee">Fonds insuffisants</string>
@ -834,6 +840,7 @@
<string name="markets_portfolio_block_subtitle">Dans votre portfolio</string>
<string name="markets_portfolio_block_title">Votre portfolio</string>
<string name="markets_portfolio_block_token_unsupported">**Token non pris en charge**. Ce token n\'est actuellement pas pris en charge dans le portefeuille</string>
<string name="markets_portfolio_eligible_block_title">Autres tokens éligibles</string>
<string name="markets_pulse_common_title">Rythme du marché</string>
<string name="markets_quick_actions">Actions rapides</string>
<string name="markets_search_header_title">Rechercher sur le marché</string>
@ -1482,6 +1489,7 @@
<string name="staking_error_no_validators_message">Aucun validateur disponible pour le moment. Veuillez réessayer plus tard.</string>
<string name="staking_error_no_validators_title">Staking indisponible</string>
<string name="staking_error_unavailable_region">Le staking est indisponible dans votre région</string>
<string name="staking_error_unavailable_region_description">Le staking n\'est actuellement pas disponible dans votre région. Si vous avez un VPN activé, essayez de le désactiver.</string>
<string name="staking_give_permission_fee_footer">Le réseau facturera des frais dapprobation de jeton pour vérifier que vous autorisez lutilisation de votre jeton pour le jalonnement.</string>
<string name="staking_legal">En utilisant la fonctionnalité de staking, vous acceptez les %1$s et %2$s du fournisseur</string>
<string name="staking_locked">Bloqué</string>

View file

@ -277,6 +277,7 @@
<string name="common_added">追加済み</string>
<string name="common_address">アドレス</string>
<string name="common_all">すべて</string>
<string name="common_all_accounts">すべてのアカウント</string>
<string name="common_allow">許可する</string>
<string name="common_amount">金額</string>
<string name="common_analytics">アナリティクス</string>
@ -321,6 +322,7 @@
<string name="common_create">作成</string>
<string name="common_crypto_fiat_format">%1$s ( %2$s )</string>
<string name="common_custom">設定</string>
<string name="common_day"></string>
<plurals name="common_days">
<item quantity="other">%d 日</item>
</plurals>
@ -343,7 +345,7 @@
<string name="common_error">エラー</string>
<string name="common_estimated_fee">入金時のネットワーク手数料</string>
<string name="common_exchange">スワップ</string>
<string name="common_explore">移動す</string>
<string name="common_explore">詳しく見</string>
<string name="common_explore_transaction_history">取引履歴を調べる</string>
<string name="common_explorer">エクスプローラー</string>
<string name="common_fee_error">手数料を取得できませんでした</string>
@ -363,6 +365,7 @@
<string name="common_get_token">トークンを取得</string>
<string name="common_go_to_provider">プロバイダーへ移動</string>
<string name="common_go_to_token">トークンへ移動</string>
<string name="common_go_to_verification">確認画面へ進む</string>
<string name="common_got_it">わかりました</string>
<string name="common_hide">非表示</string>
<string name="common_hold_to">%sまで長押し</string>
@ -374,7 +377,7 @@
<item quantity="other">%d時間前</item>
</plurals>
<string name="common_import">インポート</string>
<string name="common_in"></string>
<string name="common_in"></string>
<string name="common_in_progress">進行中</string>
<string name="common_insufficient_balance">残高不足</string>
<string name="common_later">後で</string>
@ -388,13 +391,21 @@
<item quantity="other">%d分前</item>
</plurals>
<string name="common_month"></string>
<string-array name="common_month_in">
<item>9月</item>
<item>10月</item>
<item>11月</item>
<item>12月</item>
</string-array>
<string name="common_more">さらに</string>
<string name="common_negative">ネガティブ</string>
<string name="common_network">ネットワーク</string>
<string name="common_network_fee_title">ネットワーク手数料</string>
<string name="common_network_fee_warning_content">送金額は、選択された手数料レベルをカバーするため、%1$s (%2$s) 減額されます。</string>
<plurals name="common_networks_count">
<item quantity="other">%dネットワーク</item>
</plurals>
<string name="common_neutral">中立</string>
<string name="common_new_address">新しいアドレス</string>
<string name="common_news">ニュース</string>
<string name="common_next"></string>
@ -402,6 +413,7 @@
<string name="common_no">いいえ</string>
<string name="common_no_address">アドレスがありません</string>
<string name="common_no_results">該当する結果はありません</string>
<string name="common_none">なし</string>
<string name="common_not_added">未追加</string>
<string name="common_not_available">利用できません</string>
<string name="common_not_now">今はしない</string>
@ -414,9 +426,11 @@
<string name="common_origin_ring">プライマリーリング</string>
<string name="common_passphrase">パスフレーズ</string>
<string name="common_paste">ペースト</string>
<string name="common_positive">ポジティブ</string>
<string name="common_privacy_policy">プライバシーポリシー</string>
<string name="common_range">%1$s-%2$s</string>
<string name="common_range_with_space">%1$s — %2$s</string>
<string name="common_rate">レート</string>
<string name="common_read_more">続きを読む</string>
<string name="common_receive">受け取る</string>
<string name="common_received">受け取り済み</string>
@ -477,6 +491,7 @@
<item quantity="other">%d トークン</item>
</plurals>
<string name="common_transaction_failed">取引が失敗しました</string>
<string name="common_transaction_id">トランザクションID</string>
<string name="common_transaction_status">取引状況</string>
<string name="common_transactions">取引</string>
<string name="common_transfer">送金</string>
@ -623,32 +638,34 @@
<string name="express_exchange_notification_long_transaction_time_title">長い取引時間</string>
<string name="express_exchange_notification_refund_text">OKXまたはブリッジのルールにより、取引金額は%1$sでウォレットに返金されました。%2$s</string>
<string name="express_exchange_notification_refund_title">金額は %1$s%2$sネットワークで返金されました</string>
<string name="express_exchange_notification_refunded_in_text">OKX取引所のルールに基づき、%1$sは%2$sネットワーク上のお客様のウォレットに返金されました。</string>
<string name="express_exchange_notification_refunded_in_title">%sで返金済み</string>
<string name="express_exchange_notification_verification_text">確認するには、プロバイダーのウェブサイトにアクセスしてください。</string>
<string name="express_exchange_notification_verification_title">プロバイダーによる本人確認手続きが必要です。</string>
<string name="express_exchange_status_bought">買付完了</string>
<string name="express_exchange_status_buying">買付待ち</string>
<string name="express_exchange_status_buying_active">買付中...</string>
<string name="express_exchange_status_bought">購入が完了しました</string>
<string name="express_exchange_status_buying">購入待ち</string>
<string name="express_exchange_status_buying_active">購入処理待ち...</string>
<string name="express_exchange_status_canceled">取引はキャンセルされました</string>
<string name="express_exchange_status_confirmed">入金確認済み</string>
<string name="express_exchange_status_confirming">確認待ち</string>
<string name="express_exchange_status_confirming_active">確認待ち...</string>
<string name="express_exchange_status_exchanged">交換完了</string>
<string name="express_exchange_status_exchanged">スワップ完了</string>
<string name="express_exchange_status_exchanging">交換待ち</string>
<string name="express_exchange_status_exchanging_active">スワップ待ち...</string>
<string name="express_exchange_status_failed">交換に失敗しました</string>
<string name="express_exchange_status_paused">交換は一時停止されました</string>
<string name="express_exchange_status_failed">スワップに失敗しました</string>
<string name="express_exchange_status_paused">スワップは一時停止されました</string>
<string name="express_exchange_status_received">入金済み</string>
<string name="express_exchange_status_receiving">入金待ち</string>
<string name="express_exchange_status_receiving_active">入金待ち...</string>
<string name="express_exchange_status_refunded">払い戻し完了</string>
<string name="express_exchange_status_refunding">払い戻し待ち</string>
<string name="express_exchange_status_refunded">返金完了</string>
<string name="express_exchange_status_refunding">返金待ち</string>
<string name="express_exchange_status_sending">あなたへ送金しています</string>
<string name="express_exchange_status_sending_active">資金の送金中...</string>
<string name="express_exchange_status_sending_active">送金中...</string>
<string name="express_exchange_status_sent">送金済み資金</string>
<string name="express_exchange_status_subtitle">プロバイダー提供のデータ。推定額は市場動向により変更される場合があります。</string>
<string name="express_exchange_status_title">交換ステータス</string>
<string name="express_exchange_status_verifying">確認が必要です</string>
<string name="express_exchange_status_waiting_tx_hash">取引ハッシュを待機中</string>
<string name="express_exchange_status_waiting_tx_hash">トランザクションハッシュ待ち</string>
<string name="express_exchange_token_list_subtitle">ウォレットに追加されたすべてのトークンのリスト</string>
<string name="express_fetch_best_rates">現在のレートを取得中...</string>
<string name="express_floating_rate">変動レート</string>
@ -707,14 +724,16 @@
<string name="feedback_subject_support_tangem">Tangemへのフィードバック</string>
<string name="feedback_subject_tx_failed">取引を送信できません</string>
<string name="feedback_token_description_error">コインの説明エラー</string>
<string name="force_update_banner_message">正常に動作するよう、アプリを最新バージョンに更新してください。</string>
<string name="force_update_banner_title">アップデートが必要です</string>
<string name="force_update_brick_description">このバージョンのアプリはサポート対象外となっており、このデバイスでは更新できません。</string>
<string name="force_update_brick_title">更新できません</string>
<string name="force_update_action">今すぐアップデート</string>
<string name="force_update_banner_message">Tangemアプリをアップデートして、新機能や最新の改善をご利用ください。</string>
<string name="force_update_banner_title">新しいバージョンをご利用いただけます</string>
<string name="force_update_brick_description">TangemはこのOSバージョンではご利用いただけません。資産は安全です。対応している別のデバイスでウォレットを引き続きご利用ください。</string>
<string name="force_update_brick_title">アップデートできません</string>
<string name="force_update_button">アップデート</string>
<string name="force_update_os_description">使用中のOSが古くなっています。アプリを引き続き使用するには、OSを更新してください。</string>
<string name="force_update_os_title">OSの更新が必要です</string>
<string name="force_update_warning_message">正常に動作するよう、アプリを最新バージョンに更新してください。</string>
<string name="force_update_os_description">お使いのOSは古いバージョンです。現在は引き続きTangemをご利用いただけますが、今後のアプリのアップデートを受け取るには、OSを更新する必要があります。</string>
<string name="force_update_os_title">OSをアップデートしてください</string>
<string name="force_update_required_action">アプリをアップデートしてください</string>
<string name="force_update_warning_message">アプリを引き続きご利用いただくには、Tangemを最新バージョンにアップデートしてください。</string>
<string name="force_update_warning_title">アップデートが必要です</string>
<string name="gasless_not_enough_funds_to_cover_token_fee">残高不足</string>
<string name="gasless_transaction_fee">取引手数料</string>
@ -895,6 +914,7 @@
<string name="markets_portfolio_block_subtitle">ポートフォリオ内</string>
<string name="markets_portfolio_block_title">ポートフォリオ</string>
<string name="markets_portfolio_block_token_unsupported">**トークンはサポートされていません**。このトークンは現在、このウォレットではサポートされていません</string>
<string name="markets_portfolio_eligible_block_title">その他の対象トークン</string>
<string name="markets_pulse_common_title">マーケット動向</string>
<string name="markets_quick_actions">クイックアクション</string>
<string name="markets_search_clear_all_hints">すべてクリア</string>
@ -1463,7 +1483,7 @@
<string name="send_rate_floating_info_title">変動レート</string>
<string name="send_rate_is_fixed">レートは固定されています</string>
<string name="send_recent_transactions">最近の取引</string>
<string name="send_recipient">受取人</string>
<string name="send_recipient">送金先</string>
<string name="send_recipient_address_error">有効なアドレスではありません</string>
<string name="send_recipient_address_footer">トークンの損失を避けるために、受信ウォレットアドレスが**%sネットワーク**上にあることを確認してください</string>
<string name="send_recipient_label">送金先</string>
@ -1564,6 +1584,7 @@
<string name="staking_error_no_validators_message">現在、利用可能なバリデーターは見つかりません。しばらくしてからもう一度お試しください。</string>
<string name="staking_error_no_validators_title">ステーキングは利用できません</string>
<string name="staking_error_unavailable_region">お住まいの地域ではステーキングをご利用いただけません</string>
<string name="staking_error_unavailable_region_description">現在、お住まいの地域ではステーキングをご利用いただけません。VPNをご利用の場合は、無効にしてから再度お試しください。</string>
<string name="staking_give_permission_fee_footer">ネットワークは、ステーキングのためにトークンの使用を承認していることを確認するために、トークン承認手数料を請求します。</string>
<string name="staking_legal">ステーキング機能を使用すると、プロバイダーの%1$sと%2$sに同意したことになります</string>
<string name="staking_locked">ロック中</string>
@ -1719,7 +1740,7 @@
<string name="swapping_fee_estimation_error_text">手数料見積りエラーです。サポートにフィードバックをお送りください。</string>
<string name="swapping_from_account_title">送信元</string>
<string name="swapping_from_title">スワップする</string>
<string name="swapping_from_title_v2"></string>
<string name="swapping_from_title_v2"></string>
<string name="swapping_high_price_impact_description">選択したトークンをこの量を交換すると、価格に大きな影響が生じ、結果が減少します。</string>
<string name="swapping_high_price_impact_text">流動性が低いため、受取額が大幅に少なくなる可能性があります。金額を減らすか、別のプロバイダーをお試しください。</string>
<string name="swapping_high_price_impact_title">価格への影響が甚大です</string>
@ -2030,7 +2051,7 @@
<string name="tokens_list_unavailable_to_sell_header">売却できません</string>
<string name="tokens_list_unavailable_to_swap_header">%sからのスワップは利用できません</string>
<string name="tokens_list_unavailable_to_swap_source_header">スワップはできません</string>
<string name="transaction_history_claiming_reward">報酬を請求</string>
<string name="transaction_history_claiming_reward">報酬受け取り</string>
<string name="transaction_history_contract_address">コントラクト: %s</string>
<string name="transaction_history_disabling_yield_mode">利息モードを無効化中</string>
<string name="transaction_history_earned_from_stake">ステーキングで獲得した金額</string>
@ -2039,7 +2060,7 @@
<string name="transaction_history_from_inline_address">%%image%% %s から</string>
<string name="transaction_history_multiple_addresses">複数のアドレス</string>
<string name="transaction_history_not_supported_description">現在、このブロックチェーンでは取引履歴はサポートされていません。しかしご心配なく!弊社で対応中です。その間、エクスプローラーで確認することができます。</string>
<string name="transaction_history_operation">オペレーション</string>
<string name="transaction_history_operation">処理</string>
<string name="transaction_history_pending">保留中</string>
<string name="transaction_history_rewards_restaked">報酬を再ステーキングしました</string>
<string name="transaction_history_rewards_restaking">報酬の再ステーキング</string>
@ -2068,6 +2089,9 @@
<string name="twins_recreate_toolbar">Tangemツイン</string>
<string name="twins_recreate_warning">この操作は元に戻せません。古いウォレットにはアクセスできなくなります。</string>
<string name="twins_scan_twin_with_number">番号%sのツインカードをタップし、操作が終了するまで取り外さないでください。</string>
<string name="tx_history_onramp_top_up">チャージ</string>
<string name="tx_history_onramp_topped_up">チャージ済み</string>
<string name="tx_history_you_paid">支払額</string>
<string name="unexpected_error_description">しばらくしてからもう一度お試しください。問題が解決しない場合は、サポートにお問い合わせください。</string>
<string name="unexpected_error_title">問題が発生しました。</string>
<string name="universal_error">エラーが発生しました。エラー コード:%s。サポートにお問い合わせください。</string>
@ -2239,7 +2263,7 @@
<string name="wallet_marketplace_block_title">%s市場価格</string>
<string name="wallet_marketprice_block_update_time">直近24時間</string>
<string name="wallet_network_group_title">%sネットワーク</string>
<string name="wallet_notification_address_copied">アドレスがクリップボードにコピーされました</string>
<string name="wallet_notification_address_copied">アドレスをクリップボードにコピーしました</string>
<string name="wallet_notification_no_internet">インターネット接続がありません</string>
<string name="wallet_promo_banner_button_title">今すぐ10 % オフで購入</string>
<string name="wallet_promo_banner_description">1.3万種類以上の暗号資産にアクセス。ワンタップで買付、売却、スワップ、ステーキングが可能です。\nバックアップ用に最大3枚のカードを連携できます。</string>
@ -2598,13 +2622,15 @@
<string name="yield_module_token_details_earn_notification_processing">利息モードの有効化</string>
<string name="yield_module_token_details_earn_notification_subtitle">利息モード - %1$s%% APY</string>
<string name="yield_module_token_details_earn_notification_title">利息モード</string>
<string name="yield_module_transaction_deploy_contract">利息モードコントラクトのデプロイ</string>
<string name="yield_module_transaction_deploy_contract">利息モードのコントラクトをデプロイ</string>
<string name="yield_module_transaction_enter">利息モードが有効になりました</string>
<string name="yield_module_transaction_enter_subtitle">%1$sがAaveに供給されました</string>
<string name="yield_module_transaction_exit">利息モードが無効になりました</string>
<string name="yield_module_transaction_exit_subtitle">%1$sがAaveから引き出されました</string>
<string name="yield_module_transaction_initialize">利息モードを初期化しました</string>
<string name="yield_module_transaction_reactivate">利息モードが再度有効になりました</string>
<string name="yield_module_transaction_returned">返却済み</string>
<string name="yield_module_transaction_supplied">供給済み</string>
<string name="yield_module_transaction_topup">Aaveへの供給</string>
<string name="yield_module_transaction_topup_subtitle">%1$sがAaveに供給されました</string>
<string name="yield_module_transaction_withdraw">Aaveから引き出す</string>

View file

@ -281,6 +281,7 @@
<string name="common_added">Adicionado</string>
<string name="common_address">Endereço</string>
<string name="common_all">Todos</string>
<string name="common_all_accounts">Todas as contas</string>
<string name="common_allow">Permitir</string>
<string name="common_amount">Quantia</string>
<string name="common_analytics">Análises</string>
@ -290,6 +291,10 @@
<string name="common_approve">Aprovar</string>
<string name="common_approved">Aprovado</string>
<string name="common_approving">Aprovando</string>
<plurals name="common_assets_count">
<item quantity="one">%d ativo</item>
<item quantity="other">%d ativos</item>
</plurals>
<string name="common_attention">Atenção</string>
<string name="common_available_networks">Redes disponíveis</string>
<string name="common_backup">Backup</string>
@ -325,6 +330,7 @@
<string name="common_create">Criar</string>
<string name="common_crypto_fiat_format">%1$s (%2$s)</string>
<string name="common_custom">Personalizado</string>
<string name="common_day">dia</string>
<plurals name="common_days">
<item quantity="one">dia</item>
<item quantity="other">dias</item>
@ -399,7 +405,22 @@
<item quantity="other">OUTRO</item>
</plurals>
<string name="common_month">mês</string>
<string-array name="common_month_in">
<item>Janeiro</item>
<item>Fevereiro</item>
<item>Março</item>
<item>Abril</item>
<item>Maio</item>
<item>Junho</item>
<item>Julho</item>
<item>Agosto</item>
<item>Setembro</item>
<item>Outubro</item>
<item>Novembro</item>
<item>Dezembro</item>
</string-array>
<string name="common_more">Mais</string>
<string name="common_negative">Negativo</string>
<string name="common_network">Rede</string>
<string name="common_network_fee_title">Taxa de rede</string>
<string name="common_network_fee_warning_content">O valor enviado será reduzido em %1$s (%2$s) para cobrir o nível de taxa selecionado</string>
@ -407,6 +428,7 @@
<item quantity="one">UM</item>
<item quantity="other">OUTRO</item>
</plurals>
<string name="common_neutral">Neutro</string>
<string name="common_new_address">Novo endereço</string>
<string name="common_news">Notícias</string>
<string name="common_next">Próximo</string>
@ -414,6 +436,7 @@
<string name="common_no">Não</string>
<string name="common_no_address">Sem endereço</string>
<string name="common_no_results">Nenhum resultado</string>
<string name="common_none">Nenhum</string>
<string name="common_not_added">Não adicionado</string>
<string name="common_not_available">Não disponível</string>
<string name="common_not_now">Agora não</string>
@ -427,6 +450,7 @@
<string name="common_other">Outro</string>
<string name="common_passphrase">Senha</string>
<string name="common_paste">Colar</string>
<string name="common_positive">Positivo</string>
<string name="common_privacy_policy">política de Privacidade</string>
<string name="common_range">%1$s-%2$s</string>
<string name="common_range_with_space">%1$s — %2$s</string>
@ -728,6 +752,7 @@
<string name="for_you_description">Analise seu portfólio e explore oportunidades de ganhos.</string>
<string name="for_you_earn_opportunities">Oportunidades de ganhar</string>
<string name="for_you_earn_opportunities_all_tokens_active">Todos os seus recursos estão em ação, explore novas oportunidades.</string>
<string name="for_you_earn_opportunities_explore">Explore todos os tokens</string>
<string name="for_you_earn_opportunities_no_available_tokens">Levante-se %1$s anualmente</string>
<string name="for_you_earn_opportunities_tokens_rewards">recompensas potenciais máximas %1$s</string>
<string name="for_you_earn_per_year">%1$s/ano</string>
@ -741,6 +766,7 @@
<string name="force_update_button">Atualizar</string>
<string name="force_update_os_description">Seu sistema operacional está desatualizado. Atualize-o para continuar usando o aplicativo.</string>
<string name="force_update_os_title">Atualize seu sistema operacional</string>
<string name="force_update_required_action">Atualizar aplicativo</string>
<string name="force_update_warning_message">Por favor, atualize o aplicativo para a versão mais recente para garantir o funcionamento correto.</string>
<string name="force_update_warning_title">Atualização necessária</string>
<string name="gasless_not_enough_funds_to_cover_token_fee">Fundos insuficientes</string>
@ -908,9 +934,11 @@
<item quantity="one">%d ativo</item>
<item quantity="other">%d ativos</item>
</plurals>
<string name="market_chart_bubble_no_amount">Nenhum valor em fichas</string>
<string name="market_chart_bubble_no_data">Sem dados</string>
<string name="market_chart_bubble_total_value">Valor total</string>
<string name="market_chart_can_not_load_data">Não foi possível carregar os dados.</string>
<string name="market_chart_no_amount">Você não possui nenhum token com o valor</string>
<string name="market_chart_top_holding">Posição superior %s</string>
<string name="markets_about_coin_header">Sobre a moeda</string>
<string name="markets_add_to_my_portfolio_description">Para comprar, trocar ou receber este ativo, adicione-o à sua carteira.</string>
@ -1311,6 +1339,8 @@
<string name="organize_tokens_sort_by_balance">Por equilíbrio</string>
<string name="organize_tokens_title">Organizar tokens</string>
<string name="organize_tokens_ungroup">Desagrupar</string>
<string name="prediction_account_subtitle">Adicione fundos e comece a fazer previsões.</string>
<string name="prediction_account_title">Conta de previsão</string>
<string name="promo_campaign_already_activated_subtitle">O cashback elegível será distribuído para:</string>
<string name="promo_campaign_already_activated_title">Você já está matriculado(a) em %1$s</string>
<string name="promo_campaign_eligible_tokens">Tokens elegíveis</string>
@ -1834,6 +1864,12 @@
<string name="tangem_pay_terms_fees_limits">Termos, taxas e limites</string>
<string name="tangem_pay_terms_limits">Termos e Limites</string>
<string name="tangem_pay_transaction_declined_notification_text">O banco rejeitou esta solicitação de transação.</string>
<string name="tangem_pay_transaction_details_cashback_below_min">O valor mínimo da transação é %1$s</string>
<string name="tangem_pay_transaction_details_cashback_cap_reached">O limite mensal foi atingido.</string>
<string name="tangem_pay_transaction_details_cashback_mcc_excluded">MCC excluído</string>
<string name="tangem_pay_transaction_details_cashback_none">Sem reembolso</string>
<string name="tangem_pay_transaction_details_cashback_refund">Devolução da compra</string>
<string name="tangem_pay_transaction_details_cashback_region_excluded">Região excluída</string>
<string name="tangem_pay_transaction_details_category">Categoria</string>
<string name="tangem_pay_transaction_details_mcc">MCC</string>
<string name="tangem_pay_transaction_fee_notification_text">Uma taxa é cobrada de acordo com as tarifas de serviço</string>
@ -1864,6 +1900,9 @@
<string name="tangempay_card_details_add_funds">Adicionar fundos</string>
<string name="tangempay_card_details_add_funds_subtitle">Opções de recarga</string>
<string name="tangempay_card_details_add_to_wallet_button_text">Adicionar ao Google Wallet</string>
<string name="tangempay_card_details_awaiting_deposit_cancel_button">Cancelar %1$s, mude-se para %2$s</string>
<string name="tangempay_card_details_awaiting_deposit_subtitle">Para pagar a mensalidade do plano e começar a usar o cartão.</string>
<string name="tangempay_card_details_awaiting_deposit_title">Recarregue sua conta em %1$s</string>
<string name="tangempay_card_details_card_number">Número do cartão</string>
<string name="tangempay_card_details_change_pin">Código PIN</string>
<string name="tangempay_card_details_change_pin_success_description">O cartão está totalmente pronto para pagamentos.</string>
@ -1898,6 +1937,8 @@
<string name="tangempay_card_details_rename_card_placeholder">Nome do cartão</string>
<string name="tangempay_card_details_reveal_text">Revelar</string>
<string name="tangempay_card_details_show_details">Detalhes</string>
<string name="tangempay_card_details_system_downgrade_subtitle">Se permanecer abaixo de zero, seu %1$s Os cartões serão fechados em %2$s</string>
<string name="tangempay_card_details_system_downgrade_title">Recarregue sua conta em breve.</string>
<string name="tangempay_card_details_title">Detalhes</string>
<string name="tangempay_card_details_unable_to_rename_card_description">Por favor, tente novamente mais tarde.</string>
<string name="tangempay_card_details_unfreeze_card">Descongelar cartão</string>
@ -1925,17 +1966,49 @@
<item quantity="one">%d cartão</item>
<item quantity="other">%d cartões</item>
</plurals>
<string name="tangempay_cashback_accruals_calc_description">Processamos as compras em até 5 dias após a operação e contabilizamos apenas as transações concluídas.</string>
<string name="tangempay_cashback_accruals_calc_title">Como calculamos o cashback?</string>
<string name="tangempay_cashback_accruals_exceptions_description">Não haverá reembolso em dinheiro para compras presenciais em estabelecimentos comerciais da UE; o mesmo se aplica a saques, transferências, pagamentos eletrônicos, contas de celular, serviços governamentais e algumas outras categorias.</string>
<string name="tangempay_cashback_accruals_exceptions_title">Exceções</string>
<string name="tangempay_cashback_accruals_pay_description">Do dia 2 ao dia 5 do próximo mês</string>
<string name="tangempay_cashback_accruals_pay_title">Como pagamos o cashback?</string>
<string name="tangempay_cashback_accruals_subtitle">Limites e exceções</string>
<string name="tangempay_cashback_accruals_title">Acréscimos</string>
<string name="tangempay_cashback_additional_permanent">Permanente</string>
<string name="tangempay_cashback_additional_title">Cashback adicional</string>
<string name="tangempay_cashback_additional_until">Até %1$s</string>
<string name="tangempay_cashback_deactivated_description">Isso ocorreu devido ao seu comportamento suspeito. Entre em contato com o suporte para saber mais.</string>
<string name="tangempay_cashback_deactivated_title">Cashback desativado</string>
<string name="tangempay_cashback_deposit_banner">Cashback %1$s para %2$s será depositado até %3$s</string>
<string name="tangempay_cashback_deposited_on">Será depositado em %1$s</string>
<string name="tangempay_cashback_details_cap">%1$s máximo por mês</string>
<string name="tangempay_cashback_details_eu_excluded">Não há reembolso em compras presenciais em estabelecimentos comerciais da UE.</string>
<string name="tangempay_cashback_details_paid_in">Pago em %1$s</string>
<string name="tangempay_cashback_details_tier">%1$s%% para todas as compras com o seu %2$s cartões, compra mínima %3$s</string>
<string name="tangempay_cashback_earned_title">%1$s ganho em %2$s</string>
<string name="tangempay_cashback_empty_subtitle">O valor arrecadado será exibido aqui.</string>
<string name="tangempay_cashback_empty_title">Comece a gastar e ganhe cashback</string>
<string name="tangempay_cashback_error_title">Não foi possível carregar a página.\nToque para recarregar</string>
<string name="tangempay_cashback_rate_subtitle">Com o seu %1$s plano</string>
<string name="tangempay_cashback_rate_title">Cashback %1$s%%</string>
<string name="tangempay_cashback_rate_title_up_to">Cashback de até %1$s%%</string>
<string name="tangempay_cashback_refund_banner">Recebemos um reembolso por uma compra para a qual já havíamos recebido cashback.</string>
<string name="tangempay_cashback_title">Cashback</string>
<string name="tangempay_cashback_total_earned">%1$s ganhos no total</string>
<string name="tangempay_cashback_widget_title">%1$s cashback em %2$s</string>
<string name="tangempay_change_pin_code">Alterar código PIN</string>
<string name="tangempay_changing_plan">Alterar seu plano</string>
<string name="tangempay_come_back_if_forget_pin">Volte ao aplicativo se você se esquecer.</string>
<string name="tangempay_common_card">Cartão</string>
<string name="tangempay_common_error_loading">Erro ao carregar</string>
<string name="tangempay_current_plan_active_till_notification">Seu %1$s O plano está ativo até %2$s, então nós o transferiremos para %3$s. %4$s Não será cobrado.</string>
<string name="tangempay_current_plan_change">Alterar plano</string>
<string name="tangempay_current_plan_fee_charged_notification">%1$s Será cobrada uma taxa mensal em %2$s</string>
<string name="tangempay_current_plan_section_card">relacionado a cartões</string>
<string name="tangempay_current_plan_section_plan">Plano relacionado</string>
<string name="tangempay_current_plan_stay_button">Continue em %1$s</string>
<string name="tangempay_current_plan_stay_sheet_body">Sua transição em %1$s será cancelado</string>
<string name="tangempay_current_plan_stay_sheet_title">Você deseja continuar? %1$s?</string>
<string name="tangempay_current_plan_title">Plano atual</string>
<string name="tangempay_daily_limit_hint" formatted="false">Defina um limite a partir de %s para %s</string>
<string name="tangempay_daily_limit_set_button">Definir limites</string>
@ -1994,6 +2067,9 @@
<string name="tangempay_kyc_rejected_description_span">Seu perfil.</string>
<string name="tangempay_main_select_plan">Selecione o plano</string>
<string name="tangempay_maximum_cards_issued_description">Você pode ter até 3 cartões. Exclua um para adicionar um novo.</string>
<string name="tangempay_maximum_cards_issued_for_plan_description">Atualize seu plano para ter mais cartões.</string>
<string name="tangempay_maximum_cards_issued_for_plan_title">O limite de cartões para este plano foi atingido.</string>
<string name="tangempay_maximum_cards_issued_for_plan_upgrade_btn">Plano de atualização</string>
<string name="tangempay_maximum_cards_issued_title">Número máximo de cartões emitidos</string>
<string name="tangempay_newonboard_Q1_body">Sim — para usar um cartão Visa regulado, a verificação de identidade é obrigatória. O KYC é feito pela Sumsub, parceira de compliance.</string>
<string name="tangempay_newonboard_Q1_title">Preciso enviar meus documentos?</string>
@ -2047,7 +2123,16 @@
<string name="tangempay_select_plan_btn_select">Selecionar</string>
<string name="tangempay_select_plan_btn_upgrade">Plano de atualização</string>
<string name="tangempay_select_plan_compare">Comparar planos</string>
<string name="tangempay_select_plan_confirm_downgrade_title">Seu %1$s plano e %2$s Os cartões ficarão ativos até %3$s</string>
<string name="tangempay_select_plan_confirm_point_cancel_till">Você pode cancelar essa transição até %1$s</string>
<string name="tangempay_select_plan_confirm_point_cards_closed">Seu %1$s Os cartões serão fechados.</string>
<string name="tangempay_select_plan_confirm_point_monthly_fee">%1$s A taxa mensal será debitada da sua conta.</string>
<string name="tangempay_select_plan_confirm_point_move_on_date">Sobre %1$s nós o transferiremos para %2$s plano</string>
<string name="tangempay_select_plan_confirm_point_no_fee">Nenhuma taxa será cobrada.</string>
<string name="tangempay_select_plan_confirm_point_virtual_card">Você receberá sua versão virtual. %1$s em minutos</string>
<string name="tangempay_select_plan_confirm_switch_title">Você está mudando para %1$s</string>
<string name="tangempay_select_plan_confirm_title">Confirmar seleção</string>
<string name="tangempay_select_plan_confirm_upgrade_title">Iremos emitir %1$s para você</string>
<string name="tangempay_select_plan_title">Selecione um plano</string>
<string name="tangempay_service_unavailable_description">Estamos resolvendo um problema técnico. Por favor, tente novamente mais tarde.</string>
<string name="tangempay_service_unavailable_title">Serviço temporariamente indisponível</string>
@ -2072,8 +2157,11 @@
<string name="tangempay_topup_swap_body">Recarregue sua conta com qualquer token da carteira</string>
<string name="tangempay_topup_swap_title">Da sua Tangem Wallet</string>
<string name="tangempay_usdc_on_polygon_network">USDC na rede Polygon</string>
<string name="tangempay_va_account_details">Detalhes da conta</string>
<string name="tangempay_va_available_to_deposit_day">Disponível para depósito por dia:</string>
<string name="tangempay_va_banking_details_error_description">Por favor, tente novamente ou entre em contato com o suporte se o problema persistir.</string>
<string name="tangempay_va_banking_details_error_title">Não foi possível carregar os dados bancários.</string>
<string name="tangempay_va_limit_resetting_everyday">O limite é reiniciado todos os dias.</string>
<string name="tangempay_visa_benefits">Benefícios do visto</string>
<string name="tangempay_withdrawal_note_description">Os fundos de compras reembolsadas não serão devolvidos ao seu saldo na blockchain nem estarão disponíveis para saque, mas permanecerão no saldo do seu cartão para compras futuras</string>
<string name="tangempay_withdrawal_note_title">Observe</string>

View file

@ -327,7 +327,7 @@
<string name="common_contact_visa_support">Обратиться в службу поддержки Visa</string>
<string name="common_continue">Продолжить</string>
<string name="common_convert">Конвертировать</string>
<string name="common_copy">Копировать</string>
<string name="common_copy">Скопировать</string>
<string name="common_copy_address">Скопировать адрес</string>
<string name="common_create">Создать</string>
<string name="common_crypto_fiat_format">%1$s (%2$s)</string>
@ -375,14 +375,15 @@
<string name="common_finish">Завершить</string>
<string name="common_forget">Забыть</string>
<string name="common_free">Комиссия</string>
<string name="common_from">Из</string>
<string name="common_from">С</string>
<string name="common_from_wallet_name">Из %s</string>
<string name="common_generate_addresses">Синхронизировать адреса</string>
<string name="common_get">Пополнить</string>
<string name="common_get_started">Начать</string>
<string name="common_get_token">Получить токен</string>
<string name="common_go_to_provider">К провайдеру</string>
<string name="common_go_to_token">Перейти в токен</string>
<string name="common_go_to_provider">Перейти к провайдеру</string>
<string name="common_go_to_token">Перейти к токену</string>
<string name="common_go_to_verification">Перейти к верификации</string>
<string name="common_got_it">Понятно</string>
<string name="common_hide">Скрыть</string>
<string name="common_hold_to">Удерживайте, чтобы %s</string>
@ -449,6 +450,7 @@
<string name="common_privacy_policy">Политикой конфиденциальности</string>
<string name="common_range">%1$s-%2$s</string>
<string name="common_range_with_space">%1$s — %2$s</string>
<string name="common_rate">Курс</string>
<string name="common_read_more">Подробнее</string>
<string name="common_receive">Получить</string>
<string name="common_received">Получено</string>
@ -472,6 +474,7 @@
<string name="common_send">Отправить</string>
<string name="common_send_colon">Отправка:</string>
<string name="common_send_tx_error">Не удалось отправить транзакцию</string>
<string name="common_send_with_swap">Отправка&amp;Обмен</string>
<string name="common_sending">Отправка</string>
<string name="common_sent">Отправлено</string>
<string name="common_server_unavailable">Сервер недоступен, повторите попытку позднее</string>
@ -512,6 +515,7 @@
<item quantity="other">%d токенов</item>
</plurals>
<string name="common_transaction_failed">Ошибка транзакции</string>
<string name="common_transaction_id">ID транзакции</string>
<string name="common_transaction_status">Статус транзакции</string>
<string name="common_transactions">Транзакции</string>
<string name="common_transfer">Перевод</string>
@ -650,33 +654,35 @@
<string name="express_error_provider_amount_roundup">Ошибка %1$s. Выбранный провайдер не может обработать указанную транзакцию. Округлите значение до %2$s или измените его.</string>
<string name="express_error_swap_pair_unavailable">Выбранный провайдер недоступен для обмена. Попробуйте позже. (Код: %s)</string>
<string name="express_error_swap_unavailable">В данный момент обмен невозможен. Попробуйте позже. (Код: %s)</string>
<string name="express_estimated_amount">Курс обмена</string>
<string name="express_estimated_amount">Ожидаемая сумма</string>
<string name="express_exchange_by">Обмен через %s</string>
<string name="express_exchange_notification_failed_text">Чтобы вернуть ваши деньги, посетите сайт провайдера</string>
<string name="express_exchange_notification_failed_text">Перейдите на сайт провайдера для возврата средств</string>
<string name="express_exchange_notification_failed_title">Операция не выполнена провайдером</string>
<string name="express_exchange_notification_long_transaction_time_text">Ваш обмен занимает больше времени, чем обычно, но ваши средства полностью в безопасности и будут доставлены. Если возникли вопросы, вы всегда можете связаться с поддержкой провайдера.</string>
<string name="express_exchange_notification_long_transaction_time_title">Долгое время транзакции</string>
<string name="express_exchange_notification_refund_text">Отправленные средства были возвращены в %1$s на ваш кошелек в соответствии с правилами OKX или моста обмена. %2$s</string>
<string name="express_exchange_notification_refund_title">Сумма была возвращена в %1$s (%2$s сети)</string>
<string name="express_exchange_notification_verification_text">Посетите сайт провайдера для проверки</string>
<string name="express_exchange_notification_verification_title">Требуется верификация у провайдера</string>
<string name="express_exchange_notification_refunded_in_text">Средства возвращены в %1$s на ваш кошелек в сети %2$s согласно правилам OKX.</string>
<string name="express_exchange_notification_refunded_in_title">Возвращено в %s</string>
<string name="express_exchange_notification_verification_text">Перейдите на сайт провайдера для верификации</string>
<string name="express_exchange_notification_verification_title">Провайдеру требуется KYC-верификация</string>
<string name="express_exchange_status_bought">Покупка завершена</string>
<string name="express_exchange_status_buying">Ожидание покупки</string>
<string name="express_exchange_status_buying_active">Ожидание покупки...</string>
<string name="express_exchange_status_canceled">Транзакция отменена</string>
<string name="express_exchange_status_confirmed">Депозит подтверждён</string>
<string name="express_exchange_status_confirming">Ожидает подтверждения</string>
<string name="express_exchange_status_confirming_active">Ожидает подтверждения...</string>
<string name="express_exchange_status_confirming_active">Ожидание подтверждения...</string>
<string name="express_exchange_status_exchanged">Обмен завершён</string>
<string name="express_exchange_status_exchanging">Ожидает обмена</string>
<string name="express_exchange_status_exchanging_active">Ожидает обмена...</string>
<string name="express_exchange_status_exchanging_active">Ожидание обмена...</string>
<string name="express_exchange_status_failed">Ошибка обмена</string>
<string name="express_exchange_status_paused">Обмен остановлен</string>
<string name="express_exchange_status_paused">Обмен приостановлен</string>
<string name="express_exchange_status_received">Депозит получен</string>
<string name="express_exchange_status_receiving">Ожидание депозита</string>
<string name="express_exchange_status_receiving_active">Ожидаем пополнения...</string>
<string name="express_exchange_status_refunded">Средства возвращены</string>
<string name="express_exchange_status_refunding">Возврат средств</string>
<string name="express_exchange_status_receiving_active">Ожидание пополнения...</string>
<string name="express_exchange_status_refunded">Возврат завершен</string>
<string name="express_exchange_status_refunding">Ожидание возврата</string>
<string name="express_exchange_status_sending">Отправка средств</string>
<string name="express_exchange_status_sending_active">Отправка средств...</string>
<string name="express_exchange_status_sent">Средства отправлены</string>
@ -741,10 +747,16 @@
<string name="feedback_subject_support_tangem">Обращение в поддержку Tangem</string>
<string name="feedback_subject_tx_failed">Не могу отправить транзакцию</string>
<string name="feedback_token_description_error">Ошибка в описании монеты</string>
<string name="force_update_banner_message">Для обеспечения корректной работы обновите приложение до последней версии.</string>
<string name="force_update_banner_title">Необходимо обновление</string>
<string name="force_update_action">Обновить</string>
<string name="force_update_banner_message">Обновите приложение Tangem, чтобы открыть новые функции и получить последние улучшения.</string>
<string name="force_update_banner_title">Доступна новая версия</string>
<string name="force_update_brick_description">Tangem не может работать с этой версией ОС. Ваши средства в безопасности — продолжить работу с кошельком можно на другом поддерживаемом устройстве.</string>
<string name="force_update_brick_title">Обновление недоступно</string>
<string name="force_update_button">Обновить</string>
<string name="force_update_warning_message">Пожалуйста, обновите приложение до последней версии, чтобы обеспечить его корректную работу.</string>
<string name="force_update_os_description">Ваша версия ОС устарела. Tangem пока продолжит работать, но для будущих обновлений приложения потребуется обновить систему.</string>
<string name="force_update_os_title">Обновите систему</string>
<string name="force_update_required_action">Обновить приложение</string>
<string name="force_update_warning_message">Эта версия Tangem больше не поддерживается. Обновите приложение, чтобы продолжить его использование.</string>
<string name="force_update_warning_title">Требуется обновление</string>
<string name="gasless_not_enough_funds_to_cover_token_fee">Недостаточно средств</string>
<string name="gasless_transaction_fee">Комиссия за транзакцию</string>
@ -928,6 +940,7 @@
<string name="markets_portfolio_block_subtitle">В вашем портфеле</string>
<string name="markets_portfolio_block_title">Ваш портфель</string>
<string name="markets_portfolio_block_token_unsupported">**Токен не поддерживается**. В настоящее время этот токен не поддерживается в кошельке</string>
<string name="markets_portfolio_eligible_block_title">Другие поддерживаемые токены</string>
<string name="markets_pulse_common_title">Пульс рынка</string>
<string name="markets_quick_actions">Быстрые действия</string>
<string name="markets_search_clear_all_hints">Очистить всё</string>
@ -1637,6 +1650,7 @@
<string name="staking_error_no_validators_message">В данный момент нет доступных валидаторов. Попробуйте позже.</string>
<string name="staking_error_no_validators_title">Стейкинг недоступен</string>
<string name="staking_error_unavailable_region">Стейкинг недоступен в вашем регионе</string>
<string name="staking_error_unavailable_region_description">Стейкинг сейчас недоступен в вашем регионе. Если у вас включён VPN, попробуйте его отключить.</string>
<string name="staking_give_permission_fee_footer">Комиссия сети за одобрение токена будет взиматься за подтверждение того, что именно вы разрешаете использовать ваш токен для стейкинга.</string>
<string name="staking_legal">Пользуясь стейкинг сервисом, вы соглашаетесь с %1$s и %2$s</string>
<string name="staking_locked">Заблокировано</string>
@ -2142,6 +2156,8 @@
<string name="twins_recreate_toolbar">Tangem Twin</string>
<string name="twins_recreate_warning">Это действие необратимо. У вас не будет доступа к старому кошельку.</string>
<string name="twins_scan_twin_with_number">Приложите twin-карту с номером %s и не убирайте до окончания операции</string>
<string name="tx_history_onramp_top_up">Пополнение</string>
<string name="tx_history_onramp_topped_up">Пополнено</string>
<string name="unexpected_error_description">Пожалуйста, попробуйте позже. Если проблема сохранится, обратитесь в поддержку.</string>
<string name="unexpected_error_title">Что-то пошло не так</string>
<string name="universal_error">Произошла ошибка. Код ошибки: %s. Попробуйте, пожалуйста, снова. Если проблема будет продолжать возникать — обратитесь в службу поддержки.</string>

View file

@ -327,7 +327,7 @@
<string name="common_contact_visa_support">Звернутися до служби підтримки Visa</string>
<string name="common_continue">Продовжити</string>
<string name="common_convert">Конвертувати</string>
<string name="common_copy">Копіювати</string>
<string name="common_copy">Скопіювати</string>
<string name="common_copy_address">Скопіювати адресу</string>
<string name="common_create">Створити</string>
<string name="common_crypto_fiat_format">%1$s (%2$s)</string>
@ -382,7 +382,8 @@
<string name="common_get_started">Почати</string>
<string name="common_get_token">Отримати токен</string>
<string name="common_go_to_provider">Перейти до провайдера</string>
<string name="common_go_to_token">Перейти до токену</string>
<string name="common_go_to_token">Перейти до токена</string>
<string name="common_go_to_verification">Перейти до верифікації</string>
<string name="common_got_it">Зrozуміло</string>
<string name="common_hide">Приховати</string>
<string name="common_hold_to">Утримуйте, щоб %s</string>
@ -449,6 +450,7 @@
<string name="common_privacy_policy">Політикою конфіденційності</string>
<string name="common_range">%1$s-%2$s</string>
<string name="common_range_with_space">%1$s — %2$s</string>
<string name="common_rate">Курс</string>
<string name="common_read_more">Детальніше</string>
<string name="common_receive">Отримати</string>
<string name="common_received">Отримано</string>
@ -500,7 +502,7 @@
<string name="common_tap_and_hold_hint">Натисніть і утримуйте</string>
<string name="common_terms_and_conditions">умови участі</string>
<string name="common_terms_of_use">Умовами використання</string>
<string name="common_to">До</string>
<string name="common_to">На</string>
<string name="common_to_wallet_name">На %s</string>
<string name="common_today">Сьогодні</string>
<string name="common_token">Токен</string>
@ -512,6 +514,7 @@
<item quantity="other">%d токенів</item>
</plurals>
<string name="common_transaction_failed">Помилка транзакції</string>
<string name="common_transaction_id">ID транзакції</string>
<string name="common_transaction_status">Статус транзакції</string>
<string name="common_transactions">Транзакції</string>
<string name="common_transfer">Переказ</string>
@ -650,35 +653,37 @@
<string name="express_error_provider_amount_roundup">Помилка %1$s. Обраний провайдер не може обробити зазначену транзакцію. Округліть значення до %2$s або змініть його.</string>
<string name="express_error_swap_pair_unavailable">Наразі обраний провайдер недоступний для обміну. Спробуй пізніше. (Код: %s)</string>
<string name="express_error_swap_unavailable">Наразі обмін неможливий. Спробуй пізніше. (Код: %s)</string>
<string name="express_estimated_amount">Курс обміну</string>
<string name="express_estimated_amount">Очікувана сума</string>
<string name="express_exchange_by">Обмін через %s</string>
<string name="express_exchange_notification_failed_text">Щоб повернути ваші кошти, відвідайте сайт провайдера</string>
<string name="express_exchange_notification_failed_text">Перейдіть на сайт провайдера для повернення коштів</string>
<string name="express_exchange_notification_failed_title">Операція не виконана провайдером</string>
<string name="express_exchange_notification_long_transaction_time_text">Ваш обмін триває довше, ніж зазвичай, але ваші кошти повністю в безпеці та будуть доставлені. Якщо ви хвилюєтесь, ви завжди можете звернутися до служби підтримки провайдера.</string>
<string name="express_exchange_notification_long_transaction_time_title">Тривалий час транзакції</string>
<string name="express_exchange_notification_refund_text">Сума транзакції була повернута в %1$s на ваш гаманець відповідно до правил OKX або мосту обміну. %2$s</string>
<string name="express_exchange_notification_refund_title">Сума була повернута в %1$s (%2$s мережі)</string>
<string name="express_exchange_notification_verification_text">Відвідайте сайт провайдера для перевірки</string>
<string name="express_exchange_notification_verification_title">Провайдер вимагає проходження KYC верифікації</string>
<string name="express_exchange_status_bought">Куплено</string>
<string name="express_exchange_status_buying">Купівля</string>
<string name="express_exchange_status_buying_active">Купівля...</string>
<string name="express_exchange_notification_refunded_in_text">Кошти повернуто в %1$s на ваш гаманець у мережі %2$s згідно з правилами OKX.</string>
<string name="express_exchange_notification_refunded_in_title">Повернуто в %s</string>
<string name="express_exchange_notification_verification_text">Перейдіть на сайт провайдера для верифікації</string>
<string name="express_exchange_notification_verification_title">Провайдеру потрібна KYC-верифікація</string>
<string name="express_exchange_status_bought">Купівлю завершено</string>
<string name="express_exchange_status_buying">Очікування купівлі</string>
<string name="express_exchange_status_buying_active">Очікування купівлі...</string>
<string name="express_exchange_status_canceled">Скасовано</string>
<string name="express_exchange_status_confirmed">Підтверджено</string>
<string name="express_exchange_status_confirming">Підтвердження</string>
<string name="express_exchange_status_confirming_active">Підтвердження...</string>
<string name="express_exchange_status_exchanged">Обміняно</string>
<string name="express_exchange_status_confirming_active">Очікування підтвердження...</string>
<string name="express_exchange_status_exchanged">Обмін завершено</string>
<string name="express_exchange_status_exchanging">Обмін</string>
<string name="express_exchange_status_exchanging_active">Обмін...</string>
<string name="express_exchange_status_failed">Не вдалося</string>
<string name="express_exchange_status_paused">Призупинено</string>
<string name="express_exchange_status_exchanging_active">Очікування обміну...</string>
<string name="express_exchange_status_failed">Помилка обміну</string>
<string name="express_exchange_status_paused">Обмін призупинено</string>
<string name="express_exchange_status_received">Депозит отримано</string>
<string name="express_exchange_status_receiving">Очікуємо на депозит</string>
<string name="express_exchange_status_receiving_active">Очікуємо на депозит...</string>
<string name="express_exchange_status_refunded">Повернено</string>
<string name="express_exchange_status_refunding">Повернення коштів</string>
<string name="express_exchange_status_receiving_active">Очікування поповнення...</string>
<string name="express_exchange_status_refunded">Повернення завершено</string>
<string name="express_exchange_status_refunding">Очікування повернення</string>
<string name="express_exchange_status_sending">Надсилаємо вам</string>
<string name="express_exchange_status_sending_active">Надсилаємо вам...</string>
<string name="express_exchange_status_sending_active">Надсилання коштів...</string>
<string name="express_exchange_status_sent">Надіслано</string>
<string name="express_exchange_status_subtitle">Дані провайдера. Орієнтовна сума може бути змінена через ринкові умови.</string>
<string name="express_exchange_status_title">Статус обміну</string>
@ -928,6 +933,7 @@
<string name="markets_portfolio_block_subtitle">У вашому портфелі</string>
<string name="markets_portfolio_block_title">Ваш портфель</string>
<string name="markets_portfolio_block_token_unsupported">**Токен не підтримується**. Наразі цей токен не підтримується в гаманці</string>
<string name="markets_portfolio_eligible_block_title">Інші підтримувані токени</string>
<string name="markets_pulse_common_title">Пульс ринку</string>
<string name="markets_quick_actions">Швидкі дії</string>
<string name="markets_search_clear_all_hints">Очистити все</string>
@ -1637,6 +1643,7 @@
<string name="staking_error_no_validators_message">Наразі немає вільних валідаторів. Повторіть спробу пізніше.</string>
<string name="staking_error_no_validators_title">Стейкінг недоступний</string>
<string name="staking_error_unavailable_region">Стейкінг недоступний у вашому регіоні</string>
<string name="staking_error_unavailable_region_description">Стейкінг наразі недоступний у вашому регіоні. Якщо у вас увімкнено VPN, спробуйте його вимкнути.</string>
<string name="staking_give_permission_fee_footer">Комісія за схвалення токену, щоб верифікувати, що саме ви дозволяєте використання токена для стейкінгу.</string>
<string name="staking_legal">Використовуючи функцію стейкінгу, ви погоджуєтесь з %1$s та %2$s</string>
<string name="staking_locked">Заблоковано</string>
@ -2142,6 +2149,8 @@
<string name="twins_recreate_toolbar">Tangem Twin</string>
<string name="twins_recreate_warning">Ця дія є незворотною. Ви не матимете доступу до старого гаманця.</string>
<string name="twins_scan_twin_with_number">Прикладіть twin-картку з номером %s та не прибирайте її до завершення операції</string>
<string name="tx_history_onramp_top_up">Поповнення</string>
<string name="tx_history_onramp_topped_up">Поповнено</string>
<string name="unexpected_error_description">Спробуйте ще раз пізніше. Якщо проблема залишиться, зверніться до служби підтримки.</string>
<string name="unexpected_error_title">Щось пішло не так!</string>
<string name="universal_error">Виникла помилка. Код помилки: %s. Спробуйте, будь ласка, знову. Якщо проблема знову виникає — зверніться до нашої служби підтримки.</string>

View file

@ -362,6 +362,7 @@
<string name="common_get_token">获取代币</string>
<string name="common_go_to_provider">前往服务提供商</string>
<string name="common_go_to_token">前往代币</string>
<string name="common_go_to_verification">前往验证</string>
<string name="common_got_it">知道了</string>
<string name="common_hide">隐藏</string>
<string name="common_hold_to">保持到 %s</string>
@ -416,6 +417,7 @@
<string name="common_privacy_policy">隐私政策</string>
<string name="common_range">%1$s-%2$s</string>
<string name="common_range_with_space">%1$s — %2$s</string>
<string name="common_rate">评分</string>
<string name="common_read_more">阅读更多</string>
<string name="common_receive">接收</string>
<string name="common_received">已收到</string>
@ -439,6 +441,7 @@
<string name="common_send">发送</string>
<string name="common_send_colon">发送:</string>
<string name="common_send_tx_error">交易发送失败</string>
<string name="common_send_with_swap">发送和交换</string>
<string name="common_sending">发送中</string>
<string name="common_sent">发送</string>
<string name="common_server_unavailable">服务器不可用,请稍后再试。</string>
@ -476,6 +479,7 @@
<item quantity="other">%d代币</item>
</plurals>
<string name="common_transaction_failed">交易失败</string>
<string name="common_transaction_id">交易 ID</string>
<string name="common_transaction_status">交易状态</string>
<string name="common_transactions">交易</string>
<string name="common_transfer">转让</string>
@ -622,6 +626,8 @@
<string name="express_exchange_notification_long_transaction_time_title">交易时间长</string>
<string name="express_exchange_notification_refund_text">由于 OKX 或桥接规则的原因,交易金额以 %1$s 的形式退还至您的钱包。 %2$s</string>
<string name="express_exchange_notification_refund_title">退款已退还到 %1$s (%2$s 网络) 。</string>
<string name="express_exchange_notification_refunded_in_text">根据OKX交易所的规定您的资金已以 %1$s 的形式退还至您在 %2$s 网络上的钱包中。</string>
<string name="express_exchange_notification_refunded_in_title">退款于 %s</string>
<string name="express_exchange_notification_verification_text">访问提供商网站进行验证</string>
<string name="express_exchange_notification_verification_title">提供商要求进行 KYC 验证</string>
<string name="express_exchange_status_bought">购买已完成</string>
@ -655,6 +661,7 @@
<string name="express_legal_two_placeholders">使用兑换功能即表示您同意提供商的条款 %1$s 和 %2$s。</string>
<string name="express_more_providers_soon">更多服务提供商即将推出</string>
<string name="express_provider">提供商</string>
<string name="express_provider_best_dex_rate">最佳DEX汇率</string>
<string name="express_provider_best_rate">最佳汇率</string>
<string name="express_provider_fca_warning_list">FCA警告清单</string>
<string name="express_provider_fixed_rate_is_unavailable">固定利率不可用</string>
@ -705,9 +712,15 @@
<string name="feedback_subject_support_tangem">Tangem反馈</string>
<string name="feedback_subject_tx_failed">无法发送交易</string>
<string name="feedback_token_description_error">代币描述错误</string>
<string name="force_update_action">立即更新</string>
<string name="force_update_banner_message">请将应用更新至最新版本,以确保其正常运行</string>
<string name="force_update_banner_title">需要更新</string>
<string name="force_update_brick_description">Tangem 无法在此操作系统版本上运行。您的资金是安全的,您可以在另一台受支持的设备上继续使用您的钱包。</string>
<string name="force_update_brick_title">无法更新</string>
<string name="force_update_button">更新</string>
<string name="force_update_os_description">您的操作系统版本过旧。Tangem 目前仍可继续运行,但您需要更新操作系统才能接收未来的应用更新。</string>
<string name="force_update_os_title">更新您的操作系统</string>
<string name="force_update_required_action">更新应用</string>
<string name="force_update_warning_message">请将应用更新至最新版本,以确保正常运行。</string>
<string name="force_update_warning_title">需要更新</string>
<string name="gasless_not_enough_funds_to_cover_token_fee">资金不足</string>
@ -888,6 +901,7 @@
<string name="markets_portfolio_block_add_token_title">**添加到您的投资组合**,即可开始购买、交换或接收此资产</string>
<string name="markets_portfolio_block_subtitle">在您的投资组合中</string>
<string name="markets_portfolio_block_title">您的投资组合</string>
<string name="markets_portfolio_eligible_block_title">其他符合条件的代币</string>
<string name="markets_pulse_common_title">市场脉搏</string>
<string name="markets_quick_actions">快速行动</string>
<string name="markets_search_clear_all_hints">全部清除</string>
@ -1556,6 +1570,7 @@
<string name="staking_error_no_validators_message">目前没有可用的验证节点。请稍后再试。</string>
<string name="staking_error_no_validators_title">质押功能不可用</string>
<string name="staking_error_unavailable_region">您所在的地区暂不支持质押功能</string>
<string name="staking_error_unavailable_region_description">您所在的地区目前无法使用质押功能。如果您已启用 VPN请尝试将其关闭。</string>
<string name="staking_give_permission_fee_footer">网络将收取代币批准费,以验证您是否授权使用您的代币进行质押。</string>
<string name="staking_legal">使用质押功能即表示您同意提供商的 %1$s 和 %2$s</string>
<string name="staking_locked">已锁定</string>
@ -2058,6 +2073,9 @@
<string name="twins_recreate_toolbar">Tangem Twin</string>
<string name="twins_recreate_warning">此操作不可逆。您将无法访问旧钱包。</string>
<string name="twins_scan_twin_with_number">轻触带有数字 %s 的第二张卡片,操作结束前请勿移除。</string>
<string name="tx_history_onramp_top_up">充值</string>
<string name="tx_history_onramp_topped_up">已充值</string>
<string name="tx_history_you_paid">您已支付</string>
<string name="unexpected_error_description">请稍后再试。如果问题仍然存在,请联系客服。</string>
<string name="unexpected_error_title">出问题了!</string>
<string name="universal_error">我们遇到了一个错误。错误代码: %s请联系我们的客服。</string>
@ -2594,6 +2612,8 @@
<string name="yield_module_transaction_exit_subtitle">%1$s 已从 Aave 提款</string>
<string name="yield_module_transaction_initialize">收益模式已初始化</string>
<string name="yield_module_transaction_reactivate">收益模式重新激活</string>
<string name="yield_module_transaction_returned">已退回</string>
<string name="yield_module_transaction_supplied">提供</string>
<string name="yield_module_transaction_topup">供应给 Aave</string>
<string name="yield_module_transaction_topup_subtitle">%1$s 供应给 Aave</string>
<string name="yield_module_transaction_withdraw">从 Aave提款</string>

View file

@ -281,15 +281,21 @@
<string name="common_added">Added</string>
<string name="common_address">Address</string>
<string name="common_all">All</string>
<string name="common_all_accounts">All accounts</string>
<string name="common_allow">Allow</string>
<string name="common_amount">Amount</string>
<string name="common_analytics">Analytics</string>
<string name="common_and">and</string>
<string name="common_apply">Apply</string>
<string name="common_apply_selected">Apply selected</string>
<string name="common_approval">Approval</string>
<string name="common_approve">Approve</string>
<string name="common_approved">Approved</string>
<string name="common_approving">Approving</string>
<plurals name="common_assets_count">
<item quantity="one">%d asset</item>
<item quantity="other">%d assets</item>
</plurals>
<string name="common_attention">Attention</string>
<string name="common_available_networks">Available networks</string>
<string name="common_backup">Backup</string>
@ -325,6 +331,7 @@
<string name="common_create">Create</string>
<string name="common_crypto_fiat_format">%1$s (%2$s)</string>
<string name="common_custom">Custom</string>
<string name="common_day">day</string>
<plurals name="common_days">
<item quantity="one">%d day</item>
<item quantity="other">%d days</item>
@ -399,7 +406,22 @@
<item quantity="other">%d minutes ago</item>
</plurals>
<string name="common_month">month</string>
<string-array name="common_month_in">
<item>January</item>
<item>February</item>
<item>March</item>
<item>April</item>
<item>May</item>
<item>June</item>
<item>July</item>
<item>August</item>
<item>September</item>
<item>October</item>
<item>November</item>
<item>December</item>
</string-array>
<string name="common_more">More</string>
<string name="common_negative">Negative</string>
<string name="common_network">Network</string>
<string name="common_network_fee_title">Network fee</string>
<string name="common_network_fee_warning_content">Amount sent will be reduced by %1$s (%2$s) to cover the selected fee level</string>
@ -407,6 +429,7 @@
<item quantity="one">%d network</item>
<item quantity="other">%d networks</item>
</plurals>
<string name="common_neutral">Neutral</string>
<string name="common_new_address">New address</string>
<string name="common_news">News</string>
<string name="common_next">Next</string>
@ -414,6 +437,7 @@
<string name="common_no">No</string>
<string name="common_no_address">No address</string>
<string name="common_no_results">No results</string>
<string name="common_none">None</string>
<string name="common_not_added">Not Added</string>
<string name="common_not_available">Not available</string>
<string name="common_not_now">Not now</string>
@ -427,6 +451,7 @@
<string name="common_other">Other</string>
<string name="common_passphrase">Passphrase</string>
<string name="common_paste">Paste</string>
<string name="common_positive">Positive</string>
<string name="common_privacy_policy">Privacy Policy</string>
<string name="common_range">%1$s-%2$s</string>
<string name="common_range_with_space">%1$s — %2$s</string>
@ -449,6 +474,7 @@
<string name="common_second_no_param">sec</string>
<string name="common_see_all">See all</string>
<string name="common_seed_phrase">Seed phrase</string>
<string name="common_select_account">Select account</string>
<string name="common_select_action">Select action</string>
<string name="common_sell">Sell</string>
<string name="common_send">Send</string>
@ -729,21 +755,22 @@
<string name="for_you_description">Review portfolio and explore earn opportunities</string>
<string name="for_you_earn_opportunities">Earn opportunities</string>
<string name="for_you_earn_opportunities_all_tokens_active">All your assets are at work, explore new opportunities</string>
<string name="for_you_earn_opportunities_explore">Explore all tokens</string>
<string name="for_you_earn_opportunities_no_available_tokens">Get up to %1$s annually</string>
<string name="for_you_earn_opportunities_tokens_rewards">Max potential rewards %1$s</string>
<string name="for_you_earn_per_year">%1$s/year</string>
<string name="for_you_portfolio_review_title">Portfolio review</string>
<string name="for_you_title">For You</string>
<string name="force_update_action">Update now</string>
<string name="force_update_banner_message">Update the app to its latest version to ensure proper functionality</string>
<string name="force_update_banner_title">Update needed</string>
<string name="force_update_brick_description">This version of the app is no longer supported and can\'t be updated on this device.</string>
<string name="force_update_banner_message">Update the Tangem app to unlock new features and enjoy the latest improvements.</string>
<string name="force_update_banner_title">New version available</string>
<string name="force_update_brick_description">Tangem cant run on this OS version. Your funds are safe, and you can continue using your wallet on another supported device.</string>
<string name="force_update_brick_title">Update Unavailable</string>
<string name="force_update_button">Update</string>
<string name="force_update_os_description">Your operating system is out of date. Please update it to continue using the app.</string>
<string name="force_update_os_title">Update Your OS</string>
<string name="force_update_os_description">Your OS is outdated. Tangem will continue to work for now, but youll need to update it to receive future app updates.</string>
<string name="force_update_os_title">Update your OS</string>
<string name="force_update_required_action">Update app</string>
<string name="force_update_warning_message">Please update the app to its latest version to ensure proper functionality.</string>
<string name="force_update_warning_message">Please update Tangem to the latest version to continue using the app.</string>
<string name="force_update_warning_title">Update required</string>
<string name="gasless_not_enough_funds_to_cover_token_fee">Not enough funds</string>
<string name="gasless_transaction_fee">Transaction fee</string>
@ -1315,6 +1342,8 @@
<string name="organize_tokens_sort_by_balance">By balance</string>
<string name="organize_tokens_title">Organize tokens</string>
<string name="organize_tokens_ungroup">Ungroup</string>
<string name="prediction_account_subtitle">Add funds &amp; start predict</string>
<string name="prediction_account_title">Prediction account</string>
<string name="promo_campaign_already_activated_subtitle">Eligible cashback will be distributed to:</string>
<string name="promo_campaign_already_activated_title">You\'re already enrolled in %1$s</string>
<string name="promo_campaign_eligible_tokens">Eligible tokens</string>
@ -1838,6 +1867,12 @@
<string name="tangem_pay_terms_fees_limits">Terms, Fees &amp; Limits</string>
<string name="tangem_pay_terms_limits">Terms and fees</string>
<string name="tangem_pay_transaction_declined_notification_text">The bank rejected this transaction request.</string>
<string name="tangem_pay_transaction_details_cashback_below_min">Min trx amount is %1$s</string>
<string name="tangem_pay_transaction_details_cashback_cap_reached">Monthly cap reached</string>
<string name="tangem_pay_transaction_details_cashback_mcc_excluded">MCC excluded</string>
<string name="tangem_pay_transaction_details_cashback_none">No cashback</string>
<string name="tangem_pay_transaction_details_cashback_refund">Return of purchase</string>
<string name="tangem_pay_transaction_details_cashback_region_excluded">Excluded region</string>
<string name="tangem_pay_transaction_details_category">Category</string>
<string name="tangem_pay_transaction_details_mcc">MCC</string>
<string name="tangem_pay_transaction_fee_notification_text">A fee is charged due to the service tariffs</string>
@ -1938,7 +1973,7 @@
<string name="tangempay_cashback_accruals_calc_title">How we calculate cashback?</string>
<string name="tangempay_cashback_accruals_exceptions_description">No cashback will be awarded for in-person/in-store purchases at EU merchants; also for withdrawals, transfers, quasi-cash, mobile phone bills, government services and certain other categories</string>
<string name="tangempay_cashback_accruals_exceptions_title">Exceptions</string>
<string name="tangempay_cashback_accruals_pay_description">From the 2nd and the 5th of the next month</string>
<string name="tangempay_cashback_accruals_pay_description">From the 2nd to the 5th of the next month</string>
<string name="tangempay_cashback_accruals_pay_title">How we pay cashback?</string>
<string name="tangempay_cashback_accruals_subtitle">Limits and exceptions</string>
<string name="tangempay_cashback_accruals_title">Accruals</string>
@ -1965,6 +2000,11 @@
<string name="tangempay_cashback_total_earned">%1$s earned in total</string>
<string name="tangempay_cashback_widget_title">%1$s cashback in %2$s</string>
<string name="tangempay_change_pin_code">Change PIN-code</string>
<string name="tangempay_changing_plan">Changing your plan</string>
<string name="tangempay_choose_network_fast_way">Fast way</string>
<string name="tangempay_choose_network_other_ways">Other ways</string>
<string name="tangempay_choose_network_row_error">Failed to get address, please try again</string>
<string name="tangempay_choose_network_row_loading">Preparing address…</string>
<string name="tangempay_come_back_if_forget_pin">Come back to the app if you forget it.</string>
<string name="tangempay_common_card">Card</string>
<string name="tangempay_common_error_loading">Error loading</string>
@ -2033,7 +2073,10 @@
<string name="tangempay_kyc_rejected_description">Sorry, we couldn\'t verify</string>
<string name="tangempay_kyc_rejected_description_span">your profile.</string>
<string name="tangempay_main_select_plan">Select plan</string>
<string name="tangempay_maximum_cards_issued_description">You can have up to 3 cards. Delete one to add a new card.</string>
<string name="tangempay_maximum_cards_issued_description">Delete one to add a new card</string>
<string name="tangempay_maximum_cards_issued_for_plan_description">Upgrade plan to have more cards</string>
<string name="tangempay_maximum_cards_issued_for_plan_title">Cards limit reached for this plan</string>
<string name="tangempay_maximum_cards_issued_for_plan_upgrade_btn">Upgrade plan</string>
<string name="tangempay_maximum_cards_issued_title">Maximum Cards Issued</string>
<string name="tangempay_newonboard_Q1_body">Yes to operate a regulated Visa card, identity verification is mandatory. KYC is handled by Sumsub (compliance partner).</string>
<string name="tangempay_newonboard_Q1_title">Do I have to share my docs?</string>
@ -2065,6 +2108,9 @@
<string name="tangempay_onboarding_setup_wallet_description">And link a payment card to it</string>
<string name="tangempay_onboarding_setup_wallet_title">We\'ll set up a wallet</string>
<string name="tangempay_onboarding_title">Get your Tangem Pay Card in minutes</string>
<string name="tangempay_other_networks_header">Other networks</string>
<string name="tangempay_other_networks_subtitle">And then swap it to payment account with 1:1 rate and zero fees</string>
<string name="tangempay_other_networks_title">Send USDT / USDC on any chain to your Tangem Wallet</string>
<string name="tangempay_pay_support">Pay Support</string>
<string name="tangempay_payment_account">Payment account</string>
<string name="tangempay_payment_account_sync_needed">Session expired</string>
@ -2083,7 +2129,7 @@
<string name="tangempay_remove_account_alert_description">Tangem Pay will be removed from the main screen and won\'t appear again, even after reinstalling the app.</string>
<string name="tangempay_remove_account_alert_title">Remove account?</string>
<string name="tangempay_select_plan_btn_cancel">Cancel</string>
<string name="tangempay_select_plan_btn_downgrade">Downgrade plan</string>
<string name="tangempay_select_plan_btn_downgrade">Continue</string>
<string name="tangempay_select_plan_btn_select">Select</string>
<string name="tangempay_select_plan_btn_upgrade">Upgrade plan</string>
<string name="tangempay_select_plan_compare">Compare plans</string>
@ -2117,6 +2163,7 @@
<string name="tangempay_topup_bank_transfer_body">Receive fiat USD via ACH/FedWire</string>
<string name="tangempay_topup_bank_transfer_title">Bank transfer</string>
<string name="tangempay_topup_receive_body">Send USDC Polygon to your accounts address </string>
<string name="tangempay_topup_receive_body_multichain">Send USDC or USDT to your accounts address</string>
<string name="tangempay_topup_receive_title">From another wallet or exchange</string>
<string name="tangempay_topup_swap_body">Use crypto from your wallet to top up your payment account</string>
<string name="tangempay_topup_swap_title">From your Tangem Wallet</string>
@ -2173,6 +2220,7 @@
<string name="token_summary_last_update_subtitle">Last update: %1$s</string>
<string name="token_summary_negative_outlook_title">Negative outlook</string>
<string name="token_summary_neutral_outlook_title">Neutral outlook</string>
<string name="token_summary_outlook_is_not_available">Outlook for this token is not available</string>
<string name="token_summary_positive_outlook_title">Positive outlook</string>
<string name="token_summary_title">Token summary</string>
<string name="token_swap_changelly_promotion_message">Exchange this token for another at %1$s service fees from February %2$s-%3$s.</string>
@ -2185,6 +2233,8 @@
<string name="tokens_list_unavailable_to_swap_source_header">Unavailable for swap</string>
<string name="transaction_history_claiming_reward">Claiming reward</string>
<string name="transaction_history_contract_address">contract: %s</string>
<string name="transaction_history_detail_page_amount_signed_value" formatted="false">%1s %2s %3s</string>
<string name="transaction_history_detail_page_amount_value" formatted="false">%1s %2s</string>
<string name="transaction_history_disabling_yield_mode">Disabling Yield mode</string>
<string name="transaction_history_earned_from_stake">Earned from stake</string>
<string name="transaction_history_empty_transactions">You don\'t have any transactions yet</string>
@ -2499,7 +2549,7 @@
</plurals>
<plurals name="warning_missing_derivation_no_nfc_message">
<item quantity="one">Sync addresses to get an address for %d network</item>
<item quantity="other">Sync addresses to get an addresses for %d networks</item>
<item quantity="other">Sync addresses to get addresses for %d networks</item>
</plurals>
<string name="warning_missing_derivation_title">Some addresses are missing</string>
<string name="warning_mobile_access_denied_message">Use %s or access code to unlock access to your wallet</string>

View file

@ -1,3 +1,4 @@
import java.io.ByteArrayOutputStream
import java.security.MessageDigest
plugins {
@ -20,6 +21,10 @@ abstract class VerifyDesignTokensTask : DefaultTask() {
@get:PathSensitive(PathSensitivity.RELATIVE)
abstract val iconsDir: DirectoryProperty
@get:InputDirectory
@get:PathSensitive(PathSensitivity.RELATIVE)
abstract val assetsDir: DirectoryProperty
@get:InputFile
@get:PathSensitive(PathSensitivity.NONE)
abstract val hashFile: RegularFileProperty
@ -47,29 +52,38 @@ abstract class VerifyDesignTokensTask : DefaultTask() {
"Run: git submodule update --init --recursive"
}
val assetsDirValue = assetsDir.get().asFile
require(assetsDirValue.exists() && assetsDirValue.isDirectory) {
"ds-tokens assets folder not found: ${assetsDirValue.absolutePath}\n" +
"Run: git submodule update --init --recursive"
}
val tokensInputHash = hashTreeHex(tokensDirValue, "json")
val iconsHash = hashTreeHex(iconsDirValue, "svg")
val assetsHash = hashTreeHex(assetsDirValue, "svg")
// Mirror build-tokens.mjs: sha256(tokensInputHash + 0x00 + iconsHash), all hex strings.
// Mirror build-tokens.mjs: sha256(tokensInputHash + 0x00 + iconsHash + 0x00 + assetsHash).
val outer = MessageDigest.getInstance("SHA-256")
outer.update(tokensInputHash.toByteArray())
outer.update(0)
outer.update(iconsHash.toByteArray())
outer.update(0)
outer.update(assetsHash.toByteArray())
val actual = outer.digest()
.joinToString("") { b: Byte -> b.toInt().and(0xFF).toString(16).padStart(2, '0') }
val expected = hashFileValue.readText().trim()
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 +93,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"
@ -100,6 +129,7 @@ android {
val verifyDesignTokens = tasks.register<VerifyDesignTokensTask>("verifyDesignTokens") {
tokensDir.set(file("ds-tokens/tokens"))
iconsDir.set(file("ds-tokens/icons"))
assetsDir.set(file("ds-tokens/assets"))
hashFile.set(file("src/main/java/com/tangem/core/ui/res/generated/.tokens-hash"))
stampFile.set(layout.buildDirectory.file("tokens-verified.stamp"))
}
@ -109,51 +139,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 +196,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
}

Some files were not shown because too many files have changed in this diff Show more