Updated on 2026-08-14
This commit is contained in:
commit
c2cd7e6b4c
675 changed files with 9577 additions and 4375 deletions
17
core/analytics/models/detekt-baseline-main.xml
Normal file
17
core/analytics/models/detekt-baseline-main.xml
Normal file
|
|
@ -0,0 +1,17 @@
|
|||
<?xml version="1.0" ?>
|
||||
<SmellBaseline>
|
||||
<ManuallySuppressedIssues/>
|
||||
<CurrentIssues>
|
||||
<ID>MultilineLambdaItParameter:TechAnalyticsEvent.kt$TechAnalyticsEvent.KeyboardIdentifier${ put("Package", it) put("GPUrl", "https://play.google.com/store/apps/details?id=$packageName") }</ID>
|
||||
<ID>UseEmptyCounterpart:AnalyticsEvent.kt$AnalyticsEvent$mapOf()</ID>
|
||||
<ID>UseEmptyCounterpart:Basic.kt$Basic$mapOf()</ID>
|
||||
<ID>UseEmptyCounterpart:ExceptionAnalyticsEvent.kt$ExceptionAnalyticsEvent$mapOf()</ID>
|
||||
<ID>UseEmptyCounterpart:MainScreenAnalyticsEvent.kt$MainScreenAnalyticsEvent$mapOf()</ID>
|
||||
<ID>UseEmptyCounterpart:OnboardingAnalyticsEvent.kt$OnboardingAnalyticsEvent$mapOf()</ID>
|
||||
<ID>UseEmptyCounterpart:OnboardingAnalyticsEvent.kt$OnboardingAnalyticsEvent.CreateWallet$mapOf()</ID>
|
||||
<ID>UseEmptyCounterpart:OnboardingAnalyticsEvent.kt$OnboardingAnalyticsEvent.Error$mapOf()</ID>
|
||||
<ID>UseEmptyCounterpart:OnboardingAnalyticsEvent.kt$OnboardingAnalyticsEvent.Onboarding$mapOf()</ID>
|
||||
<ID>UseEmptyCounterpart:OnboardingAnalyticsEvent.kt$OnboardingAnalyticsEvent.SeedPhrase$mapOf()</ID>
|
||||
<ID>UseEmptyCounterpart:TechAnalyticsEvent.kt$TechAnalyticsEvent$mapOf()</ID>
|
||||
</CurrentIssues>
|
||||
</SmellBaseline>
|
||||
|
|
@ -11,11 +11,6 @@ sealed class AnalyticsParam {
|
|||
companion object
|
||||
}
|
||||
|
||||
sealed class TokenBalanceState(val value: String) {
|
||||
data object Empty : TokenBalanceState("Empty")
|
||||
data object Full : TokenBalanceState("Full")
|
||||
}
|
||||
|
||||
sealed class RateApp(val value: String) {
|
||||
data object Liked : RateApp("Liked")
|
||||
data object Disliked : RateApp("Disliked")
|
||||
|
|
@ -83,12 +78,14 @@ sealed class AnalyticsParam {
|
|||
data object Onboarding : ScreensSources("Onboarding")
|
||||
data object LongTap : ScreensSources("Long Tap")
|
||||
data object Markets : ScreensSources("Markets")
|
||||
data object HotWallet : ScreensSources("Hot Wallet")
|
||||
data object TangemPay : ScreensSources("Tangem Pay")
|
||||
data object WalletSettings : ScreensSources("Wallet Settings")
|
||||
data object Upgrade : ScreensSources("Upgrade")
|
||||
data object HardwareWallet : ScreensSources("Hardware Wallet")
|
||||
data object ImportWallet : ScreensSources("Import Wallet")
|
||||
data object CreateWalletIntro : ScreensSources("Create Wallet Intro")
|
||||
data object AddNewWallet : ScreensSources("Add New Wallet")
|
||||
data object CreateWallet : ScreensSources("Create Wallet")
|
||||
}
|
||||
|
||||
sealed class TxSentFrom(val value: String) {
|
||||
|
|
@ -203,8 +200,9 @@ sealed class AnalyticsParam {
|
|||
Pending(value = "Pending"),
|
||||
}
|
||||
|
||||
enum class EnsStatus(val value: String) {
|
||||
EMPTY("Empty"), FULL("Full")
|
||||
enum class EmptyFull(val value: String) {
|
||||
Empty("Empty"),
|
||||
Full("Full"),
|
||||
}
|
||||
|
||||
enum class ProductType(val value: String) {
|
||||
|
|
@ -268,5 +266,6 @@ sealed class AnalyticsParam {
|
|||
const val CHOSEN_TOKEN = "Token Chosen"
|
||||
const val ENS = "ENS"
|
||||
const val ENS_ADDRESS = "ENS Address"
|
||||
const val ACCOUNT_DERIVATION_FROM = "Account Derivation (from)"
|
||||
}
|
||||
}
|
||||
|
|
@ -10,11 +10,11 @@ sealed class Basic(
|
|||
) : Basic(
|
||||
event = "Card Was Scanned",
|
||||
params = mapOf(
|
||||
AnalyticsParam.SOURCE to source.value,
|
||||
AnalyticsParam.Key.SOURCE to source.value,
|
||||
),
|
||||
)
|
||||
|
||||
class SignedIn(
|
||||
class SignedInLegacy(
|
||||
currency: AnalyticsParam.WalletType,
|
||||
batch: String,
|
||||
signInType: SignInType,
|
||||
|
|
@ -24,8 +24,8 @@ sealed class Basic(
|
|||
) : Basic(
|
||||
event = "Signed in",
|
||||
params = buildMap {
|
||||
put(AnalyticsParam.CURRENCY, currency.value)
|
||||
put(AnalyticsParam.BATCH, batch)
|
||||
put(AnalyticsParam.Key.CURRENCY, currency.value)
|
||||
put(AnalyticsParam.Key.BATCH, batch)
|
||||
put("Wallet Type", if (isImported) "Seed Phrase" else "Seedless")
|
||||
put("Sign in type", signInType.name)
|
||||
put("Wallets Count", walletsCount)
|
||||
|
|
@ -39,10 +39,37 @@ sealed class Basic(
|
|||
}
|
||||
}
|
||||
|
||||
class SignedIn(
|
||||
signInType: SignInType,
|
||||
walletsCount: Int,
|
||||
) : Basic(
|
||||
event = "Signed in",
|
||||
params = buildMap {
|
||||
put("Sign in type", signInType.value)
|
||||
put("Wallets Count", walletsCount.toString())
|
||||
},
|
||||
) {
|
||||
enum class SignInType(val value: String) {
|
||||
Card("Card"),
|
||||
Biometric("Biometric"),
|
||||
NoSecurity("No Security"),
|
||||
AccessCode("Access Code"),
|
||||
}
|
||||
}
|
||||
|
||||
class ButtonBuy(
|
||||
source: AnalyticsParam.ScreensSources,
|
||||
) : Basic(
|
||||
event = "Button - Buy",
|
||||
params = buildMap {
|
||||
put(AnalyticsParam.Key.SOURCE, source.value)
|
||||
},
|
||||
)
|
||||
|
||||
class ToppedUp(userWalletId: String, currency: AnalyticsParam.WalletType) :
|
||||
Basic(
|
||||
event = "Topped up",
|
||||
params = mapOf(AnalyticsParam.CURRENCY to currency.value),
|
||||
params = mapOf(AnalyticsParam.Key.CURRENCY to currency.value),
|
||||
),
|
||||
OneTimeAnalyticsEvent {
|
||||
|
||||
|
|
@ -53,16 +80,16 @@ sealed class Basic(
|
|||
Basic(
|
||||
event = "Transaction sent",
|
||||
params = buildMap {
|
||||
this[AnalyticsParam.SOURCE] = sentFrom.value
|
||||
this[AnalyticsParam.Key.SOURCE] = sentFrom.value
|
||||
if (sentFrom is AnalyticsParam.TxData) {
|
||||
this[AnalyticsParam.BLOCKCHAIN] = sentFrom.blockchain
|
||||
this[AnalyticsParam.TOKEN_PARAM] = sentFrom.token
|
||||
this[AnalyticsParam.Key.BLOCKCHAIN] = sentFrom.blockchain
|
||||
this[AnalyticsParam.Key.TOKEN_PARAM] = sentFrom.token
|
||||
sentFrom.feeType?.value?.let {
|
||||
this[AnalyticsParam.FEE_TYPE] = it
|
||||
this[AnalyticsParam.Key.FEE_TYPE] = it
|
||||
}
|
||||
}
|
||||
if (sentFrom is AnalyticsParam.TxSentFrom.Approve) {
|
||||
this[AnalyticsParam.PERMISSION_TYPE] = sentFrom.permissionType
|
||||
this[AnalyticsParam.Key.PERMISSION_TYPE] = sentFrom.permissionType
|
||||
}
|
||||
this["Memo"] = memoType.name
|
||||
},
|
||||
|
|
@ -79,7 +106,7 @@ sealed class Basic(
|
|||
class ButtonSupport(source: AnalyticsParam.ScreensSources) : Basic(
|
||||
event = "Request Support",
|
||||
params = mapOf(
|
||||
AnalyticsParam.SOURCE to source.value,
|
||||
AnalyticsParam.Key.SOURCE to source.value,
|
||||
),
|
||||
)
|
||||
|
||||
|
|
@ -89,7 +116,7 @@ sealed class Basic(
|
|||
) : Basic(
|
||||
event = "Biometry Failed",
|
||||
params = mapOf(
|
||||
AnalyticsParam.SOURCE to source.value,
|
||||
AnalyticsParam.Key.SOURCE to source.value,
|
||||
"Reason" to reason.value,
|
||||
),
|
||||
) {
|
||||
|
|
|
|||
|
|
@ -36,26 +36,34 @@ sealed class MainScreenAnalyticsEvent(
|
|||
},
|
||||
)
|
||||
|
||||
data object ButtonReceive : MainScreenAnalyticsEvent(
|
||||
class ButtonReceive : MainScreenAnalyticsEvent(
|
||||
event = "Button - Receive",
|
||||
)
|
||||
|
||||
data object LimitsClicked : MainScreenAnalyticsEvent(
|
||||
class LimitsClicked : MainScreenAnalyticsEvent(
|
||||
event = "Limits Clicked",
|
||||
)
|
||||
|
||||
data object NoticeBalancesInfo : MainScreenAnalyticsEvent(
|
||||
class NoticeBalancesInfo : MainScreenAnalyticsEvent(
|
||||
event = "Notice - Balances Info",
|
||||
)
|
||||
|
||||
data object NoticeLimitsInfo : MainScreenAnalyticsEvent(
|
||||
class NoticeLimitsInfo : MainScreenAnalyticsEvent(
|
||||
event = "Notice - Limits Info",
|
||||
)
|
||||
|
||||
data object ButtonExplore : MainScreenAnalyticsEvent(
|
||||
class ButtonExplore : MainScreenAnalyticsEvent(
|
||||
event = "Button - Explore",
|
||||
)
|
||||
|
||||
class AccountShowTokens : MainScreenAnalyticsEvent(
|
||||
event = "Button - Account Show Tokens",
|
||||
)
|
||||
|
||||
class AccountHideTokens : MainScreenAnalyticsEvent(
|
||||
event = "Button - Account Hide Tokens",
|
||||
)
|
||||
|
||||
data class ButtonSwap(val status: AnalyticsParam.Status) : MainScreenAnalyticsEvent(
|
||||
event = "Button - Swap",
|
||||
params = mapOf(AnalyticsParam.STATUS to status.value),
|
||||
|
|
@ -66,11 +74,11 @@ sealed class MainScreenAnalyticsEvent(
|
|||
params = mapOf(AnalyticsParam.STATUS to status.value),
|
||||
)
|
||||
|
||||
data object BuyScreenOpened : MainScreenAnalyticsEvent(event = "Buy Screen Opened")
|
||||
class BuyScreenOpened : MainScreenAnalyticsEvent(event = "Buy Screen Opened")
|
||||
|
||||
data object SwapScreenOpened : MainScreenAnalyticsEvent(event = "Swap Screen Opened")
|
||||
class SwapScreenOpened : MainScreenAnalyticsEvent(event = "Swap Screen Opened")
|
||||
|
||||
data object SellScreenOpened : MainScreenAnalyticsEvent(event = "Sell Screen Opened")
|
||||
class SellScreenOpened : MainScreenAnalyticsEvent(event = "Sell Screen Opened")
|
||||
|
||||
data class BuyTokenClicked(val currencySymbol: String) : MainScreenAnalyticsEvent(
|
||||
event = "Buy Token Clicked",
|
||||
|
|
|
|||
|
|
@ -12,11 +12,92 @@ sealed class OnboardingAnalyticsEvent(
|
|||
sealed class Onboarding(
|
||||
event: String,
|
||||
params: Map<String, String> = mapOf(),
|
||||
) : OnboardingAnalyticsEvent(category = "Onboarding", event = event, params = params) {
|
||||
|
||||
class Started(
|
||||
source: String,
|
||||
) : Onboarding(
|
||||
event = "Onboarding Started",
|
||||
params = mapOf(
|
||||
AnalyticsParam.SOURCE to source,
|
||||
),
|
||||
)
|
||||
|
||||
class Finished(
|
||||
source: String,
|
||||
) : Onboarding(
|
||||
event = "Onboarding Finished",
|
||||
params = mapOf(
|
||||
AnalyticsParam.SOURCE to source,
|
||||
),
|
||||
)
|
||||
|
||||
class ButtonMobileWallet(
|
||||
source: String,
|
||||
) : Onboarding(
|
||||
event = "Button - Mobile Wallet",
|
||||
params = mapOf(
|
||||
AnalyticsParam.SOURCE to source,
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
sealed class CreateWallet(
|
||||
event: String,
|
||||
params: Map<String, String> = mapOf(),
|
||||
) : OnboardingAnalyticsEvent(category = "Onboarding / Create Wallet", event = event, params = params) {
|
||||
|
||||
class ButtonCreateWallet : CreateWallet("Button - Create Wallet")
|
||||
|
||||
class WalletCreatedSuccessfully(
|
||||
source: String,
|
||||
creationType: WalletCreationType = WalletCreationType.NewSeed,
|
||||
seedPhraseLength: Int? = null,
|
||||
passPhraseState: AnalyticsParam.EmptyFull,
|
||||
) : CreateWallet(
|
||||
event = "Wallet Created Successfully",
|
||||
params = buildMap {
|
||||
put(AnalyticsParam.SOURCE, source)
|
||||
put("Creation Type", creationType.value)
|
||||
put("Passphrase", passPhraseState.value)
|
||||
if (seedPhraseLength != null) {
|
||||
put("Seed Phrase Length", seedPhraseLength.toString())
|
||||
}
|
||||
},
|
||||
)
|
||||
|
||||
sealed class WalletCreationType(val value: String) {
|
||||
data object NewSeed : WalletCreationType(value = "New Seed")
|
||||
data object SeedImport : WalletCreationType(value = "Seed Import")
|
||||
}
|
||||
}
|
||||
|
||||
sealed class SeedPhrase(
|
||||
event: String,
|
||||
params: Map<String, String> = mapOf(),
|
||||
) : OnboardingAnalyticsEvent(category = "Onboarding / Seed Phrase", event = event, params = params) {
|
||||
|
||||
class CreateMobileScreenOpened(
|
||||
source: String,
|
||||
) : SeedPhrase(
|
||||
event = "Create Mobile Screen Opened",
|
||||
params = mapOf(
|
||||
AnalyticsParam.SOURCE to source,
|
||||
),
|
||||
)
|
||||
class ButtonImportWallet : SeedPhrase("Button - Import Wallet")
|
||||
class ImportSeedPhraseScreenOpened : SeedPhrase("Import Seed Phrase Screen Opened")
|
||||
class ButtonImport : SeedPhrase("Button - Import")
|
||||
}
|
||||
|
||||
sealed class Error(
|
||||
event: String,
|
||||
params: Map<String, String> = mapOf(),
|
||||
) : OnboardingAnalyticsEvent(category = "Error", event = event, params = params) {
|
||||
|
||||
data class OfflineAttestationFailed(
|
||||
val source: AnalyticsParam.ScreensSources,
|
||||
) : Onboarding(
|
||||
) : Error(
|
||||
event = "Offline Attestation Failed",
|
||||
params = mapOf(AnalyticsParam.SOURCE to source.value),
|
||||
)
|
||||
|
|
|
|||
|
|
@ -0,0 +1,52 @@
|
|||
package com.tangem.core.analytics.models.event
|
||||
|
||||
import com.tangem.core.analytics.models.AnalyticsEvent
|
||||
import com.tangem.core.analytics.models.AnalyticsParam
|
||||
|
||||
sealed class SignIn(
|
||||
event: String,
|
||||
params: Map<String, String> = emptyMap(),
|
||||
) : AnalyticsEvent("Sign In", event, params) {
|
||||
|
||||
data class ScreenOpened(
|
||||
val walletsCount: Int,
|
||||
) : SignIn(
|
||||
event = "Sign In Screen Opened",
|
||||
params = mapOf(
|
||||
"Wallets Count" to walletsCount.toString(),
|
||||
),
|
||||
)
|
||||
|
||||
class ButtonBiometricSignIn : SignIn(event = "Button - Biometric Sign In")
|
||||
|
||||
class ButtonUnlockAllWithBiometric : SignIn(event = "Button - Unlock All With Biometric")
|
||||
|
||||
class ErrorBiometricUpdated : SignIn(event = "Error - Biometric Updated")
|
||||
|
||||
class ButtonWallet(
|
||||
signInType: SignInType,
|
||||
walletsCount: Int,
|
||||
) : SignIn(
|
||||
event = "Button - Wallet",
|
||||
params = buildMap {
|
||||
put("Wallets Count", walletsCount.toString())
|
||||
put("Sign in type", signInType.value)
|
||||
},
|
||||
) {
|
||||
enum class SignInType(val value: String) {
|
||||
Card("Card"),
|
||||
Biometric("Biometric"),
|
||||
NoSecurity("No Security"),
|
||||
AccessCode("Access Code"),
|
||||
}
|
||||
}
|
||||
|
||||
data class ButtonAddWallet(
|
||||
val sources: AnalyticsParam.ScreensSources,
|
||||
) : SignIn(
|
||||
event = "Button - Add Wallet",
|
||||
params = mapOf(
|
||||
AnalyticsParam.SOURCE to sources.value,
|
||||
),
|
||||
)
|
||||
}
|
||||
|
|
@ -33,7 +33,7 @@
|
|||
},
|
||||
{
|
||||
"name": "HOT_WALLET_ENABLED",
|
||||
"version": "undefined"
|
||||
"version": "5.32.0"
|
||||
},
|
||||
{
|
||||
"name": "TANGEM_PAY_ENABLED",
|
||||
|
|
|
|||
|
|
@ -22,6 +22,9 @@ enum class ApiEnvironment {
|
|||
@Json(name = "STAGE")
|
||||
STAGE,
|
||||
|
||||
@Json(name = "STAGE_2")
|
||||
STAGE_2,
|
||||
|
||||
@Json(name = "MOCK")
|
||||
MOCK,
|
||||
|
||||
|
|
|
|||
|
|
@ -30,6 +30,7 @@ internal class Express(
|
|||
createDev2Environment(),
|
||||
createDev3Environment(),
|
||||
createStageEnvironment(),
|
||||
createStage2Environment(),
|
||||
createMockedEnvironment(),
|
||||
createProdEnvironment(),
|
||||
)
|
||||
|
|
@ -73,6 +74,12 @@ internal class Express(
|
|||
headers = createHeaders(isProd = false),
|
||||
)
|
||||
|
||||
private fun createStage2Environment(): ApiEnvironmentConfig = ApiEnvironmentConfig(
|
||||
environment = ApiEnvironment.STAGE_2,
|
||||
baseUrl = "[REDACTED_ENV_URL]",
|
||||
headers = createHeaders(isProd = false),
|
||||
)
|
||||
|
||||
private fun createMockedEnvironment(): ApiEnvironmentConfig = ApiEnvironmentConfig(
|
||||
environment = ApiEnvironment.MOCK,
|
||||
baseUrl = "[REDACTED_ENV_URL]",
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
package com.tangem.datasource.api.common.config
|
||||
|
||||
import com.tangem.datasource.BuildConfig
|
||||
import com.tangem.domain.staking.model.ethpool.P2PStakingConfig
|
||||
import com.tangem.lib.auth.P2PEthPoolAuthProvider
|
||||
import com.tangem.utils.ProviderSuspend
|
||||
|
||||
|
|
@ -22,12 +23,7 @@ internal class P2PEthPool(
|
|||
private fun getInitialEnvironment(): ApiEnvironment {
|
||||
return when (BuildConfig.BUILD_TYPE) {
|
||||
MOCKED_BUILD_TYPE -> ApiEnvironment.MOCK
|
||||
DEBUG_BUILD_TYPE,
|
||||
INTERNAL_BUILD_TYPE,
|
||||
EXTERNAL_BUILD_TYPE,
|
||||
RELEASE_BUILD_TYPE,
|
||||
-> ApiEnvironment.PROD
|
||||
else -> error("Unknown build type [${BuildConfig.BUILD_TYPE}]")
|
||||
else -> if (P2PStakingConfig.USE_TESTNET) ApiEnvironment.DEV else ApiEnvironment.PROD
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -77,6 +77,7 @@ internal class YieldSupply(
|
|||
ApiEnvironment.DEV_2,
|
||||
ApiEnvironment.DEV_3,
|
||||
ApiEnvironment.STAGE,
|
||||
ApiEnvironment.STAGE_2,
|
||||
-> environmentConfigStorage.getConfigSync().yieldModuleApiKeyDev
|
||||
ApiEnvironment.PROD -> environmentConfigStorage.getConfigSync().yieldModuleApiKey
|
||||
} ?: error("No tangem tech api config provided")
|
||||
|
|
|
|||
|
|
@ -23,9 +23,7 @@ interface P2PEthPoolApi {
|
|||
* @param network Ethereum pool network: "mainnet" or "hoodi" (testnet)
|
||||
*/
|
||||
@GET("api/v1/staking/pool/{network}/vaults")
|
||||
suspend fun getVaults(
|
||||
@Path("network") network: String = "mainnet",
|
||||
): ApiResponse<P2PEthPoolResponse<P2PEthPoolVaultsResponse>>
|
||||
suspend fun getVaults(@Path("network") network: String): ApiResponse<P2PEthPoolResponse<P2PEthPoolVaultsResponse>>
|
||||
|
||||
/**
|
||||
* Prepare deposit transaction
|
||||
|
|
|
|||
|
|
@ -38,6 +38,6 @@ interface NewsApi {
|
|||
|
||||
private companion object {
|
||||
|
||||
private const val NEWS_PATH = "api/v1/news"
|
||||
private const val NEWS_PATH = "v1/news"
|
||||
}
|
||||
}
|
||||
|
|
@ -5,11 +5,5 @@ import com.squareup.moshi.JsonClass
|
|||
|
||||
@JsonClass(generateAdapter = true)
|
||||
data class NewsTrendingResponse(
|
||||
@Json(name = "meta") val meta: NewsTrendingMetaDto,
|
||||
@Json(name = "items") val items: List<NewsArticleDto>,
|
||||
)
|
||||
|
||||
@JsonClass(generateAdapter = true)
|
||||
data class NewsTrendingMetaDto(
|
||||
@Json(name = "limit") val limit: Int,
|
||||
)
|
||||
|
|
@ -50,6 +50,12 @@ interface TangemTechApi {
|
|||
@Body userTokens: UserTokensResponse,
|
||||
): ApiResponse<Unit>
|
||||
|
||||
@PUT("/v1/wallets/{walletId}/tokens")
|
||||
suspend fun saveTokens(
|
||||
@Path(value = "walletId") userId: String,
|
||||
@Body userTokens: UserTokensResponse,
|
||||
): ApiResponse<Unit>
|
||||
|
||||
/** Returns referral status by [walletId] */
|
||||
@GET("v1/referral/{walletId}")
|
||||
suspend fun getReferralStatus(@Path("walletId") walletId: String): ApiResponse<ReferralResponse>
|
||||
|
|
@ -129,6 +135,12 @@ interface TangemTechApi {
|
|||
@Body body: List<WalletIdBody>,
|
||||
): ApiResponse<Unit>
|
||||
|
||||
@PUT("/v1/user-wallets/applications/{application_id}/wallets")
|
||||
suspend fun associateApplicationIdWithWalletsV2(
|
||||
@Path("application_id") applicationId: String,
|
||||
@Body body: AssociateApplicationIdWithWalletsBody,
|
||||
): ApiResponse<Unit>
|
||||
|
||||
@GET("v1/user-wallets/wallets/{wallet_id}")
|
||||
suspend fun getWalletById(@Path("wallet_id") walletId: String): ApiResponse<WalletResponse>
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,9 @@
|
|||
package com.tangem.datasource.api.tangemTech.models
|
||||
|
||||
import com.squareup.moshi.Json
|
||||
import com.squareup.moshi.JsonClass
|
||||
|
||||
@JsonClass(generateAdapter = true)
|
||||
data class AssociateAppWithWalletsErrorResponse(
|
||||
@Json(name = "missingWalletIds") val missingWalletIds: List<String>,
|
||||
)
|
||||
|
|
@ -0,0 +1,9 @@
|
|||
package com.tangem.datasource.api.tangemTech.models
|
||||
|
||||
import com.squareup.moshi.Json
|
||||
import com.squareup.moshi.JsonClass
|
||||
|
||||
@JsonClass(generateAdapter = true)
|
||||
data class AssociateApplicationIdWithWalletsBody(
|
||||
@Json(name = "walletIds") val walletIds: List<String>,
|
||||
)
|
||||
|
|
@ -19,6 +19,7 @@ data class GetWalletAccountsResponse(
|
|||
@Json(name = "group") val group: GroupType?,
|
||||
@Json(name = "sort") val sort: SortType?,
|
||||
@Json(name = "totalAccounts") val totalAccounts: Int,
|
||||
@Json(name = "totalArchivedAccounts") val totalArchivedAccounts: Int,
|
||||
)
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -15,4 +15,7 @@ interface AppCurrencyResponseStore {
|
|||
|
||||
/** Get [CurrenciesResponse.Currency] synchronously or null */
|
||||
suspend fun getSyncOrNull(): CurrenciesResponse.Currency?
|
||||
|
||||
/** Store [CurrenciesResponse.Currency] */
|
||||
suspend fun store(currency: CurrenciesResponse.Currency)
|
||||
}
|
||||
|
|
@ -5,6 +5,7 @@ import com.tangem.datasource.local.preferences.AppPreferencesStore
|
|||
import com.tangem.datasource.local.preferences.PreferencesKeys
|
||||
import com.tangem.datasource.local.preferences.utils.getObject
|
||||
import com.tangem.datasource.local.preferences.utils.getObjectSyncOrNull
|
||||
import com.tangem.datasource.local.preferences.utils.storeObject
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
|
||||
/**
|
||||
|
|
@ -25,4 +26,11 @@ internal class DefaultAppCurrencyResponseStore(
|
|||
PreferencesKeys.SELECTED_APP_CURRENCY_KEY,
|
||||
)
|
||||
}
|
||||
|
||||
override suspend fun store(currency: CurrenciesResponse.Currency) {
|
||||
appPreferencesStore.storeObject(
|
||||
PreferencesKeys.SELECTED_APP_CURRENCY_KEY,
|
||||
currency,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -1,7 +1,10 @@
|
|||
package com.tangem.datasource.di
|
||||
|
||||
import com.tangem.datasource.local.datastore.RuntimeDataStore
|
||||
import com.tangem.datasource.local.datastore.RuntimeSharedStore
|
||||
import com.tangem.datasource.local.promo.DefaultPromoBannerStore
|
||||
import com.tangem.datasource.local.promo.DefaultPromoStoriesStore
|
||||
import com.tangem.datasource.local.promo.PromoBannerStore
|
||||
import com.tangem.datasource.local.promo.PromoStoriesStore
|
||||
import dagger.Module
|
||||
import dagger.Provides
|
||||
|
|
@ -18,4 +21,10 @@ object PromoStoreModule {
|
|||
fun providePromoStoriesStore(): PromoStoriesStore {
|
||||
return DefaultPromoStoriesStore(dataStore = RuntimeDataStore())
|
||||
}
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
fun providePromoBannerStore(): PromoBannerStore {
|
||||
return DefaultPromoBannerStore(dataStore = RuntimeSharedStore())
|
||||
}
|
||||
}
|
||||
|
|
@ -5,6 +5,7 @@ import androidx.datastore.core.DataStore
|
|||
import androidx.datastore.core.DataStoreFactory
|
||||
import androidx.datastore.dataStoreFile
|
||||
import com.squareup.moshi.Moshi
|
||||
import com.tangem.datasource.api.ethpool.models.response.P2PEthPoolAccountResponse
|
||||
import com.tangem.datasource.api.stakekit.models.response.model.YieldBalanceWrapperDTO
|
||||
import com.tangem.datasource.api.stakekit.models.response.model.YieldDTO
|
||||
import com.tangem.datasource.local.datastore.RuntimeDataStore
|
||||
|
|
@ -77,6 +78,24 @@ internal object StakingStoreModule {
|
|||
return DefaultStakingActionsStore(dataStore = RuntimeDataStore())
|
||||
}
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
fun provideP2PBalancesPersistenceStore(
|
||||
@NetworkMoshi moshi: Moshi,
|
||||
@ApplicationContext context: Context,
|
||||
dispatchers: CoroutineDispatcherProvider,
|
||||
): DataStore<Map<String, Set<P2PEthPoolAccountResponse>>> {
|
||||
return DataStoreFactory.create(
|
||||
serializer = MoshiDataStoreSerializer(
|
||||
moshi = moshi,
|
||||
types = mapWithStringKeyTypes(valueTypes = setTypes<P2PEthPoolAccountResponse>()),
|
||||
defaultValue = emptyMap(),
|
||||
),
|
||||
produceFile = { context.dataStoreFile(fileName = "p2p_balances") },
|
||||
scope = CoroutineScope(context = dispatchers.io + SupervisorJob()),
|
||||
)
|
||||
}
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
fun provideP2PEthPoolVaultsStore(
|
||||
|
|
|
|||
|
|
@ -0,0 +1,19 @@
|
|||
package com.tangem.datasource.local.promo
|
||||
|
||||
import com.tangem.datasource.api.promotion.models.PromoBannerResponse
|
||||
import com.tangem.datasource.local.datastore.RuntimeSharedStore
|
||||
|
||||
internal class DefaultPromoBannerStore(
|
||||
private val dataStore: RuntimeSharedStore<Map<String, PromoBannerResponse>>,
|
||||
) : PromoBannerStore {
|
||||
|
||||
override suspend fun getSyncOrNull(promoId: String): PromoBannerResponse? {
|
||||
return dataStore.getSyncOrNull()?.get(promoId)
|
||||
}
|
||||
|
||||
override suspend fun store(promoId: String, promoBanner: PromoBannerResponse) {
|
||||
dataStore.update(emptyMap()) { current ->
|
||||
current + (promoId to promoBanner)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,10 @@
|
|||
package com.tangem.datasource.local.promo
|
||||
|
||||
import com.tangem.datasource.api.promotion.models.PromoBannerResponse
|
||||
|
||||
interface PromoBannerStore {
|
||||
|
||||
suspend fun getSyncOrNull(promoId: String): PromoBannerResponse?
|
||||
|
||||
suspend fun store(promoId: String, promoBanner: PromoBannerResponse)
|
||||
}
|
||||
|
|
@ -4,28 +4,32 @@ import com.tangem.datasource.api.stakekit.models.response.model.BalanceDTO
|
|||
import com.tangem.datasource.api.stakekit.models.response.model.YieldBalanceWrapperDTO
|
||||
import com.tangem.domain.models.StatusSource
|
||||
import com.tangem.domain.models.staking.BalanceItem
|
||||
import com.tangem.domain.models.staking.StakingBalance
|
||||
import com.tangem.domain.models.staking.StakingID
|
||||
import com.tangem.domain.models.staking.YieldBalance
|
||||
import com.tangem.domain.models.staking.YieldBalanceItem
|
||||
import com.tangem.utils.converter.Converter
|
||||
import kotlinx.datetime.Instant
|
||||
|
||||
class YieldBalanceConverter(
|
||||
/**
|
||||
* Converts StakeKit DTO to [StakingBalance].
|
||||
* Returns [StakingBalance.Data.StakeKit] for non-empty balances, [StakingBalance.Empty] otherwise.
|
||||
*/
|
||||
class StakingBalanceConverter(
|
||||
private val source: StatusSource,
|
||||
) : Converter<YieldBalanceWrapperDTO, YieldBalance?> {
|
||||
) : Converter<YieldBalanceWrapperDTO, StakingBalance?> {
|
||||
|
||||
constructor(isCached: Boolean) : this(source = if (isCached) StatusSource.CACHE else StatusSource.ACTUAL)
|
||||
|
||||
override fun convert(value: YieldBalanceWrapperDTO): YieldBalance? {
|
||||
override fun convert(value: YieldBalanceWrapperDTO): StakingBalance? {
|
||||
val stakingId = StakingID(
|
||||
integrationId = value.integrationId ?: return null,
|
||||
address = value.addresses.address,
|
||||
)
|
||||
|
||||
return if (value.balances.isEmpty()) {
|
||||
YieldBalance.Empty(stakingId = stakingId, source = source)
|
||||
StakingBalance.Empty(stakingId = stakingId, source = source)
|
||||
} else {
|
||||
YieldBalance.Data(
|
||||
StakingBalance.Data.StakeKit(
|
||||
stakingId = stakingId,
|
||||
balance = YieldBalanceItem(
|
||||
items = value.balances
|
||||
|
|
@ -12,6 +12,7 @@ import com.tangem.datasource.api.common.config.ApiConfig.Companion.MOCKED_BUILD_
|
|||
import com.tangem.datasource.api.common.config.ApiConfig.Companion.RELEASE_BUILD_TYPE
|
||||
import com.tangem.datasource.api.common.config.managers.MockEnvironmentConfigStorage.Companion.BLOCK_AID_API_KEY
|
||||
import com.tangem.datasource.api.common.config.managers.MockEnvironmentConfigStorage.Companion.TANGEM_API_KEY
|
||||
import com.tangem.domain.staking.model.ethpool.P2PStakingConfig
|
||||
import com.tangem.lib.auth.ExpressAuthProvider
|
||||
import com.tangem.lib.auth.P2PEthPoolAuthProvider
|
||||
import com.tangem.lib.auth.StakeKitAuthProvider
|
||||
|
|
@ -299,11 +300,17 @@ internal class ProdApiConfigsManagerTest {
|
|||
}
|
||||
|
||||
private fun createP2PModel(): TestModel {
|
||||
val (environment, baseUrl) = if (P2PStakingConfig.USE_TESTNET) {
|
||||
ApiEnvironment.DEV to "https://api-test.p2p.org/"
|
||||
} else {
|
||||
ApiEnvironment.PROD to "https://api.p2p.org/"
|
||||
}
|
||||
|
||||
return TestModel(
|
||||
id = ApiConfig.ID.P2PEthPool,
|
||||
expected = ApiEnvironmentConfig(
|
||||
environment = ApiEnvironment.PROD,
|
||||
baseUrl = "https://api.p2p.org/",
|
||||
environment = environment,
|
||||
baseUrl = baseUrl,
|
||||
headers = mapOf(
|
||||
"Authorization" to ProviderSuspend { "Bearer $P2P_API_KEY" },
|
||||
"accept" to ProviderSuspend { "application/json" },
|
||||
|
|
|
|||
22
core/pagination/detekt-baseline-main.xml
Normal file
22
core/pagination/detekt-baseline-main.xml
Normal file
|
|
@ -0,0 +1,22 @@
|
|||
<?xml version="1.0" ?>
|
||||
<SmellBaseline>
|
||||
<ManuallySuppressedIssues/>
|
||||
<CurrentIssues>
|
||||
<ID>BooleanPropertyNaming:BatchAction.kt$BatchAction.UpdateBatches$val async: Boolean = false</ID>
|
||||
<ID>BooleanPropertyNaming:BatchFetchResult.kt$BatchFetchResult.Success$val empty: Boolean</ID>
|
||||
<ID>BooleanPropertyNaming:BatchFetchResult.kt$BatchFetchResult.Success$val last: Boolean</ID>
|
||||
<ID>BooleanPropertyNaming:BatchListSource.kt$DefaultBatchListSource$val started = job.start()</ID>
|
||||
<ID>MultilineLambdaItParameter:BatchListSource.kt$DefaultBatchListSource${ currentCoroutineContext().ensureActive() BatchFetchResult.Error(it) }</ID>
|
||||
<ID>MultilineLambdaItParameter:BatchListSource.kt$DefaultBatchListSource${ if (predicate(it.first)) { it.second.cancel() null } else { it } }</ID>
|
||||
<ID>MultilineLambdaItParameter:CursorBatchFetcher.kt$CursorBatchFetcher${ currentCoroutineContext().ensureActive() return BatchFetchResult.Error(it) }</ID>
|
||||
<ID>MultilineLambdaItParameter:LimitOffsetBatchFetcher.kt$LimitOffsetBatchFetcher${ currentCoroutineContext().ensureActive() BatchFetchResult.Error(it) }</ID>
|
||||
<ID>NamedArguments:BatchListSource.kt$DefaultBatchListSource(fetchDispatcher, context, generateNewKey, batchFetcher, null)</ID>
|
||||
<ID>NamedArguments:BatchListSource.kt$DefaultBatchListSource(fetchDispatcher, context, generateNewKey, batchFetcher, updateFetcher)</ID>
|
||||
<ID>NestedScopeFunctions:BatchListSource.kt$DefaultBatchListSource$also { currentCoroutineContext().ensureActive() }</ID>
|
||||
<ID>SuspendFunSwallowedCancellation:BatchListSource.kt$DefaultBatchListSource$runCatching</ID>
|
||||
<ID>SuspendFunSwallowedCancellation:CursorBatchFetcher.kt$CursorBatchFetcher$runCatching</ID>
|
||||
<ID>SuspendFunSwallowedCancellation:LimitOffsetBatchFetcher.kt$LimitOffsetBatchFetcher$runCatching</ID>
|
||||
<ID>UseEmptyCounterpart:BatchListSource.kt$DefaultBatchListSource$listOf()</ID>
|
||||
<ID>UseOrEmpty:BatchListSource.kt$DefaultBatchListSource$batch?.let { listOf(it) } ?: emptyList()</ID>
|
||||
</CurrentIssues>
|
||||
</SmellBaseline>
|
||||
|
|
@ -24,7 +24,6 @@
|
|||
<ID>NoNameShadowing:TextAnimatedCounter.kt$char</ID>
|
||||
<ID>PropertyUsedBeforeDeclaration:InputManager.kt$InputManager$_query</ID>
|
||||
<ID>ReusedModifierInstance:EllipsisText.kt$Text( text = layoutText, color = color, style = style, fontStyle = fontStyle, textDecoration = textDecoration, textAlign = textAlign, softWrap = softWrap, maxLines = 1, onTextLayout = { textLayoutResultState.value = it }, modifier = modifier, )</ID>
|
||||
<ID>ReusedModifierInstance:Label.kt$Row( verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.spacedBy(4.dp), modifier = modifier .padding(horizontal = 4.dp) .clip(TangemTheme.shapes.roundedCorners8) .background(color = backgroundColor) .then( if (state.onClick != null) { Modifier.clickable( interactionSource = remember { MutableInteractionSource() }, indication = ripple(), onClick = state.onClick, ) } else { Modifier }, ) .padding(horizontal = 8.dp, vertical = 4.dp), ) { Text( modifier = Modifier.weight(1.0f, fill = false), text = text.resolveReference(), style = TangemTheme.typography.caption1, color = textColor, ) AnimatedVisibility(state.icon != null) { val wrappedIcon = remember(this) { requireNotNull(state.icon) } Icon( imageVector = ImageVector.vectorResource(wrappedIcon), tint = iconColor, contentDescription = null, modifier = Modifier .size(16.dp) .clickable( interactionSource = remember { MutableInteractionSource() }, indication = ripple(bounded = false), onClick = { state.onIconClick?.invoke() }, ), ) } }</ID>
|
||||
<ID>ReusedModifierInstance:TangemRadioButton.kt$AnimatedVisibility( visible = isSelected, label = "Radio button animation", modifier = modifier .size(TangemTheme.dimens.size24), ) { Icon( painter = painterResource(id = R.drawable.ic_check_circle_24), contentDescription = null, tint = TangemTheme.colors.control.checked, ) }</ID>
|
||||
<ID>ReusedModifierInstance:TokenPrice.kt$Icon( modifier = modifier, painter = painterResource( id = when (animatedType) { PriceChangeType.UP -> R.drawable.ic_arrow_up_8 PriceChangeType.DOWN -> R.drawable.ic_arrow_down_8 PriceChangeType.NEUTRAL -> R.drawable.ic_elipse_8 }, ), tint = when (animatedType) { PriceChangeType.UP -> TangemTheme.colors.icon.accent PriceChangeType.DOWN -> TangemTheme.colors.icon.warning PriceChangeType.NEUTRAL -> TangemTheme.colors.icon.inactive }, contentDescription = null, )</ID>
|
||||
<ID>UnnecessaryEventHandlerParameter:PinTextField.kt$onValueChange: (String) -> Unit</ID>
|
||||
|
|
|
|||
|
|
@ -220,6 +220,8 @@ fun SecondaryButtonIconEnd(
|
|||
modifier: Modifier = Modifier,
|
||||
showProgress: Boolean = false,
|
||||
enabled: Boolean = true,
|
||||
size: TangemButtonSize = TangemButtonSize.Default,
|
||||
shape: Shape = size.toShape(),
|
||||
) {
|
||||
TangemButton(
|
||||
modifier = modifier,
|
||||
|
|
@ -230,6 +232,8 @@ fun SecondaryButtonIconEnd(
|
|||
enabled = enabled,
|
||||
showProgress = showProgress,
|
||||
textStyle = TangemTheme.typography.subtitle1,
|
||||
size = size,
|
||||
shape = shape,
|
||||
)
|
||||
}
|
||||
|
||||
|
|
@ -244,6 +248,8 @@ fun SecondaryButtonIconStart(
|
|||
modifier: Modifier = Modifier,
|
||||
showProgress: Boolean = false,
|
||||
enabled: Boolean = true,
|
||||
size: TangemButtonSize = TangemButtonSize.Default,
|
||||
shape: Shape = size.toShape(),
|
||||
) {
|
||||
TangemButton(
|
||||
modifier = modifier,
|
||||
|
|
@ -254,6 +260,8 @@ fun SecondaryButtonIconStart(
|
|||
enabled = enabled,
|
||||
showProgress = showProgress,
|
||||
textStyle = TangemTheme.typography.subtitle1,
|
||||
size = size,
|
||||
shape = shape,
|
||||
)
|
||||
}
|
||||
// endregion SecondaryButton
|
||||
|
|
|
|||
|
|
@ -0,0 +1,6 @@
|
|||
package com.tangem.core.ui.components.bottomsheets.state
|
||||
|
||||
enum class BottomSheetState {
|
||||
EXPANDED,
|
||||
COLLAPSED,
|
||||
}
|
||||
|
|
@ -8,6 +8,7 @@ import androidx.compose.foundation.background
|
|||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.interaction.MutableInteractionSource
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.foundation.shape.CircleShape
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.material3.ripple
|
||||
|
|
@ -18,10 +19,16 @@ import androidx.compose.ui.Alignment
|
|||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.clip
|
||||
import androidx.compose.ui.graphics.vector.ImageVector
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.res.vectorResource
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.unit.dp
|
||||
import coil.compose.SubcomposeAsyncImage
|
||||
import coil.request.ImageRequest
|
||||
import com.tangem.core.ui.R
|
||||
import com.tangem.core.ui.components.CircleShimmer
|
||||
import com.tangem.core.ui.components.label.entity.LabelLeadingContentUM
|
||||
import com.tangem.core.ui.components.label.entity.LabelSize
|
||||
import com.tangem.core.ui.components.label.entity.LabelStyle
|
||||
import com.tangem.core.ui.components.label.entity.LabelUM
|
||||
import com.tangem.core.ui.extensions.TextReference
|
||||
|
|
@ -37,6 +44,7 @@ import com.tangem.core.ui.res.TangemThemePreview
|
|||
*
|
||||
* @see <a href="https://www.figma.com/design/14ISV23YB1yVW1uNVwqrKv/Android?node-id=4480-1459&t=2QTpi1G7FeTexTFS-4">Figma</a>
|
||||
*/
|
||||
@Suppress("LongMethod", "CyclomaticComplexMethod")
|
||||
@Composable
|
||||
fun Label(state: LabelUM, modifier: Modifier = Modifier) {
|
||||
val backgroundColor by animateColorAsState(
|
||||
|
|
@ -63,12 +71,28 @@ fun Label(state: LabelUM, modifier: Modifier = Modifier) {
|
|||
},
|
||||
)
|
||||
|
||||
AnimatedContent(targetState = state.text) { text ->
|
||||
val horizontalArrangementSize = remember {
|
||||
when (state.size) {
|
||||
LabelSize.REGULAR -> 4.dp
|
||||
LabelSize.BIG -> 8.dp
|
||||
}
|
||||
}
|
||||
|
||||
val paddings = remember {
|
||||
when (state.size) {
|
||||
LabelSize.REGULAR -> PaddingValues(horizontal = 8.dp, vertical = 4.dp)
|
||||
LabelSize.BIG -> PaddingValues(horizontal = 16.dp, vertical = 8.dp)
|
||||
}
|
||||
}
|
||||
|
||||
AnimatedContent(
|
||||
modifier = modifier,
|
||||
targetState = state.text,
|
||||
) { text ->
|
||||
Row(
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
horizontalArrangement = Arrangement.spacedBy(4.dp),
|
||||
modifier = modifier
|
||||
.padding(horizontal = 4.dp)
|
||||
horizontalArrangement = Arrangement.spacedBy(horizontalArrangementSize),
|
||||
modifier = Modifier
|
||||
.clip(TangemTheme.shapes.roundedCorners8)
|
||||
.background(color = backgroundColor)
|
||||
.then(
|
||||
|
|
@ -82,8 +106,34 @@ fun Label(state: LabelUM, modifier: Modifier = Modifier) {
|
|||
Modifier
|
||||
},
|
||||
)
|
||||
.padding(horizontal = 8.dp, vertical = 4.dp),
|
||||
.padding(paddings),
|
||||
) {
|
||||
state.leadingContent.let { leadingContentUM ->
|
||||
when (leadingContentUM) {
|
||||
is LabelLeadingContentUM.Token -> {
|
||||
SubcomposeAsyncImage(
|
||||
modifier = Modifier.size(16.dp),
|
||||
model = ImageRequest.Builder(context = LocalContext.current)
|
||||
.data(leadingContentUM.iconUrl)
|
||||
.crossfade(enable = true)
|
||||
.allowHardware(enable = false)
|
||||
.build(),
|
||||
loading = { CircleShimmer() },
|
||||
error = {
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.background(
|
||||
color = TangemTheme.colors.background.tertiary,
|
||||
shape = CircleShape,
|
||||
),
|
||||
)
|
||||
},
|
||||
contentDescription = null,
|
||||
)
|
||||
}
|
||||
LabelLeadingContentUM.None -> Unit
|
||||
}
|
||||
}
|
||||
Text(
|
||||
modifier = Modifier.weight(1.0f, fill = false),
|
||||
text = text.resolveReference(),
|
||||
|
|
@ -109,6 +159,8 @@ fun Label(state: LabelUM, modifier: Modifier = Modifier) {
|
|||
}
|
||||
}
|
||||
|
||||
@Suppress("LongMethod")
|
||||
@OptIn(ExperimentalLayoutApi::class)
|
||||
@Preview(showBackground = true)
|
||||
@Preview(showBackground = true, uiMode = Configuration.UI_MODE_NIGHT_YES)
|
||||
@Composable
|
||||
|
|
@ -118,47 +170,93 @@ private fun LabelPreview() {
|
|||
verticalArrangement = Arrangement.spacedBy(8.dp),
|
||||
modifier = Modifier.padding(16.dp),
|
||||
) {
|
||||
Label(
|
||||
state = LabelUM(
|
||||
text = TextReference.Str("Regular Label"),
|
||||
style = LabelStyle.REGULAR,
|
||||
),
|
||||
)
|
||||
Label(
|
||||
state = LabelUM(
|
||||
text = TextReference.Str("Accent Label"),
|
||||
style = LabelStyle.ACCENT,
|
||||
),
|
||||
)
|
||||
Label(
|
||||
state = LabelUM(
|
||||
text = TextReference.Str("Warning Label"),
|
||||
style = LabelStyle.WARNING,
|
||||
),
|
||||
)
|
||||
Label(
|
||||
state = LabelUM(
|
||||
text = TextReference.Str(
|
||||
"Regular long long long long long long long long long long long long Label",
|
||||
FlowRow(
|
||||
horizontalArrangement = Arrangement.spacedBy(8.dp),
|
||||
verticalArrangement = Arrangement.spacedBy(8.dp),
|
||||
) {
|
||||
Label(
|
||||
state = LabelUM(
|
||||
text = TextReference.Str("Regular Label"),
|
||||
style = LabelStyle.REGULAR,
|
||||
),
|
||||
style = LabelStyle.REGULAR,
|
||||
icon = R.drawable.ic_information_24,
|
||||
),
|
||||
)
|
||||
Label(
|
||||
state = LabelUM(
|
||||
text = TextReference.Str("Accent Label"),
|
||||
style = LabelStyle.ACCENT,
|
||||
icon = R.drawable.ic_information_24,
|
||||
),
|
||||
)
|
||||
Label(
|
||||
state = LabelUM(
|
||||
text = TextReference.Str("Warning Label"),
|
||||
style = LabelStyle.WARNING,
|
||||
icon = R.drawable.ic_information_24,
|
||||
),
|
||||
)
|
||||
)
|
||||
Label(
|
||||
state = LabelUM(
|
||||
leadingContent = LabelLeadingContentUM.Token(
|
||||
iconUrl = "https://s3.eu-central-1.amazonaws.com/tangem.api/coins/large/euro-coin.png",
|
||||
),
|
||||
text = TextReference.Str("Regular Label"),
|
||||
style = LabelStyle.REGULAR,
|
||||
),
|
||||
)
|
||||
Label(
|
||||
state = LabelUM(
|
||||
text = TextReference.Str("Accent Label"),
|
||||
style = LabelStyle.ACCENT,
|
||||
),
|
||||
)
|
||||
Label(
|
||||
state = LabelUM(
|
||||
text = TextReference.Str("Warning Label"),
|
||||
style = LabelStyle.WARNING,
|
||||
),
|
||||
)
|
||||
}
|
||||
FlowRow(
|
||||
horizontalArrangement = Arrangement.spacedBy(8.dp),
|
||||
verticalArrangement = Arrangement.spacedBy(8.dp),
|
||||
) {
|
||||
Label(
|
||||
state = LabelUM(
|
||||
text = TextReference.Str(
|
||||
"Regular long long long long long long long long long long long long Label",
|
||||
),
|
||||
style = LabelStyle.REGULAR,
|
||||
icon = R.drawable.ic_information_24,
|
||||
),
|
||||
)
|
||||
Label(
|
||||
state = LabelUM(
|
||||
text = TextReference.Str("Accent Label"),
|
||||
style = LabelStyle.ACCENT,
|
||||
icon = R.drawable.ic_information_24,
|
||||
),
|
||||
)
|
||||
Label(
|
||||
state = LabelUM(
|
||||
text = TextReference.Str("Warning Label"),
|
||||
style = LabelStyle.WARNING,
|
||||
icon = R.drawable.ic_information_24,
|
||||
),
|
||||
)
|
||||
}
|
||||
FlowRow(
|
||||
horizontalArrangement = Arrangement.spacedBy(8.dp),
|
||||
verticalArrangement = Arrangement.spacedBy(8.dp),
|
||||
) {
|
||||
Label(
|
||||
state = LabelUM(
|
||||
text = TextReference.Str("Regular Label"),
|
||||
style = LabelStyle.REGULAR,
|
||||
size = LabelSize.BIG,
|
||||
),
|
||||
)
|
||||
Label(
|
||||
state = LabelUM(
|
||||
text = TextReference.Str("Accent Label"),
|
||||
style = LabelStyle.ACCENT,
|
||||
size = LabelSize.BIG,
|
||||
icon = R.drawable.ic_information_24,
|
||||
),
|
||||
)
|
||||
Label(
|
||||
state = LabelUM(
|
||||
text = TextReference.Str("Warning Label"),
|
||||
style = LabelStyle.WARNING,
|
||||
size = LabelSize.BIG,
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,16 +1,29 @@
|
|||
package com.tangem.core.ui.components.label.entity
|
||||
|
||||
import androidx.annotation.DrawableRes
|
||||
import androidx.compose.runtime.Immutable
|
||||
import com.tangem.core.ui.extensions.TextReference
|
||||
|
||||
data class LabelUM(
|
||||
val text: TextReference,
|
||||
val style: LabelStyle,
|
||||
val style: LabelStyle = LabelStyle.REGULAR,
|
||||
val size: LabelSize = LabelSize.REGULAR,
|
||||
val leadingContent: LabelLeadingContentUM = LabelLeadingContentUM.None,
|
||||
@DrawableRes val icon: Int? = null,
|
||||
val onIconClick: (() -> Unit)? = null,
|
||||
val onClick: (() -> Unit)? = null,
|
||||
)
|
||||
|
||||
@Immutable
|
||||
sealed class LabelLeadingContentUM {
|
||||
data object None : LabelLeadingContentUM()
|
||||
data class Token(val iconUrl: String) : LabelLeadingContentUM()
|
||||
}
|
||||
|
||||
enum class LabelStyle {
|
||||
REGULAR, ACCENT, WARNING,
|
||||
}
|
||||
|
||||
enum class LabelSize {
|
||||
REGULAR, BIG,
|
||||
}
|
||||
|
|
@ -0,0 +1,130 @@
|
|||
package com.tangem.core.ui.components.pager
|
||||
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.foundation.lazy.LazyListScope
|
||||
import androidx.compose.foundation.lazy.LazyRow
|
||||
import androidx.compose.foundation.lazy.rememberLazyListState
|
||||
import androidx.compose.foundation.pager.PagerState
|
||||
import androidx.compose.foundation.pager.rememberPagerState
|
||||
import androidx.compose.foundation.shape.CircleShape
|
||||
import androidx.compose.runtime.*
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.clip
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.graphics.Shape
|
||||
import androidx.compose.ui.platform.LocalDensity
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.unit.Dp
|
||||
import androidx.compose.ui.unit.dp
|
||||
import com.tangem.core.ui.res.TangemTheme
|
||||
import com.tangem.core.ui.res.TangemThemePreview
|
||||
|
||||
/**
|
||||
* Horizontal pager indicator
|
||||
*
|
||||
* @param pagerState state of pager
|
||||
* @param indicatorCount counter of visible indicator items
|
||||
*/
|
||||
@Composable
|
||||
fun PagerIndicator(pagerState: PagerState, modifier: Modifier = Modifier, indicatorCount: Int = 5) {
|
||||
val listState = rememberLazyListState()
|
||||
|
||||
val indicatorColor = TangemTheme.colors.control.key
|
||||
val overlayColor = TangemTheme.colors.overlay.secondary
|
||||
val indicatorSize = 8.dp
|
||||
val spacing = 4.dp
|
||||
|
||||
val totalWidth: Dp = indicatorSize * indicatorCount + spacing * (indicatorCount - 1)
|
||||
val widthInPx = LocalDensity.current.run { indicatorSize.toPx() }
|
||||
|
||||
val currentItem by remember {
|
||||
derivedStateOf {
|
||||
pagerState.currentPage
|
||||
}
|
||||
}
|
||||
|
||||
val itemCount = pagerState.pageCount
|
||||
|
||||
LaunchedEffect(key1 = currentItem) {
|
||||
val viewportSize = listState.layoutInfo.viewportSize
|
||||
listState.animateScrollToItem(
|
||||
currentItem,
|
||||
(widthInPx / 2 - viewportSize.width / 2).toInt(),
|
||||
)
|
||||
}
|
||||
|
||||
Box(
|
||||
modifier = modifier
|
||||
.height(32.dp)
|
||||
.background(
|
||||
color = overlayColor,
|
||||
shape = CircleShape,
|
||||
)
|
||||
.padding(horizontal = 16.dp, vertical = 12.dp),
|
||||
contentAlignment = Alignment.Center,
|
||||
) {
|
||||
LazyRow(
|
||||
modifier = Modifier
|
||||
.width(totalWidth),
|
||||
state = listState,
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
horizontalArrangement = Arrangement.spacedBy(4.dp),
|
||||
userScrollEnabled = false,
|
||||
) {
|
||||
indicatorItems(
|
||||
itemCount = itemCount,
|
||||
currentItem = currentItem,
|
||||
indicatorShape = CircleShape,
|
||||
activeColor = indicatorColor,
|
||||
inActiveColor = indicatorColor.copy(alpha = 0.5f),
|
||||
indicatorSize = indicatorSize,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Suppress("LongParameterList")
|
||||
private fun LazyListScope.indicatorItems(
|
||||
itemCount: Int,
|
||||
currentItem: Int,
|
||||
indicatorShape: Shape,
|
||||
activeColor: Color,
|
||||
inActiveColor: Color,
|
||||
indicatorSize: Dp,
|
||||
) {
|
||||
items(itemCount) { index ->
|
||||
|
||||
val isSelected = index == currentItem
|
||||
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.clip(indicatorShape)
|
||||
.size(indicatorSize)
|
||||
.background(
|
||||
if (isSelected) activeColor else inActiveColor,
|
||||
indicatorShape,
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Preview(showBackground = true)
|
||||
@Composable
|
||||
private fun PagerIndicatorPreviewFirstPage() {
|
||||
TangemThemePreview {
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.background(TangemTheme.colors.background.primary)
|
||||
.padding(),
|
||||
contentAlignment = Alignment.Center,
|
||||
) {
|
||||
val pagerState = rememberPagerState(
|
||||
initialPage = 0,
|
||||
pageCount = { 10 },
|
||||
)
|
||||
PagerIndicator(pagerState = pagerState)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -246,7 +246,8 @@ data class EventMessageAction(
|
|||
*
|
||||
* @param onClick The action to perform when the button is clicked. By default, it dismisses the message.
|
||||
* */
|
||||
fun cancelAction(onClick: () -> Unit = onDismissRequest) = EventMessageAction(
|
||||
fun cancelAction(isWarning: Boolean = false, onClick: () -> Unit = onDismissRequest) = EventMessageAction(
|
||||
isWarning = isWarning,
|
||||
title = resourceReference(id = R.string.common_cancel),
|
||||
onClick = onClick,
|
||||
)
|
||||
|
|
|
|||
|
|
@ -39,6 +39,30 @@ object Dialogs {
|
|||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Hot wallet creation not supported dialog
|
||||
*
|
||||
* @param leastSupportedVersion least supported OS version name ex. "Android 10"
|
||||
* @param onDismiss lambda be invoked when dialog is dismissed
|
||||
*/
|
||||
fun hotWalletCreationNotSupportedDialog(leastSupportedVersion: String, onDismiss: () -> Unit = {}): DialogMessage {
|
||||
return DialogMessage(
|
||||
title = resourceReference(
|
||||
id = R.string.mobile_wallet_requires_min_os_warning_title,
|
||||
formatArgs = wrappedList(leastSupportedVersion),
|
||||
),
|
||||
message = resourceReference(
|
||||
id = R.string.mobile_wallet_requires_min_os_warning_body,
|
||||
formatArgs = wrappedList(leastSupportedVersion),
|
||||
),
|
||||
firstAction = EventMessageAction(
|
||||
title = resourceReference(R.string.common_got_it),
|
||||
onClick = {},
|
||||
),
|
||||
onDismissRequest = onDismiss,
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Universal error dialog
|
||||
*/
|
||||
|
|
|
|||
|
|
@ -3,4 +3,5 @@ package com.tangem.core.ui.test
|
|||
object DetailsScreenTestTags {
|
||||
const val SCREEN_CONTAINER = "DETAILS_SCREEN_CONTAINER"
|
||||
const val SCREEN_ITEM = "DETAILS_SCREEN_ITEM"
|
||||
const val VERSION_NAME = "DETAILS_SCREEN_VERSION_NAME"
|
||||
}
|
||||
|
|
@ -5,6 +5,7 @@ object SendScreenTestTags {
|
|||
|
||||
const val AMOUNT_CONTAINER_TITLE = "SEND_SCREEN_AMOUNT_CONTAINER_TITLE"
|
||||
const val INPUT_TEXT_FIELD = "SEND_SCREEN_INPUT_TEXT_FIELD"
|
||||
const val AMOUNT_ERROR_TEXT = "SEND_SCREEN_AMOUNT_ERROR_TEXT"
|
||||
const val EQUIVALENT_INPUT_AMOUNT = "SEND_SCREEN_EQUIVALENT_INPUT_AMOUNT"
|
||||
const val EXCHANGE_ICON = "SEND_SCREEN_EXCHANGE_ICON"
|
||||
const val TOKEN_NAME = "SEND_SCREEN_TOKEN_NAME"
|
||||
|
|
|
|||
|
|
@ -1,6 +1,10 @@
|
|||
package com.tangem.core.ui.utils
|
||||
|
||||
import android.text.format.DateFormat
|
||||
import com.tangem.core.ui.utils.DateTimeFormatters.dateDDMMYYYY
|
||||
import com.tangem.core.ui.utils.DateTimeFormatters.dateMMMdd
|
||||
import com.tangem.core.ui.utils.DateTimeFormatters.dateTimeFormatter
|
||||
import com.tangem.core.ui.utils.DateTimeFormatters.dateYYYY
|
||||
import org.joda.time.DateTime
|
||||
import org.joda.time.format.DateTimeFormat
|
||||
import org.joda.time.format.DateTimeFormatter
|
||||
|
|
@ -80,6 +84,13 @@ object DateTimeFormatters {
|
|||
getBestFormatterBySkeleton("yyyy")
|
||||
}
|
||||
|
||||
/**
|
||||
* Example: "June 31"
|
||||
*/
|
||||
val dateDMMM: DateTimeFormatter by lazy {
|
||||
getBestFormatterBySkeleton("d MMMM")
|
||||
}
|
||||
|
||||
/**
|
||||
* Example: "31.06.2020 12:00", "06/31/2020 12:00", "06/31/2020 12:00 PM"
|
||||
*/
|
||||
|
|
|
|||
12
core/ui/src/main/res/drawable/ic_explore_16.xml
Normal file
12
core/ui/src/main/res/drawable/ic_explore_16.xml
Normal file
|
|
@ -0,0 +1,12 @@
|
|||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="16dp"
|
||||
android:height="16dp"
|
||||
android:viewportWidth="16"
|
||||
android:viewportHeight="16">
|
||||
<path
|
||||
android:pathData="M2.667,8C2.667,5.055 5.055,2.667 8,2.667C10.946,2.667 13.333,5.055 13.333,8C13.333,10.946 10.946,13.333 8,13.333C5.055,13.333 2.667,10.946 2.667,8ZM1.333,8C1.333,11.682 4.318,14.667 8,14.667C11.682,14.667 14.667,11.682 14.667,8C14.667,4.318 11.682,1.334 8,1.334C4.318,1.334 1.333,4.318 1.333,8Z"
|
||||
android:fillColor="#919191"/>
|
||||
<path
|
||||
android:pathData="M5.032,11.297C4.984,11.287 4.94,11.263 4.906,11.229C4.871,11.194 4.847,11.15 4.837,11.102C4.827,11.054 4.832,11.005 4.85,10.959L6.315,7.297C6.416,7.045 6.567,6.816 6.758,6.625C6.95,6.433 7.178,6.282 7.429,6.182L11.092,4.717C11.137,4.699 11.188,4.695 11.235,4.704C11.283,4.714 11.328,4.738 11.362,4.772C11.396,4.807 11.42,4.851 11.43,4.899C11.44,4.947 11.435,4.997 11.417,5.042L9.952,8.704C9.851,8.956 9.7,9.185 9.509,9.376C9.317,9.568 9.089,9.718 8.838,9.819L5.175,11.284C5.13,11.302 5.08,11.306 5.032,11.297ZM8.28,8.736C8.425,8.707 8.559,8.636 8.664,8.531C8.768,8.426 8.84,8.292 8.869,8.147C8.898,8.001 8.884,7.85 8.827,7.713C8.77,7.576 8.673,7.46 8.55,7.377C8.427,7.295 8.282,7.251 8.134,7.251C7.935,7.251 7.744,7.33 7.603,7.47C7.463,7.611 7.384,7.802 7.384,8.001C7.384,8.149 7.427,8.294 7.51,8.417C7.592,8.54 7.709,8.637 7.846,8.694C7.983,8.75 8.134,8.765 8.28,8.736Z"
|
||||
android:fillColor="#919191"/>
|
||||
</vector>
|
||||
15
core/ui/src/main/res/drawable/ic_heart_20.xml
Normal file
15
core/ui/src/main/res/drawable/ic_heart_20.xml
Normal file
|
|
@ -0,0 +1,15 @@
|
|||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="20dp"
|
||||
android:height="20dp"
|
||||
android:viewportWidth="20"
|
||||
android:viewportHeight="20">
|
||||
<group>
|
||||
<clip-path
|
||||
android:pathData="M0,0h20v20h-20z"/>
|
||||
<path
|
||||
android:pathData="M13.75,1.75C16.73,1.75 19.083,4.093 19.084,7.083C19.084,8.9 18.257,10.495 16.961,12.081C15.673,13.657 13.843,15.315 11.713,17.247L10.505,18.347L10,18.806L9.495,18.347L8.288,17.247C6.157,15.315 4.327,13.657 3.039,12.081C1.742,10.495 0.917,8.9 0.917,7.083C0.917,4.093 3.27,1.75 6.25,1.75C7.646,1.75 8.986,2.29 10,3.174C11.013,2.29 12.353,1.75 13.75,1.75Z"
|
||||
android:strokeWidth="1.5"
|
||||
android:fillColor="#00000000"
|
||||
android:strokeColor="#1E1E1E"/>
|
||||
</group>
|
||||
</vector>
|
||||
15
core/ui/src/main/res/drawable/ic_quick_recap_16.xml
Normal file
15
core/ui/src/main/res/drawable/ic_quick_recap_16.xml
Normal file
|
|
@ -0,0 +1,15 @@
|
|||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="16dp"
|
||||
android:height="16dp"
|
||||
android:viewportWidth="16"
|
||||
android:viewportHeight="16">
|
||||
<path
|
||||
android:pathData="M1.873,2.461C2.205,2.461 2.473,2.73 2.473,3.061V8.464C2.473,9.9 3.638,11.064 5.073,11.064H11.855V10.146C11.855,9.831 12.203,9.639 12.469,9.809L14.858,11.326C15.105,11.483 15.105,11.844 14.858,12.002L12.469,13.519C12.203,13.688 11.855,13.497 11.855,13.181V12.264H5.073C2.975,12.264 1.273,10.562 1.273,8.464V3.061C1.273,2.73 1.542,2.461 1.873,2.461Z"
|
||||
android:fillColor="#0099FF"/>
|
||||
<path
|
||||
android:pathData="M9.473,6.413C9.805,6.413 10.073,6.682 10.073,7.013C10.073,7.345 9.805,7.613 9.473,7.613H5.673C5.342,7.613 5.074,7.345 5.073,7.013C5.073,6.682 5.342,6.413 5.673,6.413H9.473Z"
|
||||
android:fillColor="#0099FF"/>
|
||||
<path
|
||||
android:pathData="M13.273,2.696C13.605,2.696 13.873,2.964 13.873,3.296C13.873,3.627 13.605,3.896 13.273,3.896H5.673C5.342,3.896 5.074,3.627 5.073,3.296C5.073,2.964 5.342,2.696 5.673,2.696H13.273Z"
|
||||
android:fillColor="#0099FF"/>
|
||||
</vector>
|
||||
14
core/utils/detekt-baseline-main.xml
Normal file
14
core/utils/detekt-baseline-main.xml
Normal file
|
|
@ -0,0 +1,14 @@
|
|||
<?xml version="1.0" ?>
|
||||
<SmellBaseline>
|
||||
<ManuallySuppressedIssues/>
|
||||
<CurrentIssues>
|
||||
<ID>BooleanPropertyNaming:Retryer.kt$Retryer$val result = try { block(iteration) } catch (e: CancellationException) { throw e } catch (e: Error) { throw e } catch (_: Exception) { false }</ID>
|
||||
<ID>MultilineLambdaItParameter:Converter.kt$Converter${ try { convert(it) } catch (throwable: Throwable) { onError?.invoke(throwable) null } }</ID>
|
||||
<ID>MultilineLambdaItParameter:PeriodicTask.kt$PeriodicTask${ if (!isActive.get()) { return@onFailure } onError.invoke(it) }</ID>
|
||||
<ID>MultilineLambdaItParameter:PeriodicTask.kt$PeriodicTask${ if (!isActive.get()) { return@onSuccess } onSuccess.invoke(it) }</ID>
|
||||
<ID>NullableBooleanCheck:JobHolder.kt$JobHolder$job?.isActive ?: false</ID>
|
||||
<ID>PropertyUsedBeforeDeclaration:JobHolder.kt$JobHolder$job</ID>
|
||||
<ID>SuspendFunSwallowedCancellation:CoroutineExt.kt$runCatching</ID>
|
||||
<ID>VarCouldBeVal:PeriodicTask.kt$PeriodicTask$private var isActive: AtomicBoolean = AtomicBoolean(false)</ID>
|
||||
</CurrentIssues>
|
||||
</SmellBaseline>
|
||||
Loading…
Add table
Add a link
Reference in a new issue