Updated on 2026-08-14

This commit is contained in:
Tangem 2025-12-30 10:56:55 +03:00
commit cc3401a412
943 changed files with 15667 additions and 6451 deletions

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

View file

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

View file

@ -0,0 +1,15 @@
package com.tangem.core.analytics.models
/**
* Marker interface for AppsFlyer events
* Only events implementing this interface will be sent to AppsFlyer
*/
interface AppsFlyerOnlyEvent
/**
* Marker interface for AppsFlyer included events
* Events implementing this interface will be sent to AppsFlyer along with other analytics handlers
*/
interface AppsFlyerIncludedEvent {
val appsFlyerReplacedEvent: String
}

View file

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

View file

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

View file

@ -2,6 +2,8 @@ package com.tangem.core.analytics.models.event
import com.tangem.core.analytics.models.AnalyticsEvent
import com.tangem.core.analytics.models.AnalyticsParam
import com.tangem.core.analytics.models.AppsFlyerIncludedEvent
import com.tangem.core.analytics.models.AppsFlyerOnlyEvent
sealed class OnboardingAnalyticsEvent(
category: String,
@ -12,11 +14,100 @@ sealed class OnboardingAnalyticsEvent(
sealed class Onboarding(
event: String,
params: Map<String, String> = mapOf(),
) : OnboardingAnalyticsEvent(category = "Onboarding", event = event, params = params) {
class AppsFlyerOnlyEntryScreenView : Onboarding(event = "wallet_entry_screen_view"), AppsFlyerOnlyEvent
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())
}
},
), AppsFlyerIncludedEvent {
override val appsFlyerReplacedEvent = when (creationType) {
WalletCreationType.NewSeed -> "wallet_created_successfully"
WalletCreationType.SeedImport -> "wallet_imported"
}
}
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),
)

View file

@ -0,0 +1,54 @@
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")
data class ErrorBiometricUpdated(
val isFromUnlockAll: Boolean,
) : 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,
),
)
}

View file

@ -27,11 +27,7 @@ interface AnalyticsHandler : AnalyticsEventHandler {
fun id(): String
fun send(eventId: String, params: Map<String, String> = emptyMap())
override fun send(event: AnalyticsEvent) {
send(event.id, event.params)
}
override fun send(event: AnalyticsEvent)
}
interface AnalyticsHandlerHolder {

View file

@ -0,0 +1,23 @@
package com.tangem.core.analytics.filter
import com.tangem.core.analytics.api.AnalyticsEventFilter
import com.tangem.core.analytics.api.AnalyticsHandler
import com.tangem.core.analytics.models.AnalyticsEvent
import com.tangem.core.analytics.models.AppsFlyerIncludedEvent
import com.tangem.core.analytics.models.AppsFlyerOnlyEvent
class AppsFlyerEventFilter : AnalyticsEventFilter {
override fun canBeAppliedTo(event: AnalyticsEvent): Boolean =
event is AppsFlyerOnlyEvent || event is AppsFlyerIncludedEvent
override suspend fun canBeSent(event: AnalyticsEvent): Boolean = true
override fun canBeConsumedByHandler(handler: AnalyticsHandler, event: AnalyticsEvent): Boolean {
return when (event) {
is AppsFlyerOnlyEvent -> handler.id() == "AppsFlyer"
is AppsFlyerIncludedEvent -> true
else -> false
}
}
}

View file

@ -33,12 +33,16 @@
},
{
"name": "HOT_WALLET_ENABLED",
"version": "undefined"
"version": "5.32.0"
},
{
"name": "TANGEM_PAY_ENABLED",
"version": "5.31.0"
},
{
"name": "TANGEM_PAY_ENTRYPOINT_ENABLED",
"version": "undefined"
},
{
"name": "NEW_TOKEN_RECEIVE_ENABLED",
"version": "5.28.0"

View file

@ -22,6 +22,9 @@ enum class ApiEnvironment {
@Json(name = "STAGE")
STAGE,
@Json(name = "STAGE_2")
STAGE_2,
@Json(name = "MOCK")
MOCK,

View file

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

View file

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

View file

@ -31,7 +31,7 @@ internal class TangemPay(
private fun createDevEnvironment(): ApiEnvironmentConfig = ApiEnvironmentConfig(
environment = ApiEnvironment.DEV,
baseUrl = "https://api.dev.us.paera.com/bff/",
baseUrl = "https://api.dev.us.paera.com/bff-v2/",
headers = createHeaders(),
)
@ -43,7 +43,7 @@ internal class TangemPay(
private fun createProdEnvironment(): ApiEnvironmentConfig = ApiEnvironmentConfig(
environment = ApiEnvironment.PROD,
baseUrl = "https://api.us.paera.com/bff/",
baseUrl = "https://api.us.paera.com/bff-v2/",
headers = createHeaders(),
)

View file

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

View file

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

View file

@ -26,6 +26,7 @@ data class TokenMarketListResponse(
@Json(name = "market_cap") val marketCap: BigDecimal?,
@Json(name = "is_under_market_cap_limit") val isUnderMarketCapLimit: Boolean?,
@Json(name = "staking_opportunities") val stakingOpportunities: List<StakingOpportunities>?,
@Json(name = "max_yield_apy") val maxYieldApy: BigDecimal?,
) {
@JsonClass(generateAdapter = true)

View file

@ -38,6 +38,6 @@ interface NewsApi {
private companion object {
private const val NEWS_PATH = "api/v1/news"
private const val NEWS_PATH = "v1/news"
}
}

View file

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

View file

@ -3,109 +3,13 @@ package com.tangem.datasource.api.pay
import com.tangem.datasource.api.common.response.ApiResponse
import com.tangem.datasource.api.pay.models.request.*
import com.tangem.datasource.api.pay.models.response.*
import retrofit2.http.Body
import retrofit2.http.GET
import retrofit2.http.Header
import retrofit2.http.POST
import retrofit2.http.PUT
import retrofit2.http.Path
import retrofit2.http.Query
import retrofit2.http.*
private const val TX_HISTORY_PAGING_DEFAULT_LIMIT = 20
@Suppress("TooManyFunctions")
interface TangemPayApi {
// region: auth
@POST("v1/auth/challenge")
suspend fun generateNonceByCardId(@Body request: GenerateNoneByCardIdRequest): ApiResponse<GenerateNonceResponse>
@POST("v1/auth/challenge")
suspend fun generateNonceByCardWallet(
@Body request: GenerateNoneByCardWalletRequest,
): ApiResponse<GenerateNonceResponse>
@POST("v1/auth/token")
suspend fun getAccessTokenByCardId(@Body request: GetAccessTokenByCardIdRequest): ApiResponse<JWTResponse>
@POST("v1/auth/token")
suspend fun getTokenByCustomerWallet(@Body request: GetTokenByCustomerWalletRequest): ApiResponse<JWTResponse>
@POST("v1/auth/token/refresh")
suspend fun refreshCustomerWalletAccessToken(
@Body request: RefreshCustomerWalletAccessTokenRequest,
): ApiResponse<JWTResponse>
@POST("v1/auth/token")
suspend fun getAccessTokenByCardWallet(@Body request: GetAccessTokenByCardWalletRequest): ApiResponse<JWTResponse>
@POST("v1/auth/token/refresh")
suspend fun refreshCardIdAccessToken(@Body request: RefreshTokenByCardIdRequest): ApiResponse<JWTResponse>
@POST("v1/auth/token/refresh")
suspend fun refreshCardWalletAccessToken(@Body request: RefreshTokenByCardWalletRequest): ApiResponse<JWTResponse>
@POST("v1/auth/token/exchange")
suspend fun exchangeAccessToken(@Body request: ExchangeAccessTokenRequest): ApiResponse<JWTResponse>
// endregion
// region: activation
@POST("v1/activation/status")
suspend fun getRemoteActivationStatus(
@Header("Authorization") authHeader: String,
@Body request: ActivationStatusRequest,
): ApiResponse<CardActivationRemoteStateResponse>
@POST("v1/activation/acceptance/message")
suspend fun getCardWalletAcceptance(
@Header("Authorization") authHeader: String,
@Body request: GetCardWalletAcceptanceRequest,
): ApiResponse<VisaDataToSignResponse>
@POST("v1/activation/acceptance/message")
suspend fun getCustomerWalletAcceptance(
@Header("Authorization") authHeader: String,
@Body request: GetCustomerWalletAcceptanceRequest,
): ApiResponse<VisaDataToSignResponse>
@POST("v1/activation/data")
suspend fun activateByCardWallet(
@Header("Authorization") authHeader: String,
@Body body: ActivationByCardWalletRequest,
): ApiResponse<Unit>
@POST("v1/activation/data")
suspend fun activateByCustomerWallet(
@Header("Authorization") authHeader: String,
@Body body: ActivationByCustomerWalletRequest,
): ApiResponse<Unit>
@POST("v1/activation/pin")
suspend fun setPinCode(
@Header("Authorization") authHeader: String,
@Body body: SetPinCodeRequest,
): ApiResponse<Unit>
// endregion
@GET("customer/info")
suspend fun getCustomerInfo(
@Header("Authorization") authHeader: String,
@Query("card_id") cardId: String,
): ApiResponse<VisaCustomerInfo>
@GET("product_instance/transactions")
suspend fun getTxHistory(
@Header("Authorization") authHeader: String,
@Query("customer_id") customerId: String,
@Query("product_instance_id") productInstanceId: String,
@Query("limit") limit: Int,
@Query("offset") offset: Int,
): ApiResponse<VisaTxHistoryResponse>
@GET("v1/customer/transactions")
suspend fun getTangemPayTxHistory(
@Header("Authorization") authHeader: String,
@ -125,9 +29,18 @@ interface TangemPayApi {
@Path("customer_wallet_id") customerWalletId: String,
): ApiResponse<CheckCustomerWalletResponse>
@PATCH("v1/customer/pay-enabled")
suspend fun setTangemPayEnabledStatus(
@Header("Authorization") authHeader: String,
@Body body: SetTangemPayEnabledRequest,
): ApiResponse<Any>
@POST("v1/deeplink/validate")
suspend fun validateDeeplink(@Body body: DeeplinkValidityRequest): ApiResponse<DeeplinkValidityResponse>
@GET("v1/customer/eligibility")
suspend fun checkCustomerEligibility(): ApiResponse<CustomerEligibilityResponse>
@GET("v1/order/{order_id}")
suspend fun getOrder(
@Header("Authorization") authHeader: String,
@ -149,6 +62,12 @@ interface TangemPayApi {
@Body body: CardDetailsRequest,
): ApiResponse<CardDetailsResponse>
@GET("v1/customer/card/pin")
suspend fun getPin(
@Header("Authorization") authHeader: String,
@Header("X-Session-Id") sessionId: String,
): ApiResponse<GetPinResponse>
@PUT("v1/customer/card/pin")
suspend fun setPin(
@Header("Authorization") authHeader: String,

View file

@ -0,0 +1,14 @@
package com.tangem.datasource.api.pay.models.request
import com.squareup.moshi.Json
import com.squareup.moshi.JsonClass
@JsonClass(generateAdapter = true)
data class GetPinResponse(@Json(name = "result") val result: Result?) {
@JsonClass(generateAdapter = true)
data class Result(
@Json(name = "secret") val secret: String,
@Json(name = "iv") val iv: String,
)
}

View file

@ -0,0 +1,9 @@
package com.tangem.datasource.api.pay.models.request
import com.squareup.moshi.Json
import com.squareup.moshi.JsonClass
@JsonClass(generateAdapter = true)
data class SetTangemPayEnabledRequest(
@Json(name = "is_tangem_pay_enabled") val isTangemPayEnabled: Boolean,
)

View file

@ -4,5 +4,4 @@ import com.squareup.moshi.Json
data class CardBalanceResponse(
@Json(name = "result") val result: BalanceResponse?,
@Json(name = "error") val error: String?,
)

View file

@ -5,7 +5,6 @@ import com.squareup.moshi.JsonClass
data class CardDetailsResponse(
@Json(name = "result") val result: Result?,
@Json(name = "error") val error: String?,
) {
@JsonClass(generateAdapter = true)
data class Result(

View file

@ -5,5 +5,10 @@ import com.squareup.moshi.JsonClass
@JsonClass(generateAdapter = true)
data class CheckCustomerWalletResponse(
@Json(name = "id") val id: String?,
)
@Json(name = "result") val result: Result?,
) {
data class Result(
@Json(name = "id") val id: String?,
@Json(name = "is_tangem_pay_enabled") val isTangemPayEnabled: Boolean?,
)
}

View file

@ -0,0 +1,14 @@
package com.tangem.datasource.api.pay.models.response
import com.squareup.moshi.Json
import com.squareup.moshi.JsonClass
@JsonClass(generateAdapter = true)
data class CustomerEligibilityResponse(
@Json(name = "result") val result: Result?,
) {
@JsonClass(generateAdapter = true)
data class Result(
@Json(name = "is_tangem_pay_available") val isTangemPayAvailable: Boolean,
)
}

View file

@ -6,17 +6,16 @@ import com.squareup.moshi.JsonClass
@JsonClass(generateAdapter = true)
data class CustomerMeResponse(
@Json(name = "result") val result: Result?,
@Json(name = "error") val error: String?,
) {
@JsonClass(generateAdapter = true)
data class Result(
@Json(name = "id") val id: String,
@Json(name = "state") val state: String,
@Json(name = "createdAt") val createdAt: String,
@Json(name = "created_at") val createdAt: String,
@Json(name = "product_instance") val productInstance: ProductInstance?,
@Json(name = "payment_account") val paymentAccount: PaymentAccount?,
@Json(name = "kyc") val kyc: Kyc?,
@Json(name = "depositAddress") val depositAddress: String?,
@Json(name = "deposit_address") val depositAddress: String?,
@Json(name = "card") val card: Card?,
@Json(name = "balance") val balance: BalanceResponse?,
)
@ -24,49 +23,49 @@ data class CustomerMeResponse(
@JsonClass(generateAdapter = true)
data class ProductInstance(
@Json(name = "id") val id: String,
@Json(name = "cid") val cid: String,
@Json(name = "cid") val cid: String?,
@Json(name = "card_id") val cardId: String,
@Json(name = "card_wallet_address") val cardWalletAddress: String,
@Json(name = "card_wallet_address") val cardWalletAddress: String?,
@Json(name = "status") val status: Status,
@Json(name = "updated_at") val updatedAt: String,
@Json(name = "payment_account_id") val paymentAccountId: String,
) {
@JsonClass(generateAdapter = false)
enum class Status {
@Json(name = "new")
@Json(name = "NEW")
NEW,
@Json(name = "ready_for_manufacturing")
@Json(name = "READY_FOR_MANUFACTURING")
READY_FOR_MANUFACTURING,
@Json(name = "manufacturing")
@Json(name = "MANUFACTURING")
MANUFACTURING,
@Json(name = "sent_to_delivery")
@Json(name = "SENT_TO_DELIVERY")
SENT_TO_DELIVERY,
@Json(name = "delivered")
@Json(name = "DELIVERED")
DELIVERED,
@Json(name = "activating")
@Json(name = "ACTIVATING")
ACTIVATING,
@Json(name = "active")
@Json(name = "ACTIVE")
ACTIVE,
@Json(name = "blocked")
@Json(name = "BLOCKED")
BLOCKED,
@Json(name = "deactivating")
@Json(name = "DEACTIVATING")
DEACTIVATING,
@Json(name = "deactivated")
@Json(name = "DEACTIVATED")
DEACTIVATED,
@Json(name = "canceled")
@Json(name = "CANCELED")
CANCELED,
@Json(name = "unknown")
@Json(name = "UNKNOWN")
UNKNOWN,
}
}
@ -91,11 +90,12 @@ data class CustomerMeResponse(
@JsonClass(generateAdapter = true)
data class Card(
@Json(name = "token") val token: String,
@Json(name = "expiration_month") val expirationMonth: Int,
@Json(name = "expiration_year") val expirationYear: Int,
@Json(name = "expiration_month") val expirationMonth: String,
@Json(name = "expiration_year") val expirationYear: String,
@Json(name = "emboss_name") val embossName: String,
@Json(name = "card_type") val cardType: String,
@Json(name = "card_status") val cardStatus: String,
@Json(name = "card_number_end") val cardNumberEnd: String,
@Json(name = "is_pin_set") val isPinSet: Boolean?,
)
}

View file

@ -6,7 +6,6 @@ import com.squareup.moshi.JsonClass
@JsonClass(generateAdapter = true)
data class DeeplinkValidityResponse(
@Json(name = "result") val result: Result?,
@Json(name = "error") val error: String?,
) {
@JsonClass(generateAdapter = true)
data class Result(

View file

@ -6,7 +6,6 @@ import com.squareup.moshi.JsonClass
@JsonClass(generateAdapter = true)
class FreezeUnfreezeCardResponse(
@Json(name = "result") val result: Result?,
@Json(name = "error") val error: String?,
) {
@JsonClass(generateAdapter = true)
data class Result(

View file

@ -6,7 +6,6 @@ import com.squareup.moshi.JsonClass
@JsonClass(generateAdapter = true)
data class OrderResponse(
@Json(name = "result") val result: Result?,
@Json(name = "error") val error: String?,
) {
@JsonClass(generateAdapter = true)
data class Result(

View file

@ -4,7 +4,7 @@ import com.squareup.moshi.Json
import com.squareup.moshi.JsonClass
@JsonClass(generateAdapter = true)
data class VisaErrorResponse(
data class TangemPayErrorResponse(
@Json(name = "error") val error: Error,
) {
@JsonClass(generateAdapter = true)

View file

@ -7,7 +7,6 @@ import java.math.BigDecimal
@JsonClass(generateAdapter = true)
data class TangemPayTxHistoryResponse(
@Json(name = "error") val error: String?,
@Json(name = "result") val result: Result,
) {
@JsonClass(generateAdapter = true)

View file

@ -6,7 +6,6 @@ import com.squareup.moshi.JsonClass
@JsonClass(generateAdapter = true)
data class WithdrawDataResponse(
@Json(name = "result") val result: Result?,
@Json(name = "error") val error: String?,
) {
@JsonClass(generateAdapter = true)
data class Result(

View file

@ -6,7 +6,6 @@ import com.squareup.moshi.JsonClass
@JsonClass(generateAdapter = true)
data class WithdrawResponse(
@Json(name = "result") val result: Result?,
@Json(name = "error") val error: String?,
) {
@JsonClass(generateAdapter = true)
data class Result(

View file

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

View file

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

View file

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

View file

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

View file

@ -0,0 +1,104 @@
package com.tangem.datasource.api.visa
import com.tangem.datasource.api.common.response.ApiResponse
import com.tangem.datasource.api.pay.models.request.RefreshTokenByCardIdRequest
import com.tangem.datasource.api.pay.models.request.RefreshTokenByCardWalletRequest
import com.tangem.datasource.api.pay.models.request.SetPinCodeRequest
import com.tangem.datasource.api.visa.models.request.ActivationByCardWalletRequest
import com.tangem.datasource.api.visa.models.request.ActivationByCustomerWalletRequest
import com.tangem.datasource.api.visa.models.request.ActivationStatusRequest
import com.tangem.datasource.api.visa.models.request.ExchangeAccessTokenRequest
import com.tangem.datasource.api.visa.models.request.GenerateNoneByCardIdRequest
import com.tangem.datasource.api.visa.models.request.GenerateNoneByCardWalletRequest
import com.tangem.datasource.api.visa.models.request.GetAccessTokenByCardIdRequest
import com.tangem.datasource.api.visa.models.request.GetAccessTokenByCardWalletRequest
import com.tangem.datasource.api.visa.models.request.GetCardWalletAcceptanceRequest
import com.tangem.datasource.api.visa.models.request.GetCustomerWalletAcceptanceRequest
import com.tangem.datasource.api.visa.models.response.CardActivationRemoteStateResponse
import com.tangem.datasource.api.visa.models.response.GenerateNonceResponse
import com.tangem.datasource.api.visa.models.response.JWTResponse
import com.tangem.datasource.api.visa.models.response.VisaCustomerInfo
import com.tangem.datasource.api.visa.models.response.VisaDataToSignResponse
import com.tangem.datasource.api.visa.models.response.VisaTxHistoryResponse
import retrofit2.http.Body
import retrofit2.http.GET
import retrofit2.http.Header
import retrofit2.http.POST
import retrofit2.http.Query
interface VisaApi {
@POST("v1/auth/token/refresh")
suspend fun refreshCardWalletAccessToken(@Body request: RefreshTokenByCardWalletRequest): ApiResponse<JWTResponse>
@POST("v1/auth/challenge")
suspend fun generateNonceByCardId(@Body request: GenerateNoneByCardIdRequest): ApiResponse<GenerateNonceResponse>
@POST("v1/auth/challenge")
suspend fun generateNonceByCardWallet(
@Body request: GenerateNoneByCardWalletRequest,
): ApiResponse<GenerateNonceResponse>
@POST("v1/auth/token")
suspend fun getAccessTokenByCardId(@Body request: GetAccessTokenByCardIdRequest): ApiResponse<JWTResponse>
@POST("v1/auth/token")
suspend fun getAccessTokenByCardWallet(@Body request: GetAccessTokenByCardWalletRequest): ApiResponse<JWTResponse>
@POST("v1/auth/token/refresh")
suspend fun refreshCardIdAccessToken(@Body request: RefreshTokenByCardIdRequest): ApiResponse<JWTResponse>
@POST("v1/auth/token/exchange")
suspend fun exchangeAccessToken(@Body request: ExchangeAccessTokenRequest): ApiResponse<JWTResponse>
@POST("v1/activation/status")
suspend fun getRemoteActivationStatus(
@Header("Authorization") authHeader: String,
@Body request: ActivationStatusRequest,
): ApiResponse<CardActivationRemoteStateResponse>
@POST("v1/activation/acceptance/message")
suspend fun getCardWalletAcceptance(
@Header("Authorization") authHeader: String,
@Body request: GetCardWalletAcceptanceRequest,
): ApiResponse<VisaDataToSignResponse>
@POST("v1/activation/acceptance/message")
suspend fun getCustomerWalletAcceptance(
@Header("Authorization") authHeader: String,
@Body request: GetCustomerWalletAcceptanceRequest,
): ApiResponse<VisaDataToSignResponse>
@POST("v1/activation/data")
suspend fun activateByCardWallet(
@Header("Authorization") authHeader: String,
@Body body: ActivationByCardWalletRequest,
): ApiResponse<Unit>
@POST("v1/activation/data")
suspend fun activateByCustomerWallet(
@Header("Authorization") authHeader: String,
@Body body: ActivationByCustomerWalletRequest,
): ApiResponse<Unit>
@POST("v1/activation/pin")
suspend fun setPinCode(
@Header("Authorization") authHeader: String,
@Body body: SetPinCodeRequest,
): ApiResponse<Unit>
@GET("customer/info")
suspend fun getCustomerInfo(
@Header("Authorization") authHeader: String,
@Query("card_id") cardId: String,
): ApiResponse<VisaCustomerInfo>
@GET("product_instance/transactions")
suspend fun getTxHistory(
@Header("Authorization") authHeader: String,
@Query("customer_id") customerId: String,
@Query("product_instance_id") productInstanceId: String,
@Query("limit") limit: Int,
@Query("offset") offset: Int,
): ApiResponse<VisaTxHistoryResponse>
}

View file

@ -1,4 +1,4 @@
package com.tangem.datasource.api.pay.models.request
package com.tangem.datasource.api.visa.models.request
import com.squareup.moshi.Json
import com.squareup.moshi.JsonClass

View file

@ -1,4 +1,4 @@
package com.tangem.datasource.api.pay.models.request
package com.tangem.datasource.api.visa.models.request
import com.squareup.moshi.Json
import com.squareup.moshi.JsonClass

View file

@ -1,4 +1,4 @@
package com.tangem.datasource.api.pay.models.request
package com.tangem.datasource.api.visa.models.request
import com.squareup.moshi.Json
import com.squareup.moshi.JsonClass

View file

@ -1,4 +1,4 @@
package com.tangem.datasource.api.pay.models.request
package com.tangem.datasource.api.visa.models.request
import com.squareup.moshi.Json
import com.squareup.moshi.JsonClass

View file

@ -1,4 +1,4 @@
package com.tangem.datasource.api.pay.models.request
package com.tangem.datasource.api.visa.models.request
import com.squareup.moshi.Json
import com.squareup.moshi.JsonClass

View file

@ -1,4 +1,4 @@
package com.tangem.datasource.api.pay.models.request
package com.tangem.datasource.api.visa.models.request
import com.squareup.moshi.Json
import com.squareup.moshi.JsonClass

View file

@ -1,4 +1,4 @@
package com.tangem.datasource.api.pay.models.request
package com.tangem.datasource.api.visa.models.request
import com.squareup.moshi.Json
import com.squareup.moshi.JsonClass

View file

@ -1,4 +1,4 @@
package com.tangem.datasource.api.pay.models.request
package com.tangem.datasource.api.visa.models.request
import com.squareup.moshi.Json
import com.squareup.moshi.JsonClass

View file

@ -1,4 +1,4 @@
package com.tangem.datasource.api.pay.models.request
package com.tangem.datasource.api.visa.models.request
import com.squareup.moshi.Json
import com.squareup.moshi.JsonClass

View file

@ -1,4 +1,4 @@
package com.tangem.datasource.api.pay.models.request
package com.tangem.datasource.api.visa.models.request
import com.squareup.moshi.Json
import com.squareup.moshi.JsonClass

View file

@ -1,4 +1,4 @@
package com.tangem.datasource.api.pay.models.response
package com.tangem.datasource.api.visa.models.response
import com.squareup.moshi.Json
import com.squareup.moshi.JsonClass

View file

@ -1,4 +1,4 @@
package com.tangem.datasource.api.pay.models.response
package com.tangem.datasource.api.visa.models.response
import com.squareup.moshi.Json
import com.squareup.moshi.JsonClass

View file

@ -1,4 +1,4 @@
package com.tangem.datasource.api.pay.models.response
package com.tangem.datasource.api.visa.models.response
import com.squareup.moshi.Json
import com.squareup.moshi.JsonClass

View file

@ -1,4 +1,4 @@
package com.tangem.datasource.api.pay.models.response
package com.tangem.datasource.api.visa.models.response
import com.squareup.moshi.Json
import com.squareup.moshi.JsonClass

View file

@ -1,4 +1,4 @@
package com.tangem.datasource.api.pay.models.response
package com.tangem.datasource.api.visa.models.response
import com.squareup.moshi.Json
import com.squareup.moshi.JsonClass

View file

@ -1,4 +1,4 @@
package com.tangem.datasource.api.pay.models.response
package com.tangem.datasource.api.visa.models.response
import com.squareup.moshi.Json
import com.squareup.moshi.JsonClass

View file

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

View file

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

View file

@ -5,7 +5,6 @@ import com.tangem.datasource.api.common.blockaid.BlockAidApi
import com.tangem.datasource.api.common.config.ApiConfig
import com.tangem.datasource.api.common.config.ApiConfig.Companion.MOCKED_BUILD_TYPE
import com.tangem.datasource.api.common.config.ApiConfigs
import com.tangem.datasource.api.common.config.MoonPay
import com.tangem.datasource.api.common.config.managers.ApiConfigsManager
import com.tangem.datasource.api.common.config.managers.DevApiConfigsManager
import com.tangem.datasource.api.common.config.managers.MockApiConfigsManager
@ -21,6 +20,7 @@ import com.tangem.datasource.api.pay.TangemPayAuthApi
import com.tangem.datasource.api.stakekit.StakeKitApi
import com.tangem.datasource.api.tangemTech.TangemTechApi
import com.tangem.datasource.api.tangemTech.YieldSupplyApi
import com.tangem.datasource.api.visa.VisaApi
import com.tangem.datasource.di.utils.RetrofitApiBuilder
import com.tangem.datasource.di.utils.RetrofitApiBuilder.Timeouts
import com.tangem.datasource.local.preferences.AppPreferencesStore
@ -129,7 +129,16 @@ internal object NetworkModule {
@Provides
@Singleton
fun provideTangemVisaApi(retrofitApiBuilder: RetrofitApiBuilder): TangemPayApi {
fun provideTangemPayApi(retrofitApiBuilder: RetrofitApiBuilder): TangemPayApi {
return retrofitApiBuilder.build(
apiConfigId = ApiConfig.ID.TangemPay,
applyTimeoutAnnotations = false,
)
}
@Provides
@Singleton
fun provideVisaApi(retrofitApiBuilder: RetrofitApiBuilder): VisaApi {
return retrofitApiBuilder.build(
apiConfigId = ApiConfig.ID.TangemPay,
applyTimeoutAnnotations = false,

View file

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

View file

@ -1,25 +1,25 @@
package com.tangem.datasource.local.news.trending
import com.tangem.datasource.local.datastore.RuntimeSharedStore
import com.tangem.domain.models.news.ShortArticle
import com.tangem.domain.models.news.TrendingNews
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.map
private typealias TrendingCache = Map<String, List<ShortArticle>>
private typealias TrendingCache = Map<String, TrendingNews>
internal class DefaultTrendingNewsStore(
private val store: RuntimeSharedStore<TrendingCache>,
) : TrendingNewsStore {
override fun get(key: String): Flow<List<ShortArticle>> {
return store.get().map { it[key].orEmpty() }
override fun get(key: String): Flow<TrendingNews> {
return store.get().map { it[key] ?: TrendingNews.Data(emptyList()) }
}
override suspend fun getSyncOrNull(key: String): List<ShortArticle>? {
override suspend fun getSyncOrNull(key: String): TrendingNews? {
return store.getSyncOrNull()?.get(key)
}
override suspend fun store(key: String, value: List<ShortArticle>) {
override suspend fun store(key: String, value: TrendingNews) {
store.update(emptyMap()) { current ->
current + (key to value)
}

View file

@ -1,15 +1,15 @@
package com.tangem.datasource.local.news.trending
import com.tangem.domain.models.news.ShortArticle
import com.tangem.domain.models.news.TrendingNews
import kotlinx.coroutines.flow.Flow
interface TrendingNewsStore {
fun get(key: String): Flow<List<ShortArticle>>
fun get(key: String): Flow<TrendingNews>
suspend fun getSyncOrNull(key: String): List<ShortArticle>?
suspend fun getSyncOrNull(key: String): TrendingNews?
suspend fun store(key: String, value: List<ShortArticle>)
suspend fun store(key: String, value: TrendingNews)
suspend fun clear()
}

View file

@ -27,6 +27,8 @@ object PreferencesKeys {
val SAVE_USER_WALLETS_KEY by lazy { booleanPreferencesKey(name = "saveUserWallets") }
val ROOT_DETECTED_WARNING_SHOWN_KEY by lazy { booleanPreferencesKey(name = "rootDetectedWarningShown") }
val SHOULD_SHOW_ASK_BIOMETRY_KEY by lazy { booleanPreferencesKey("saveUserWalletShown") }
val APP_LAUNCH_COUNT_KEY by lazy { intPreferencesKey(name = "launchCount") }
@ -77,8 +79,8 @@ object PreferencesKeys {
val SHOULD_SHOW_MARKETS_TOOLTIP_KEY by lazy { booleanPreferencesKey(name = "shouldShowMarketsTooltip") }
val MARKETS_STAKING_NOTIFICATION_HIDE_CLICKED_KEY by lazy {
booleanPreferencesKey(name = "marketsStakingNotificationHideClicked")
val MARKETS_YIELD_SUPPLY_NOTIFICATION_HIDE_CLICKED_KEY by lazy {
booleanPreferencesKey(name = "marketsYieldSupplyNotificationHideClicked")
}
val WALLET_FIRST_USAGE_DATE_KEY by lazy { longPreferencesKey(name = "walletFirstUsageDate") }
@ -108,6 +110,8 @@ object PreferencesKeys {
val ONRAMP_TRANSACTIONS_STATUSES_KEY by lazy { stringPreferencesKey(name = "onrampTransactionsStatuses") }
val ONRAMP_HANDLED_TRANSACTIONS_KEY by lazy { stringPreferencesKey(name = "onrampHandledTransactions") }
val ONBOARDING_FINALIZE_SCAN_RESPONSE_KEY by lazy { stringPreferencesKey(name = "onboardingFinalizeScanResponse") }
val IS_GOOGLE_SERVICES_AVAILABLE_KEY by lazy { booleanPreferencesKey(name = "isGoogleServicesAvailable") }
@ -150,6 +154,7 @@ object PreferencesKeys {
}
val TANGEM_PAY_WITHDRAW_ORDERS_KEY by lazy { stringPreferencesKey(name = "tangemPayWithdrawOrders") }
val TANGEM_PAY_ELIGIBILITY_KEY by lazy { booleanPreferencesKey(name = "tangemPayEligibility") }
fun getShouldShowNotificationKey(key: String) = booleanPreferencesKey("showShowNotificationUM_$key")
// endregion
@ -192,6 +197,9 @@ object PreferencesKeys {
fun getTangemPayCheckCustomerByWalletId(userWalletId: UserWalletId) =
booleanPreferencesKey("tangem_pay_check_customer_by_wallet_id_$userWalletId")
fun getTangemPayHideOnboardingKey(userWalletId: UserWalletId) =
booleanPreferencesKey("tangem_pay_hide_onboarding_key_$userWalletId")
// endregion
}

View file

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

View file

@ -3,14 +3,18 @@ package com.tangem.datasource.local.visa
import com.tangem.domain.models.wallet.UserWalletId
import com.tangem.domain.visa.model.TangemPayAuthTokens
@Suppress("TooManyFunctions")
interface TangemPayStorage {
suspend fun storeCustomerWalletAddress(userWalletId: UserWalletId, customerWalletAddress: String)
suspend fun getCustomerWalletAddress(userWalletId: UserWalletId): String?
suspend fun clearCustomerWalletAddress(userWalletId: UserWalletId)
suspend fun storeAuthTokens(customerWalletAddress: String, tokens: TangemPayAuthTokens)
suspend fun getAuthTokens(customerWalletAddress: String): TangemPayAuthTokens?
suspend fun clearAuthTokens(customerWalletAddress: String)
suspend fun storeOrderId(customerWalletAddress: String, orderId: String)
@ -30,5 +34,11 @@ interface TangemPayStorage {
suspend fun deleteWithdrawOrder(userWalletId: UserWalletId)
suspend fun getHideMainOnboardingBanner(userWalletId: UserWalletId): Boolean
suspend fun storeHideOnboardingBanner(userWalletId: UserWalletId, hide: Boolean)
suspend fun storeTangemPayEligibility(eligibility: Boolean)
suspend fun getTangemPayEligibility(): Boolean
suspend fun clearAll(userWalletId: UserWalletId, customerWalletAddress: String)
}

View file

@ -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
@ -250,7 +251,7 @@ internal class ProdApiConfigsManagerTest {
id = ApiConfig.ID.TangemPay,
expected = ApiEnvironmentConfig(
environment = ApiEnvironment.DEV,
baseUrl = "https://api.dev.us.paera.com/bff/",
baseUrl = "https://api.dev.us.paera.com/bff-v2/",
headers = mapOf(
"version" to ProviderSuspend { VERSION_NAME },
"platform" to ProviderSuspend { "Android" },
@ -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" },

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

View file

@ -4,7 +4,7 @@
<string name="access_code_alert_skip_ok">Trotzdem überspringen</string>
<string name="access_code_alert_skip_title">Zugangscode nicht festgelegt</string>
<string name="access_code_alert_validation_cancel">Code ändern</string>
<string name="access_code_alert_validation_description">Dein Zugangscode dient zum Entsperren Deiner Wallet und zum Schutz des Zugriffs auf Deine Vermögenswerte.</string>
<string name="access_code_alert_validation_description">Dein Zugangscode entsperrt und schützt den Zugriff auf Deine Geldbörse.</string>
<string name="access_code_alert_validation_ok">Trotzdem verwenden</string>
<string name="access_code_alert_validation_title">Dieser Zugangscode kann leicht erraten werden</string>
<string name="access_code_check_title">Zugangscode eingeben</string>
@ -49,6 +49,7 @@
<string name="account_form_placeholder_new_account">Neues Konto</string>
<string name="account_form_title_create">Konto hinzufügen</string>
<string name="account_form_title_edit">Konto bearbeiten</string>
<string name="account_generic_error_dialog_message">Bitte versuche es später erneut. Sollte das Problem weiterhin bestehen, kontaktiere bitte unseren Support. Wir helfen Dir gerne bei der Lösung.</string>
<string name="account_label_tokens_info">%1$s in %2$s</string>
<string name="account_main_account_title">Hauptkonto</string>
<string name="account_recover_limit_dialog_description">Du hast das Limit von %1$s aktiven Konten bereits überschritten. Archiviere eines zur Wiederherstellung</string>
@ -96,7 +97,7 @@
<string name="alert_manage_tokens_unsupported_message">Tokens im %1$s -Netzwerk werden von dieser Karte oder Ring aufgrund einer Firmware-Einschränkung nicht unterstützt.</string>
<string name="alert_troubleshooting_scan_card_title">Hast du Probleme beim Scannen deiner Karte oder Ring?</string>
<string name="alert_unsupported_card">Diese Karte oder Ring ist für die Zusammenarbeit mit Tangem nicht geeignet</string>
<string name="app_settings_access_code_warning">Lege zunächst einen Zugangscode fest, um die Biometrie zu aktivieren.</string>
<string name="app_settings_access_code_warning">Lege einen Zugangscode fest, um die Biometrie zu aktivieren.</string>
<string name="app_settings_biometrics_footer">Verwende die %1$s um Deine Wallet zu entsperren und sensible Aktionen wie das Signieren von Transaktionen zu bestätigen. Bei Hardware-Wallets ist zum Signieren weiterhin eine Karte oder ein Ring erforderlich.</string>
<string name="app_settings_default_fee">Standardgebühr</string>
<string name="app_settings_default_fee_footer">Aktiviere die Option Standardgebühr, um die Transaktionsgebühren automatisch festzulegen und die Gebührenseite beim Senden von Geldern zu überspringen. Du kannst bei Bedarf jederzeit zu dieser Seite zurückkehren.</string>
@ -145,10 +146,10 @@
<string name="balance_hidden_title">Guthaben sind ausgeblendet</string>
<string name="beta_mode_warning_message">Laut den Blockchain-Entwicklern befinden sich der Kaspa-Token derzeit in der Betaphase. Bleibe dran für Updates!</string>
<string name="beta_mode_warning_title">Beta-Phase</string>
<string name="biometric_disabled_warning_description">Die biometrischen Daten sind auf Deinem Gerät deaktiviert, sodass Du sie nicht zum Entsperren Deine Wallet verwenden kannst. Aktiviere die Biometrie in den Einstellungen Deines Geräts, um diese Methode wieder zu verwenden.</string>
<string name="biometric_disabled_warning_description">Die Biometrie ist auf Deinem Gerät deaktiviert, daher kannst Du sie nicht zum Entsperren Deiner Wallets verwenden. Aktiviere die Biometrie in den Geräteeinstellungen, um diese Methode wieder nutzen zu können.</string>
<string name="biometric_disabled_warning_title">Biometrische Authentifizierung deaktiviert</string>
<string name="biometric_lockout_permanent_warning_description">Bitte Karte oder Ring scannen</string>
<string name="biometric_lockout_permanent_warning_description_2">Du hast das Limit an biometrischen Entsperrversuchen erreicht. Bitte entsperren Deine Wallet durch Antippen Deines Geräts oder gib den Zugangscode ein.</string>
<string name="biometric_lockout_permanent_warning_description_2">Du hast das Limit an biometrischen Entsperrversuchen erreicht. Bitte entsperre Deine Wallet mit einer Karte/einem Ring oder gib Deinen Zugangscode ein.</string>
<string name="biometric_lockout_permanent_warning_title">Biometrische Authentifizierung gesperrt</string>
<string name="biometric_lockout_warning_description">Bitte versuche es in 30 Sekunden erneut oder scanne die Karte oder Ring</string>
<string name="biometric_lockout_warning_description_2">Die biometrische Anmeldung ist vorübergehend gesperrt. Bitte versuche es in 30 Sekunden erneut oder entsperre Deine Wallet durch Antippen Deines Geräts oder mit einem Zugangscode.</string>
@ -374,6 +375,7 @@
<string name="common_terms_and_conditions">Allgemeine Geschäftsbedingungen</string>
<string name="common_terms_of_use">Nutzungsbedingungen</string>
<string name="common_to">An</string>
<string name="common_to_wallet_name">Zu %s</string>
<string name="common_today">Heute</string>
<plurals name="common_tokens_count">
<item quantity="one">%d Token</item>
@ -385,6 +387,7 @@
<string name="common_transfer">Überweisung</string>
<string name="common_unable_to_load">Die Daten konnten nicht geladen werden…</string>
<string name="common_understand">Ich verstehe</string>
<string name="common_understand_continue">Ich verstehe, fahre bitte fort.</string>
<string name="common_unknown_error">Es ist ein Fehler aufgetreten. Bitte versuche es erneut.</string>
<string name="common_unreachable">Nicht erreichbar</string>
<string name="common_unstake">Staking beenden</string>
@ -398,6 +401,7 @@
<string name="currency_subtitle_expanded">Verfügbare Netzwerke</string>
<string name="custom_token_another_account_dialog_description">Die Ableitung Deines Tokens entspricht der Ableitung von %1$s. Dein Token wird diesem Konto gutgeschrieben.</string>
<string name="custom_token_another_account_dialog_title">Die Herleitung stammt aus einem anderen Bericht.</string>
<string name="custom_token_another_account_snackbar_text">Token hinzugefügt %1$s Konto</string>
<string name="custom_token_contract_address_input_title">Vertragsadresse</string>
<string name="custom_token_creation_error_invalid_contract_address">Vertragsadresse ist ungültig</string>
<string name="custom_token_creation_error_network_not_selected">Bitte wähle das Netzwerk</string>
@ -725,6 +729,7 @@
<string name="markets_sort_by_top_gainers_title">Top-Gewinner</string>
<string name="markets_sort_by_top_losers_title">Top-Verlierer</string>
<string name="markets_sort_by_trending_title">Beliebt</string>
<string name="markets_sort_by_yield_mode_title">Ertragsmodus</string>
<string name="markets_staking_banner_description_placeholder">Staking ist der einfachste Weg, um Belohnungen für Deine Kryptowährung zu erhalten. %s</string>
<string name="markets_staking_banner_title">Verdiene bis zu %s APY</string>
<string name="markets_token_added">Token hinzugefügt</string>
@ -800,8 +805,17 @@
<string name="mobile_wallet_requires_min_os_warning_title">Mobile Wallet erfordert %1$s oder später</string>
<string name="news_all_news">Alle Neuigkeiten</string>
<string name="news_like">Gefällt mir</string>
<plurals name="news_published_hours_ago">
<item quantity="one">%dStunde her</item>
<item quantity="other">%dStunden her</item>
</plurals>
<plurals name="news_published_minutes_ago">
<item quantity="one">%dMinute her</item>
<item quantity="other">%dMinuten her</item>
</plurals>
<string name="news_quick_recap">Kurze Zusammenfassung</string>
<string name="news_related_news">Verwandte Nachrichten</string>
<string name="news_related_tokens">Verwandte Token</string>
<string name="news_sources">Quellen</string>
<string name="news_stay_in_the_loop">Auf dem Laufenden bleiben</string>
<string name="nfc_error_unavailable">NFC ist auf deinem Gerät nicht verfügbar</string>
@ -978,6 +992,7 @@
<string name="onramp_currency_other">Andere Währungen</string>
<string name="onramp_currency_popular">Beliebte Fiats</string>
<string name="onramp_currency_search">Suche nach Währung</string>
<string name="onramp_error_transaction_already_processed">Diese Transaktion wurde bereits verarbeitet. Es sind keine weiteren Maßnahmen erforderlich.</string>
<string name="onramp_fetching_best_rates">Die besten Preise erzielen...</string>
<string name="onramp_instant_status">Sofort</string>
<string name="onramp_legal">Durch die Nutzung der Onramp-Funktionalität stimmst Du den %1$s und %2$s des Anbieters zu.</string>
@ -1074,6 +1089,7 @@
<string name="reset_card_to_factory_button_title">Karte oder Ring zurücksetzen</string>
<string name="reset_card_to_factory_condition_1">Mir ist bewusst, dass ich nach der Durchführung dieser Aktion keinen Zugriff mehr auf die aktuelle Wallet habe.</string>
<string name="reset_card_to_factory_condition_2">Mir ist klar, dass ich diese Karte oder Ring nicht verwenden kann, um meinen Zugangscode auf den anderen Karten oder Ringe der aktuellen Wallet wiederherzustellen</string>
<string name="reset_card_to_factory_condition_3">Mir ist bewusst, dass ich den Zugang zu meiner Tangem Pay Karte und allen darauf befindlichen Geldern vollständig verliere, ohne die Möglichkeit der Wiederherstellung</string>
<string name="reset_card_with_backup_to_factory_message">Durch das Zurücksetzen auf Werkseinstellungen wird die Wallet vollständig von der ausgewählten Karte oder Ring gelöscht. Du kannst die aktuelle Wallet nicht wiederherstellen oder die Karte oder Ring verwenden, um den Zugangscode wiederherzustellen.</string>
<string name="reset_card_without_backup_to_factory_message">Beim Zurücksetzen auf die Werkseinstellungen wird die Wallet der ausgewählten Karte oder Ring vollständig gelöscht und aus der App entfernt. Es ist nicht möglich, die aktuelle Wallet wiederherzustellen.</string>
<string name="reset_cards_dialog_complete_description">Alle Tangem-Geräte wurden zurückgesetzt.</string>
@ -1227,6 +1243,8 @@
<string name="staking_account_initialization_footer">Eine Netzwerkgebühr ist eine kleine Zahlung, die erforderlich ist, um Deine Transaktion auf der Blockchain zu verarbeiten und zu bestätigen.</string>
<string name="staking_account_initialization_message">Um mit dem Staking zu beginnen, muss Dein TON-Konto mit einer Transaktion von 1 TON aktiviert werden. Das Guthaben verbleibt auf Deinem Konto, dieser Schritt dient lediglichder aktivierung für das Staking.</string>
<string name="staking_account_initialization_title">Kontoaktivierung</string>
<string name="staking_alert_network_fee_updated_message">Die Netzwerkgebühr hat sich geändert. Bitte überprüfe den neuen Betrag, bevor Du fortfährst.</string>
<string name="staking_alert_network_fee_updated_title">Netzwerkgebühr aktualisiert</string>
<string name="staking_amount_requirement_error">Die Anzahl der zu stakenden Krypros muss mindesten %s betragen</string>
<string name="staking_amount_tron_integer_error">Der Stakingbetrag wird aufgrund der Netzwerkregeln auf %1$s TRX aufgerundet.</string>
<string name="staking_amount_tron_integer_error_unstaking">Der Betrag der unstaked wird, wird aufgrund der Netzwerkregeln auf %1$s TRX gerundet.</string>
@ -1265,6 +1283,7 @@
<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>
<string name="staking_max_amount_requirement_error">Höchstbetrag: %s</string>
<string name="staking_migrate">Migrieren</string>
<string name="staking_native">Natives Staking</string>
<string name="staking_no_validators_error_message">Derzeit sind keine aktiven Validatoren für das Staking verfügbar. Bitte versuchen Sie es später erneut.</string>
@ -1417,6 +1436,7 @@
<string name="tangem_pay_freeze_card_success">Ihre Karte ist eingefroren.</string>
<string name="tangem_pay_get_help">Hilfe erhalten</string>
<string name="tangem_pay_other">Andere</string>
<string name="tangem_pay_rooted_device_subtitle">Nicht nutzbar auf gerooteten Geräten</string>
<string name="tangem_pay_status_completed">Abgeschlossen</string>
<string name="tangem_pay_status_declined">Abgelehnt</string>
<string name="tangem_pay_status_pending">Ausstehend</string>
@ -1429,6 +1449,7 @@
<string name="tangem_pay_unfreeze_card_failed">Entsperren der Karte fehlgeschlagen. Versuchen Sie es später erneut.</string>
<string name="tangem_pay_unfreeze_card_success">Ihre Karte ist entsperrt.</string>
<string name="tangem_pay_withdrawal">Abhebung</string>
<string name="tangempay_account_unable_to_use_rooted">Auf gerooteten Geräten nicht nutzbar.</string>
<string name="tangempay_cancel_kyc">KYC abbrechen</string>
<string name="tangempay_card_details_add_funds">Guthaben hinzufügen</string>
<string name="tangempay_card_details_add_funds_subtitle">Aufladeoptionen</string>
@ -1466,6 +1487,7 @@
<string name="tangempay_card_details_swap_description">Tausche beliebige Vermögenswerte in Deinem Portfolio gegen eine Karte.</string>
<string name="tangempay_card_details_title">Kartendetails</string>
<string name="tangempay_card_details_unfreeze_card">Karte entsperren</string>
<string name="tangempay_card_details_view_pin_code_description">Komm zurück zur App, falls du es vergisst.</string>
<string name="tangempay_card_details_view_pin_code_title">Dein PIN-Code</string>
<string name="tangempay_card_details_withdraw">Auszahlung</string>
<string name="tangempay_card_details_withdraw_error_title">Auszahlung derzeit nicht möglich</string>
@ -1473,6 +1495,7 @@
<string name="tangempay_card_details_withdraw_in_progress_title">Auszahlung läuft</string>
<string name="tangempay_change_pin_code">PIN-Code ändern</string>
<string name="tangempay_come_back_if_forget_pin">Kehren Sie zur App zurück, falls Sie ihn vergessen.</string>
<string name="tangempay_factory_settings_warning_title">Mir ist bewusst, dass ich den Zugriff auf meine Tangem Pay Card und alle darauf befindlichen Guthaben vollständig und ohne Möglichkeit der Wiederherstellung verliere.</string>
<string name="tangempay_failed_to_issue_card">Kartenausstellung fehlgeschlagen</string>
<string name="tangempay_failed_to_issue_card_retry_description">Ein technischer Fehler ist aufgetreten, bitte versuchen Sie es erneut, indem Sie auf die Schaltfläche unten klicken</string>
<string name="tangempay_failed_to_issue_card_support_description">Ein technischer Fehler ist aufgetreten, bitte kontaktieren Sie den Support</string>
@ -1491,6 +1514,7 @@
<string name="tangempay_kyc_in_progress">KYC in Bearbeitung</string>
<string name="tangempay_kyc_in_progress_notification_button">Status anzeigen</string>
<string name="tangempay_kyc_in_progress_notification_title">KYC für Tangem Pay in Arbeit</string>
<string name="tangempay_kyc_in_progress_popup_description">Über die Schaltflächen unten kannst Du Deinen aktuellen KYC-Status einsehen oder ihn abbrechen.</string>
<string name="tangempay_onboarding_banner_description">Holen Sie sich Ihre kostenlose virtuelle Tangem Visa-Karte</string>
<string name="tangempay_onboarding_banner_title">Nutzen Sie USDC für alltägliche Zahlungen</string>
<string name="tangempay_onboarding_get_card_button_text">Karte erhalten</string>
@ -1503,12 +1527,15 @@
<string name="tangempay_onboarding_title">Erhalten Sie Ihre kostenlose Tangem Pay Card in wenigen Minuten</string>
<string name="tangempay_payment_account">Zahlungskonto</string>
<string name="tangempay_payment_account_sync_needed">Zahlungskonto ist nicht synchronisiert</string>
<string name="tangempay_pin_validation_error_message">Ungültige PIN: Sequenzen oder Wiederholungen vermeiden</string>
<string name="tangempay_service_unavailable_description">Wir beheben ein technisches Problem. Bitte versuchen Sie es später erneut.</string>
<string name="tangempay_service_unavailable_title">Service vorübergehend nicht verfügbar</string>
<string name="tangempay_service_unreachable_try_later">Daten können derzeit nicht angezeigt werden, Kartenzahlungen funktionieren jedoch weiterhin.</string>
<string name="tangempay_sync_needed">Synchronisation erforderlich</string>
<string name="tangempay_set_pin_code">Satz \nPIN-Code</string>
<string name="tangempay_sync_needed">Nicht synchronisiert</string>
<string name="tangempay_sync_needed_restore_access">Zugang wiederherstellen</string>
<string name="tangempay_tangem_visa_card">Nutzen Sie USDC für alltägliche Zahlungen</string>
<string name="tangempay_temporarily_unavailable">Tangem Pay ist vorübergehend nicht verfügbar</string>
<string name="tangempay_temporarily_unavailable">Tangem Pay ist vorübergehend nicht erreichbar.</string>
<string name="tangempay_title">Tangem Pay</string>
<string name="tangempay_use_tangem_device_to_restore_payment_account">Klicken Sie auf die Schaltfläche unten, um den Zugriff wiederherzustellen</string>
<string name="tangempay_your_pin_code">Ihr PIN-Code</string>
@ -1975,7 +2002,7 @@
<string name="xtz_withdrawal_message_warning">Damit Sie beim nächsten Aufladen Ihrer Brieftasche keine erhöhte Provision zahlen, soll der Betrag um %s XTZ reduziert werden</string>
<string name="yield_module_alert_description">Wenn der Yield-Modus aktiviert ist, gehen alle zukünftigen Einzahlungen an diese Adresse an Aave. Du kannst über Dein Guthaben weiterhin frei verfügen.</string>
<string name="yield_module_alert_title">Deine %s wird an Aave übermittelt</string>
<string name="yield_module_amount_not_transfered_to_aave_title">Die Lieferung von %1$s %2$s an Aave steht noch aus.</string>
<string name="yield_module_amount_not_transfered_to_aave_title">Lieferung %1$s %2$s nach Aave</string>
<string name="yield_module_approve_needed_notification_cta">Genehmigen</string>
<string name="yield_module_approve_needed_notification_description">Die Genehmigung Deines Tokens wurde widerrufen. Erteilen diese erneut, um die Servicefunktionalität fortzusetzen.</string>
<string name="yield_module_approve_needed_notification_title">Genehmigung erforderlich</string>
@ -2056,6 +2083,7 @@
<string name="yield_module_token_details_earn_notification_earning_on_your_balance_title">Ertragsmodus</string>
<string name="yield_module_token_details_earn_notification_processing">Bearbeitung Deiner Einzahlung</string>
<string name="yield_module_token_details_earn_notification_title">Ertragsmodus</string>
<string name="yield_module_transaction_deploy_contract">Yield-Mode-Vertragsbereitstellung</string>
<string name="yield_module_transaction_enter">Ertragsmodus aktivieren</string>
<string name="yield_module_transaction_enter_subtitle">%1$s geliefert an Aave</string>
<string name="yield_module_transaction_exit">Ertragsmodus deaktiviert</string>

View file

@ -1400,6 +1400,7 @@
<string name="tangempay_onboarding_title">Obtén tu tarjeta Tangem Pay gratuita en minutos</string>
<string name="tangempay_payment_account">Cuenta de pago</string>
<string name="tangempay_payment_account_sync_needed">La cuenta de pago no está sincronizada</string>
<string name="tangempay_pin_validation_error_message">PIN no válido: evitar secuencias o repeticiones</string>
<string name="tangempay_service_unavailable_description">Estamos solucionando un problema técnico. Por favor, inténtelo de nuevo más tarde.</string>
<string name="tangempay_service_unavailable_title">Servicio temporalmente no disponible</string>
<string name="tangempay_service_unreachable_try_later">No es posible mostrar los datos en este momento, pero los pagos con tarjeta siguen funcionando.</string>

View file

@ -1392,6 +1392,7 @@
<string name="tangempay_onboarding_title">Obtenez votre carte Tangem Pay gratuite en quelques minutes</string>
<string name="tangempay_payment_account">Compte de paiement</string>
<string name="tangempay_payment_account_sync_needed">Le compte de paiement n\'est pas synchronisé</string>
<string name="tangempay_pin_validation_error_message">Code PIN invalide : évitez les séquences ou les répétitions</string>
<string name="tangempay_service_unavailable_description">Nous réparons un problème technique. Veuillez réessayer plus tard.</string>
<string name="tangempay_service_unavailable_title">Service temporairement indisponible</string>
<string name="tangempay_service_unreachable_try_later">Les données ne peuvent pas être affichées pour le moment, mais les paiements par carte fonctionnent toujours.</string>

View file

@ -164,6 +164,7 @@
<string name="tangempay_onboarding_title">Ottieni la tua carta Tangem Pay gratuita in pochi minuti</string>
<string name="tangempay_payment_account">Conto di pagamento</string>
<string name="tangempay_payment_account_sync_needed">Il conto di pagamento non è sincronizzato</string>
<string name="tangempay_pin_validation_error_message">PIN non valido: evitare sequenze o ripetizioni</string>
<string name="tangempay_service_unavailable_description">Stiamo risolvendo un problema tecnico. Riprova più tardi.</string>
<string name="tangempay_service_unavailable_title">Servizio temporaneamente non disponibile</string>
<string name="tangempay_service_unreachable_try_later">Al momento non è possibile visualizzare i dati, ma i pagamenti con carta continuano a funzionare.</string>

View file

@ -1504,6 +1504,7 @@
<string name="tangempay_onboarding_title">無料のTangem Payカードを数分でゲットしましょう</string>
<string name="tangempay_payment_account">支払いアカウント</string>
<string name="tangempay_payment_account_sync_needed">支払アカウントが同期されていません</string>
<string name="tangempay_pin_validation_error_message">無効な暗証番号:連続や繰り返しを避けてください</string>
<string name="tangempay_service_unavailable_description">技術的な問題を修正しています。後でもう一度お試しください。</string>
<string name="tangempay_service_unavailable_title">サービスは一時的に利用できません</string>
<string name="tangempay_service_unreachable_try_later">現在、データを表示できませんが、カードでのお支払いは引き続きご利用いただけます。</string>

View file

@ -1532,6 +1532,7 @@
<string name="tangempay_onboarding_title">Откройте виртуальную \nTangem Pay Card</string>
<string name="tangempay_payment_account">Платежный аккаунт</string>
<string name="tangempay_payment_account_sync_needed">Платежный аккаунт не синхронизирован</string>
<string name="tangempay_pin_validation_error_message">Слабый ПИН: не используйте повторы или последовательности.</string>
<string name="tangempay_service_unavailable_description">Мы устраняем техническую проблему. Пожалуйста, попробуйте позже.</string>
<string name="tangempay_service_unavailable_title">Сервис временно недоступен</string>
<string name="tangempay_service_unreachable_try_later">Не можем показать данные карты, но оплаты продолжают работать.</string>

View file

@ -61,8 +61,8 @@
<string name="account_unsaved_dialog_message_create">Are you sure you want to discard new account?</string>
<string name="account_unsaved_dialog_message_edit">Are you sure you want to discard edits?</string>
<string name="account_unsaved_dialog_title">Unsaved Changes</string>
<string name="accounts_migration_alert_message">Some custom tokens were moved from “%1$s” to “%2$s” as their derivation belongs to that account.</string>
<string name="accounts_migration_alert_title">Some custom tokens were moved</string>
<string name="accounts_migration_alert_message">Some custom tokens will be automatically moved from “%1$s” to “%2$s” as their derivation belongs to that account.</string>
<string name="accounts_migration_alert_title">Some custom tokens will be automatically moved</string>
<string name="action_buttons_buy_empty_search_message">Cant find your token? Go to the Market section on the main page and add it to your portfolio for purchase</string>
<string name="action_buttons_sell_empty_search_message">Cant find your token? Go to the Market section on the main page and add it to your portfolio for selling.</string>
<string name="action_buttons_sell_navigation_bar_title">Sell</string>
@ -816,6 +816,7 @@
</plurals>
<string name="news_quick_recap">Quick recap</string>
<string name="news_related_news">Related News</string>
<string name="news_related_tokens">Related tokens</string>
<string name="news_sources">Sources</string>
<string name="news_stay_in_the_loop">Stay in the loop</string>
<string name="nfc_error_unavailable">NFC is not available on your device</string>
@ -1527,9 +1528,11 @@
<string name="tangempay_onboarding_title">Get your free Tangem Pay Card in minutes</string>
<string name="tangempay_payment_account">Payment account</string>
<string name="tangempay_payment_account_sync_needed">Payment account is not synced</string>
<string name="tangempay_pin_validation_error_message">Invalid PIN: avoid sequences or repeats</string>
<string name="tangempay_service_unavailable_description">Were fixing a technical issue. Please try again later.</string>
<string name="tangempay_service_unavailable_title">Service temporarily unavailable</string>
<string name="tangempay_service_unreachable_try_later">Unable to display details. However, card payments are still working.</string>
<string name="tangempay_set_pin_code">Set \nPIN code</string>
<string name="tangempay_sync_needed">Not synced</string>
<string name="tangempay_sync_needed_restore_access">Restore access</string>
<string name="tangempay_tangem_visa_card">Use USDC for everyday payments</string>

1
core/security/.gitignore vendored Normal file
View file

@ -0,0 +1 @@
/build

View file

@ -0,0 +1,4 @@
plugins {
alias(deps.plugins.kotlin.jvm)
id("configuration")
}

View file

@ -0,0 +1,9 @@
package com.tangem.security
interface DeviceSecurityInfoProvider {
val isRooted: Boolean
val isBootloaderUnlocked: Boolean
val isXposed: Boolean
}
fun DeviceSecurityInfoProvider.isSecurityExposed(): Boolean = isRooted || isBootloaderUnlocked || isXposed

View file

@ -66,4 +66,6 @@ dependencies {
testImplementation(deps.test.junit)
testImplementation(deps.test.mockk)
testImplementation(deps.test.truth)
testImplementation(deps.test.junit5)
testRuntimeOnly(deps.test.junit5.engine)
}

View file

@ -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 -&gt; R.drawable.ic_arrow_up_8 PriceChangeType.DOWN -&gt; R.drawable.ic_arrow_down_8 PriceChangeType.NEUTRAL -&gt; R.drawable.ic_elipse_8 }, ), tint = when (animatedType) { PriceChangeType.UP -&gt; TangemTheme.colors.icon.accent PriceChangeType.DOWN -&gt; TangemTheme.colors.icon.warning PriceChangeType.NEUTRAL -&gt; TangemTheme.colors.icon.inactive }, contentDescription = null, )</ID>
<ID>UnnecessaryEventHandlerParameter:PinTextField.kt$onValueChange: (String) -&gt; Unit</ID>

View file

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

View file

@ -37,44 +37,46 @@ fun DialogFullScreen(
decorFitsSystemWindows = false,
),
content = {
val activityWindow = getActivityWindow()
val dialogWindow = getDialogWindow()
val parentView = LocalView.current.parent as View
SideEffect {
if (activityWindow != null && dialogWindow != null) {
val attributes = WindowManager.LayoutParams().apply {
copyFrom(activityWindow.attributes)
if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.Q) {
softInputMode = WindowManager.LayoutParams.SOFT_INPUT_ADJUST_RESIZE
} else {
flags = WindowManager.LayoutParams.FLAG_LAYOUT_NO_LIMITS
}
type = dialogWindow.attributes.type
}
dialogWindow.attributes = attributes
parentView.layoutParams =
FrameLayout.LayoutParams(
activityWindow.decorView.width,
activityWindow.decorView.height,
)
}
}
if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.Q) {
val systemUiController = rememberSystemUiController(getActivityWindow())
val dialogSystemUiController = rememberSystemUiController(getDialogWindow())
ProvideSystemBarsIconsController {
val activityWindow = getActivityWindow()
val dialogWindow = getDialogWindow()
val parentView = LocalView.current.parent as View
SideEffect {
systemUiController.setSystemBarsColor(color = Color.Transparent)
dialogSystemUiController.setSystemBarsColor(color = Color.Transparent)
if (activityWindow != null && dialogWindow != null) {
val attributes = WindowManager.LayoutParams().apply {
copyFrom(activityWindow.attributes)
if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.Q) {
softInputMode = WindowManager.LayoutParams.SOFT_INPUT_ADJUST_RESIZE
} else {
flags = WindowManager.LayoutParams.FLAG_LAYOUT_NO_LIMITS
}
type = dialogWindow.attributes.type
}
dialogWindow.attributes = attributes
parentView.layoutParams =
FrameLayout.LayoutParams(
activityWindow.decorView.width,
activityWindow.decorView.height,
)
}
}
}
SystemBarsIconsDisposable(darkIcons = LocalIsInDarkTheme.current.not())
if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.Q) {
val systemUiController = rememberSystemUiController(getActivityWindow())
val dialogSystemUiController = rememberSystemUiController(getDialogWindow())
Surface(modifier = Modifier.fillMaxSize(), color = Color.Transparent) {
content()
SideEffect {
systemUiController.setSystemBarsColor(color = Color.Transparent)
dialogSystemUiController.setSystemBarsColor(color = Color.Transparent)
}
}
SystemBarsIconsDisposable(darkIcons = LocalIsInDarkTheme.current.not())
Surface(modifier = Modifier.fillMaxSize(), color = Color.Transparent) {
content()
}
}
},
)

View file

@ -308,13 +308,14 @@ private fun DialogContent(type: DialogType, modifier: Modifier = Modifier) {
}
}
@OptIn(ExperimentalLayoutApi::class)
@Composable
private fun DialogButtons(
confirmButton: DialogButtonUM,
dismissButton: DialogButtonUM?,
modifier: Modifier = Modifier,
) {
Row(
FlowRow(
modifier = modifier,
horizontalArrangement = Arrangement.spacedBy(
space = TangemTheme.dimens.spacing4,

View file

@ -41,6 +41,9 @@ data class MessageBottomSheetUMV2(
}
}
@Immutable
data class IconImage(@DrawableRes internal var res: Int) : Element
@Immutable
data class Chip(
internal var text: TextReference,
@ -54,6 +57,7 @@ data class MessageBottomSheetUMV2(
@Immutable
data class InfoBlock(
internal var icon: Icon? = null,
internal var iconImage: IconImage? = null,
internal var chip: Chip? = null,
var title: TextReference? = null,
var body: TextReference? = null,
@ -106,6 +110,10 @@ fun MessageBottomSheetUMV2.InfoBlock.icon(@DrawableRes res: Int, init: MessageBo
icon = MessageBottomSheetUMV2.Icon(res).apply(init)
}
fun MessageBottomSheetUMV2.InfoBlock.iconImage(@DrawableRes res: Int) = apply {
iconImage = MessageBottomSheetUMV2.IconImage(res)
}
fun MessageBottomSheetUMV2.InfoBlock.chip(text: TextReference, init: MessageBottomSheetUMV2.Chip.() -> Unit = {}) =
apply {
chip = MessageBottomSheetUMV2.Chip(text).apply(init)

View file

@ -1,10 +1,10 @@
package com.tangem.core.ui.components.bottomsheets.message
import androidx.compose.foundation.Image
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material3.Icon
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember
@ -25,6 +25,7 @@ import com.tangem.core.ui.components.bottomsheets.modal.TangemModalBottomSheetTi
import com.tangem.core.ui.components.buttons.common.TangemButton
import com.tangem.core.ui.components.buttons.common.TangemButtonIconPosition
import com.tangem.core.ui.components.buttons.common.TangemButtonsDefaults
import com.tangem.core.ui.components.icons.HighlightedIcon
import com.tangem.core.ui.extensions.TextReference
import com.tangem.core.ui.extensions.resolveReference
import com.tangem.core.ui.res.TangemTheme
@ -91,9 +92,7 @@ fun MessageBottomSheetV2Content(state: MessageBottomSheetUMV2, modifier: Modifie
@Composable
private fun ContentContainer(state: MessageBottomSheetUMV2.InfoBlock, modifier: Modifier = Modifier) {
Column(modifier = modifier, horizontalAlignment = Alignment.CenterHorizontally) {
state.icon?.let {
BottomSheetIcon(it)
}
BottomSheetIconContainer(state.icon, state.iconImage)
state.title?.let { title ->
Text(
modifier = Modifier
@ -125,6 +124,26 @@ private fun ContentContainer(state: MessageBottomSheetUMV2.InfoBlock, modifier:
}
}
@Suppress("CanBeNonNullable")
@Composable
private fun BottomSheetIconContainer(
icon: MessageBottomSheetUMV2.Icon?,
iconImage: MessageBottomSheetUMV2.IconImage?,
modifier: Modifier = Modifier,
) {
if (icon != null) {
BottomSheetIcon(icon, modifier)
} else if (iconImage != null) {
Image(
modifier = modifier
.size(TangemTheme.dimens.size56)
.clip(CircleShape),
painter = painterResource(id = iconImage.res),
contentDescription = null,
)
}
}
@Composable
private fun BottomSheetIcon(icon: MessageBottomSheetUMV2.Icon, modifier: Modifier = Modifier) {
val tint = when (icon.type) {
@ -144,20 +163,11 @@ private fun BottomSheetIcon(icon: MessageBottomSheetUMV2.Icon, modifier: Modifie
MessageBottomSheetUMV2.Icon.BackgroundType.Warning -> TangemTheme.colors.icon.warning
}
Box(
modifier = modifier
.size(TangemTheme.dimens.size56)
.clip(CircleShape)
.background(backgroundColor.copy(alpha = 0.1F)),
contentAlignment = Alignment.Center,
content = {
Icon(
modifier = Modifier.size(TangemTheme.dimens.size32),
painter = painterResource(icon.res),
contentDescription = null,
tint = tint,
)
},
HighlightedIcon(
modifier = modifier,
icon = icon.res,
iconTint = tint,
backgroundColor = backgroundColor,
)
}
@ -248,4 +258,33 @@ private fun Preview() {
onDismissRequest = {},
)
}
}
@Preview
@Composable
private fun Preview2() {
TangemThemePreview {
MessageBottomSheetV2(
messageBottomSheetUM {
infoBlock {
iconImage = MessageBottomSheetUMV2.IconImage(R.drawable.img_visa_notification)
title = TextReference.Str("Title Title Title")
body = TextReference.Str("Body")
chip(text = TextReference.Str("Some chip information"))
}
primaryButton {
text = TextReference.Str("Test")
icon = R.drawable.ic_tangem_24
}
secondaryButton {
icon = R.drawable.ic_tangem_24
text = TextReference.Str("asdasd")
onClick {
closeBs()
}
}
},
onDismissRequest = {},
)
}
}

View file

@ -0,0 +1,6 @@
package com.tangem.core.ui.components.bottomsheets.state
enum class BottomSheetState {
EXPANDED,
COLLAPSED,
}

View file

@ -39,8 +39,8 @@ fun PinTextField(
pinTextColor: PinTextColor,
onValueChange: (String) -> Unit,
modifier: Modifier = Modifier,
focusRequester: FocusRequester = remember { FocusRequester() },
) {
val focusRequester = remember { FocusRequester() }
val textFieldValue = remember(value) {
TextFieldValue(value, selection = TextRange(value.length))
}

View file

@ -0,0 +1,39 @@
package com.tangem.core.ui.components.icons
import androidx.annotation.DrawableRes
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.material3.Icon
import androidx.compose.runtime.Composable
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.res.painterResource
import com.tangem.core.ui.res.TangemTheme
@Composable
fun HighlightedIcon(
@DrawableRes icon: Int,
iconTint: Color,
modifier: Modifier = Modifier,
backgroundColor: Color = iconTint,
) {
Box(
modifier = modifier
.size(TangemTheme.dimens.size56)
.clip(CircleShape)
.background(backgroundColor.copy(alpha = 0.1F)),
contentAlignment = Alignment.Center,
content = {
Icon(
modifier = Modifier.size(TangemTheme.dimens.size32),
painter = painterResource(icon),
contentDescription = null,
tint = iconTint,
)
},
)
}

View file

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

View file

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

View file

@ -296,6 +296,7 @@ private fun SinglePrimaryButton(config: NotificationButtonsState.PrimaryButtonCo
modifier = Modifier.fillMaxWidth(),
size = TangemButtonSize.WideAction,
enabled = isEnabled,
showProgress = config.shouldShowProgress,
)
} else {
PrimaryButton(
@ -304,6 +305,7 @@ private fun SinglePrimaryButton(config: NotificationButtonsState.PrimaryButtonCo
modifier = Modifier.fillMaxWidth(),
size = TangemButtonSize.WideAction,
enabled = isEnabled,
showProgress = config.shouldShowProgress,
)
}
}

View file

@ -38,6 +38,7 @@ data class NotificationConfig(
val additionalText: TextReference? = null,
@DrawableRes val iconResId: Int? = null,
val onClick: () -> Unit,
val shouldShowProgress: Boolean = false,
) : ButtonsState()
data class SecondaryButtonConfig(

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -1,7 +1,12 @@
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.DateTimeZone
import org.joda.time.format.DateTimeFormat
import org.joda.time.format.DateTimeFormatter
import org.joda.time.format.DateTimeFormatterBuilder
@ -80,6 +85,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"
*/
@ -87,6 +99,23 @@ object DateTimeFormatters {
getBestFormatterBySkeleton("dd.MM.yyyy HH:mm")
}
/**
* Local full date formatter (e.g., "dd MMMM, HH:mm")
*/
val localFullDate: DateTimeFormatter by lazy {
DateTimeFormatterBuilder()
.appendDayOfMonth(2)
.appendLiteral(' ')
.appendMonthOfYearText()
.appendLiteral(", ")
.appendHourOfDay(2)
.appendLiteral(':')
.appendMinuteOfHour(2)
.toFormatter()
.withLocale(Locale.getDefault())
.withZone(DateTimeZone.getDefault())
}
fun formatDate(date: DateTime, formatter: DateTimeFormatter = dateFormatter): String {
return formatter.print(date)
}

View file

@ -37,4 +37,51 @@ fun Long.toTimeFormat(formatter: DateTimeFormatter = DateTimeFormatters.timeForm
*/
fun Long.formatAsDateTime(formatter: DateTimeFormatter): String {
return DateTimeFormatters.formatDate(date = DateTime(this, DateTimeZone.getDefault()), formatter = formatter)
}
/**
* Parses an ISO 8601 date string and compares it to the current UTC time.
*
* @param now The current date to compare against.
* @return A [FormattedDate] subclass.
*/
@Suppress("MagicNumber")
fun getFormattedDate(createdAt: String, now: DateTime): FormattedDate {
val pastDateUtc = try {
DateTime.parse(createdAt)
} catch (_: Exception) {
return FormattedDate.FullDate(createdAt)
}
val pastDateLocal = pastDateUtc.withZone(DateTimeZone.getDefault())
val isToday = pastDateLocal.isToday()
val diffInMillis = now.millis - pastDateUtc.millis
val diffInMinutes = diffInMillis / (1000 * 60)
val diffInHours = diffInMillis / (1000 * 60 * 60)
return when {
diffInMinutes < 1 -> FormattedDate.MinutesAgo(1)
diffInMinutes < 60 -> FormattedDate.MinutesAgo(diffInMinutes.toInt())
diffInHours < 12 && isToday -> FormattedDate.HoursAgo(diffInHours.toInt())
isToday -> {
val timeString = DateTimeFormatters.timeFormatter.print(pastDateLocal)
FormattedDate.Today(timeString)
}
else -> {
val dateString = DateTimeFormatters.localFullDate.print(pastDateLocal)
FormattedDate.FullDate(dateString)
}
}
}
/**
* Representing different formatted date representations.
*/
sealed class FormattedDate {
data class MinutesAgo(val minutes: Int) : FormattedDate()
data class HoursAgo(val hours: Int) : FormattedDate()
data class Today(val time: String) : FormattedDate()
data class FullDate(val date: String) : FormattedDate()
}

View file

@ -0,0 +1,13 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android" android:height="24dp" android:viewportHeight="40" android:viewportWidth="40" android:width="24dp">
<path android:fillColor="#000000" android:pathData="M15.849,26.896C16.954,26.896 17.849,27.728 17.85,28.753V33.615C17.849,34.641 16.954,35.473 15.849,35.473C14.745,35.473 13.85,34.641 13.849,33.615V28.753C13.85,27.727 14.745,26.896 15.849,26.896Z"/>
<path android:fillColor="#000000" android:pathData="M32.454,20.958C33.558,20.958 34.454,21.819 34.454,22.879V33.552C34.454,34.612 33.558,35.473 32.454,35.473C31.35,35.473 30.454,34.612 30.454,33.552V22.879C30.454,21.818 31.349,20.958 32.454,20.958Z"/>
<path android:fillColor="#000000" android:pathData="M7.547,29.467C8.651,29.467 9.547,30.362 9.547,31.467V33.472C9.547,34.576 8.651,35.472 7.547,35.472C6.442,35.472 5.547,34.576 5.547,33.472V31.467C5.547,30.362 6.442,29.467 7.547,29.467Z"/>
<path android:fillColor="#000000" android:pathData="M24.151,23.976C25.256,23.976 26.151,24.755 26.151,25.716V33.732C26.151,34.693 25.256,35.472 24.151,35.472C23.047,35.472 22.151,34.693 22.151,33.732V25.716C22.152,24.755 23.047,23.976 24.151,23.976Z"/>
<path android:fillColor="#000000" android:pathData="M29.46,4.062C29.927,4.036 30.392,4.174 30.77,4.457C31.203,4.781 31.486,5.266 31.556,5.801L32.671,14.396C32.813,15.491 32.04,16.495 30.944,16.637C29.849,16.779 28.846,16.006 28.704,14.911L28.228,11.251C26.87,12.973 25.156,14.988 23.389,16.644C19.165,20.601 14.718,22.22 9.224,21.976C8.12,21.927 7.265,20.992 7.314,19.889C7.363,18.786 8.298,17.931 9.401,17.98C13.802,18.175 17.202,16.958 20.653,13.725C22.214,12.263 23.781,10.43 25.071,8.796L20.395,9.536C19.305,9.709 18.28,8.965 18.107,7.874C17.935,6.784 18.679,5.759 19.769,5.586L29.259,4.083L29.46,4.062Z"/>
</vector>

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

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

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