Updated on 2026-08-14
This commit is contained in:
commit
cebe0dcdf1
563 changed files with 28013 additions and 2788 deletions
|
|
@ -3,7 +3,6 @@ plugins {
|
|||
alias(deps.plugins.kotlin.android)
|
||||
alias(deps.plugins.kotlin.kapt)
|
||||
alias(deps.plugins.hilt.android)
|
||||
alias(deps.plugins.ksp)
|
||||
id("configuration")
|
||||
}
|
||||
|
||||
|
|
@ -12,19 +11,23 @@ android {
|
|||
}
|
||||
|
||||
dependencies {
|
||||
/** DI */
|
||||
|
||||
// region DI
|
||||
implementation(deps.hilt.android)
|
||||
kapt(deps.hilt.kapt)
|
||||
// endregion
|
||||
|
||||
/** Other libraries */
|
||||
// region Kotlin
|
||||
implementation(deps.kotlin.coroutines)
|
||||
// endregion
|
||||
|
||||
/** Core modules */
|
||||
// region Other libraries
|
||||
implementation(deps.amplitude.experiment)
|
||||
// endregion
|
||||
|
||||
// region Core modules
|
||||
implementation(projects.core.analytics.models)
|
||||
implementation(projects.core.datasource)
|
||||
implementation(projects.core.utils)
|
||||
|
||||
implementation(projects.domain.models)
|
||||
|
||||
/** Amplitude experiment */
|
||||
implementation(deps.amplitude.experiment)
|
||||
// endregion
|
||||
}
|
||||
|
|
@ -3,28 +3,39 @@ plugins {
|
|||
alias(deps.plugins.kotlin.kapt)
|
||||
id("configuration")
|
||||
}
|
||||
|
||||
dependencies {
|
||||
|
||||
/** DI */
|
||||
// region DI
|
||||
implementation(deps.hilt.core)
|
||||
kapt(deps.hilt.kapt)
|
||||
// endregion
|
||||
|
||||
/** Analytics - Models */
|
||||
api(projects.core.analytics.models)
|
||||
|
||||
/** Domain */
|
||||
implementation(projects.domain.analytics)
|
||||
implementation(projects.domain.models)
|
||||
|
||||
/** Other */
|
||||
// region Kotlin
|
||||
implementation(deps.kotlin.coroutines)
|
||||
// endregion
|
||||
|
||||
/** Core shouldn't depend on core, but in case with utils and logging its necessary */
|
||||
// region Tangem
|
||||
implementation(tangemDeps.card.core) // for calculating user id hash
|
||||
// endregion
|
||||
|
||||
// region Core modules
|
||||
api(projects.core.analytics.models)
|
||||
// Core shouldn't depend on core, but with utils and logging it's necessary.
|
||||
implementation(projects.core.utils)
|
||||
// endregion
|
||||
|
||||
/** For calculating user id hash */
|
||||
implementation(tangemDeps.card.core)
|
||||
// region Domain
|
||||
api(projects.domain.analytics)
|
||||
// endregion
|
||||
|
||||
/** Tests */
|
||||
testImplementation(projects.test.core)
|
||||
// region Domain models
|
||||
api(projects.domain.models)
|
||||
// endregion
|
||||
|
||||
// region Tests
|
||||
testImplementation(deps.test.junit5)
|
||||
testImplementation(deps.test.mockk)
|
||||
testImplementation(deps.test.truth)
|
||||
// endregion
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
|
|
@ -11,10 +11,6 @@
|
|||
"name": "VISA_ONBOARDING_ENABLED",
|
||||
"version": "undefined"
|
||||
},
|
||||
{
|
||||
"name": "STAKING_ETH_ENABLED",
|
||||
"version": "5.39"
|
||||
},
|
||||
{
|
||||
"name": "TWI_485_USEDESK_ENABLED",
|
||||
"version": "undefined"
|
||||
|
|
@ -23,10 +19,6 @@
|
|||
"name": "APP_REDESIGN_ENABLED",
|
||||
"version": "6.0"
|
||||
},
|
||||
{
|
||||
"name": "GASLESS_APPROVAL_ENABLED",
|
||||
"version": "5.37"
|
||||
},
|
||||
{
|
||||
"name": "DYNAMIC_ADDRESSES_ENABLED",
|
||||
"version": "5.39"
|
||||
|
|
@ -51,10 +43,6 @@
|
|||
"name": "HEDERA_ERC20_ENABLED",
|
||||
"version": "5.37"
|
||||
},
|
||||
{
|
||||
"name": "ADD_AND_MANAGE_TOKENS_ENABLED",
|
||||
"version": "5.38"
|
||||
},
|
||||
{
|
||||
"name": "WALLET_CONNECT_BITCOIN_ENABLED",
|
||||
"version": "undefined"
|
||||
|
|
@ -115,10 +103,6 @@
|
|||
"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"
|
||||
|
|
@ -147,10 +131,18 @@
|
|||
"name": "AND_15741_VISA_PAY_REMOVE_ACCOUNT",
|
||||
"version": "undefined"
|
||||
},
|
||||
{
|
||||
"name": "AND_16041_VISA_TIERS_PLUS_PLAN",
|
||||
"version": "undefined"
|
||||
},
|
||||
{
|
||||
"name": "TWI_83_ADDRESS_BOOK_ENABLED",
|
||||
"version": "undefined"
|
||||
},
|
||||
{
|
||||
"name": "AND_15632_GASLESS_YIELD_WITHDRAW_ENABLED",
|
||||
"version": "undefined"
|
||||
},
|
||||
{
|
||||
"name": "AND_15489_EXPRESS_SHARE_BUTTON_ENABLED",
|
||||
"version": "6.0"
|
||||
|
|
@ -170,5 +162,17 @@
|
|||
{
|
||||
"name": "AND_14829_WARNINGS_REFACTORING_ENABLED",
|
||||
"version": "undefined"
|
||||
},
|
||||
{
|
||||
"name": "TWI_1469_FOR_YOU_ENABLED",
|
||||
"version": "undefined"
|
||||
},
|
||||
{
|
||||
"name": "TWI_1367_HIGH_FEE_WARNING_ENABLED",
|
||||
"version": "undefined"
|
||||
},
|
||||
{
|
||||
"name": "TWI_1638_VA_MVP0_ENABLED",
|
||||
"version": "6.1"
|
||||
}
|
||||
]
|
||||
|
|
|
|||
|
|
@ -39,16 +39,13 @@ internal class FeatureTogglesNamingConventionTest {
|
|||
/** Toggles created before the AND_/TWI_ naming convention. Do NOT add new entries. */
|
||||
val EXCLUDED_TOGGLES_LIST = setOf(
|
||||
"ADDRESS_SYNC_ENABLED",
|
||||
"ADD_AND_MANAGE_TOKENS_ENABLED",
|
||||
"APP_REDESIGN_ENABLED",
|
||||
"ASSETS_DISCOVERY_ENABLED",
|
||||
"DYNAMIC_ADDRESSES_ENABLED",
|
||||
"GASLESS_APPROVAL_ENABLED",
|
||||
"HEDERA_ERC20_ENABLED",
|
||||
"NEW_CARD_SCANNING_ENABLED",
|
||||
"SOLANA_SCALED_UI_AMOUNT_ENABLED",
|
||||
"SOLANA_TX_HISTORY_ENABLED",
|
||||
"STAKING_ETH_ENABLED",
|
||||
"SWAP_AB_ENABLED",
|
||||
"VIRTUAL_ACCOUNTS_ENABLED",
|
||||
"VISA_ONBOARDING_ENABLED",
|
||||
|
|
|
|||
|
|
@ -60,70 +60,79 @@ 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)
|
||||
ksp(deps.room.compiler)
|
||||
// endregion
|
||||
|
||||
/** Security */
|
||||
implementation(deps.spongecastle.core)
|
||||
// region Other libraries
|
||||
api(deps.jodatime)
|
||||
runtimeOnly(deps.spongecastle.core)
|
||||
// endregion
|
||||
|
||||
/** Chucker */
|
||||
// region Chucker
|
||||
debugImplementation(deps.chucker)
|
||||
mockedImplementation(deps.chucker)
|
||||
externalImplementation(deps.chuckerStub)
|
||||
internalImplementation(deps.chuckerStub)
|
||||
releaseImplementation(deps.chuckerStub)
|
||||
// endregion
|
||||
|
||||
/** Local storages */
|
||||
api(deps.androidx.datastore)
|
||||
implementation(deps.room.runtime)
|
||||
implementation(deps.room.ktx)
|
||||
ksp(deps.room.compiler)
|
||||
// region Tangem
|
||||
api(tangemDeps.blockchain)
|
||||
api(tangemDeps.card.core)
|
||||
// endregion
|
||||
|
||||
// region Core modules
|
||||
api(projects.core.analytics)
|
||||
implementation(projects.core.analytics.models)
|
||||
api(projects.core.utils)
|
||||
implementation(projects.core.res)
|
||||
// endregion
|
||||
|
||||
// region Domain models
|
||||
api(projects.domain.models)
|
||||
api(projects.domain.nft.models)
|
||||
api(projects.domain.onramp.models)
|
||||
api(projects.domain.staking.models)
|
||||
api(projects.domain.txhistory.models)
|
||||
api(projects.domain.visa.models)
|
||||
api(projects.domain.walletConnect.models)
|
||||
api(projects.domain.wallets.models)
|
||||
api(projects.domain.yieldSupply.models)
|
||||
// endregion
|
||||
|
||||
// region Tests
|
||||
testImplementation(projects.test.core)
|
||||
// endregion
|
||||
}
|
||||
|
|
@ -2,7 +2,7 @@
|
|||
"formatVersion": 1,
|
||||
"database": {
|
||||
"version": 1,
|
||||
"identityHash": "55f2651d215126dd0465b9c711165cba",
|
||||
"identityHash": "5d37870ed1a5b37c62e9836dc3fd061c",
|
||||
"entities": [
|
||||
{
|
||||
"tableName": "express_provider",
|
||||
|
|
@ -510,7 +510,7 @@
|
|||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "onrampAvailable",
|
||||
"fieldPath": "isOnrampAvailable",
|
||||
"columnName": "onramp_available",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
|
|
@ -551,11 +551,66 @@
|
|||
"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"
|
||||
]
|
||||
}
|
||||
}
|
||||
],
|
||||
"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, '55f2651d215126dd0465b9c711165cba')"
|
||||
"INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, '5d37870ed1a5b37c62e9836dc3fd061c')"
|
||||
]
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,25 @@
|
|||
package com.tangem.datasource.api.addressbook
|
||||
|
||||
import com.tangem.datasource.api.addressbook.models.SyncAddressBooksRequest
|
||||
import com.tangem.datasource.api.addressbook.models.SyncAddressBooksResponse
|
||||
import com.tangem.datasource.api.addressbook.models.UpdateAddressBookRequest
|
||||
import com.tangem.datasource.api.addressbook.models.UpdateAddressBookResponse
|
||||
import com.tangem.datasource.api.common.response.ApiResponse
|
||||
import retrofit2.http.Body
|
||||
import retrofit2.http.Header
|
||||
import retrofit2.http.PUT
|
||||
import retrofit2.http.POST
|
||||
import retrofit2.http.Path
|
||||
|
||||
interface AddressBookApi {
|
||||
|
||||
@POST("v1/address-books/sync")
|
||||
suspend fun syncAddressBooks(@Body body: SyncAddressBooksRequest): ApiResponse<SyncAddressBooksResponse>
|
||||
|
||||
@PUT("v1/address-books/{walletId}")
|
||||
suspend fun updateAddressBook(
|
||||
@Path("walletId") walletId: String,
|
||||
@Header("If-Match") eTag: String?,
|
||||
@Body body: UpdateAddressBookRequest,
|
||||
): ApiResponse<UpdateAddressBookResponse>
|
||||
}
|
||||
|
|
@ -0,0 +1,22 @@
|
|||
package com.tangem.datasource.api.addressbook.models
|
||||
|
||||
import com.squareup.moshi.Json
|
||||
import com.squareup.moshi.JsonClass
|
||||
|
||||
/**
|
||||
* Request body for `POST /address-books/sync`.
|
||||
*
|
||||
* Each [Wallet.etag] is optional: when it matches the backend's etag, that wallet is omitted from the
|
||||
* response and the local copy is kept.
|
||||
*/
|
||||
@JsonClass(generateAdapter = true)
|
||||
data class SyncAddressBooksRequest(
|
||||
@Json(name = "wallets") val wallets: List<Wallet>,
|
||||
) {
|
||||
|
||||
@JsonClass(generateAdapter = true)
|
||||
data class Wallet(
|
||||
@Json(name = "walletId") val walletId: String,
|
||||
@Json(name = "etag") val etag: String? = null,
|
||||
)
|
||||
}
|
||||
|
|
@ -0,0 +1,27 @@
|
|||
package com.tangem.datasource.api.addressbook.models
|
||||
|
||||
import com.squareup.moshi.Json
|
||||
import com.squareup.moshi.JsonClass
|
||||
|
||||
/**
|
||||
* Response body for `POST /address-books/sync`.
|
||||
*
|
||||
* [items] contains only the wallets whose backend etag differs from the one sent in the request; wallets
|
||||
* with a matching etag are omitted and their local copy must be kept.
|
||||
*/
|
||||
@JsonClass(generateAdapter = true)
|
||||
data class SyncAddressBooksResponse(
|
||||
@Json(name = "items") val items: List<Item>,
|
||||
) {
|
||||
|
||||
@JsonClass(generateAdapter = true)
|
||||
data class Item(
|
||||
@Json(name = "walletId") val walletId: String,
|
||||
@Json(name = "etag") val etag: String,
|
||||
@Json(name = "version") val version: String,
|
||||
@Json(name = "updatedAt") val updatedAt: String,
|
||||
@Json(name = "nonce") val nonce: String,
|
||||
@Json(name = "ciphertext") val ciphertext: String,
|
||||
@Json(name = "authTag") val authTag: String,
|
||||
)
|
||||
}
|
||||
|
|
@ -0,0 +1,13 @@
|
|||
package com.tangem.datasource.api.addressbook.models
|
||||
|
||||
import com.squareup.moshi.Json
|
||||
import com.squareup.moshi.JsonClass
|
||||
|
||||
/** Request body for `PUT /address-books/{walletId}`. */
|
||||
@JsonClass(generateAdapter = true)
|
||||
data class UpdateAddressBookRequest(
|
||||
@Json(name = "version") val version: String,
|
||||
@Json(name = "nonce") val nonce: String,
|
||||
@Json(name = "ciphertext") val ciphertext: String,
|
||||
@Json(name = "authTag") val authTag: String,
|
||||
)
|
||||
|
|
@ -0,0 +1,12 @@
|
|||
package com.tangem.datasource.api.addressbook.models
|
||||
|
||||
import com.squareup.moshi.Json
|
||||
import com.squareup.moshi.JsonClass
|
||||
|
||||
/** Response body for `PUT /address-books/{walletId}`. */
|
||||
@JsonClass(generateAdapter = true)
|
||||
data class UpdateAddressBookResponse(
|
||||
@Json(name = "walletId") val walletId: String,
|
||||
@Json(name = "etag") val etag: String,
|
||||
@Json(name = "updatedAt") val updatedAt: String,
|
||||
)
|
||||
|
|
@ -0,0 +1,21 @@
|
|||
package com.tangem.datasource.api.gasless
|
||||
|
||||
import com.tangem.datasource.api.common.response.ApiResponse
|
||||
import com.tangem.datasource.api.gasless.models.GaslessBatchTransactionRequest
|
||||
import com.tangem.datasource.api.gasless.models.GaslessServiceResponse
|
||||
import com.tangem.datasource.api.gasless.models.GaslessSignedTransactionResultDTO
|
||||
import com.tangem.datasource.api.gasless.models.GaslessTransactionRequest
|
||||
import retrofit2.http.Body
|
||||
import retrofit2.http.POST
|
||||
|
||||
interface GaslessTxServiceApiV2 {
|
||||
@POST("api/v2/transaction/sign")
|
||||
suspend fun signGaslessTransaction(
|
||||
@Body transaction: GaslessTransactionRequest,
|
||||
): ApiResponse<GaslessServiceResponse<GaslessSignedTransactionResultDTO>>
|
||||
|
||||
@POST("api/v2/transaction/batch-sign")
|
||||
suspend fun signGaslessBatchTransaction(
|
||||
@Body transaction: GaslessBatchTransactionRequest,
|
||||
): ApiResponse<GaslessServiceResponse<GaslessSignedTransactionResultDTO>>
|
||||
}
|
||||
|
|
@ -0,0 +1,41 @@
|
|||
package com.tangem.datasource.api.gasless.models
|
||||
|
||||
import com.squareup.moshi.Json
|
||||
import com.squareup.moshi.JsonClass
|
||||
|
||||
/**
|
||||
* Request body for gasless batch transaction submission (v2 `POST /api/v2/transaction/batch-sign`).
|
||||
* Represents a batch of transactions with fee delegation metadata.
|
||||
*
|
||||
* The top-level payload field is `gaslessTransaction` (shared shape with single sign — see
|
||||
* gasless-service `BatchSignRequestDto`), carrying `transactions[]`, `fee`, `nonce`.
|
||||
*/
|
||||
@JsonClass(generateAdapter = true)
|
||||
data class GaslessBatchTransactionRequest(
|
||||
@Json(name = "gaslessTransaction")
|
||||
val gaslessTransaction: GaslessBatchTransactionDataDTO,
|
||||
|
||||
@Json(name = "signature")
|
||||
val signature: String,
|
||||
|
||||
@Json(name = "userAddress")
|
||||
val userAddress: String,
|
||||
|
||||
@Json(name = "chainId")
|
||||
val chainId: Int,
|
||||
|
||||
@Json(name = "eip7702auth")
|
||||
val eip7702Auth: Eip7702AuthorizationDTO? = null,
|
||||
)
|
||||
|
||||
@JsonClass(generateAdapter = true)
|
||||
data class GaslessBatchTransactionDataDTO(
|
||||
@Json(name = "transactions")
|
||||
val transactions: List<TransactionData>,
|
||||
|
||||
@Json(name = "fee")
|
||||
val fee: FeeData,
|
||||
|
||||
@Json(name = "nonce")
|
||||
val nonce: String,
|
||||
)
|
||||
|
|
@ -45,6 +45,9 @@ data class TransactionData(
|
|||
@Json(name = "value")
|
||||
val value: String,
|
||||
|
||||
@Json(name = "gasLimit")
|
||||
val gasLimit: String? = null,
|
||||
|
||||
@Json(name = "data")
|
||||
val data: String,
|
||||
)
|
||||
|
|
|
|||
|
|
@ -23,6 +23,13 @@ interface TangemPayApi {
|
|||
@GET("v1/customer/me")
|
||||
suspend fun getCustomerMe(@Header("Authorization") authHeader: String): ApiResponse<CustomerMeResponse>
|
||||
|
||||
/** Fiat bank requisites for the Virtual Account on-ramp (VA MVP0, TWI-1638). */
|
||||
@GET("v1/account/bank-credentials/{product_instance_id}")
|
||||
suspend fun getBankCredentials(
|
||||
@Header("Authorization") authHeader: String,
|
||||
@Path("product_instance_id") productInstanceId: String,
|
||||
): ApiResponse<BankCredentialsResponse>
|
||||
|
||||
@GET("v1/customer/wallets/{customer_wallet_id}")
|
||||
suspend fun checkCustomerWalletId(
|
||||
@Path("customer_wallet_id") customerWalletId: String,
|
||||
|
|
@ -40,6 +47,12 @@ interface TangemPayApi {
|
|||
@GET("v1/eligibility/channels")
|
||||
suspend fun getEligibilityChannels(): ApiResponse<TangemPayEligibilityChannels>
|
||||
|
||||
/** Eligibility channels fetched with the user (customer-wallet) token (VA MVP0, TWI-1638). */
|
||||
@GET("v1/eligibility/channels")
|
||||
suspend fun getUserEligibilityChannels(
|
||||
@Header("Authorization") authHeader: String,
|
||||
): ApiResponse<TangemPayEligibilityChannels>
|
||||
|
||||
@GET("v1/order/{order_id}")
|
||||
suspend fun getOrder(
|
||||
@Header("Authorization") authHeader: String,
|
||||
|
|
|
|||
|
|
@ -0,0 +1,19 @@
|
|||
package com.tangem.datasource.api.pay.models.response
|
||||
|
||||
import com.squareup.moshi.Json
|
||||
import com.squareup.moshi.JsonClass
|
||||
|
||||
/**
|
||||
* Response of `bff-v2/v1/account/bank-credentials/{product_instance_id}` — fiat bank requisites for the
|
||||
* Virtual Account on-ramp (VA MVP0, TWI-1638).
|
||||
*/
|
||||
@JsonClass(generateAdapter = true)
|
||||
data class BankCredentialsResponse(
|
||||
@Json(name = "type") val type: String?,
|
||||
@Json(name = "beneficiary_name") val beneficiaryName: String?,
|
||||
@Json(name = "beneficiary_address") val beneficiaryAddress: String?,
|
||||
@Json(name = "beneficiary_bank_name") val beneficiaryBankName: String?,
|
||||
@Json(name = "beneficiary_bank_address") val beneficiaryBankAddress: String?,
|
||||
@Json(name = "account_number") val accountNumber: String?,
|
||||
@Json(name = "routing_number") val routingNumber: String?,
|
||||
)
|
||||
|
|
@ -21,6 +21,32 @@ data class CustomerMeResponse(
|
|||
@Json(name = "balance") val balance: BalanceResponse?,
|
||||
@Json(name = "product_instances") val productInstances: List<ProductInstance>,
|
||||
@Json(name = "cards") val cards: List<Card>,
|
||||
@Json(name = "customer_tariff_plan") val customerTariffPlan: CustomerTariffPlan? = null,
|
||||
)
|
||||
|
||||
@JsonClass(generateAdapter = true)
|
||||
data class CustomerTariffPlan(
|
||||
@Json(name = "status") val status: String?,
|
||||
@Json(name = "next_billing_at") val nextBillingAt: String?,
|
||||
@Json(name = "pending_transition_at") val pendingTransitionAt: String?,
|
||||
@Json(name = "tariff_plan") val tariffPlan: TariffPlan?,
|
||||
@Json(name = "pending_tariff_plan") val pendingTariffPlan: TariffPlan?,
|
||||
)
|
||||
|
||||
@JsonClass(generateAdapter = true)
|
||||
data class TariffPlan(
|
||||
@Json(name = "id") val id: String?,
|
||||
@Json(name = "type") val type: String?,
|
||||
@Json(name = "name") val name: String?,
|
||||
@Json(name = "description_items") val descriptionItems: List<DescriptionItem>?,
|
||||
)
|
||||
|
||||
@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)
|
||||
|
|
@ -35,7 +61,17 @@ data class CustomerMeResponse(
|
|||
@Json(name = "display_name") val displayName: String?,
|
||||
@Json(name = "actual_card_limit") val actualCardLimit: CardLimit?,
|
||||
@Json(name = "admin_card_limit") val adminCardLimit: CardLimit?,
|
||||
@Json(name = "product_specification_data_type") val specificationDataType: SpecificationDataType,
|
||||
) {
|
||||
@JsonClass(generateAdapter = false)
|
||||
enum class SpecificationDataType {
|
||||
@Json(name = "ACCOUNT")
|
||||
ACCOUNT,
|
||||
|
||||
@Json(name = "CARD")
|
||||
CARD,
|
||||
}
|
||||
|
||||
@JsonClass(generateAdapter = false)
|
||||
enum class Status {
|
||||
@Json(name = "NEW")
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
package com.tangem.datasource.di
|
||||
|
||||
import com.tangem.datasource.BuildConfig
|
||||
import com.tangem.datasource.api.addressbook.AddressBookApi
|
||||
import com.tangem.datasource.api.auth.AuthApi
|
||||
import com.tangem.datasource.api.common.blockaid.BlockAidApi
|
||||
import com.tangem.datasource.api.surveysparrow.SurveySparrowApi
|
||||
|
|
@ -18,6 +19,7 @@ import com.tangem.datasource.api.news.NewsApi
|
|||
import com.tangem.datasource.api.onramp.OnrampApi
|
||||
import com.tangem.datasource.api.ethpool.P2PEthPoolApi
|
||||
import com.tangem.datasource.api.gasless.GaslessTxServiceApi
|
||||
import com.tangem.datasource.api.gasless.GaslessTxServiceApiV2
|
||||
import com.tangem.datasource.api.pay.TangemPayApi
|
||||
import com.tangem.datasource.api.pay.TangemPayAuthApi
|
||||
import com.tangem.datasource.api.stakekit.StakeKitApi
|
||||
|
|
@ -117,6 +119,16 @@ internal object NetworkModule {
|
|||
)
|
||||
}
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
fun provideAddressBookApi(retrofitApiBuilder: RetrofitApiBuilder): AddressBookApi {
|
||||
return retrofitApiBuilder.build(
|
||||
apiConfigId = ApiConfig.ID.TangemTech,
|
||||
applyTimeoutAnnotations = false,
|
||||
sessionAuth = false,
|
||||
)
|
||||
}
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
fun provideYieldSupplyApi(retrofitApiBuilder: RetrofitApiBuilder): YieldSupplyApi {
|
||||
|
|
@ -252,4 +264,20 @@ internal object NetworkModule {
|
|||
),
|
||||
)
|
||||
}
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
fun provideGaslessTxServiceApiV2(retrofitApiBuilder: RetrofitApiBuilder): GaslessTxServiceApiV2 {
|
||||
return retrofitApiBuilder.build(
|
||||
apiConfigId = ApiConfig.ID.GaslessTxService,
|
||||
applyTimeoutAnnotations = false,
|
||||
sessionAuth = false,
|
||||
timeouts = Timeouts(
|
||||
callTimeoutSeconds = TIMEOUT_60_SECONDS,
|
||||
connectTimeoutSeconds = TIMEOUT_60_SECONDS,
|
||||
readTimeoutSeconds = TIMEOUT_60_SECONDS,
|
||||
writeTimeoutSeconds = TIMEOUT_60_SECONDS,
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -5,6 +5,7 @@ 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.TokenInfoDao
|
||||
import dagger.Module
|
||||
import dagger.Provides
|
||||
import dagger.hilt.InstallIn
|
||||
|
|
@ -37,5 +38,8 @@ internal interface TxHistoryModule {
|
|||
|
||||
@Provides
|
||||
fun provideSyncStateDao(database: TxHistoryDatabase): ExpressSyncStateDao = database.syncStateDao()
|
||||
|
||||
@Provides
|
||||
fun provideTokenInfoDao(database: TxHistoryDatabase): TokenInfoDao = database.tokenInfoDao()
|
||||
}
|
||||
}
|
||||
|
|
@ -156,7 +156,7 @@ object PreferencesKeys {
|
|||
val TANGEM_PAY_ACTIVE_WITHDRAW_ORDERS_KEY by lazy {
|
||||
stringPreferencesKey(name = "tangemPayActiveWithdrawOrdersKey")
|
||||
}
|
||||
val TANGEM_PAY_ELIGIBILITY_KEY by lazy { stringSetPreferencesKey(name = "tangemPayEligibilityList") }
|
||||
val TANGEM_PAY_ELIGIBILITY_KEY by lazy { stringSetPreferencesKey(name = "tangemPayEligibilityListV2") }
|
||||
|
||||
fun getShouldShowNotificationKey(key: String) = booleanPreferencesKey("showShowNotificationUM_$key")
|
||||
// endregion
|
||||
|
|
|
|||
|
|
@ -4,11 +4,13 @@ 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.TokenInfoDao
|
||||
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,
|
||||
|
|
@ -18,6 +20,7 @@ import com.tangem.datasource.local.txhistory.db.entity.express.OnrampCountryEnti
|
|||
ExpressOnrampEntity::class,
|
||||
ExpressSyncStateEntity::class,
|
||||
OnrampCountryEntity::class,
|
||||
TokenInfoEntity::class,
|
||||
],
|
||||
)
|
||||
abstract class TxHistoryDatabase : RoomDatabase() {
|
||||
|
|
@ -25,4 +28,6 @@ abstract class TxHistoryDatabase : RoomDatabase() {
|
|||
abstract fun expressHistoryDao(): ExpressHistoryDao
|
||||
|
||||
abstract fun syncStateDao(): ExpressSyncStateDao
|
||||
|
||||
abstract fun tokenInfoDao(): TokenInfoDao
|
||||
}
|
||||
|
|
@ -0,0 +1,33 @@
|
|||
package com.tangem.datasource.local.txhistory.db.dao
|
||||
|
||||
import androidx.room.Dao
|
||||
import androidx.room.Insert
|
||||
import androidx.room.OnConflictStrategy
|
||||
import androidx.room.Query
|
||||
import com.tangem.datasource.local.txhistory.db.entity.express.TokenInfoEntity
|
||||
|
||||
@Dao
|
||||
interface TokenInfoDao {
|
||||
|
||||
@Insert(onConflict = OnConflictStrategy.REPLACE)
|
||||
suspend fun upsert(items: List<TokenInfoEntity>)
|
||||
|
||||
/**
|
||||
* Cached tokens for the given networks/contracts. Filters each column independently, so the result is a
|
||||
* cross-product superset of the `(networkId, contractAddress)` pairs — the caller must match exact pairs.
|
||||
* Contract match is case-insensitive; pass [minUpdatedAt] = `now - ttl` to drop stale rows.
|
||||
*/
|
||||
@Query(
|
||||
"""
|
||||
SELECT * FROM token_info
|
||||
WHERE network_id IN (:networkIds)
|
||||
AND contract_address COLLATE NOCASE IN (:contractAddresses)
|
||||
AND updated_at >= :minUpdatedAt
|
||||
""",
|
||||
)
|
||||
suspend fun getCached(
|
||||
networkIds: Collection<String>,
|
||||
contractAddresses: Collection<String>,
|
||||
minUpdatedAt: Long = 0,
|
||||
): List<TokenInfoEntity>
|
||||
}
|
||||
|
|
@ -0,0 +1,37 @@
|
|||
package com.tangem.datasource.local.txhistory.db.entity.express
|
||||
|
||||
import androidx.room.ColumnInfo
|
||||
import androidx.room.Entity
|
||||
|
||||
/**
|
||||
* Cached token data fetched from [TangemTechApi.getCoins], keyed by network id + contract address.
|
||||
*/
|
||||
@Entity(
|
||||
tableName = "token_info",
|
||||
primaryKeys = ["network_id", "contract_address"],
|
||||
)
|
||||
data class TokenInfoEntity(
|
||||
|
||||
@ColumnInfo(name = "network_id")
|
||||
val networkId: String,
|
||||
|
||||
@ColumnInfo(name = "contract_address")
|
||||
val contractAddress: String,
|
||||
|
||||
/** Coin id from the backend (used as the token's raw currency id and for the icon URL). */
|
||||
@ColumnInfo(name = "coin_id")
|
||||
val coinId: String,
|
||||
|
||||
@ColumnInfo(name = "name")
|
||||
val name: String,
|
||||
|
||||
@ColumnInfo(name = "symbol")
|
||||
val symbol: String,
|
||||
|
||||
@ColumnInfo(name = "decimals")
|
||||
val decimals: Int,
|
||||
|
||||
/** Last refresh timestamp in epoch milliseconds, used to evict stale entries. */
|
||||
@ColumnInfo(name = "updated_at")
|
||||
val updatedAt: Long,
|
||||
)
|
||||
|
|
@ -10,12 +10,25 @@ android {
|
|||
}
|
||||
|
||||
dependencies {
|
||||
api(projects.core.utils)
|
||||
|
||||
api(deps.decompose)
|
||||
api(deps.androidx.appCompat)
|
||||
implementation(deps.kotlin.coroutines)
|
||||
|
||||
// region DI
|
||||
implementation(deps.hilt.core)
|
||||
kapt(deps.hilt.kapt)
|
||||
// endregion
|
||||
|
||||
// region Kotlin
|
||||
api(deps.kotlin.coroutines)
|
||||
// endregion
|
||||
|
||||
// region AndroidX
|
||||
api(deps.androidx.appCompat)
|
||||
// endregion
|
||||
|
||||
// region Other libraries
|
||||
api(deps.decompose)
|
||||
// endregion
|
||||
|
||||
// region Core modules
|
||||
api(projects.core.utils)
|
||||
// endregion
|
||||
}
|
||||
|
|
@ -9,8 +9,13 @@ android {
|
|||
}
|
||||
|
||||
dependencies {
|
||||
api(projects.core.error)
|
||||
|
||||
implementation(tangemDeps.card.core)
|
||||
implementation(tangemDeps.blockchain)
|
||||
// region Tangem
|
||||
api(tangemDeps.blockchain)
|
||||
api(tangemDeps.card.core)
|
||||
// endregion
|
||||
|
||||
// region Core modules
|
||||
api(projects.core.error)
|
||||
// endregion
|
||||
}
|
||||
|
|
@ -1,8 +1,6 @@
|
|||
plugins {
|
||||
alias(deps.plugins.android.library)
|
||||
alias(deps.plugins.kotlin.android)
|
||||
alias(deps.plugins.kotlin.kapt)
|
||||
alias(deps.plugins.hilt.android)
|
||||
id("configuration")
|
||||
}
|
||||
|
||||
|
|
@ -11,10 +9,12 @@ android {
|
|||
}
|
||||
|
||||
dependencies {
|
||||
implementation(projects.core.utils)
|
||||
|
||||
// region DI
|
||||
implementation(deps.hilt.android)
|
||||
kapt(deps.hilt.kapt)
|
||||
// endregion
|
||||
|
||||
implementation(deps.material)
|
||||
// region AndroidX
|
||||
implementation(deps.androidx.core)
|
||||
// endregion
|
||||
}
|
||||
|
|
@ -4,7 +4,7 @@ plugins {
|
|||
}
|
||||
|
||||
dependencies {
|
||||
// region Coroutines
|
||||
implementation(deps.kotlin.coroutines)
|
||||
// region Kotlin
|
||||
api(deps.kotlin.coroutines)
|
||||
// endregion
|
||||
}
|
||||
|
|
@ -10,12 +10,17 @@ android {
|
|||
|
||||
dependencies {
|
||||
|
||||
implementation(projects.core.utils)
|
||||
// region AndroidX
|
||||
api(deps.androidx.annotation)
|
||||
// endregion
|
||||
|
||||
|
||||
// region Firebase libraries
|
||||
// region Firebase
|
||||
implementation(platform(deps.firebase.bom))
|
||||
implementation(deps.firebase.analytics)
|
||||
implementation(deps.firebase.crashlytics)
|
||||
// endregion
|
||||
|
||||
// region Core modules
|
||||
implementation(projects.core.utils)
|
||||
// endregion
|
||||
}
|
||||
|
|
@ -1550,6 +1550,7 @@
|
|||
<string name="staking_enabled">Staking aktiviert</string>
|
||||
<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_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>
|
||||
|
|
|
|||
|
|
@ -1421,6 +1421,7 @@
|
|||
<string name="staking_enabled">Staking activé</string>
|
||||
<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_give_permission_fee_footer">Le réseau facturera des frais d’approbation de jeton pour vérifier que vous autorisez l’utilisation 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>
|
||||
|
|
|
|||
|
|
@ -1520,6 +1520,7 @@
|
|||
<string name="staking_enabled">ステーキングが有効です</string>
|
||||
<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_give_permission_fee_footer">ネットワークは、ステーキングのためにトークンの使用を承認していることを確認するために、トークン承認手数料を請求します。</string>
|
||||
<string name="staking_legal">ステーキング機能を使用すると、プロバイダーの%1$sと%2$sに同意したことになります</string>
|
||||
<string name="staking_locked">ロック中</string>
|
||||
|
|
|
|||
|
|
@ -1512,6 +1512,7 @@
|
|||
<string name="staking_enabled">已启用质押</string>
|
||||
<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_give_permission_fee_footer">网络将收取代币批准费,以验证您是否授权使用您的代币进行质押。</string>
|
||||
<string name="staking_legal">使用质押功能即表示您同意提供商的 %1$s 和 %2$s</string>
|
||||
<string name="staking_locked">已锁定</string>
|
||||
|
|
|
|||
|
|
@ -115,10 +115,17 @@
|
|||
<string name="address_book_enter_address">Enter address</string>
|
||||
<string name="address_book_invalid_address_error">Invalid address</string>
|
||||
<string name="address_book_keep_editing">Keep editing</string>
|
||||
<string name="address_book_max_networks_alert_description">You can not create more than 20 addresses. Delete one to add new.</string>
|
||||
<string name="address_book_max_networks_alert_title">Can\'t add new address </string>
|
||||
<string name="address_book_name_empty_error">Contact name is required</string>
|
||||
<string name="address_book_name_invalid_chars_error">Contact name contains invalid characters</string>
|
||||
<string name="address_book_name_max_chars_error">Contact name must not exceed 50 characters</string>
|
||||
<string name="address_book_name_taken_error">That name is already taken on this wallet</string>
|
||||
<string name="address_book_new_contact">New contact</string>
|
||||
<string name="address_book_no_contacts">No contacts yet</string>
|
||||
<string name="address_book_no_contacts_description">Contacts added will appear here</string>
|
||||
<string name="address_book_remove_address">Remove address</string>
|
||||
<string name="address_book_save_to_wallet_title">Save to Wallet</string>
|
||||
<string name="address_book_save_wallet_to_description">This contact will be linked to this wallet’s address book.</string>
|
||||
<string name="address_book_select_network">Select network</string>
|
||||
<string name="address_book_title">Address book</string>
|
||||
|
|
@ -352,6 +359,7 @@
|
|||
<string name="common_get_token">Get token</string>
|
||||
<string name="common_go_to_provider">Go to provider</string>
|
||||
<string name="common_go_to_token">Go to token</string>
|
||||
<string name="common_go_to_verification">Go to verification</string>
|
||||
<string name="common_got_it">Got it</string>
|
||||
<string name="common_hide">Hide</string>
|
||||
<string name="common_hold_to">Hold to %s</string>
|
||||
|
|
@ -410,6 +418,7 @@
|
|||
<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>
|
||||
<string name="common_rate">Rate</string>
|
||||
<string name="common_read_more">Read more</string>
|
||||
<string name="common_receive">Receive</string>
|
||||
<string name="common_received">Received</string>
|
||||
|
|
@ -701,6 +710,8 @@
|
|||
<string name="feedback_subject_support_tangem">Tangem feedback</string>
|
||||
<string name="feedback_subject_tx_failed">Can\'t send a transaction</string>
|
||||
<string name="feedback_token_description_error">Coin description error</string>
|
||||
<string name="for_you_description">Review portfolio and explore earn opportunities</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>
|
||||
|
|
@ -732,6 +743,8 @@
|
|||
<string name="hardware_wallet_key_feature_title">Key Generation</string>
|
||||
<string name="hardware_wallet_security_feature_description">All cryptographic operations happen inside the secure chip, certified against cloning and physical tampering.</string>
|
||||
<string name="hardware_wallet_security_feature_title">Hardware-Level Security</string>
|
||||
<string name="high_fee_warning_description">Network activity is high. You can continue now or try again later when fees may be lower.</string>
|
||||
<string name="high_fee_warning_title">Network fee is higher than usual</string>
|
||||
<string name="home_button_add_existing_wallet">Add Existing Wallet</string>
|
||||
<string name="home_button_create_new_wallet">Create New Wallet</string>
|
||||
<string name="home_button_order">Order Tangem</string>
|
||||
|
|
@ -1402,6 +1415,7 @@
|
|||
<string name="send_extras_hint_memo">Memo</string>
|
||||
<string name="send_fee_unreachable_error_text">Check your network connection</string>
|
||||
<string name="send_fee_unreachable_error_title">Network fee info unreachable</string>
|
||||
<string name="send_from_account_in_wallet_name">from %1$s in %2$s</string>
|
||||
<string name="send_from_title">You send</string>
|
||||
<string name="send_from_wallet_name">From %s</string>
|
||||
<string name="send_gas_limit">Gas limit</string>
|
||||
|
|
@ -1937,6 +1951,10 @@
|
|||
<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>
|
||||
<string name="tangempay_current_plan_title">Current plan</string>
|
||||
<string name="tangempay_current_plan_section_card">Card related</string>
|
||||
<string name="tangempay_current_plan_section_plan">Plan related</string>
|
||||
<string name="tangempay_current_plan_change">Change plan</string>
|
||||
<string name="tangempay_pin_validation_error_message">Invalid PIN: avoid sequences or repeats</string>
|
||||
<string name="tangempay_reissue_card_confirm">Replace card</string>
|
||||
<string name="tangempay_reissue_card_description">This generates a new set of card details. Your old details will stop working. You can\'t undo this.</string>
|
||||
|
|
|
|||
|
|
@ -109,51 +109,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 +166,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
|
||||
}
|
||||
|
|
@ -3,19 +3,22 @@ package com.tangem.core.ui.ds.button.action
|
|||
import android.content.res.Configuration
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.key
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.layout.Layout
|
||||
import androidx.compose.ui.platform.LocalDensity
|
||||
import androidx.compose.ui.platform.testTag
|
||||
import androidx.compose.ui.semantics.disabled
|
||||
import androidx.compose.ui.semantics.semantics
|
||||
import androidx.compose.ui.text.style.TextAlign
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.tooling.preview.PreviewParameter
|
||||
import androidx.compose.ui.tooling.preview.PreviewParameterProvider
|
||||
import androidx.compose.ui.unit.Constraints
|
||||
import androidx.compose.ui.unit.dp
|
||||
import com.tangem.core.ui.R
|
||||
import com.tangem.core.ui.ds.button.SecondaryTangemButton
|
||||
|
|
@ -32,6 +35,8 @@ import com.tangem.core.ui.test.BaseActionButtonsBlockTestTags
|
|||
import kotlinx.collections.immutable.ImmutableList
|
||||
import kotlinx.collections.immutable.persistentListOf
|
||||
|
||||
private val ACTION_BUTTONS_SPACING = 10.dp
|
||||
|
||||
/**
|
||||
* Action buttons row
|
||||
*
|
||||
|
|
@ -42,42 +47,73 @@ import kotlinx.collections.immutable.persistentListOf
|
|||
*/
|
||||
@Composable
|
||||
fun ActionButtons(buttons: ImmutableList<TangemButtonUM>, modifier: Modifier = Modifier) {
|
||||
Row(
|
||||
horizontalArrangement = Arrangement.spacedBy(14.dp),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
val spacingPx = with(LocalDensity.current) { ACTION_BUTTONS_SPACING.roundToPx() }
|
||||
|
||||
Layout(
|
||||
modifier = modifier,
|
||||
) {
|
||||
buttons.forEachIndexed { index, button ->
|
||||
key(button.text to index) {
|
||||
val textColor = if (button.isEnabled) {
|
||||
TangemTheme.colors2.text.neutral.primary
|
||||
} else {
|
||||
TangemTheme.colors2.text.status.disabled
|
||||
}
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.padding(horizontal = TangemTheme.dimens2.x2_5)
|
||||
.testTag(BaseActionButtonsBlockTestTags.ACTION_BUTTON)
|
||||
.semantics { if (!button.isEnabled) disabled() },
|
||||
verticalArrangement = Arrangement.spacedBy(TangemTheme.dimens2.x2),
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
) {
|
||||
SecondaryTangemButton(
|
||||
tangemIconUM = button.tangemIconUM,
|
||||
onClick = button.onClick,
|
||||
isEnabled = button.isEnabled,
|
||||
shape = TangemButtonShape.Rounded,
|
||||
onLongClick = button.onLongClick,
|
||||
)
|
||||
Text(
|
||||
text = button.text.orEmpty().resolveReference(),
|
||||
style = TangemTheme.typography2.subheadlineMedium14,
|
||||
color = textColor,
|
||||
maxLines = 1,
|
||||
)
|
||||
content = {
|
||||
buttons.forEachIndexed { index, button ->
|
||||
key(button.text to index) {
|
||||
ActionButton(button = button)
|
||||
}
|
||||
}
|
||||
},
|
||||
) { measurables, constraints ->
|
||||
if (measurables.isEmpty()) {
|
||||
return@Layout layout(constraints.minWidth, constraints.minHeight) {}
|
||||
}
|
||||
val cellWidth = measurables.maxOf { it.maxIntrinsicWidth(constraints.maxHeight) }
|
||||
val cellConstraints = Constraints(
|
||||
minWidth = cellWidth,
|
||||
maxWidth = cellWidth,
|
||||
minHeight = 0,
|
||||
maxHeight = constraints.maxHeight,
|
||||
)
|
||||
val placeables = measurables.map { it.measure(cellConstraints) }
|
||||
|
||||
val contentWidth = cellWidth * placeables.size + spacingPx * (placeables.size - 1)
|
||||
val width = if (constraints.hasBoundedWidth) maxOf(constraints.maxWidth, contentWidth) else contentWidth
|
||||
val height = placeables.maxOf { it.height }
|
||||
|
||||
layout(width, height) {
|
||||
var x = (width - contentWidth) / 2
|
||||
placeables.forEach { placeable ->
|
||||
placeable.place(x = x, y = (height - placeable.height) / 2)
|
||||
x += cellWidth + spacingPx
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun ActionButton(button: TangemButtonUM, modifier: Modifier = Modifier) {
|
||||
val textColor = if (button.isEnabled) {
|
||||
TangemTheme.colors2.text.neutral.primary
|
||||
} else {
|
||||
TangemTheme.colors2.text.status.disabled
|
||||
}
|
||||
Column(
|
||||
modifier = modifier
|
||||
.padding(horizontal = TangemTheme.dimens2.x2_5)
|
||||
.testTag(BaseActionButtonsBlockTestTags.ACTION_BUTTON)
|
||||
.semantics { if (!button.isEnabled) disabled() },
|
||||
verticalArrangement = Arrangement.spacedBy(TangemTheme.dimens2.x2),
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
) {
|
||||
SecondaryTangemButton(
|
||||
tangemIconUM = button.tangemIconUM,
|
||||
onClick = button.onClick,
|
||||
isEnabled = button.isEnabled,
|
||||
shape = TangemButtonShape.Rounded,
|
||||
onLongClick = button.onLongClick,
|
||||
)
|
||||
Text(
|
||||
text = button.text.orEmpty().resolveReference(),
|
||||
style = TangemTheme.typography2.subheadlineMedium14,
|
||||
color = textColor,
|
||||
textAlign = TextAlign.Center,
|
||||
maxLines = 1,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -25,7 +25,6 @@ import androidx.compose.runtime.saveable.rememberSaveable
|
|||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.BiasAlignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.alpha
|
||||
import androidx.compose.ui.draw.clip
|
||||
import androidx.compose.ui.focus.FocusManager
|
||||
import androidx.compose.ui.focus.FocusRequester
|
||||
|
|
@ -201,6 +200,7 @@ private fun DecorationBox(
|
|||
Field(
|
||||
state = state,
|
||||
innerTextField = innerTextField,
|
||||
modifier = if (state.query.isEmpty()) Modifier else Modifier.weight(1f),
|
||||
)
|
||||
ClearButton(
|
||||
state = state,
|
||||
|
|
@ -217,58 +217,45 @@ private fun DecorationBox(
|
|||
}
|
||||
|
||||
@Composable
|
||||
private fun Field(state: SearchBarUM, innerTextField: @Composable () -> Unit) {
|
||||
private fun Field(state: SearchBarUM, modifier: Modifier = Modifier, innerTextField: @Composable () -> Unit) {
|
||||
Box(
|
||||
contentAlignment = Alignment.CenterStart,
|
||||
modifier = Modifier
|
||||
modifier = modifier
|
||||
.heightIn(min = TangemTheme.dimens2.x5)
|
||||
.width(IntrinsicSize.Max),
|
||||
.then(if (state.query.isEmpty()) Modifier.width(IntrinsicSize.Max) else Modifier),
|
||||
) {
|
||||
innerTextField()
|
||||
|
||||
val placeholderOpacity by remember(state.query) {
|
||||
derivedStateOf {
|
||||
if (state.query.isNotEmpty()) {
|
||||
0f
|
||||
} else {
|
||||
1f
|
||||
}
|
||||
}
|
||||
if (state.query.isEmpty()) {
|
||||
Text(
|
||||
text = state.placeholderText.resolveReference(),
|
||||
color = TangemTheme.colors2.text.neutral.tertiary,
|
||||
style = TangemTheme.typography2.bodySemibold16,
|
||||
maxLines = 1,
|
||||
overflow = TextOverflow.Ellipsis,
|
||||
modifier = Modifier.testTag(SearchBarTestTags.PLACEHOLDER_TEXT),
|
||||
)
|
||||
}
|
||||
|
||||
Text(
|
||||
text = state.placeholderText.resolveReference(),
|
||||
color = TangemTheme.colors2.text.neutral.tertiary,
|
||||
style = TangemTheme.typography2.bodySemibold16,
|
||||
maxLines = 1,
|
||||
overflow = TextOverflow.Ellipsis,
|
||||
modifier = Modifier
|
||||
.testTag(SearchBarTestTags.PLACEHOLDER_TEXT)
|
||||
.alpha(placeholderOpacity),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun ClearButton(state: SearchBarUM) {
|
||||
if (state.query.isNotEmpty()) {
|
||||
Box(modifier = Modifier.fillMaxWidth()) {
|
||||
Icon(
|
||||
imageVector = ImageVector.vectorResource(R.drawable.ic_close_new_20),
|
||||
tint = TangemTheme.colors2.graphic.neutral.tertiary,
|
||||
contentDescription = null,
|
||||
modifier = Modifier
|
||||
.align(Alignment.CenterEnd)
|
||||
.size(TangemTheme.dimens2.x5)
|
||||
.clip(CircleShape)
|
||||
.clickable(
|
||||
onClick = state.onClearClick,
|
||||
interactionSource = remember { MutableInteractionSource() },
|
||||
indication = ripple(bounded = false),
|
||||
)
|
||||
.testTag(SearchBarTestTags.CLEAR_BUTTON),
|
||||
)
|
||||
}
|
||||
Icon(
|
||||
imageVector = ImageVector.vectorResource(R.drawable.ic_close_new_20),
|
||||
tint = TangemTheme.colors2.graphic.neutral.tertiary,
|
||||
contentDescription = null,
|
||||
modifier = Modifier
|
||||
.size(TangemTheme.dimens2.x5)
|
||||
.clip(CircleShape)
|
||||
.clickable(
|
||||
onClick = state.onClearClick,
|
||||
interactionSource = remember { MutableInteractionSource() },
|
||||
indication = ripple(bounded = false),
|
||||
)
|
||||
.testTag(SearchBarTestTags.CLEAR_BUTTON),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -11,13 +11,15 @@ import androidx.compose.ui.Alignment
|
|||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.graphics.takeOrElse
|
||||
import androidx.compose.ui.graphics.vector.ImageVector
|
||||
import androidx.compose.ui.res.vectorResource
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.unit.dp
|
||||
import com.tangem.core.ui.R
|
||||
import com.tangem.core.ui.res.TangemTheme
|
||||
import com.tangem.core.ui.res.TangemThemePreviewRedesign
|
||||
import com.tangem.core.ui.res.generated.icons.Icons
|
||||
import com.tangem.core.ui.res.generated.icons.ic_cloud_24_filled
|
||||
|
||||
private const val DEFAULT_DEVICE_ICON_COLOR = 0xFF595963
|
||||
private const val DEFAULT_BORDER_COLOR = 0x1A1E1E1E
|
||||
|
||||
/**
|
||||
* Composable function for displaying a wallet icon based on the provided [DeviceIconUM] state.
|
||||
|
|
@ -57,9 +59,9 @@ fun TangemDeviceIcon(state: DeviceIconUM, modifier: Modifier = Modifier) {
|
|||
)
|
||||
DeviceIconUM.Mobile -> Icon(
|
||||
modifier = modifier,
|
||||
imageVector = ImageVector.vectorResource(R.drawable.ic_shield_24),
|
||||
imageVector = Icons.ic_cloud_24_filled,
|
||||
contentDescription = null,
|
||||
tint = TangemTheme.colors2.graphic.status.attention,
|
||||
tint = Color(DEFAULT_DEVICE_ICON_COLOR),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -73,10 +75,10 @@ private fun DeviceIcon(
|
|||
tColor: Color?,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
val main = mainColor.takeOrElse { TangemTheme.colors2.graphic.neutral.tertiaryConstant }
|
||||
val second = secondColor?.takeOrElse { TangemTheme.colors2.graphic.neutral.tertiaryConstant }
|
||||
val third = thirdColor?.takeOrElse { TangemTheme.colors2.graphic.neutral.tertiaryConstant }
|
||||
val borderColor = TangemTheme.colors2.border.walletIcon
|
||||
val main = mainColor.takeOrElse { Color(DEFAULT_DEVICE_ICON_COLOR) }
|
||||
val second = secondColor?.takeOrElse { Color(DEFAULT_DEVICE_ICON_COLOR) }
|
||||
val third = thirdColor?.takeOrElse { Color(DEFAULT_DEVICE_ICON_COLOR) }
|
||||
val borderColor = Color(DEFAULT_BORDER_COLOR)
|
||||
|
||||
val imageVector = remember(isRing, main, second, third, borderColor, tColor) {
|
||||
when {
|
||||
|
|
|
|||
|
|
@ -67,7 +67,10 @@ fun TangemTab(
|
|||
interactionSource = remember { MutableInteractionSource() },
|
||||
indication = ripple(),
|
||||
)
|
||||
.padding(all = TangemTheme.dimens2.x3),
|
||||
.padding(
|
||||
vertical = TangemTheme.dimens2.x2,
|
||||
horizontal = TangemTheme.dimens2.x3,
|
||||
),
|
||||
) {
|
||||
Text(
|
||||
text = text.resolveReference(),
|
||||
|
|
|
|||
|
|
@ -9,7 +9,7 @@ import androidx.compose.foundation.layout.Column
|
|||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.widthIn
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.foundation.shape.CircleShape
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.getValue
|
||||
|
|
@ -108,7 +108,7 @@ fun TangemButton(
|
|||
enabled = isEnabled,
|
||||
color = backgroundColor,
|
||||
border = resolveBorder(isFocused = isFocused, colorTokens = colorTokens, contentAlpha = contentAlpha),
|
||||
shape = RoundedCornerShape(999.dp),
|
||||
shape = CircleShape,
|
||||
interactionSource = interactionSource,
|
||||
isMaterial = variant == TangemButton.Variant.Material,
|
||||
) {
|
||||
|
|
|
|||
|
|
@ -250,7 +250,7 @@ private fun CloseButton(onClick: () -> Unit) {
|
|||
@Composable
|
||||
private fun Preview(@PreviewParameter(TangemSearchStateProvider::class) state: TangemSearch.State) {
|
||||
TangemThemePreviewRedesign {
|
||||
Box(modifier = Modifier.background(TangemTheme.colors3.bg.secondary)) {
|
||||
Box(modifier = Modifier.background(TangemTheme.colors3.bg.tertiary)) {
|
||||
TangemSearch(
|
||||
state = state,
|
||||
modifier = Modifier
|
||||
|
|
|
|||
|
|
@ -12,8 +12,12 @@ import androidx.compose.ui.Alignment
|
|||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.clip
|
||||
import androidx.compose.ui.geometry.Offset
|
||||
import androidx.compose.ui.geometry.Size
|
||||
import androidx.compose.ui.graphics.Brush
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.graphics.LinearGradientShader
|
||||
import androidx.compose.ui.graphics.Shader
|
||||
import androidx.compose.ui.graphics.ShaderBrush
|
||||
import androidx.compose.ui.graphics.Shape
|
||||
import androidx.compose.ui.unit.DpOffset
|
||||
import androidx.compose.ui.unit.dp
|
||||
|
|
@ -113,7 +117,7 @@ fun TangemSurface(
|
|||
@Composable
|
||||
private fun Modifier.materialShadow(shape: Shape): Modifier = softLayerShadow(
|
||||
radius = 40.dp,
|
||||
color = Color.Black.copy(alpha = 0.10f),
|
||||
color = Color.Black.copy(alpha = 0.12f),
|
||||
shape = shape,
|
||||
spread = 0.dp,
|
||||
offset = DpOffset(x = 0.dp, y = 8.dp),
|
||||
|
|
@ -141,19 +145,22 @@ private fun Modifier.materialBorder(shape: Shape): Modifier = border(
|
|||
@Composable
|
||||
private fun Modifier.materialFill(): Modifier {
|
||||
val isBlurEnabled = LocalHazeState.current.blurEnabled
|
||||
val material = TangemTheme.colors3.material
|
||||
val hazed = hazeEffectTangem(
|
||||
style = HazeStyle(
|
||||
backgroundColor = TangemTheme.colors3.material.fill.blur,
|
||||
backgroundColor = Color.Transparent,
|
||||
blurRadius = 32.dp,
|
||||
tints = emptyList(),
|
||||
tints = listOf(
|
||||
HazeTint(material.fill.blur),
|
||||
),
|
||||
),
|
||||
) {
|
||||
fallbackTint = HazeTint(Color.Transparent)
|
||||
}
|
||||
return hazed.conditionalCompose(!isBlurEnabled) {
|
||||
// Paint the opaque fill first, then layer the translucent tint on top so both are visible.
|
||||
background(TangemTheme.colors3.material.fill.solid)
|
||||
.background(TangemTheme.colors3.material.tint.solid)
|
||||
background(material.fill.solid)
|
||||
.background(material.tint.solid)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -162,13 +169,22 @@ private fun Modifier.materialFill(): Modifier {
|
|||
@ReadOnlyComposable
|
||||
private fun materialBorderBrush(): Brush {
|
||||
val border = TangemTheme.colors3.material.border
|
||||
return Brush.linearGradient(
|
||||
0f to border.start,
|
||||
0.5f to border.mid,
|
||||
1f to border.end,
|
||||
start = Offset.Zero,
|
||||
end = Offset.Infinite,
|
||||
)
|
||||
val startColor = border.start
|
||||
val midColor = Color.Transparent
|
||||
val endColor = border.end
|
||||
return object : ShaderBrush() {
|
||||
override fun createShader(size: Size): Shader {
|
||||
val w = size.width
|
||||
val h = size.height
|
||||
val k = 2f * w * h / (w * w + h * h)
|
||||
return LinearGradientShader(
|
||||
from = Offset.Zero,
|
||||
to = Offset(x = k * h, y = k * w),
|
||||
colors = listOf(startColor, midColor, midColor, endColor),
|
||||
colorStops = listOf(0f, 0.40f, 0.60f, 1f),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// endregion
|
||||
|
|
|
|||
|
|
@ -47,6 +47,9 @@ private enum class SlotId { Start, Content, Group, End }
|
|||
* @param contentAlign How [contentColumn] is aligned horizontally within the bar.
|
||||
* @param windowInsets Top inset applied above the row. Pass `WindowInsets(0)` inside a bottom
|
||||
* sheet / modal.
|
||||
* @param contentPadding Inner padding applied to the row, inside [windowInsets]. Defaults to
|
||||
* [TangemTopNavigation.DefaultContentPadding] (top 8, bottom 16, horizontal 16). Override a single
|
||||
* edge via [com.tangem.core.ui.extensions.copy], e.g. `DefaultContentPadding.copy(top = 16.dp)`.
|
||||
* @param blurBackground Whether the fade behind the row should blur the content below.
|
||||
* @param startButton Leading slot. Typically a back button (see [TangemButton.Back]).
|
||||
* @param endButtonsGroup Optional pill-grouped secondary actions placed just before [endButton].
|
||||
|
|
@ -59,6 +62,7 @@ fun TangemTopNavigation(
|
|||
modifier: Modifier = Modifier,
|
||||
contentAlign: TangemTopNavigation.ContentAlign = TangemTopNavigation.ContentAlign.Start,
|
||||
windowInsets: WindowInsets = WindowInsets.statusBars,
|
||||
contentPadding: PaddingValues = TangemTopNavigation.DefaultContentPadding,
|
||||
blurBackground: Boolean = true,
|
||||
startButton: (@Composable () -> Unit)? = null,
|
||||
endButtonsGroup: (@Composable RowScope.() -> Unit)? = null,
|
||||
|
|
@ -89,12 +93,7 @@ fun TangemTopNavigation(
|
|||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.windowInsetsPadding(windowInsets)
|
||||
.padding(
|
||||
top = 8.dp,
|
||||
bottom = 16.dp,
|
||||
start = 16.dp,
|
||||
end = 16.dp,
|
||||
),
|
||||
.padding(contentPadding),
|
||||
content = {
|
||||
val displayedStart = rememberLastNonNull(startButton)
|
||||
Box(modifier = Modifier.layoutId(SlotId.Start)) {
|
||||
|
|
@ -208,6 +207,7 @@ fun TangemTopNavigation(
|
|||
subtitle: TextReference? = null,
|
||||
contentAlign: TangemTopNavigation.ContentAlign = TangemTopNavigation.ContentAlign.Start,
|
||||
windowInsets: WindowInsets = WindowInsets.statusBars,
|
||||
contentPadding: PaddingValues = TangemTopNavigation.DefaultContentPadding,
|
||||
blurBackground: Boolean = true,
|
||||
onBack: (() -> Unit)? = null,
|
||||
endButtonsGroup: (@Composable RowScope.() -> Unit)? = null,
|
||||
|
|
@ -217,6 +217,7 @@ fun TangemTopNavigation(
|
|||
modifier = modifier,
|
||||
contentAlign = contentAlign,
|
||||
windowInsets = windowInsets,
|
||||
contentPadding = contentPadding,
|
||||
blurBackground = blurBackground,
|
||||
startButton = onBack?.let { { TangemButton.Back(onClick = it) } },
|
||||
endButtonsGroup = endButtonsGroup,
|
||||
|
|
@ -233,6 +234,7 @@ fun TangemTopNavigation(
|
|||
subtitle: TextReference? = null,
|
||||
contentAlign: TangemTopNavigation.ContentAlign = TangemTopNavigation.ContentAlign.Start,
|
||||
windowInsets: WindowInsets = WindowInsets.statusBars,
|
||||
contentPadding: PaddingValues = TangemTopNavigation.DefaultContentPadding,
|
||||
blurBackground: Boolean = true,
|
||||
endButtonsGroup: (@Composable RowScope.() -> Unit)? = null,
|
||||
onClose: (() -> Unit)? = null,
|
||||
|
|
@ -242,6 +244,7 @@ fun TangemTopNavigation(
|
|||
modifier = modifier,
|
||||
contentAlign = contentAlign,
|
||||
windowInsets = windowInsets,
|
||||
contentPadding = contentPadding,
|
||||
blurBackground = blurBackground,
|
||||
startButton = startButton,
|
||||
endButtonsGroup = endButtonsGroup,
|
||||
|
|
@ -258,6 +261,7 @@ fun TangemTopNavigation(
|
|||
subtitle: TextReference? = null,
|
||||
contentAlign: TangemTopNavigation.ContentAlign = TangemTopNavigation.ContentAlign.Start,
|
||||
windowInsets: WindowInsets = WindowInsets.statusBars,
|
||||
contentPadding: PaddingValues = TangemTopNavigation.DefaultContentPadding,
|
||||
blurBackground: Boolean = true,
|
||||
onBack: (() -> Unit)? = null,
|
||||
endButton: @Composable () -> Unit,
|
||||
|
|
@ -266,6 +270,7 @@ fun TangemTopNavigation(
|
|||
modifier = modifier,
|
||||
contentAlign = contentAlign,
|
||||
windowInsets = windowInsets,
|
||||
contentPadding = contentPadding,
|
||||
blurBackground = blurBackground,
|
||||
startButton = onBack?.let { { TangemButton.Back(onClick = it) } },
|
||||
endButton = endButton,
|
||||
|
|
@ -308,7 +313,7 @@ private fun ColumnScope.TitleSubtitle(title: TextReference, subtitle: TextRefere
|
|||
) {
|
||||
displayedSubtitle?.let { text ->
|
||||
Column {
|
||||
Spacer(Modifier.height(2.dp))
|
||||
Spacer(Modifier.height(4.dp))
|
||||
TangemNavigationText(text = text, role = TangemNavigationText.Role.Subtitle)
|
||||
}
|
||||
}
|
||||
|
|
@ -317,6 +322,15 @@ private fun ColumnScope.TitleSubtitle(title: TextReference, subtitle: TextRefere
|
|||
|
||||
object TangemTopNavigation {
|
||||
|
||||
/** Default inner padding of the row: top 8, bottom 16, horizontal 16. */
|
||||
@Suppress("MagicNumber")
|
||||
val DefaultContentPadding: PaddingValues = PaddingValues(
|
||||
top = 8.dp,
|
||||
bottom = 16.dp,
|
||||
start = 16.dp,
|
||||
end = 16.dp,
|
||||
)
|
||||
|
||||
/** Horizontal alignment of the center content slot. */
|
||||
enum class ContentAlign {
|
||||
Start, Center
|
||||
|
|
|
|||
|
|
@ -0,0 +1,22 @@
|
|||
package com.tangem.core.ui.extensions
|
||||
|
||||
import androidx.compose.foundation.layout.PaddingValues
|
||||
import androidx.compose.foundation.layout.calculateEndPadding
|
||||
import androidx.compose.foundation.layout.calculateStartPadding
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.platform.LocalLayoutDirection
|
||||
import androidx.compose.ui.unit.Dp
|
||||
|
||||
/**
|
||||
* Returns a copy of this [PaddingValues] with the given edges overridden, leaving the rest unchanged.
|
||||
*/
|
||||
@Composable
|
||||
fun PaddingValues.copy(start: Dp? = null, top: Dp? = null, end: Dp? = null, bottom: Dp? = null): PaddingValues {
|
||||
val layoutDirection = LocalLayoutDirection.current
|
||||
return PaddingValues(
|
||||
start = start ?: calculateStartPadding(layoutDirection),
|
||||
top = top ?: calculateTopPadding(),
|
||||
end = end ?: calculateEndPadding(layoutDirection),
|
||||
bottom = bottom ?: calculateBottomPadding(),
|
||||
)
|
||||
}
|
||||
|
|
@ -52,11 +52,13 @@ open class BigDecimalCryptoFormatStyled(
|
|||
fun BigDecimalFormatScope.crypto(
|
||||
symbol: String,
|
||||
decimals: Int,
|
||||
ignoreSymbolPosition: Boolean = false,
|
||||
locale: Locale = Locale.getDefault(),
|
||||
): BigDecimalCryptoFormat {
|
||||
return BigDecimalCryptoFormat(
|
||||
symbol = symbol,
|
||||
decimals = decimals,
|
||||
shouldIgnoreSymbolPosition = ignoreSymbolPosition,
|
||||
locale = locale,
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,4 +2,5 @@ package com.tangem.core.ui.test
|
|||
|
||||
object AppSettingsScreenTestTags {
|
||||
const val CURRENCY_BUTTON = "APP_SETTINGS_SCREEN_CURRENCY_BUTTON"
|
||||
const val BACK_BUTTON = "APP_SETTINGS_SCREEN_BACK_BUTTON"
|
||||
}
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 2.4 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 3.3 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 5.4 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 8 KiB |
Loading…
Add table
Add a link
Reference in a new issue