Updated on 2026-08-14
This commit is contained in:
parent
330ae73357
commit
e7efe7b76a
4636 changed files with 234864 additions and 63507 deletions
|
|
@ -1,48 +1,25 @@
|
|||
plugins {
|
||||
id("com.android.library")
|
||||
kotlin("android")
|
||||
kotlin("kapt")
|
||||
id("com.google.dagger.hilt.android")
|
||||
}
|
||||
|
||||
android {
|
||||
defaultConfig {
|
||||
compileSdk = AppConfig.compileSdkVersion
|
||||
minSdk = AppConfig.minSdkVersion
|
||||
targetSdk = AppConfig.targetSdkVersion
|
||||
}
|
||||
|
||||
kotlinOptions {
|
||||
jvmTarget = JavaVersion.VERSION_1_8.toString()
|
||||
}
|
||||
|
||||
compileOptions {
|
||||
sourceCompatibility = JavaVersion.VERSION_1_8
|
||||
targetCompatibility = JavaVersion.VERSION_1_8
|
||||
isCoreLibraryDesugaringEnabled = false
|
||||
}
|
||||
|
||||
buildTypes {
|
||||
create("debug_beta") {
|
||||
initWith(getByName("release"))
|
||||
BuildConfigFieldFactory(
|
||||
fields = listOf(
|
||||
Field.Environment("release"),
|
||||
Field.TestActionEnabled(true),
|
||||
Field.LogEnabled(true),
|
||||
),
|
||||
builder = ::buildConfigField,
|
||||
).create()
|
||||
}
|
||||
}
|
||||
alias(deps.plugins.kotlin.jvm)
|
||||
alias(deps.plugins.kotlin.kapt)
|
||||
id("configuration")
|
||||
}
|
||||
|
||||
dependencies {
|
||||
|
||||
/** DI */
|
||||
implementation(Library.hilt)
|
||||
kapt(Library.hiltKapt)
|
||||
implementation(deps.hilt.core)
|
||||
kapt(deps.hilt.kapt)
|
||||
|
||||
/** Core shouldn't depends on core, but in case with utils and logging its necessary */
|
||||
implementation(project(":core:utils"))
|
||||
/** Analytics - Models */
|
||||
api(projects.core.analytics.models)
|
||||
|
||||
/** Domain */
|
||||
implementation(projects.domain.analytics)
|
||||
implementation(projects.domain.models)
|
||||
|
||||
/** Other */
|
||||
implementation(deps.kotlin.coroutines)
|
||||
|
||||
/** Core shouldn't depend on core, but in case with utils and logging its necessary */
|
||||
implementation(projects.core.utils)
|
||||
}
|
||||
1
core/analytics/models/.gitignore
vendored
Normal file
1
core/analytics/models/.gitignore
vendored
Normal file
|
|
@ -0,0 +1 @@
|
|||
/build
|
||||
4
core/analytics/models/build.gradle.kts
Normal file
4
core/analytics/models/build.gradle.kts
Normal file
|
|
@ -0,0 +1,4 @@
|
|||
plugins {
|
||||
alias(deps.plugins.kotlin.jvm)
|
||||
id("configuration")
|
||||
}
|
||||
|
|
@ -1,4 +1,4 @@
|
|||
package com.tangem.core.analytics
|
||||
package com.tangem.core.analytics.models
|
||||
|
||||
/**
|
||||
[REDACTED_AUTHOR]
|
||||
|
|
@ -8,5 +8,7 @@ open class AnalyticsEvent(
|
|||
val event: String,
|
||||
var params: Map<String, String> = mapOf(),
|
||||
val error: Throwable? = null,
|
||||
var filterData: Any? = null,
|
||||
)
|
||||
) {
|
||||
|
||||
val id: String = "[$category] $event"
|
||||
}
|
||||
|
|
@ -0,0 +1,214 @@
|
|||
package com.tangem.core.analytics.models
|
||||
|
||||
sealed class AnalyticsParam {
|
||||
|
||||
sealed class CardBalanceState(val value: String) {
|
||||
data object Empty : CardBalanceState("Empty")
|
||||
data object Full : CardBalanceState("Full")
|
||||
data object CustomToken : CardBalanceState("Custom Token")
|
||||
data object BlockchainError : CardBalanceState("Blockchain Error")
|
||||
data object NoRate : CardBalanceState("No Rate")
|
||||
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")
|
||||
data object Closed : RateApp("Close")
|
||||
}
|
||||
|
||||
enum class OnOffState(val value: String) {
|
||||
On("On"),
|
||||
Off("Off"),
|
||||
;
|
||||
|
||||
companion object {
|
||||
|
||||
fun from(enabled: Boolean): String {
|
||||
val state = if (enabled) On else Off
|
||||
|
||||
return state.value
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
sealed class OrganizeSortType(val value: String) {
|
||||
data object ByBalance : OrganizeSortType("By Balance")
|
||||
data object Manually : OrganizeSortType("Manually")
|
||||
}
|
||||
|
||||
sealed class UserCode(val value: String) {
|
||||
data object AccessCode : UserCode("Access Code")
|
||||
data object Passcode : UserCode("Passcode")
|
||||
}
|
||||
|
||||
sealed class AccessCodeRecoveryStatus(val value: String) {
|
||||
|
||||
val key: String = "Status"
|
||||
|
||||
data object Enabled : AccessCodeRecoveryStatus("Enabled")
|
||||
data object Disabled : AccessCodeRecoveryStatus("Disabled")
|
||||
|
||||
companion object {
|
||||
fun from(enabled: Boolean): AccessCodeRecoveryStatus {
|
||||
return if (enabled) Enabled else Disabled
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
sealed class Error(val value: String) {
|
||||
data object App : Error("App Error")
|
||||
data object CardSdk : Error("Card Sdk Error")
|
||||
data object BlockchainSdk : Error("Blockchain Sdk Error")
|
||||
}
|
||||
|
||||
sealed class ScreensSources(val value: String) {
|
||||
data object Settings : ScreensSources("Settings")
|
||||
data object Main : ScreensSources("Main")
|
||||
data object SignIn : ScreensSources("Sign In")
|
||||
data object Send : ScreensSources("Send")
|
||||
data object Intro : ScreensSources("Introduction")
|
||||
data object MyWallets : ScreensSources("My Wallets")
|
||||
data object Token : ScreensSources("Token")
|
||||
data object Stories : ScreensSources("Stories")
|
||||
data object Buy : ScreensSources("Buy")
|
||||
data object Swap : ScreensSources("Swap")
|
||||
data object Sell : ScreensSources("Sell")
|
||||
data object Backup : ScreensSources("Backup")
|
||||
data object Onboarding : ScreensSources("Onboarding")
|
||||
data object LongTap : ScreensSources("Long Tap")
|
||||
data object Markets : ScreensSources("Markets")
|
||||
}
|
||||
|
||||
sealed class TxSentFrom(val value: String) {
|
||||
data class Send(
|
||||
override val blockchain: String,
|
||||
override val token: String,
|
||||
override val feeType: FeeType,
|
||||
) : TxSentFrom("Send"), TxData
|
||||
|
||||
data class Swap(
|
||||
override val blockchain: String,
|
||||
override val token: String,
|
||||
override val feeType: FeeType,
|
||||
) : TxSentFrom("Swap"), TxData
|
||||
|
||||
data class Staking(
|
||||
override val blockchain: String,
|
||||
override val token: String,
|
||||
override val feeType: FeeType,
|
||||
) : TxSentFrom("Staking"), TxData
|
||||
|
||||
data class Approve(
|
||||
override val blockchain: String,
|
||||
override val token: String,
|
||||
override val feeType: FeeType,
|
||||
val permissionType: String,
|
||||
) : TxSentFrom("Approve"), TxData
|
||||
|
||||
data object WalletConnect : TxSentFrom("WalletConnect")
|
||||
data object Sell : TxSentFrom("Sell")
|
||||
}
|
||||
|
||||
sealed interface TxData {
|
||||
val blockchain: String
|
||||
val token: String
|
||||
val feeType: FeeType
|
||||
}
|
||||
|
||||
sealed class FeeType(val value: String) {
|
||||
data object Fixed : FeeType("Fixed")
|
||||
data object Min : FeeType("Min")
|
||||
data object Normal : FeeType("Normal")
|
||||
data object Max : FeeType("Max")
|
||||
data object Custom : FeeType("Custom")
|
||||
|
||||
companion object {
|
||||
fun fromString(feeType: String): FeeType {
|
||||
return when (feeType) {
|
||||
Min.value -> Min
|
||||
Normal.value -> Normal
|
||||
Max.value -> Max
|
||||
Fixed.value -> Fixed
|
||||
else -> Fixed
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
sealed class WalletCreationType(val value: String) {
|
||||
data object PrivateKey : WalletCreationType("Private key")
|
||||
data object NewSeed : WalletCreationType("New seed")
|
||||
data object SeedImport : WalletCreationType("Seed import")
|
||||
}
|
||||
|
||||
sealed class WalletType(val value: String) {
|
||||
data object MultiCurrency : WalletType(value = "Multicurrency")
|
||||
class SingleCurrency(currencyName: String) : WalletType(currencyName)
|
||||
}
|
||||
|
||||
enum class Validation(val value: String) {
|
||||
|
||||
OK(value = "Ok"),
|
||||
ERROR(value = "Error"),
|
||||
;
|
||||
|
||||
companion object {
|
||||
|
||||
fun from(isValid: Boolean): String {
|
||||
val status = if (isValid) OK else ERROR
|
||||
|
||||
return status.value
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
enum class Status(val value: String) {
|
||||
Success(value = "Success"),
|
||||
Error(value = "Error"),
|
||||
Pending(value = "Pending"),
|
||||
}
|
||||
|
||||
companion object Key {
|
||||
const val BLOCKCHAIN = "blockchain"
|
||||
const val TOKEN_PARAM = "Token"
|
||||
const val SOURCE = "Source"
|
||||
const val BALANCE = "Balance"
|
||||
const val TOKENS_COUNT = "Tokens Count"
|
||||
const val STATE = "State"
|
||||
const val BATCH = "Batch"
|
||||
const val TYPE = "Type"
|
||||
const val FEE_TYPE = "Fee Type"
|
||||
const val WALLET_FORM = "WalletForm"
|
||||
const val PERMISSION_TYPE = "Permission Type"
|
||||
const val PRODUCT_TYPE = "Product Type"
|
||||
const val FIRMWARE = "Firmware"
|
||||
const val CURRENCY = "Currency"
|
||||
const val ERROR_DESCRIPTION = "Error Description"
|
||||
const val ERROR_CODE = "Error Code"
|
||||
const val ERROR_KEY = "Error Key"
|
||||
const val ERROR_TYPE = "Error Type"
|
||||
const val ERROR_MESSAGE = "Error Message"
|
||||
const val CREATION_TYPE = "Creation type"
|
||||
const val DAPP_NAME = "DApp Name"
|
||||
const val DAPP_URL = "DApp Url"
|
||||
const val METHOD_NAME = "Method Name"
|
||||
const val VALIDATION = "Validation"
|
||||
const val BLOCKCHAIN_EXCEPTION_HOST = "exception_host"
|
||||
const val BLOCKCHAIN_SELECTED_HOST = "selected_host"
|
||||
const val INPUT = "Input"
|
||||
const val COUNT = "Count"
|
||||
const val DERIVATION = "Derivation"
|
||||
const val STATUS = "Status"
|
||||
const val PROVIDER = "Provider"
|
||||
const val PLACE = "Place"
|
||||
const val RESIDENCE = "Residence"
|
||||
const val PAYMENT_METHOD = "Payment Method"
|
||||
const val WATCHED = "Watched"
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,87 @@
|
|||
package com.tangem.core.analytics.models
|
||||
|
||||
sealed class Basic(
|
||||
event: String,
|
||||
params: Map<String, String> = mapOf(),
|
||||
error: Throwable? = null,
|
||||
) : AnalyticsEvent("Basic", event, params, error) {
|
||||
|
||||
class CardWasScanned(
|
||||
source: AnalyticsParam.ScreensSources,
|
||||
) : Basic(
|
||||
event = "Card Was Scanned",
|
||||
params = mapOf(
|
||||
AnalyticsParam.SOURCE to source.value,
|
||||
),
|
||||
)
|
||||
|
||||
class SignedIn(
|
||||
currency: AnalyticsParam.WalletType,
|
||||
batch: String,
|
||||
signInType: SignInType,
|
||||
walletsCount: String,
|
||||
hasBackup: Boolean?,
|
||||
) : Basic(
|
||||
event = "Signed in",
|
||||
params = buildMap {
|
||||
put(AnalyticsParam.CURRENCY, currency.value)
|
||||
put(AnalyticsParam.BATCH, batch)
|
||||
put("Sign in type", signInType.name)
|
||||
put("Wallets Count", walletsCount)
|
||||
if (hasBackup != null) {
|
||||
put("Backuped", if (hasBackup) "Yes" else "No")
|
||||
}
|
||||
},
|
||||
) {
|
||||
enum class SignInType {
|
||||
Card, Biometric
|
||||
}
|
||||
}
|
||||
|
||||
class ToppedUp(userWalletId: String, currency: AnalyticsParam.WalletType) :
|
||||
Basic(
|
||||
event = "Topped up",
|
||||
params = mapOf(AnalyticsParam.CURRENCY to currency.value),
|
||||
),
|
||||
OneTimeAnalyticsEvent {
|
||||
|
||||
override val oneTimeEventId: String = id + userWalletId
|
||||
}
|
||||
|
||||
class TransactionSent(sentFrom: AnalyticsParam.TxSentFrom, memoType: MemoType) :
|
||||
Basic(
|
||||
event = "Transaction sent",
|
||||
params = buildMap {
|
||||
this[AnalyticsParam.SOURCE] = sentFrom.value
|
||||
if (sentFrom is AnalyticsParam.TxData) {
|
||||
this[AnalyticsParam.BLOCKCHAIN] = sentFrom.blockchain
|
||||
this[AnalyticsParam.TOKEN_PARAM] = sentFrom.token
|
||||
this[AnalyticsParam.FEE_TYPE] = sentFrom.feeType.value
|
||||
}
|
||||
if (sentFrom is AnalyticsParam.TxSentFrom.Approve) {
|
||||
this[AnalyticsParam.PERMISSION_TYPE] = sentFrom.permissionType
|
||||
}
|
||||
this["Memo"] = memoType.name
|
||||
},
|
||||
) {
|
||||
enum class MemoType {
|
||||
Empty, Full, Null
|
||||
}
|
||||
|
||||
enum class WalletForm {
|
||||
Card, Ring
|
||||
}
|
||||
}
|
||||
|
||||
class ScanError(error: Throwable) : Basic(
|
||||
event = "Scan",
|
||||
error = error,
|
||||
)
|
||||
|
||||
class ButtonSupport(source: AnalyticsParam.ScreensSources) : Basic(
|
||||
event = "Request Support",
|
||||
params = mapOf(
|
||||
AnalyticsParam.SOURCE to source.value,
|
||||
),
|
||||
)
|
||||
}
|
||||
|
|
@ -0,0 +1,6 @@
|
|||
package com.tangem.core.analytics.models
|
||||
|
||||
interface OneTimeAnalyticsEvent {
|
||||
|
||||
val oneTimeEventId: String
|
||||
}
|
||||
|
|
@ -0,0 +1,86 @@
|
|||
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.AnalyticsParam.Key.ERROR_CODE
|
||||
import com.tangem.core.analytics.models.AnalyticsParam.Key.TOKEN_PARAM
|
||||
|
||||
/**
|
||||
* Main screen analytics event
|
||||
*
|
||||
* @param event event name
|
||||
* @param params params
|
||||
*/
|
||||
sealed class MainScreenAnalyticsEvent(
|
||||
event: String,
|
||||
params: Map<String, String> = mapOf(),
|
||||
) : AnalyticsEvent(category = "Main Screen", event = event, params = params) {
|
||||
|
||||
class EnableBiometrics(state: AnalyticsParam.OnOffState) : MainScreenAnalyticsEvent(
|
||||
event = "Enable Biometric",
|
||||
params = mapOf("State" to state.value),
|
||||
)
|
||||
|
||||
// region Action Buttons feature
|
||||
data class ButtonBuy(val status: AnalyticsParam.Status) : MainScreenAnalyticsEvent(
|
||||
event = "Button - Buy",
|
||||
params = mapOf(AnalyticsParam.STATUS to status.value),
|
||||
)
|
||||
|
||||
data class ButtonSwap(val status: AnalyticsParam.Status) : MainScreenAnalyticsEvent(
|
||||
event = "Button - Swap",
|
||||
params = mapOf(AnalyticsParam.STATUS to status.value),
|
||||
)
|
||||
|
||||
data class ButtonSell(val status: AnalyticsParam.Status) : MainScreenAnalyticsEvent(
|
||||
event = "Button - Sell",
|
||||
params = mapOf(AnalyticsParam.STATUS to status.value),
|
||||
)
|
||||
|
||||
data object BuyScreenOpened : MainScreenAnalyticsEvent(event = "Buy Screen Opened")
|
||||
|
||||
data object SwapScreenOpened : MainScreenAnalyticsEvent(event = "Swap Screen Opened")
|
||||
|
||||
data object SellScreenOpened : MainScreenAnalyticsEvent(event = "Sell Screen Opened")
|
||||
|
||||
data class BuyTokenClicked(val currencySymbol: String) : MainScreenAnalyticsEvent(
|
||||
event = "Buy Token Clicked",
|
||||
params = mapOf(TOKEN_PARAM to currencySymbol),
|
||||
)
|
||||
|
||||
data class SellTokenClicked(val currencySymbol: String) : MainScreenAnalyticsEvent(
|
||||
event = "Sell Token Clicked",
|
||||
params = mapOf(TOKEN_PARAM to currencySymbol),
|
||||
)
|
||||
|
||||
data class SwapTokenClicked(val currencySymbol: String) : MainScreenAnalyticsEvent(
|
||||
event = "Swap Token Clicked",
|
||||
params = mapOf(TOKEN_PARAM to currencySymbol),
|
||||
)
|
||||
|
||||
data class ReceiveTokenClicked(val currencySymbol: String) : MainScreenAnalyticsEvent(
|
||||
event = "Receive Token Clicked",
|
||||
params = mapOf(TOKEN_PARAM to currencySymbol),
|
||||
)
|
||||
|
||||
data class RemoveTokenClicked(val currencySymbol: String) : MainScreenAnalyticsEvent(
|
||||
event = "Remove Button Clicked",
|
||||
params = mapOf(TOKEN_PARAM to currencySymbol),
|
||||
)
|
||||
|
||||
data class ButtonClose(val source: AnalyticsParam.ScreensSources) : MainScreenAnalyticsEvent(
|
||||
event = "Button - Close",
|
||||
params = mapOf(AnalyticsParam.SOURCE to source.value),
|
||||
)
|
||||
|
||||
data class HotTokenClicked(val currencySymbol: String) : MainScreenAnalyticsEvent(
|
||||
event = "Hot Token Clicked",
|
||||
params = mapOf(TOKEN_PARAM to currencySymbol),
|
||||
)
|
||||
|
||||
data class HotTokenError(val errorCode: String) : MainScreenAnalyticsEvent(
|
||||
event = "Hot Token Error",
|
||||
params = mapOf(ERROR_CODE to errorCode),
|
||||
)
|
||||
// endregion
|
||||
}
|
||||
|
|
@ -0,0 +1,24 @@
|
|||
package com.tangem.core.analytics.models.event
|
||||
|
||||
import com.tangem.core.analytics.models.AnalyticsEvent
|
||||
import com.tangem.core.analytics.models.AnalyticsParam
|
||||
|
||||
sealed class OnboardingAnalyticsEvent(
|
||||
category: String,
|
||||
event: String,
|
||||
params: Map<String, String> = mapOf(),
|
||||
) : AnalyticsEvent(category, event, params) {
|
||||
|
||||
sealed class Onboarding(
|
||||
event: String,
|
||||
params: Map<String, String> = mapOf(),
|
||||
) : OnboardingAnalyticsEvent(category = "Error", event = event, params = params) {
|
||||
|
||||
data class OfflineAttestationFailed(
|
||||
val source: AnalyticsParam.ScreensSources,
|
||||
) : Onboarding(
|
||||
event = "Offline Attestation Failed",
|
||||
params = mapOf(AnalyticsParam.SOURCE to source.value),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,38 @@
|
|||
package com.tangem.core.analytics.models.event
|
||||
|
||||
import com.tangem.core.analytics.models.AnalyticsEvent
|
||||
|
||||
/**
|
||||
* Tech analytics event
|
||||
*
|
||||
* @param event event name
|
||||
* @param params params
|
||||
*/
|
||||
sealed class TechAnalyticsEvent(
|
||||
event: String,
|
||||
params: Map<String, String> = mapOf(),
|
||||
) : AnalyticsEvent(category = "Tech", event = event, params = params) {
|
||||
|
||||
class WindowObscured(state: ObscuredState) : TechAnalyticsEvent(
|
||||
event = "Window Obscured",
|
||||
params = mapOf("State" to state.name),
|
||||
) {
|
||||
|
||||
enum class ObscuredState {
|
||||
PARTIALLY,
|
||||
FULLY,
|
||||
}
|
||||
}
|
||||
|
||||
class KeyboardIdentifier(id: String, packageName: String?, isTrusted: Boolean) : TechAnalyticsEvent(
|
||||
event = "Keyboard Identifier",
|
||||
params = buildMap {
|
||||
put("Id", id)
|
||||
packageName?.let {
|
||||
put("Package", it)
|
||||
put("GPUrl", "https://play.google.com/store/apps/details?id=$packageName")
|
||||
}
|
||||
put("isTrusted", isTrusted.toString())
|
||||
},
|
||||
)
|
||||
}
|
||||
|
|
@ -1,2 +0,0 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<manifest package="com.tangem.core.analytics" />
|
||||
|
|
@ -1,24 +1,19 @@
|
|||
package com.tangem.core.analytics
|
||||
|
||||
import com.tangem.core.analytics.api.AnalyticsEventFilter
|
||||
import com.tangem.core.analytics.api.AnalyticsEventHandler
|
||||
import com.tangem.core.analytics.api.AnalyticsFilterHolder
|
||||
import com.tangem.core.analytics.api.AnalyticsHandler
|
||||
import com.tangem.core.analytics.api.AnalyticsHandlerHolder
|
||||
import com.tangem.core.analytics.api.ParamsInterceptor
|
||||
import com.tangem.core.analytics.api.ParamsInterceptorHolder
|
||||
import com.tangem.core.analytics.api.*
|
||||
import com.tangem.core.analytics.models.AnalyticsEvent
|
||||
import com.tangem.utils.coroutines.FeatureCoroutineExceptionHandler
|
||||
import kotlinx.coroutines.CoroutineName
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.SupervisorJob
|
||||
import kotlinx.coroutines.asCoroutineDispatcher
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.*
|
||||
import kotlinx.coroutines.sync.Mutex
|
||||
import kotlinx.coroutines.sync.withLock
|
||||
import java.util.concurrent.ConcurrentHashMap
|
||||
import java.util.concurrent.Executors
|
||||
|
||||
/**
|
||||
[REDACTED_AUTHOR]
|
||||
*/
|
||||
interface GlobalAnalyticsEventHandler : AnalyticsEventHandler,
|
||||
interface GlobalAnalyticsEventHandler :
|
||||
AnalyticsEventHandler,
|
||||
AnalyticsHandlerHolder,
|
||||
AnalyticsFilterHolder,
|
||||
ParamsInterceptorHolder
|
||||
|
|
@ -28,8 +23,9 @@ object Analytics : GlobalAnalyticsEventHandler {
|
|||
private val analyticsScope: CoroutineScope by lazy { createScope() }
|
||||
|
||||
private val handlers = mutableMapOf<String, AnalyticsHandler>()
|
||||
private val paramsInterceptors = mutableMapOf<String, ParamsInterceptor>()
|
||||
private val paramsInterceptors = ConcurrentHashMap<String, ParamsInterceptor>()
|
||||
private val analyticsFilters = mutableSetOf<AnalyticsEventFilter>()
|
||||
private val analyticsMutex = Mutex()
|
||||
|
||||
private val analyticsHandlers: List<AnalyticsHandler>
|
||||
get() = handlers.values.toList()
|
||||
|
|
@ -54,8 +50,8 @@ object Analytics : GlobalAnalyticsEventHandler {
|
|||
paramsInterceptors[interceptor.id()] = interceptor
|
||||
}
|
||||
|
||||
override fun removeParamsInterceptor(interceptor: ParamsInterceptor): ParamsInterceptor? {
|
||||
return paramsInterceptors.remove(interceptor.id())
|
||||
override fun removeParamsInterceptor(interceptorId: String): ParamsInterceptor? {
|
||||
return paramsInterceptors.remove(interceptorId)
|
||||
}
|
||||
|
||||
override fun send(event: AnalyticsEvent) {
|
||||
|
|
@ -63,23 +59,26 @@ object Analytics : GlobalAnalyticsEventHandler {
|
|||
event.params = applyParamsInterceptors(event)
|
||||
val eventFilter = analyticsFilters.firstOrNull { it.canBeAppliedTo(event) }
|
||||
|
||||
when {
|
||||
eventFilter == null -> analyticsHandlers.forEach { handler -> handler.send(event) }
|
||||
eventFilter.canBeSent(event) -> {
|
||||
analyticsHandlers
|
||||
.filter { handler -> eventFilter.canBeConsumedByHandler(handler, event) }
|
||||
.forEach { handler -> handler.send(event) }
|
||||
analyticsMutex.withLock {
|
||||
when {
|
||||
eventFilter == null -> analyticsHandlers.forEach { handler -> handler.send(event) }
|
||||
eventFilter.canBeSent(event) -> {
|
||||
analyticsHandlers
|
||||
.filter { handler -> eventFilter.canBeConsumedByHandler(handler, event) }
|
||||
.forEach { handler -> handler.send(event) }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun applyParamsInterceptors(event: AnalyticsEvent): MutableMap<String, String> {
|
||||
private suspend fun applyParamsInterceptors(event: AnalyticsEvent): MutableMap<String, String> {
|
||||
val interceptedParams = event.params.toMutableMap()
|
||||
paramsInterceptors.values
|
||||
.filter { it.canBeAppliedTo(event) }
|
||||
.forEach { it.intercept(interceptedParams) }
|
||||
|
||||
analyticsMutex.withLock {
|
||||
paramsInterceptors.values
|
||||
.filter { it.canBeAppliedTo(event) }
|
||||
.forEach { it.intercept(interceptedParams) }
|
||||
}
|
||||
return interceptedParams
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,8 @@
|
|||
package com.tangem.core.analytics
|
||||
|
||||
interface AppInstanceIdProvider {
|
||||
|
||||
suspend fun getAppInstanceId(): String?
|
||||
|
||||
fun getAppInstanceIdSync(): String?
|
||||
}
|
||||
|
|
@ -0,0 +1,11 @@
|
|||
package com.tangem.core.analytics
|
||||
|
||||
import com.tangem.core.analytics.api.AnalyticsEventHandler
|
||||
import com.tangem.core.analytics.models.AnalyticsEvent
|
||||
|
||||
class DummyAnalyticsEventHandler : AnalyticsEventHandler {
|
||||
|
||||
override fun send(event: AnalyticsEvent) {
|
||||
/* no-op */
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,12 @@
|
|||
package com.tangem.core.analytics
|
||||
|
||||
class DummyAppInstanceIdProvider : AppInstanceIdProvider {
|
||||
|
||||
override suspend fun getAppInstanceId(): String? {
|
||||
return null
|
||||
}
|
||||
|
||||
override fun getAppInstanceIdSync(): String? {
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
|
@ -4,15 +4,9 @@ package com.tangem.core.analytics.api
|
|||
[REDACTED_AUTHOR]
|
||||
*/
|
||||
interface EventLogger {
|
||||
fun logEvent(
|
||||
event: String,
|
||||
params: Map<String, String> = emptyMap(),
|
||||
)
|
||||
fun logEvent(event: String, params: Map<String, String> = emptyMap())
|
||||
}
|
||||
|
||||
interface ErrorEventLogger {
|
||||
fun logErrorEvent(
|
||||
error: Throwable,
|
||||
params: Map<String, String> = emptyMap(),
|
||||
)
|
||||
fun logErrorEvent(error: Throwable, params: Map<String, String> = emptyMap())
|
||||
}
|
||||
|
|
@ -1,6 +1,6 @@
|
|||
package com.tangem.core.analytics.api
|
||||
|
||||
import com.tangem.core.analytics.AnalyticsEvent
|
||||
import com.tangem.core.analytics.models.AnalyticsEvent
|
||||
|
||||
/**
|
||||
[REDACTED_AUTHOR]
|
||||
|
|
@ -16,7 +16,7 @@ interface AnalyticsEventFilter {
|
|||
* An internal filter check that, on external or internal conditions, recognizes the possibility of
|
||||
* sending an event.
|
||||
*/
|
||||
fun canBeSent(event: AnalyticsEvent): Boolean
|
||||
suspend fun canBeSent(event: AnalyticsEvent): Boolean
|
||||
|
||||
/**
|
||||
* Performs a check to see if the event can be dispatched by a specific handler
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
package com.tangem.core.analytics.api
|
||||
|
||||
import com.tangem.core.analytics.AnalyticsEvent
|
||||
import com.tangem.core.analytics.models.AnalyticsEvent
|
||||
|
||||
/**
|
||||
[REDACTED_AUTHOR]
|
||||
|
|
@ -12,20 +12,15 @@ interface AnalyticsEventHandler {
|
|||
interface AnalyticsHandler : AnalyticsEventHandler {
|
||||
fun id(): String
|
||||
|
||||
fun send(event: String, params: Map<String, String> = emptyMap())
|
||||
fun send(eventId: String, params: Map<String, String> = emptyMap())
|
||||
|
||||
override fun send(event: AnalyticsEvent) {
|
||||
send(prepareEventString(event), event.params)
|
||||
send(event.id, event.params)
|
||||
}
|
||||
|
||||
fun prepareEventString(event: AnalyticsEvent): String = "[${event.category}] ${event.event}"
|
||||
}
|
||||
|
||||
interface ErrorEventHandler {
|
||||
fun send(
|
||||
error: Throwable,
|
||||
params: Map<String, String> = emptyMap(),
|
||||
)
|
||||
fun send(error: Throwable, params: Map<String, String> = emptyMap())
|
||||
}
|
||||
|
||||
interface AnalyticsHandlerHolder {
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
package com.tangem.core.analytics.api
|
||||
|
||||
import com.tangem.core.analytics.AnalyticsEvent
|
||||
import com.tangem.core.analytics.models.AnalyticsEvent
|
||||
|
||||
/**
|
||||
[REDACTED_AUTHOR]
|
||||
|
|
@ -13,5 +13,5 @@ interface ParamsInterceptor {
|
|||
|
||||
interface ParamsInterceptorHolder {
|
||||
fun addParamsInterceptor(interceptor: ParamsInterceptor)
|
||||
fun removeParamsInterceptor(interceptor: ParamsInterceptor): ParamsInterceptor?
|
||||
fun removeParamsInterceptor(interceptorId: String): ParamsInterceptor?
|
||||
}
|
||||
|
|
@ -2,6 +2,9 @@ package com.tangem.core.analytics.di
|
|||
|
||||
import com.tangem.core.analytics.Analytics
|
||||
import com.tangem.core.analytics.api.AnalyticsEventHandler
|
||||
import com.tangem.core.analytics.api.ParamsInterceptorHolder
|
||||
import com.tangem.core.analytics.filter.OneTimeEventFilter
|
||||
import com.tangem.domain.analytics.repository.AnalyticsRepository
|
||||
import dagger.Module
|
||||
import dagger.Provides
|
||||
import dagger.hilt.InstallIn
|
||||
|
|
@ -10,11 +13,22 @@ import javax.inject.Singleton
|
|||
|
||||
@Module
|
||||
@InstallIn(SingletonComponent::class)
|
||||
class AnalyticsModule {
|
||||
internal object AnalyticsModule {
|
||||
|
||||
@Singleton
|
||||
@Provides
|
||||
fun provideAnalyticsHandler(): AnalyticsEventHandler {
|
||||
return Analytics // todo replace after refactoring calling Analytics in whole project
|
||||
}
|
||||
|
||||
@Singleton
|
||||
@Provides
|
||||
fun provideParamsInterceptorHolder(): ParamsInterceptorHolder {
|
||||
return Analytics // todo replace after refactoring calling Analytics in whole project
|
||||
}
|
||||
|
||||
@Provides
|
||||
fun provideOneTimeEventFilter(analyticsRepository: AnalyticsRepository): OneTimeEventFilter {
|
||||
return OneTimeEventFilter(analyticsRepository)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,30 @@
|
|||
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.OneTimeAnalyticsEvent
|
||||
import com.tangem.domain.analytics.repository.AnalyticsRepository
|
||||
|
||||
class OneTimeEventFilter(
|
||||
private val analyticsRepository: AnalyticsRepository,
|
||||
) : AnalyticsEventFilter {
|
||||
|
||||
override fun canBeAppliedTo(event: AnalyticsEvent): Boolean = event is OneTimeAnalyticsEvent
|
||||
|
||||
override suspend fun canBeSent(event: AnalyticsEvent): Boolean {
|
||||
if (event !is OneTimeAnalyticsEvent) return true
|
||||
|
||||
val isSent = analyticsRepository.checkIsEventSent(event.oneTimeEventId)
|
||||
|
||||
if (!isSent) {
|
||||
analyticsRepository.setIsEventSent(event.oneTimeEventId)
|
||||
}
|
||||
|
||||
return !isSent
|
||||
}
|
||||
|
||||
override fun canBeConsumedByHandler(handler: AnalyticsHandler, event: AnalyticsEvent): Boolean {
|
||||
return canBeAppliedTo(event)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,17 @@
|
|||
package com.tangem.core.analytics.utils
|
||||
|
||||
import com.tangem.domain.models.scan.ScanResponse
|
||||
|
||||
/**
|
||||
[REDACTED_AUTHOR]
|
||||
*/
|
||||
interface AnalyticsContextProxy {
|
||||
|
||||
fun setContext(scanResponse: ScanResponse)
|
||||
|
||||
fun eraseContext()
|
||||
|
||||
fun addContext(scanResponse: ScanResponse)
|
||||
|
||||
fun removeContext()
|
||||
}
|
||||
1
core/config-toggles/.gitignore
vendored
Normal file
1
core/config-toggles/.gitignore
vendored
Normal file
|
|
@ -0,0 +1 @@
|
|||
/build
|
||||
37
core/config-toggles/build.gradle.kts
Normal file
37
core/config-toggles/build.gradle.kts
Normal file
|
|
@ -0,0 +1,37 @@
|
|||
import com.tangem.plugin.configuration.configurations.extension.kaptForObfuscatingVariants
|
||||
|
||||
plugins {
|
||||
alias(deps.plugins.android.library)
|
||||
alias(deps.plugins.kotlin.android)
|
||||
alias(deps.plugins.kotlin.kapt)
|
||||
alias(deps.plugins.hilt.android)
|
||||
id("configuration")
|
||||
}
|
||||
|
||||
android {
|
||||
namespace = "com.tangem.core.configtoggle"
|
||||
}
|
||||
|
||||
dependencies {
|
||||
/** DI */
|
||||
implementation(deps.hilt.android)
|
||||
kapt(deps.hilt.kapt)
|
||||
|
||||
/** Local storages */
|
||||
implementation(deps.androidx.datastore)
|
||||
|
||||
/** Other libraries */
|
||||
implementation(deps.moshi)
|
||||
implementation(deps.moshi.kotlin)
|
||||
implementation(deps.timber)
|
||||
kaptForObfuscatingVariants(deps.moshi.kotlin.codegen)
|
||||
|
||||
/** Core modules */
|
||||
implementation(projects.core.datasource)
|
||||
implementation(projects.core.utils)
|
||||
|
||||
testImplementation(deps.test.coroutine)
|
||||
testImplementation(deps.test.junit)
|
||||
testImplementation(deps.test.mockk)
|
||||
testImplementation(deps.test.truth)
|
||||
}
|
||||
|
|
@ -0,0 +1,30 @@
|
|||
[
|
||||
{
|
||||
"name": "",
|
||||
"version": "undefined"
|
||||
},
|
||||
{
|
||||
"name": "NEXA",
|
||||
"version": "undefined"
|
||||
},
|
||||
{
|
||||
"name": "NEXA/test",
|
||||
"version": "undefined"
|
||||
},
|
||||
{
|
||||
"name": "vanar-chain",
|
||||
"version": "undefined"
|
||||
},
|
||||
{
|
||||
"name": "sonic",
|
||||
"version": "5.21.0"
|
||||
},
|
||||
{
|
||||
"name": "apechain",
|
||||
"version": "5.21.0"
|
||||
},
|
||||
{
|
||||
"name": "alephium",
|
||||
"version": "5.21.0"
|
||||
}
|
||||
]
|
||||
|
|
@ -0,0 +1,50 @@
|
|||
[
|
||||
{
|
||||
"name": "NEW_CARD_SCANNING_ENABLED",
|
||||
"version": "undefined"
|
||||
},
|
||||
{
|
||||
"name": "WC_SOLANA_TX_SIGN_ENABLED",
|
||||
"version": "5.18.0"
|
||||
},
|
||||
{
|
||||
"name": "IS_ETHEREUM_EIP_1559_ENABLED",
|
||||
"version": "5.17.0"
|
||||
},
|
||||
{
|
||||
"name": "ONRAMP_ENABLED",
|
||||
"version": "undefined"
|
||||
},
|
||||
{
|
||||
"name": "MAIN_ACTION_BUTTONS_ENABLED",
|
||||
"version": "5.19.0"
|
||||
},
|
||||
{
|
||||
"name": "ONBOARDING_CODE_REFACTORING_ENABLED",
|
||||
"version": "5.22.0"
|
||||
},
|
||||
{
|
||||
"name": "NAVIGATION_REFACTORING",
|
||||
"version": "undefined"
|
||||
},
|
||||
{
|
||||
"name": "SWAP_STORIES_ENABLED",
|
||||
"version": "5.21.0"
|
||||
},
|
||||
{
|
||||
"name": "VISA_ONBOARDING_ENABLED",
|
||||
"version": "undefined"
|
||||
},
|
||||
{
|
||||
"name": "ONRAMP_HOT_TOKENS_ENABLED",
|
||||
"version": "5.21.0"
|
||||
},
|
||||
{
|
||||
"name": "STAKING_TON_ENABLED",
|
||||
"version": "undefined"
|
||||
},
|
||||
{
|
||||
"name": "BALANCES_CACHING_ENABLED",
|
||||
"version": "5.21.0"
|
||||
}
|
||||
]
|
||||
|
|
@ -0,0 +1,8 @@
|
|||
package com.tangem.core.configtoggle.blockchain
|
||||
|
||||
interface ExcludedBlockchainsManager {
|
||||
|
||||
val excludedBlockchainsIds: Set<String>
|
||||
|
||||
suspend fun init()
|
||||
}
|
||||
|
|
@ -0,0 +1,10 @@
|
|||
package com.tangem.core.configtoggle.blockchain
|
||||
|
||||
interface MutableExcludedBlockchainsManager : ExcludedBlockchainsManager {
|
||||
|
||||
suspend fun excludeBlockchain(mainnetId: String, isExcluded: Boolean)
|
||||
|
||||
fun isMatchLocalConfig(): Boolean
|
||||
|
||||
suspend fun recoverLocalConfig()
|
||||
}
|
||||
|
|
@ -0,0 +1,80 @@
|
|||
package com.tangem.core.configtoggle.blockchain.impl
|
||||
|
||||
import com.tangem.core.configtoggle.blockchain.MutableExcludedBlockchainsManager
|
||||
import com.tangem.core.configtoggle.storage.TogglesStorage
|
||||
import com.tangem.core.configtoggle.utils.associateToggles
|
||||
import com.tangem.core.configtoggle.version.VersionProvider
|
||||
import com.tangem.datasource.local.preferences.AppPreferencesStore
|
||||
import com.tangem.datasource.local.preferences.PreferencesKeys
|
||||
import com.tangem.datasource.local.preferences.utils.getObjectMapSync
|
||||
import com.tangem.datasource.local.preferences.utils.storeObjectMap
|
||||
|
||||
internal class DefaultExcludedBlockchainsManager(
|
||||
private val localTogglesStorage: TogglesStorage,
|
||||
private val appPreferencesStore: AppPreferencesStore,
|
||||
private val versionProvider: VersionProvider,
|
||||
) : MutableExcludedBlockchainsManager {
|
||||
|
||||
private var isInitialized: Boolean = false
|
||||
|
||||
private lateinit var currentExcludedBlockchains: MutableMap<String, Boolean>
|
||||
private lateinit var localExcludedBlockchains: Map<String, Boolean>
|
||||
|
||||
override val excludedBlockchainsIds: Set<String>
|
||||
get() {
|
||||
if (!isInitialized) error("ExcludedBlockchainsManager is not initialized")
|
||||
|
||||
return currentExcludedBlockchains
|
||||
.filterValues { it }
|
||||
.keys
|
||||
}
|
||||
|
||||
override suspend fun init() {
|
||||
localTogglesStorage.populate(path = "configs/excluded_blockchains_config")
|
||||
|
||||
val storedExcludedBlockchainsIds = appPreferencesStore.getObjectMapSync<Boolean>(
|
||||
key = PreferencesKeys.EXCLUDED_BLOCKCHAINS_KEY,
|
||||
)
|
||||
|
||||
localExcludedBlockchains = localTogglesStorage.toggles
|
||||
.associateToggles(currentVersion = versionProvider.get().orEmpty())
|
||||
.mapValues { (_, isIncluded) -> !isIncluded }
|
||||
|
||||
currentExcludedBlockchains = (localExcludedBlockchains.keys + storedExcludedBlockchainsIds.keys)
|
||||
.fold(mutableMapOf()) { acc, blockchainId ->
|
||||
val isExcluded = storedExcludedBlockchainsIds[blockchainId] ?: localExcludedBlockchains[blockchainId]
|
||||
|
||||
requireNotNull(isExcluded) {
|
||||
"Unable to find $blockchainId in local or stored excluded blockchains"
|
||||
}
|
||||
|
||||
acc[blockchainId] = isExcluded
|
||||
acc
|
||||
}
|
||||
|
||||
isInitialized = true
|
||||
}
|
||||
|
||||
override suspend fun excludeBlockchain(mainnetId: String, isExcluded: Boolean) {
|
||||
currentExcludedBlockchains[mainnetId] = isExcluded
|
||||
|
||||
storeCurrent()
|
||||
}
|
||||
|
||||
override fun isMatchLocalConfig(): Boolean {
|
||||
return currentExcludedBlockchains == localExcludedBlockchains
|
||||
}
|
||||
|
||||
override suspend fun recoverLocalConfig() {
|
||||
currentExcludedBlockchains = localExcludedBlockchains.toMutableMap()
|
||||
|
||||
storeCurrent()
|
||||
}
|
||||
|
||||
private suspend fun storeCurrent() {
|
||||
appPreferencesStore.storeObjectMap(
|
||||
key = PreferencesKeys.EXCLUDED_BLOCKCHAINS_KEY,
|
||||
value = currentExcludedBlockchains,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,49 @@
|
|||
package com.tangem.core.configtoggle.di
|
||||
|
||||
import android.content.Context
|
||||
import com.tangem.core.configtoggle.BuildConfig
|
||||
import com.tangem.core.configtoggle.blockchain.ExcludedBlockchainsManager
|
||||
import com.tangem.core.configtoggle.blockchain.MutableExcludedBlockchainsManager
|
||||
import com.tangem.core.configtoggle.blockchain.impl.DefaultExcludedBlockchainsManager
|
||||
import com.tangem.core.configtoggle.storage.LocalTogglesStorage
|
||||
import com.tangem.core.configtoggle.version.DefaultVersionProvider
|
||||
import com.tangem.datasource.asset.loader.AssetLoader
|
||||
import com.tangem.datasource.local.preferences.AppPreferencesStore
|
||||
import dagger.Module
|
||||
import dagger.Provides
|
||||
import dagger.hilt.InstallIn
|
||||
import dagger.hilt.android.qualifiers.ApplicationContext
|
||||
import dagger.hilt.components.SingletonComponent
|
||||
import javax.inject.Singleton
|
||||
|
||||
@Module
|
||||
@InstallIn(SingletonComponent::class)
|
||||
internal object ExcludedBlockchainsManagerModule {
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
fun provideExcludedBlockchainsManager(
|
||||
@ApplicationContext context: Context,
|
||||
assetLoader: AssetLoader,
|
||||
appPreferencesStore: AppPreferencesStore,
|
||||
): ExcludedBlockchainsManager {
|
||||
val localTogglesStorage = LocalTogglesStorage(assetLoader)
|
||||
val versionProvider = DefaultVersionProvider(context)
|
||||
|
||||
return DefaultExcludedBlockchainsManager(
|
||||
localTogglesStorage,
|
||||
appPreferencesStore,
|
||||
versionProvider,
|
||||
)
|
||||
}
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
fun provideMutableExcludedBlockchainsManager(
|
||||
manager: ExcludedBlockchainsManager,
|
||||
): MutableExcludedBlockchainsManager? {
|
||||
if (!BuildConfig.TESTER_MENU_ENABLED) return null
|
||||
|
||||
return manager as MutableExcludedBlockchainsManager
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,46 @@
|
|||
package com.tangem.core.configtoggle.di
|
||||
|
||||
import android.content.Context
|
||||
import com.tangem.core.configtoggle.BuildConfig
|
||||
import com.tangem.core.configtoggle.feature.FeatureTogglesManager
|
||||
import com.tangem.core.configtoggle.feature.impl.DevFeatureTogglesManager
|
||||
import com.tangem.core.configtoggle.feature.impl.ProdFeatureTogglesManager
|
||||
import com.tangem.core.configtoggle.storage.LocalTogglesStorage
|
||||
import com.tangem.core.configtoggle.version.DefaultVersionProvider
|
||||
import com.tangem.datasource.asset.loader.AssetLoader
|
||||
import com.tangem.datasource.local.preferences.AppPreferencesStore
|
||||
import dagger.Module
|
||||
import dagger.Provides
|
||||
import dagger.hilt.InstallIn
|
||||
import dagger.hilt.android.qualifiers.ApplicationContext
|
||||
import dagger.hilt.components.SingletonComponent
|
||||
import javax.inject.Singleton
|
||||
|
||||
@Module
|
||||
@InstallIn(SingletonComponent::class)
|
||||
internal object FeatureTogglesManagerModule {
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
fun provideFeatureTogglesManager(
|
||||
@ApplicationContext context: Context,
|
||||
assetLoader: AssetLoader,
|
||||
appPreferencesStore: AppPreferencesStore,
|
||||
): FeatureTogglesManager {
|
||||
val localTogglesStorage = LocalTogglesStorage(assetLoader)
|
||||
val versionProvider = DefaultVersionProvider(context)
|
||||
|
||||
return if (BuildConfig.TESTER_MENU_ENABLED) {
|
||||
DevFeatureTogglesManager(
|
||||
localTogglesStorage = localTogglesStorage,
|
||||
appPreferencesStore = appPreferencesStore,
|
||||
versionProvider = versionProvider,
|
||||
)
|
||||
} else {
|
||||
ProdFeatureTogglesManager(
|
||||
localTogglesStorage = localTogglesStorage,
|
||||
versionProvider = versionProvider,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,15 @@
|
|||
package com.tangem.core.configtoggle.feature
|
||||
|
||||
/**
|
||||
* Component for getting information about the availability of feature toggles
|
||||
*
|
||||
[REDACTED_AUTHOR]
|
||||
*/
|
||||
interface FeatureTogglesManager {
|
||||
|
||||
/** Initialize manager */
|
||||
suspend fun init()
|
||||
|
||||
/** Check feature toggle availability by name [name] */
|
||||
fun isFeatureEnabled(name: String): Boolean
|
||||
}
|
||||
|
|
@ -0,0 +1,21 @@
|
|||
package com.tangem.core.configtoggle.feature
|
||||
|
||||
/**
|
||||
* Component for change information about the availability of feature toggles
|
||||
*
|
||||
[REDACTED_AUTHOR]
|
||||
*/
|
||||
interface MutableFeatureTogglesManager : FeatureTogglesManager {
|
||||
|
||||
/** Check if the current state of the feature toggles matches the local config state. */
|
||||
fun isMatchLocalConfig(): Boolean
|
||||
|
||||
/** Get feature toggles */
|
||||
fun getFeatureToggles(): Map<String, Boolean>
|
||||
|
||||
/** Change availability [isEnabled] of toggle with name [name] */
|
||||
suspend fun changeToggle(name: String, isEnabled: Boolean)
|
||||
|
||||
/** Recover local config state */
|
||||
suspend fun recoverLocalConfig()
|
||||
}
|
||||
|
|
@ -0,0 +1,74 @@
|
|||
package com.tangem.core.configtoggle.feature.impl
|
||||
|
||||
import androidx.annotation.VisibleForTesting
|
||||
import com.tangem.core.configtoggle.feature.MutableFeatureTogglesManager
|
||||
import com.tangem.core.configtoggle.storage.TogglesStorage
|
||||
import com.tangem.core.configtoggle.utils.associateToggles
|
||||
import com.tangem.core.configtoggle.version.VersionProvider
|
||||
import com.tangem.datasource.local.preferences.AppPreferencesStore
|
||||
import com.tangem.datasource.local.preferences.PreferencesKeys
|
||||
import com.tangem.datasource.local.preferences.utils.getObjectSyncOrNull
|
||||
import com.tangem.datasource.local.preferences.utils.storeObject
|
||||
import kotlin.properties.Delegates
|
||||
|
||||
/**
|
||||
* Feature toggles manager implementation in DEV build
|
||||
*
|
||||
* @property localTogglesStorage local feature toggles storage
|
||||
* @property appPreferencesStore application local store
|
||||
* @property versionProvider application version provider
|
||||
*/
|
||||
internal class DevFeatureTogglesManager(
|
||||
private val localTogglesStorage: TogglesStorage,
|
||||
private val appPreferencesStore: AppPreferencesStore,
|
||||
private val versionProvider: VersionProvider,
|
||||
) : MutableFeatureTogglesManager {
|
||||
|
||||
private var featureTogglesMap: MutableMap<String, Boolean> by Delegates.notNull()
|
||||
private var localFeatureTogglesMap: Map<String, Boolean> by Delegates.notNull()
|
||||
|
||||
override suspend fun init() {
|
||||
localTogglesStorage.populate(FeatureTogglesConstants.LOCAL_CONFIG_PATH)
|
||||
|
||||
val savedFeatureToggles = appPreferencesStore.getObjectSyncOrNull<Map<String, Boolean>>(
|
||||
key = PreferencesKeys.FEATURE_TOGGLES_KEY,
|
||||
) ?: emptyMap()
|
||||
|
||||
val localFeatureToggles = localTogglesStorage.toggles
|
||||
.associateToggles(currentVersion = versionProvider.get().orEmpty())
|
||||
|
||||
localFeatureTogglesMap = localFeatureToggles
|
||||
|
||||
featureTogglesMap = localFeatureToggles
|
||||
.mapValues { resultToggle ->
|
||||
savedFeatureToggles[resultToggle.key] ?: resultToggle.value
|
||||
}
|
||||
.toMutableMap()
|
||||
}
|
||||
|
||||
override fun isFeatureEnabled(name: String): Boolean = featureTogglesMap[name] ?: false
|
||||
|
||||
override fun isMatchLocalConfig(): Boolean = featureTogglesMap == localFeatureTogglesMap
|
||||
|
||||
override fun getFeatureToggles(): Map<String, Boolean> = featureTogglesMap
|
||||
|
||||
override suspend fun changeToggle(name: String, isEnabled: Boolean) {
|
||||
featureTogglesMap[name] ?: return
|
||||
featureTogglesMap[name] = isEnabled
|
||||
appPreferencesStore.storeFeatureToggles(value = featureTogglesMap)
|
||||
}
|
||||
|
||||
override suspend fun recoverLocalConfig() {
|
||||
featureTogglesMap = localFeatureTogglesMap.toMutableMap()
|
||||
appPreferencesStore.storeFeatureToggles(value = localFeatureTogglesMap)
|
||||
}
|
||||
|
||||
@VisibleForTesting(otherwise = VisibleForTesting.NONE)
|
||||
fun setFeatureToggles(map: MutableMap<String, Boolean>) {
|
||||
featureTogglesMap = map
|
||||
}
|
||||
|
||||
private suspend fun AppPreferencesStore.storeFeatureToggles(value: Map<String, Boolean>) {
|
||||
storeObject(PreferencesKeys.FEATURE_TOGGLES_KEY, value)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,6 @@
|
|||
package com.tangem.core.configtoggle.feature.impl
|
||||
|
||||
internal object FeatureTogglesConstants {
|
||||
|
||||
const val LOCAL_CONFIG_PATH: String = "configs/feature_toggles_config"
|
||||
}
|
||||
|
|
@ -0,0 +1,38 @@
|
|||
package com.tangem.core.configtoggle.feature.impl
|
||||
|
||||
import androidx.annotation.VisibleForTesting
|
||||
import com.tangem.core.configtoggle.feature.FeatureTogglesManager
|
||||
import com.tangem.core.configtoggle.storage.TogglesStorage
|
||||
import com.tangem.core.configtoggle.utils.associateToggles
|
||||
import com.tangem.core.configtoggle.version.VersionProvider
|
||||
import kotlin.properties.Delegates
|
||||
|
||||
/**
|
||||
* Feature toggles manager implementation in PROD build
|
||||
*
|
||||
* @property localTogglesStorage local feature toggles storage
|
||||
* @property versionProvider application version provider
|
||||
*/
|
||||
internal class ProdFeatureTogglesManager(
|
||||
private val localTogglesStorage: TogglesStorage,
|
||||
private val versionProvider: VersionProvider,
|
||||
) : FeatureTogglesManager {
|
||||
|
||||
private var featureToggles: Map<String, Boolean> by Delegates.notNull()
|
||||
|
||||
override suspend fun init() {
|
||||
localTogglesStorage.populate(FeatureTogglesConstants.LOCAL_CONFIG_PATH)
|
||||
featureToggles = localTogglesStorage.toggles
|
||||
.associateToggles(currentVersion = versionProvider.get() ?: "")
|
||||
}
|
||||
|
||||
override fun isFeatureEnabled(name: String): Boolean = featureToggles[name] ?: false
|
||||
|
||||
@VisibleForTesting(otherwise = VisibleForTesting.NONE)
|
||||
fun getProdFeatureToggles() = featureToggles
|
||||
|
||||
@VisibleForTesting(otherwise = VisibleForTesting.NONE)
|
||||
fun setProdFeatureToggles(map: Map<String, Boolean>) {
|
||||
featureToggles = map
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,20 @@
|
|||
package com.tangem.core.configtoggle.storage
|
||||
|
||||
import com.squareup.moshi.Json
|
||||
import com.squareup.moshi.JsonClass
|
||||
|
||||
/**
|
||||
* Data model with information about config toggle
|
||||
*
|
||||
* @property name toggle name
|
||||
* @property version version in which the toggle will be enabled
|
||||
*
|
||||
* IMPORTANT: if the version is "undefined", it means that toggle is disabled!
|
||||
*
|
||||
[REDACTED_AUTHOR]
|
||||
*/
|
||||
@JsonClass(generateAdapter = true)
|
||||
internal data class ConfigToggle(
|
||||
@Json(name = "name") val name: String,
|
||||
@Json(name = "version") val version: String,
|
||||
)
|
||||
|
|
@ -0,0 +1,24 @@
|
|||
package com.tangem.core.configtoggle.storage
|
||||
|
||||
import com.tangem.datasource.asset.loader.AssetLoader
|
||||
import kotlin.properties.Delegates
|
||||
|
||||
/**
|
||||
* Storage implementation for storing local feature toggles.
|
||||
* Feature toggles are declared in file [LOCAL_CONFIG_PATH].
|
||||
*
|
||||
* @property assetLoader asset loader
|
||||
*
|
||||
[REDACTED_AUTHOR]
|
||||
*/
|
||||
internal class LocalTogglesStorage(
|
||||
private val assetLoader: AssetLoader,
|
||||
) : TogglesStorage {
|
||||
|
||||
override var toggles: List<ConfigToggle> by Delegates.notNull()
|
||||
private set
|
||||
|
||||
override suspend fun populate(path: String) {
|
||||
toggles = assetLoader.loadList<ConfigToggle>(path)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,15 @@
|
|||
package com.tangem.core.configtoggle.storage
|
||||
|
||||
/**
|
||||
* Component that initializes and stores a list of feature toggles
|
||||
*
|
||||
[REDACTED_AUTHOR]
|
||||
*/
|
||||
internal interface TogglesStorage {
|
||||
|
||||
/** List of feature toggles */
|
||||
val toggles: List<ConfigToggle>
|
||||
|
||||
/** Populate the storage with toggles */
|
||||
suspend fun populate(path: String)
|
||||
}
|
||||
|
|
@ -0,0 +1,13 @@
|
|||
package com.tangem.core.configtoggle.utils
|
||||
|
||||
import com.tangem.core.configtoggle.storage.ConfigToggle
|
||||
import com.tangem.core.configtoggle.version.VersionAvailabilityContract
|
||||
|
||||
internal fun List<ConfigToggle>.associateToggles(currentVersion: String): Map<String, Boolean> {
|
||||
return associate { localToggle ->
|
||||
Pair(
|
||||
first = localToggle.name,
|
||||
second = VersionAvailabilityContract(currentVersion, localToggle.version),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,40 @@
|
|||
package com.tangem.core.configtoggle.version
|
||||
|
||||
import android.content.Context
|
||||
import android.content.pm.PackageManager
|
||||
import android.os.Build
|
||||
import com.tangem.utils.StringsSigns.MINUS
|
||||
import dagger.hilt.android.qualifiers.ApplicationContext
|
||||
import javax.inject.Inject
|
||||
|
||||
/**
|
||||
* Implementation of application version provider
|
||||
*
|
||||
* @property context application context
|
||||
*/
|
||||
internal class DefaultVersionProvider @Inject constructor(
|
||||
@ApplicationContext private val context: Context,
|
||||
) : VersionProvider {
|
||||
|
||||
override fun get(): String? {
|
||||
return runCatching { getVersionName().substringBefore(VERSION_NAME_DELIMITER) }
|
||||
.fold(onSuccess = { it }, onFailure = { null })
|
||||
}
|
||||
|
||||
private fun getVersionName(): String {
|
||||
return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
|
||||
context.packageManager
|
||||
.getPackageInfo(
|
||||
context.packageName,
|
||||
PackageManager.PackageInfoFlags.of(0),
|
||||
)
|
||||
.versionName
|
||||
} else {
|
||||
context.packageManager.getPackageInfo(context.packageName, 0).versionName
|
||||
}
|
||||
}
|
||||
|
||||
private companion object {
|
||||
const val VERSION_NAME_DELIMITER = MINUS
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,78 @@
|
|||
package com.tangem.core.configtoggle.version
|
||||
|
||||
import androidx.annotation.VisibleForTesting
|
||||
import timber.log.Timber
|
||||
|
||||
/**
|
||||
* Presentation of application version (<major>.<minor>.<fix?>).
|
||||
*
|
||||
* @param value version value as string
|
||||
*
|
||||
[REDACTED_AUTHOR]
|
||||
*/
|
||||
internal class Version private constructor(value: String) : Comparable<Version> {
|
||||
|
||||
private val major: Int
|
||||
private val minor: Int
|
||||
private val fix: Int?
|
||||
|
||||
init {
|
||||
val versions = value.split(VERSION_DELIMITER).map(String::toInt)
|
||||
|
||||
major = versions.getVersionValue(index = MAJOR_VERSION_POSITION)
|
||||
minor = versions.getVersionValue(index = MINOR_VERSION_POSITION)
|
||||
fix = versions.getOrNull(index = FIX_VERSION_POSITION)
|
||||
}
|
||||
|
||||
@VisibleForTesting(otherwise = VisibleForTesting.NONE)
|
||||
constructor(major: Int, minor: Int, fix: Int? = null) : this(
|
||||
value = "$major.$minor${if (fix != null) ".$fix" else ""}",
|
||||
)
|
||||
|
||||
override fun compareTo(other: Version): Int {
|
||||
var result = major.compareTo(other.major)
|
||||
if (result == 0) result = minor.compareTo(other.minor)
|
||||
if (result == 0) {
|
||||
when {
|
||||
fix == null && other.fix == null -> result = 0
|
||||
fix == null && other.fix != null -> result = -1
|
||||
fix != null && other.fix == null -> result = 1
|
||||
fix != null && other.fix != null -> result = fix.compareTo(other.fix)
|
||||
}
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
private fun List<Int>.getVersionValue(index: Int): Int {
|
||||
return getOrNull(index) ?: error("Invalid version")
|
||||
}
|
||||
|
||||
@VisibleForTesting(otherwise = VisibleForTesting.NONE)
|
||||
fun getMajorVersion() = major
|
||||
|
||||
@VisibleForTesting(otherwise = VisibleForTesting.NONE)
|
||||
fun getMinorVersion() = minor
|
||||
|
||||
@VisibleForTesting(otherwise = VisibleForTesting.NONE)
|
||||
fun getFixVersion() = fix
|
||||
|
||||
companion object {
|
||||
private const val MAJOR_VERSION_POSITION = 0
|
||||
private const val MINOR_VERSION_POSITION = 1
|
||||
private const val FIX_VERSION_POSITION = 2
|
||||
private const val VERSION_DELIMITER = "."
|
||||
|
||||
/**
|
||||
* Create instance with value [value].
|
||||
* If [value] doesn't meet all requirements, the function returns null.
|
||||
*/
|
||||
fun create(value: String): Version? {
|
||||
return try {
|
||||
Version(value)
|
||||
} catch (exception: Exception) {
|
||||
Timber.e(exception, "Invalid version - %s", value)
|
||||
return null
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,20 @@
|
|||
package com.tangem.core.configtoggle.version
|
||||
|
||||
/**
|
||||
* Version contract to evaluate availability of feature toggle
|
||||
*
|
||||
[REDACTED_AUTHOR]
|
||||
*/
|
||||
internal object VersionAvailabilityContract {
|
||||
|
||||
private const val DISABLED_FEATURE_TOGGLE_VERSION = "undefined"
|
||||
|
||||
/** Evaluate availability of feature toggles using [currentVersion] and [localVersion] */
|
||||
operator fun invoke(currentVersion: String, localVersion: String): Boolean {
|
||||
if (localVersion == DISABLED_FEATURE_TOGGLE_VERSION) return false
|
||||
val current = Version.create(currentVersion) ?: return false
|
||||
val local = Version.create(localVersion) ?: return false
|
||||
|
||||
return current >= local
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,8 @@
|
|||
package com.tangem.core.configtoggle.version
|
||||
|
||||
/** Application version provider */
|
||||
internal interface VersionProvider {
|
||||
|
||||
/** Get application version */
|
||||
fun get(): String?
|
||||
}
|
||||
|
|
@ -0,0 +1,71 @@
|
|||
package com.tangem.core.configtoggle.contract
|
||||
|
||||
import com.google.common.truth.Truth
|
||||
import com.tangem.core.configtoggle.version.VersionAvailabilityContract
|
||||
import org.junit.Test
|
||||
|
||||
/**
|
||||
[REDACTED_AUTHOR]
|
||||
*/
|
||||
internal class VersionAvailabilityContractTest {
|
||||
|
||||
@Test
|
||||
fun `local version is undefined`() {
|
||||
val currentVersion = "0.0.0"
|
||||
val localVersion = "undefined"
|
||||
|
||||
val actual = VersionAvailabilityContract.invoke(currentVersion, localVersion)
|
||||
|
||||
Truth.assertThat(actual).isFalse()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `invalid current version`() {
|
||||
val currentVersion = ".0.0"
|
||||
val localVersion = "0.0.0"
|
||||
|
||||
val actual = VersionAvailabilityContract.invoke(currentVersion, localVersion)
|
||||
|
||||
Truth.assertThat(actual).isFalse()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `invalid local version`() {
|
||||
val currentVersion = "0.0.0"
|
||||
val localVersion = ".0.0"
|
||||
|
||||
val actual = VersionAvailabilityContract.invoke(currentVersion, localVersion)
|
||||
|
||||
Truth.assertThat(actual).isFalse()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `current version is greater than local version`() {
|
||||
val currentVersion = "1.0.0"
|
||||
val localVersion = "0.0.0"
|
||||
|
||||
val actual = VersionAvailabilityContract.invoke(currentVersion, localVersion)
|
||||
|
||||
Truth.assertThat(actual).isTrue()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `current version is equal to local version`() {
|
||||
val currentVersion = "0.0.1"
|
||||
val localVersion = "0.0.1"
|
||||
|
||||
val actual = VersionAvailabilityContract.invoke(currentVersion, localVersion)
|
||||
|
||||
Truth.assertThat(actual).isTrue()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `current version is less than local version`() {
|
||||
val currentVersion = "0.0.0"
|
||||
val localVersion = "0.1.0"
|
||||
|
||||
val actual = VersionAvailabilityContract.invoke(currentVersion, localVersion)
|
||||
|
||||
Truth.assertThat(actual).isFalse()
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,122 @@
|
|||
package com.tangem.core.configtoggle.contract
|
||||
|
||||
import com.google.common.truth.Truth
|
||||
import com.tangem.core.configtoggle.version.Version
|
||||
import org.junit.Test
|
||||
|
||||
/**
|
||||
[REDACTED_AUTHOR]
|
||||
*/
|
||||
internal class VersionTest {
|
||||
|
||||
@Test
|
||||
fun `right versions order`() {
|
||||
val major = 1
|
||||
val minor = 2
|
||||
val fix = 3
|
||||
val versionString = "$major.$minor.$fix"
|
||||
|
||||
val version = Version.create(versionString)
|
||||
|
||||
Truth.assertThat(version?.getMajorVersion()).isEqualTo(major)
|
||||
Truth.assertThat(version?.getMinorVersion()).isEqualTo(minor)
|
||||
Truth.assertThat(version?.getFixVersion()).isEqualTo(fix)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `major version skipped`() {
|
||||
Truth.assertThat(Version.create(".0.0")).isNull()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `minor version skipped`() {
|
||||
Truth.assertThat(Version.create("0..0")).isNull()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `fix version skipped`() {
|
||||
Truth.assertThat(Version.create("0.0.")).isNull()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `optional fix version`() {
|
||||
val actual = Version.create("0.0")
|
||||
val expected = Version(major = 0, minor = 0)
|
||||
|
||||
Truth.assertThat(actual).isEquivalentAccordingToCompareTo(expected)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `full form version`() {
|
||||
val actual = Version.create("0.0.0")
|
||||
val expected = Version(major = 0, minor = 0, fix = 0)
|
||||
|
||||
Truth.assertThat(actual).isEquivalentAccordingToCompareTo(expected)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `compareTo if major version of one is greater than the other`() {
|
||||
val one = Version.create("1.0.0")
|
||||
val other = Version(major = 0, minor = 1, fix = 1)
|
||||
|
||||
val actual = one?.compareTo(other)
|
||||
|
||||
Truth.assertThat(actual).isEqualTo(ONE_IS_GREATER_THAN_OTHER)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `compareTo if minor version of one is greater than the other`() {
|
||||
val one = Version.create("1.1.0")
|
||||
val other = Version(major = 1, minor = 0, fix = 1)
|
||||
|
||||
val actual = one?.compareTo(other)
|
||||
|
||||
Truth.assertThat(actual).isEqualTo(ONE_IS_GREATER_THAN_OTHER)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `compareTo if fix version of one is greater than the other`() {
|
||||
val one = Version.create("1.1.1")
|
||||
val other = Version(major = 1, minor = 1, fix = 0)
|
||||
|
||||
val actual = one?.compareTo(other)
|
||||
|
||||
Truth.assertThat(actual).isEqualTo(ONE_IS_GREATER_THAN_OTHER)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `compareTo if one and other's fix version skipped`() {
|
||||
val one = Version.create("1.1")
|
||||
val other = Version(major = 1, minor = 1)
|
||||
|
||||
val actual = one?.compareTo(other)
|
||||
|
||||
Truth.assertThat(actual).isEqualTo(ONE_IS_EQUAL_TO_OTHER)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `compareTo if one's fix version skipped`() {
|
||||
val one = Version.create("1.1")
|
||||
val other = Version(major = 1, minor = 1, fix = 0)
|
||||
|
||||
val actual = one?.compareTo(other)
|
||||
|
||||
Truth.assertThat(actual).isEqualTo(ONE_IS_LESS_THAN_OTHER)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `compareTo if other's fix version skipped`() {
|
||||
val one = Version.create("1.1.0")
|
||||
val other = Version(major = 1, minor = 1)
|
||||
|
||||
val actual = one?.compareTo(other)
|
||||
|
||||
Truth.assertThat(actual).isEqualTo(ONE_IS_GREATER_THAN_OTHER)
|
||||
}
|
||||
|
||||
private companion object {
|
||||
const val ONE_IS_GREATER_THAN_OTHER = 1
|
||||
const val ONE_IS_EQUAL_TO_OTHER = 0
|
||||
const val ONE_IS_LESS_THAN_OTHER = -1
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,231 @@
|
|||
package com.tangem.core.configtoggle.manager
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import com.google.common.truth.Truth
|
||||
import com.tangem.core.configtoggle.feature.impl.DevFeatureTogglesManager
|
||||
import com.tangem.core.configtoggle.feature.impl.FeatureTogglesConstants
|
||||
import com.tangem.core.configtoggle.storage.ConfigToggle
|
||||
import com.tangem.core.configtoggle.storage.TogglesStorage
|
||||
import com.tangem.core.configtoggle.utils.associateToggles
|
||||
import com.tangem.core.configtoggle.version.VersionProvider
|
||||
import com.tangem.datasource.local.preferences.AppPreferencesStore
|
||||
import com.tangem.datasource.local.preferences.PreferencesKeys
|
||||
import com.tangem.datasource.local.preferences.utils.getObjectSyncOrNull
|
||||
import com.tangem.datasource.local.preferences.utils.getSyncOrNull
|
||||
import io.mockk.*
|
||||
import kotlinx.coroutines.test.runTest
|
||||
import org.junit.Test
|
||||
import kotlin.collections.set
|
||||
|
||||
/**
|
||||
[REDACTED_AUTHOR]
|
||||
*/
|
||||
@SuppressLint("CheckResult")
|
||||
internal class DevTogglesManagerTest {
|
||||
|
||||
private val localTogglesStorage = mockk<TogglesStorage>()
|
||||
private val appPreferenceStore = mockk<AppPreferencesStore>(relaxed = true)
|
||||
private val versionProvider = mockk<VersionProvider>()
|
||||
private val manager = DevFeatureTogglesManager(
|
||||
localTogglesStorage = localTogglesStorage,
|
||||
appPreferencesStore = appPreferenceStore,
|
||||
versionProvider = versionProvider,
|
||||
)
|
||||
|
||||
@Test
|
||||
fun `successfully initialize storage if shared prefs kept feature toggles`() = runTest {
|
||||
val currentVersion = "0.1.0"
|
||||
|
||||
coEvery { localTogglesStorage.populate(FeatureTogglesConstants.LOCAL_CONFIG_PATH) } just Runs
|
||||
coEvery {
|
||||
appPreferenceStore.getObjectSyncOrNull<Map<String, Boolean>>(PreferencesKeys.FEATURE_TOGGLES_KEY)
|
||||
} returns savedFeatureTogglesMap
|
||||
coEvery { localTogglesStorage.toggles } returns localFeatureToggles
|
||||
coEvery { versionProvider.get() } returns currentVersion
|
||||
|
||||
manager.init()
|
||||
|
||||
coVerifyOrder {
|
||||
localTogglesStorage.populate(FeatureTogglesConstants.LOCAL_CONFIG_PATH)
|
||||
versionProvider.get()
|
||||
}
|
||||
|
||||
val expected = localFeatureToggles
|
||||
.associateToggles(currentVersion)
|
||||
.mapValues { resultToggle ->
|
||||
savedFeatureTogglesMap[resultToggle.key] ?: resultToggle.value
|
||||
}
|
||||
|
||||
Truth.assertThat(manager.getFeatureToggles()).containsExactlyEntriesIn(expected)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `successfully initialize storage if shared prefs kept empty list`() = runTest {
|
||||
val currentVersion = "0.1.0"
|
||||
|
||||
coEvery { localTogglesStorage.populate(FeatureTogglesConstants.LOCAL_CONFIG_PATH) } just Runs
|
||||
coEvery {
|
||||
appPreferenceStore.getObjectSyncOrNull<Map<String, Boolean>>(PreferencesKeys.FEATURE_TOGGLES_KEY)
|
||||
} returns emptyMap()
|
||||
coEvery { localTogglesStorage.toggles } returns localFeatureToggles
|
||||
coEvery { versionProvider.get() } returns currentVersion
|
||||
|
||||
manager.init()
|
||||
|
||||
coVerifyOrder {
|
||||
localTogglesStorage.populate(FeatureTogglesConstants.LOCAL_CONFIG_PATH)
|
||||
versionProvider.get()
|
||||
}
|
||||
|
||||
val expected = localFeatureToggles
|
||||
.associateToggles(currentVersion)
|
||||
.mapValues { resultToggle ->
|
||||
savedFeatureTogglesMap[resultToggle.key] ?: resultToggle.value
|
||||
}
|
||||
|
||||
Truth.assertThat(manager.getFeatureToggles()).containsExactlyEntriesIn(expected)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `successfully initialize storage if shared prefs didn't keep feature toggles`() = runTest {
|
||||
val currentVersion = "0.1.0"
|
||||
|
||||
coEvery { localTogglesStorage.populate(FeatureTogglesConstants.LOCAL_CONFIG_PATH) } just Runs
|
||||
coEvery {
|
||||
appPreferenceStore.getObjectSyncOrNull<Map<String, Boolean>>(PreferencesKeys.FEATURE_TOGGLES_KEY)
|
||||
} returns null
|
||||
coEvery { localTogglesStorage.toggles } returns localFeatureToggles
|
||||
coEvery { versionProvider.get() } returns currentVersion
|
||||
|
||||
manager.init()
|
||||
|
||||
coVerifyOrder {
|
||||
localTogglesStorage.populate(FeatureTogglesConstants.LOCAL_CONFIG_PATH)
|
||||
versionProvider.get()
|
||||
}
|
||||
|
||||
val expected = localFeatureToggles
|
||||
.associateToggles(currentVersion)
|
||||
.mapValues(Map.Entry<String, Boolean>::value)
|
||||
|
||||
Truth.assertThat(manager.getFeatureToggles()).containsExactlyEntriesIn(expected)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `successfully initialize storage if versionProvider returns null`() = runTest {
|
||||
coEvery { localTogglesStorage.populate(FeatureTogglesConstants.LOCAL_CONFIG_PATH) } just Runs
|
||||
coEvery { appPreferenceStore.getSyncOrNull(PreferencesKeys.FEATURE_TOGGLES_KEY) } returns savedFeatureToggles
|
||||
coEvery { localTogglesStorage.toggles } returns localFeatureToggles
|
||||
coEvery { versionProvider.get() } returns null
|
||||
|
||||
manager.init()
|
||||
|
||||
coVerifyOrder {
|
||||
localTogglesStorage.populate(FeatureTogglesConstants.LOCAL_CONFIG_PATH)
|
||||
versionProvider.get()
|
||||
}
|
||||
|
||||
val expected = localFeatureToggles
|
||||
.associateToggles(currentVersion = "")
|
||||
.mapValues { resultToggle ->
|
||||
savedFeatureTogglesMap[resultToggle.key] ?: resultToggle.value
|
||||
}
|
||||
|
||||
Truth.assertThat(manager.getFeatureToggles()).containsExactlyEntriesIn(expected)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `get feature availability if feature toggle exists`() {
|
||||
val featureToggles = mutableMapOf(
|
||||
"INACTIVE_TEST_FEATURE_ENABLED" to true,
|
||||
"ACTIVE2_TEST_FEATURE_ENABLED" to true,
|
||||
)
|
||||
manager.setFeatureToggles(featureToggles)
|
||||
|
||||
val actual = manager.isFeatureEnabled(name = "INACTIVE_TEST_FEATURE_ENABLED")
|
||||
|
||||
Truth.assertThat(actual).isTrue()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `get feature availability if feature toggle doesn't exists`() {
|
||||
val featureToggles = mutableMapOf(
|
||||
"INACTIVE_TEST_FEATURE_ENABLED" to false,
|
||||
"ACTIVE2_TEST_FEATURE_ENABLED" to true,
|
||||
)
|
||||
manager.setFeatureToggles(featureToggles)
|
||||
|
||||
val actual = manager.isFeatureEnabled(name = "")
|
||||
|
||||
Truth.assertThat(actual).isFalse()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun getFeatureToggles() {
|
||||
val expected = mutableMapOf(
|
||||
"INACTIVE_TEST_FEATURE_ENABLED" to false,
|
||||
"ACTIVE2_TEST_FEATURE_ENABLED" to false,
|
||||
)
|
||||
|
||||
manager.setFeatureToggles(expected)
|
||||
|
||||
Truth.assertThat(manager.getFeatureToggles()).containsExactlyEntriesIn(expected)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `change toggle that contains in map`() = runTest {
|
||||
val changeableToggleName = "INACTIVE_TEST_FEATURE_ENABLED"
|
||||
val resultMap = mutableMapOf(
|
||||
changeableToggleName to false,
|
||||
"ACTIVE2_TEST_FEATURE_ENABLED" to false,
|
||||
)
|
||||
|
||||
manager.setFeatureToggles(resultMap)
|
||||
|
||||
manager.changeToggle(changeableToggleName, true)
|
||||
|
||||
resultMap[changeableToggleName] = true
|
||||
|
||||
Truth.assertThat(manager.getFeatureToggles()).containsExactlyEntriesIn(resultMap)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `change toggle that doesn't contains in map`() = runTest {
|
||||
val resultMap = mutableMapOf(
|
||||
"INACTIVE_TEST_FEATURE_ENABLED" to false,
|
||||
"ACTIVE2_TEST_FEATURE_ENABLED" to false,
|
||||
)
|
||||
|
||||
manager.setFeatureToggles(resultMap)
|
||||
|
||||
manager.changeToggle("FEATURE_TOGGLE", true)
|
||||
|
||||
Truth.assertThat(manager.getFeatureToggles()).containsExactlyEntriesIn(resultMap)
|
||||
}
|
||||
|
||||
private companion object {
|
||||
|
||||
val savedFeatureToggles = """
|
||||
[
|
||||
{
|
||||
"name": "INACTIVE_TEST_FEATURE_ENABLED",
|
||||
"version": "undefined"
|
||||
},
|
||||
{
|
||||
"name": "ACTIVE2_TEST_FEATURE_ENABLED",
|
||||
"version": "1.0.0"
|
||||
}
|
||||
]
|
||||
""".trimIndent()
|
||||
|
||||
val savedFeatureTogglesMap = mapOf(
|
||||
"INACTIVE_TEST_FEATURE_ENABLED" to false,
|
||||
"ACTIVE2_TEST_FEATURE_ENABLED" to false,
|
||||
)
|
||||
|
||||
val localFeatureToggles = listOf(
|
||||
ConfigToggle(name = "INACTIVE_TEST_FEATURE_ENABLED", version = "undefined"),
|
||||
ConfigToggle(name = "ACTIVE2_TEST_FEATURE_ENABLED", version = "1.0.0"),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,136 @@
|
|||
package com.tangem.core.configtoggle.manager
|
||||
|
||||
import android.content.pm.PackageManager
|
||||
import com.google.common.truth.Truth
|
||||
import com.tangem.core.configtoggle.feature.impl.FeatureTogglesConstants
|
||||
import com.tangem.core.configtoggle.feature.impl.ProdFeatureTogglesManager
|
||||
import com.tangem.core.configtoggle.storage.ConfigToggle
|
||||
import com.tangem.core.configtoggle.storage.TogglesStorage
|
||||
import com.tangem.core.configtoggle.utils.associateToggles
|
||||
import com.tangem.core.configtoggle.version.VersionProvider
|
||||
import io.mockk.*
|
||||
import kotlinx.coroutines.test.runTest
|
||||
import org.junit.Test
|
||||
|
||||
/**
|
||||
[REDACTED_AUTHOR]
|
||||
*/
|
||||
internal class ProdTogglesManagerTest {
|
||||
|
||||
private val localTogglesStorage = mockk<TogglesStorage>()
|
||||
private val versionProvider = mockk<VersionProvider>()
|
||||
private val manager = ProdFeatureTogglesManager(localTogglesStorage, versionProvider)
|
||||
|
||||
@Test
|
||||
fun `successfully initialize storage`() = runTest {
|
||||
val currentVersion = "1.0.0"
|
||||
|
||||
coEvery { localTogglesStorage.populate(FeatureTogglesConstants.LOCAL_CONFIG_PATH) } just Runs
|
||||
every { localTogglesStorage.toggles } returns localFeatureToggles
|
||||
every { versionProvider.get() } returns currentVersion
|
||||
|
||||
manager.init()
|
||||
|
||||
coVerifyOrder {
|
||||
localTogglesStorage.populate(FeatureTogglesConstants.LOCAL_CONFIG_PATH)
|
||||
versionProvider.get()
|
||||
}
|
||||
|
||||
val expected = localFeatureToggles.associateToggles(currentVersion)
|
||||
Truth.assertThat(manager.getProdFeatureToggles()).containsExactlyEntriesIn(expected)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `successfully initialize storage if versionProvider returns null`() = runTest {
|
||||
coEvery { localTogglesStorage.populate(FeatureTogglesConstants.LOCAL_CONFIG_PATH) } just Runs
|
||||
every { localTogglesStorage.toggles } returns localFeatureToggles
|
||||
every { versionProvider.get() } returns null
|
||||
|
||||
manager.init()
|
||||
|
||||
coVerifyOrder {
|
||||
localTogglesStorage.populate(FeatureTogglesConstants.LOCAL_CONFIG_PATH)
|
||||
versionProvider.get()
|
||||
}
|
||||
|
||||
Truth.assertThat(manager.getProdFeatureToggles()).containsExactlyEntriesIn(disabledFeatureToggles)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `failure initialize storage if localFeatureTogglesStorage throws exception`() = runTest {
|
||||
coEvery { localTogglesStorage.populate(FeatureTogglesConstants.LOCAL_CONFIG_PATH) } just Runs
|
||||
every { localTogglesStorage.toggles } throws IllegalStateException(
|
||||
"Property featureToggles should be initialized before get.",
|
||||
)
|
||||
|
||||
runCatching { manager.init() }
|
||||
.onSuccess { throw IllegalStateException("localFeatureToggles shouldn't be initialized") }
|
||||
.onFailure {
|
||||
Truth
|
||||
.assertThat(it)
|
||||
.hasMessageThat()
|
||||
.contains("Property featureToggles should be initialized before get.")
|
||||
|
||||
Truth.assertThat(it).isInstanceOf(IllegalStateException::class.java)
|
||||
}
|
||||
|
||||
coVerifyOrder { localTogglesStorage.populate(FeatureTogglesConstants.LOCAL_CONFIG_PATH) }
|
||||
verifyAll(inverse = true) { versionProvider.get() }
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `failure initialize storage if versionProvider throws exception`() = runTest {
|
||||
coEvery { localTogglesStorage.populate(FeatureTogglesConstants.LOCAL_CONFIG_PATH) } just Runs
|
||||
every { localTogglesStorage.toggles } returns localFeatureToggles
|
||||
every { versionProvider.get() } throws PackageManager.NameNotFoundException()
|
||||
|
||||
runCatching { manager.init() }
|
||||
.onSuccess { throw IllegalStateException("versionProvider should throws exception") }
|
||||
.onFailure {
|
||||
Truth.assertThat(it).isInstanceOf(PackageManager.NameNotFoundException::class.java)
|
||||
}
|
||||
|
||||
coVerifyOrder {
|
||||
localTogglesStorage.populate(FeatureTogglesConstants.LOCAL_CONFIG_PATH)
|
||||
versionProvider.get()
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `get feature availability if feature toggle exists`() {
|
||||
val featureToggles = mapOf(
|
||||
"INACTIVE_TEST_FEATURE_ENABLED" to true,
|
||||
"ACTIVE2_TEST_FEATURE_ENABLED" to true,
|
||||
)
|
||||
manager.setProdFeatureToggles(featureToggles)
|
||||
|
||||
val actual = manager.isFeatureEnabled(name = "INACTIVE_TEST_FEATURE_ENABLED")
|
||||
|
||||
Truth.assertThat(actual).isTrue()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `get feature availability if feature toggle doesn't exists`() {
|
||||
val featureToggles = mapOf(
|
||||
"INACTIVE_TEST_FEATURE_ENABLED" to false,
|
||||
"ACTIVE2_TEST_FEATURE_ENABLED" to true,
|
||||
)
|
||||
manager.setProdFeatureToggles(featureToggles)
|
||||
|
||||
val actual = manager.isFeatureEnabled(name = "")
|
||||
|
||||
Truth.assertThat(actual).isFalse()
|
||||
}
|
||||
|
||||
private companion object {
|
||||
val localFeatureToggles = listOf(
|
||||
ConfigToggle(name = "INACTIVE_TEST_FEATURE_ENABLED", version = "undefined"),
|
||||
ConfigToggle(name = "ACTIVE2_TEST_FEATURE_ENABLED", version = "1.0.0"),
|
||||
)
|
||||
|
||||
val disabledFeatureToggles = mapOf(
|
||||
"INACTIVE_TEST_FEATURE_ENABLED" to false,
|
||||
"ACTIVE2_TEST_FEATURE_ENABLED" to false,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,105 @@
|
|||
package com.tangem.core.configtoggle.storage
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import com.google.common.truth.Truth
|
||||
import com.squareup.moshi.JsonAdapter
|
||||
import com.squareup.moshi.Moshi
|
||||
import com.squareup.moshi.Types
|
||||
import com.tangem.core.configtoggle.feature.impl.FeatureTogglesConstants
|
||||
import com.tangem.datasource.asset.loader.AssetLoader
|
||||
import com.tangem.datasource.asset.reader.AssetReader
|
||||
import io.mockk.*
|
||||
import kotlinx.coroutines.test.runTest
|
||||
import org.junit.Test
|
||||
import java.io.IOException
|
||||
|
||||
/**
|
||||
[REDACTED_AUTHOR]
|
||||
*/
|
||||
@SuppressLint("CheckResult")
|
||||
internal class LocalTogglesStorageTest {
|
||||
|
||||
private val assetReader = mockk<AssetReader>()
|
||||
private val moshi = mockk<Moshi>()
|
||||
private val jsonAdapter = mockk<JsonAdapter<List<ConfigToggle>>>()
|
||||
|
||||
// Impossible to mockk AssetLoader because it implement inline functions
|
||||
private val assetLoader = AssetLoader(assetReader = assetReader, moshi = moshi)
|
||||
|
||||
private val storage = LocalTogglesStorage(assetLoader)
|
||||
|
||||
@Test
|
||||
fun `successfully initialize storage`() = runTest {
|
||||
everyReadingJson() returns json
|
||||
everyCreatingMoshiAdapter() returns jsonAdapter
|
||||
everyMappingJson() returns featureToggles
|
||||
|
||||
storage.populate(FeatureTogglesConstants.LOCAL_CONFIG_PATH)
|
||||
|
||||
coVerifyOrder {
|
||||
assetReader.read(CONFIG_FILE_NAME)
|
||||
jsonAdapter.fromJson(json)
|
||||
}
|
||||
|
||||
Truth.assertThat(storage.toggles).containsExactlyElementsIn(featureToggles)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `failure initialize storage if assetReader throws exception`() = runTest {
|
||||
everyReadingJson() returns json
|
||||
everyCreatingMoshiAdapter() returns jsonAdapter
|
||||
everyMappingJson() throws IOException()
|
||||
|
||||
storage.populate(FeatureTogglesConstants.LOCAL_CONFIG_PATH)
|
||||
|
||||
coVerifyOrder {
|
||||
assetReader.read(CONFIG_FILE_NAME)
|
||||
jsonAdapter.fromJson(json)
|
||||
}
|
||||
|
||||
Truth.assertThat(storage.toggles).containsExactlyElementsIn(emptyList<ConfigToggle>())
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `failure initialize storage if jsonAdapter throws exception`() = runTest {
|
||||
everyReadingJson() throws IOException()
|
||||
|
||||
storage.populate(FeatureTogglesConstants.LOCAL_CONFIG_PATH)
|
||||
|
||||
coVerifyOrder { assetReader.read(CONFIG_FILE_NAME) }
|
||||
verifyAll(inverse = true) { jsonAdapter.fromJson(any<String>()) }
|
||||
|
||||
Truth.assertThat(storage.toggles).containsExactlyElementsIn(emptyList<ConfigToggle>())
|
||||
}
|
||||
|
||||
private fun everyReadingJson() = coEvery { assetReader.read(CONFIG_FILE_NAME) }
|
||||
|
||||
private fun everyCreatingMoshiAdapter() = every {
|
||||
val types = Types.newParameterizedType(List::class.java, ConfigToggle::class.java)
|
||||
moshi.adapter<List<ConfigToggle>>(types)
|
||||
}
|
||||
|
||||
private fun everyMappingJson() = every { jsonAdapter.fromJson(json) }
|
||||
|
||||
private companion object {
|
||||
const val CONFIG_FILE_NAME = "configs/feature_toggles_config.json"
|
||||
|
||||
val json = """
|
||||
[
|
||||
{
|
||||
"name": "INACTIVE_TEST_FEATURE_ENABLED",
|
||||
"version": "undefined"
|
||||
},
|
||||
{
|
||||
"name": "ACTIVE2_TEST_FEATURE_ENABLED",
|
||||
"version": "1.0.0"
|
||||
}
|
||||
]
|
||||
""".trimIndent()
|
||||
|
||||
val featureToggles = listOf(
|
||||
ConfigToggle(name = "INACTIVE_TEST_FEATURE_ENABLED", version = "undefined"),
|
||||
ConfigToggle(name = "ACTIVE2_TEST_FEATURE_ENABLED", version = "1.0.0"),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -1,68 +1,87 @@
|
|||
import com.tangem.plugin.configuration.configurations.extension.kaptForObfuscatingVariants
|
||||
|
||||
plugins {
|
||||
id("com.android.library")
|
||||
kotlin("android")
|
||||
kotlin("kapt")
|
||||
id("com.google.dagger.hilt.android")
|
||||
alias(deps.plugins.android.library)
|
||||
alias(deps.plugins.kotlin.android)
|
||||
alias(deps.plugins.kotlin.kapt)
|
||||
alias(deps.plugins.hilt.android)
|
||||
alias(deps.plugins.room)
|
||||
id("configuration")
|
||||
}
|
||||
|
||||
android {
|
||||
defaultConfig {
|
||||
compileSdk = AppConfig.compileSdkVersion
|
||||
minSdk = AppConfig.minSdkVersion
|
||||
targetSdk = AppConfig.targetSdkVersion
|
||||
}
|
||||
namespace = "com.tangem.datasource"
|
||||
|
||||
kotlinOptions {
|
||||
jvmTarget = JavaVersion.VERSION_1_8.toString()
|
||||
}
|
||||
|
||||
compileOptions {
|
||||
sourceCompatibility = JavaVersion.VERSION_1_8
|
||||
targetCompatibility = JavaVersion.VERSION_1_8
|
||||
isCoreLibraryDesugaringEnabled = false
|
||||
}
|
||||
|
||||
buildTypes {
|
||||
create("debug_beta") {
|
||||
initWith(getByName("release"))
|
||||
BuildConfigFieldFactory(
|
||||
fields = listOf(
|
||||
Field.Environment("release"),
|
||||
Field.TestActionEnabled(true),
|
||||
Field.LogEnabled(true),
|
||||
),
|
||||
builder = ::buildConfigField,
|
||||
).create()
|
||||
}
|
||||
room {
|
||||
schemaDirectory("$projectDir/schemas")
|
||||
}
|
||||
}
|
||||
|
||||
dependencies {
|
||||
|
||||
/** Project */
|
||||
implementation(project(":libs:auth"))
|
||||
implementation(projects.core.analytics)
|
||||
implementation(projects.core.utils)
|
||||
implementation(projects.libs.auth)
|
||||
implementation(projects.domain.appTheme.models)
|
||||
implementation(projects.domain.core)
|
||||
implementation(projects.domain.tokens.models)
|
||||
implementation(projects.domain.wallets.models)
|
||||
implementation(projects.domain.balanceHiding.models)
|
||||
implementation(projects.domain.txhistory.models)
|
||||
implementation(projects.domain.staking.models)
|
||||
implementation(projects.domain.onramp.models)
|
||||
implementation(projects.domain.models)
|
||||
|
||||
/** Tangem libraries */
|
||||
implementation(Tangem.cardCore)
|
||||
implementation(tangemDeps.blockchain)
|
||||
implementation(tangemDeps.card.core)
|
||||
|
||||
/** DI */
|
||||
implementation(Library.hilt)
|
||||
kapt(Library.hiltKapt)
|
||||
implementation(deps.hilt.android)
|
||||
kapt(deps.hilt.kapt)
|
||||
|
||||
/** Coroutines */
|
||||
implementation(Library.coroutine)
|
||||
implementation(deps.kotlin.coroutines)
|
||||
implementation(deps.kotlin.coroutines.rx2)
|
||||
|
||||
/** Logging */
|
||||
implementation(Library.timber)
|
||||
implementation(deps.timber)
|
||||
|
||||
/** Network */
|
||||
implementation(Library.retrofit)
|
||||
implementation(Library.retrofitMoshiConverter)
|
||||
implementation(Library.moshi)
|
||||
implementation(Library.moshiKotlin)
|
||||
implementation(Library.okHttp)
|
||||
implementation(Library.okHttpLogging)
|
||||
implementation(deps.moshi)
|
||||
implementation(deps.moshi.kotlin)
|
||||
implementation(deps.moshi.adapters)
|
||||
implementation(deps.moshi.adapters.ext)
|
||||
implementation(deps.okHttp)
|
||||
implementation(deps.okHttp.prettyLogging)
|
||||
implementation(deps.retrofit)
|
||||
implementation(deps.retrofit.moshi)
|
||||
kaptForObfuscatingVariants(deps.moshi.kotlin.codegen)
|
||||
kaptForObfuscatingVariants(deps.retrofit.response.type.keeper)
|
||||
|
||||
/** Time */
|
||||
implementation(Library.jodatime)
|
||||
implementation(deps.jodatime)
|
||||
|
||||
/** Security */
|
||||
implementation(deps.spongecastle.core)
|
||||
|
||||
/** Chucker */
|
||||
debugImplementation(deps.chucker)
|
||||
debugPGImplementation(deps.chucker)
|
||||
mockedImplementation(deps.chuckerStub)
|
||||
externalImplementation(deps.chuckerStub)
|
||||
internalImplementation(deps.chuckerStub)
|
||||
releaseImplementation(deps.chuckerStub)
|
||||
|
||||
/** Local storages */
|
||||
implementation(deps.androidx.datastore)
|
||||
implementation(deps.room.runtime)
|
||||
implementation(deps.room.ktx)
|
||||
kapt(deps.room.compiler)
|
||||
|
||||
testImplementation(deps.test.coroutine)
|
||||
testImplementation(deps.test.junit)
|
||||
testImplementation(deps.test.mockk)
|
||||
testImplementation(deps.test.truth)
|
||||
}
|
||||
|
|
@ -0,0 +1,304 @@
|
|||
{
|
||||
"formatVersion": 1,
|
||||
"database": {
|
||||
"version": 1,
|
||||
"identityHash": "a8a710af25033ee27e5043d001385234",
|
||||
"entities": [
|
||||
{
|
||||
"tableName": "UserWalletEntity",
|
||||
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `name` TEXT NOT NULL, `artworkUrl` TEXT NOT NULL, `isMultiCurrency` INTEGER NOT NULL, `hasBackupError` INTEGER NOT NULL, `cardsInWallet` TEXT NOT NULL, `ordinalNumber` INTEGER NOT NULL, PRIMARY KEY(`id`))",
|
||||
"fields": [
|
||||
{
|
||||
"fieldPath": "id",
|
||||
"columnName": "id",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "name",
|
||||
"columnName": "name",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "artworkUrl",
|
||||
"columnName": "artworkUrl",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "isMultiCurrency",
|
||||
"columnName": "isMultiCurrency",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "hasBackupError",
|
||||
"columnName": "hasBackupError",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "cardsInWallet",
|
||||
"columnName": "cardsInWallet",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "ordinalNumber",
|
||||
"columnName": "ordinalNumber",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
}
|
||||
],
|
||||
"primaryKey": {
|
||||
"autoGenerate": false,
|
||||
"columnNames": [
|
||||
"id"
|
||||
]
|
||||
},
|
||||
"indices": [
|
||||
{
|
||||
"name": "index_UserWalletEntity_id",
|
||||
"unique": true,
|
||||
"columnNames": [
|
||||
"id"
|
||||
],
|
||||
"orders": [],
|
||||
"createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_UserWalletEntity_id` ON `${TABLE_NAME}` (`id`)"
|
||||
},
|
||||
{
|
||||
"name": "index_UserWalletEntity_ordinalNumber",
|
||||
"unique": true,
|
||||
"columnNames": [
|
||||
"ordinalNumber"
|
||||
],
|
||||
"orders": [],
|
||||
"createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_UserWalletEntity_ordinalNumber` ON `${TABLE_NAME}` (`ordinalNumber`)"
|
||||
}
|
||||
],
|
||||
"foreignKeys": []
|
||||
},
|
||||
{
|
||||
"tableName": "CryptoCurrenciesAccountEntity",
|
||||
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER NOT NULL, `userWalletId` TEXT NOT NULL, `title` TEXT NOT NULL, `currenciesCount` INTEGER NOT NULL, `isArchived` INTEGER NOT NULL, `ordinalNumber` INTEGER NOT NULL, PRIMARY KEY(`id`), FOREIGN KEY(`userWalletId`) REFERENCES `UserWalletEntity`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )",
|
||||
"fields": [
|
||||
{
|
||||
"fieldPath": "id",
|
||||
"columnName": "id",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "userWalletId",
|
||||
"columnName": "userWalletId",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "title",
|
||||
"columnName": "title",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "currenciesCount",
|
||||
"columnName": "currenciesCount",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "isArchived",
|
||||
"columnName": "isArchived",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "ordinalNumber",
|
||||
"columnName": "ordinalNumber",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
}
|
||||
],
|
||||
"primaryKey": {
|
||||
"autoGenerate": false,
|
||||
"columnNames": [
|
||||
"id"
|
||||
]
|
||||
},
|
||||
"indices": [
|
||||
{
|
||||
"name": "index_CryptoCurrenciesAccountEntity_id",
|
||||
"unique": false,
|
||||
"columnNames": [
|
||||
"id"
|
||||
],
|
||||
"orders": [],
|
||||
"createSql": "CREATE INDEX IF NOT EXISTS `index_CryptoCurrenciesAccountEntity_id` ON `${TABLE_NAME}` (`id`)"
|
||||
},
|
||||
{
|
||||
"name": "index_CryptoCurrenciesAccountEntity_userWalletId",
|
||||
"unique": false,
|
||||
"columnNames": [
|
||||
"userWalletId"
|
||||
],
|
||||
"orders": [],
|
||||
"createSql": "CREATE INDEX IF NOT EXISTS `index_CryptoCurrenciesAccountEntity_userWalletId` ON `${TABLE_NAME}` (`userWalletId`)"
|
||||
}
|
||||
],
|
||||
"foreignKeys": [
|
||||
{
|
||||
"table": "UserWalletEntity",
|
||||
"onDelete": "CASCADE",
|
||||
"onUpdate": "NO ACTION",
|
||||
"columns": [
|
||||
"userWalletId"
|
||||
],
|
||||
"referencedColumns": [
|
||||
"id"
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"tableName": "CryptoCurrencyEntity",
|
||||
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `currencyBackendId` TEXT, `networkId` TEXT NOT NULL, `accountId` INTEGER NOT NULL, `userWalletId` TEXT NOT NULL, `name` TEXT NOT NULL, `symbol` TEXT NOT NULL, `decimals` INTEGER NOT NULL, `contractAddress` TEXT, `derivationPath` TEXT, FOREIGN KEY(`accountId`) REFERENCES `CryptoCurrenciesAccountEntity`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE , FOREIGN KEY(`userWalletId`) REFERENCES `UserWalletEntity`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )",
|
||||
"fields": [
|
||||
{
|
||||
"fieldPath": "id",
|
||||
"columnName": "id",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "currencyBackendId",
|
||||
"columnName": "currencyBackendId",
|
||||
"affinity": "TEXT",
|
||||
"notNull": false
|
||||
},
|
||||
{
|
||||
"fieldPath": "networkId",
|
||||
"columnName": "networkId",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "accountId",
|
||||
"columnName": "accountId",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "userWalletId",
|
||||
"columnName": "userWalletId",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "name",
|
||||
"columnName": "name",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "symbol",
|
||||
"columnName": "symbol",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "decimals",
|
||||
"columnName": "decimals",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "contractAddress",
|
||||
"columnName": "contractAddress",
|
||||
"affinity": "TEXT",
|
||||
"notNull": false
|
||||
},
|
||||
{
|
||||
"fieldPath": "derivationPath",
|
||||
"columnName": "derivationPath",
|
||||
"affinity": "TEXT",
|
||||
"notNull": false
|
||||
}
|
||||
],
|
||||
"primaryKey": {
|
||||
"autoGenerate": true,
|
||||
"columnNames": [
|
||||
"id"
|
||||
]
|
||||
},
|
||||
"indices": [
|
||||
{
|
||||
"name": "index_CryptoCurrencyEntity_currencyBackendId",
|
||||
"unique": false,
|
||||
"columnNames": [
|
||||
"currencyBackendId"
|
||||
],
|
||||
"orders": [],
|
||||
"createSql": "CREATE INDEX IF NOT EXISTS `index_CryptoCurrencyEntity_currencyBackendId` ON `${TABLE_NAME}` (`currencyBackendId`)"
|
||||
},
|
||||
{
|
||||
"name": "index_CryptoCurrencyEntity_networkId",
|
||||
"unique": false,
|
||||
"columnNames": [
|
||||
"networkId"
|
||||
],
|
||||
"orders": [],
|
||||
"createSql": "CREATE INDEX IF NOT EXISTS `index_CryptoCurrencyEntity_networkId` ON `${TABLE_NAME}` (`networkId`)"
|
||||
},
|
||||
{
|
||||
"name": "index_CryptoCurrencyEntity_accountId",
|
||||
"unique": false,
|
||||
"columnNames": [
|
||||
"accountId"
|
||||
],
|
||||
"orders": [],
|
||||
"createSql": "CREATE INDEX IF NOT EXISTS `index_CryptoCurrencyEntity_accountId` ON `${TABLE_NAME}` (`accountId`)"
|
||||
},
|
||||
{
|
||||
"name": "index_CryptoCurrencyEntity_userWalletId",
|
||||
"unique": false,
|
||||
"columnNames": [
|
||||
"userWalletId"
|
||||
],
|
||||
"orders": [],
|
||||
"createSql": "CREATE INDEX IF NOT EXISTS `index_CryptoCurrencyEntity_userWalletId` ON `${TABLE_NAME}` (`userWalletId`)"
|
||||
}
|
||||
],
|
||||
"foreignKeys": [
|
||||
{
|
||||
"table": "CryptoCurrenciesAccountEntity",
|
||||
"onDelete": "CASCADE",
|
||||
"onUpdate": "NO ACTION",
|
||||
"columns": [
|
||||
"accountId"
|
||||
],
|
||||
"referencedColumns": [
|
||||
"id"
|
||||
]
|
||||
},
|
||||
{
|
||||
"table": "UserWalletEntity",
|
||||
"onDelete": "CASCADE",
|
||||
"onUpdate": "NO ACTION",
|
||||
"columns": [
|
||||
"userWalletId"
|
||||
],
|
||||
"referencedColumns": [
|
||||
"id"
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"views": [],
|
||||
"setupQueries": [
|
||||
"CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)",
|
||||
"INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, 'a8a710af25033ee27e5043d001385234')"
|
||||
]
|
||||
}
|
||||
}
|
||||
|
|
@ -1,2 +1,6 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<manifest package="com.tangem.datasource" />
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
|
||||
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
|
||||
<uses-permission android:name="android.permission.INTERNET" />
|
||||
</manifest>
|
||||
|
|
|
|||
|
|
@ -0,0 +1,14 @@
|
|||
package com.tangem.datasource.api.common
|
||||
|
||||
/**
|
||||
* Provides auth for tangemTech API
|
||||
*/
|
||||
interface AuthProvider {
|
||||
|
||||
/**
|
||||
* Returns authToken for tangem tech api
|
||||
*/
|
||||
fun getCardPublicKey(): String
|
||||
|
||||
fun getCardId(): String
|
||||
}
|
||||
|
|
@ -3,6 +3,8 @@ package com.tangem.datasource.api.common
|
|||
import com.squareup.moshi.Moshi
|
||||
import com.squareup.moshi.kotlin.reflect.KotlinJsonAdapterFactory
|
||||
import com.tangem.common.json.MoshiJsonConverter
|
||||
import com.tangem.common.json.TangemSdkAdapter
|
||||
import com.tangem.datasource.api.common.adapter.BigDecimalAdapter
|
||||
import retrofit2.converter.moshi.MoshiConverterFactory
|
||||
|
||||
/**
|
||||
|
|
@ -13,8 +15,9 @@ import retrofit2.converter.moshi.MoshiConverterFactory
|
|||
object MoshiConverter {
|
||||
|
||||
val networkMoshi: Moshi = Moshi.Builder()
|
||||
.add(KotlinJsonAdapterFactory())
|
||||
.addLast(KotlinJsonAdapterFactory())
|
||||
.add(BigDecimalAdapter())
|
||||
.add(TangemSdkAdapter.ByteArrayAdapter())
|
||||
.build()
|
||||
|
||||
val networkMoshiConverter: MoshiConverterFactory = MoshiConverterFactory.create(networkMoshi)
|
||||
|
|
|
|||
|
|
@ -1,8 +1,10 @@
|
|||
package com.tangem.datasource.api.common
|
||||
|
||||
import android.util.Log
|
||||
import com.ihsanbal.logging.Level
|
||||
import com.ihsanbal.logging.LoggingInterceptor
|
||||
import okhttp3.Interceptor
|
||||
import okhttp3.OkHttpClient
|
||||
import okhttp3.logging.HttpLoggingInterceptor
|
||||
import retrofit2.Retrofit
|
||||
import java.util.concurrent.TimeUnit
|
||||
|
||||
|
|
@ -22,7 +24,7 @@ fun createRetrofitInstance(
|
|||
}
|
||||
interceptors.forEach { okHttpBuilder.addInterceptor(it) }
|
||||
|
||||
if (logEnabled) okHttpBuilder.addInterceptor(createHttpLoggingInterceptor())
|
||||
if (logEnabled) okHttpBuilder.addInterceptor(createNetworkLoggingInterceptor())
|
||||
|
||||
return Retrofit.Builder()
|
||||
.baseUrl(baseUrl)
|
||||
|
|
@ -31,6 +33,12 @@ fun createRetrofitInstance(
|
|||
.build()
|
||||
}
|
||||
|
||||
private fun createHttpLoggingInterceptor(): HttpLoggingInterceptor = HttpLoggingInterceptor().apply {
|
||||
level = HttpLoggingInterceptor.Level.BODY
|
||||
}
|
||||
fun createNetworkLoggingInterceptor(): Interceptor {
|
||||
return LoggingInterceptor.Builder()
|
||||
.setLevel(Level.BODY)
|
||||
.log(Log.VERBOSE)
|
||||
.tag(NETWORK_LOGS_TAG)
|
||||
.build()
|
||||
}
|
||||
|
||||
private const val NETWORK_LOGS_TAG = "NetworkLogs"
|
||||
|
|
@ -0,0 +1,59 @@
|
|||
package com.tangem.datasource.api.common
|
||||
|
||||
import com.tangem.datasource.api.common.config.ApiConfig
|
||||
import com.tangem.datasource.api.common.config.managers.ApiConfigsManager
|
||||
import com.tangem.utils.ProviderSuspend
|
||||
import kotlinx.coroutines.runBlocking
|
||||
import okhttp3.HttpUrl
|
||||
import okhttp3.HttpUrl.Companion.toHttpUrl
|
||||
import okhttp3.Interceptor
|
||||
import okhttp3.Request
|
||||
import okhttp3.Response
|
||||
import okio.IOException
|
||||
|
||||
/**
|
||||
* Switch api environment [Interceptor]
|
||||
*
|
||||
* @property id api config id [ApiConfig.ID]
|
||||
* @property apiConfigsManager api configs manager
|
||||
*
|
||||
[REDACTED_AUTHOR]
|
||||
*/
|
||||
internal class SwitchEnvironmentInterceptor(
|
||||
private val id: ApiConfig.ID,
|
||||
private val apiConfigsManager: ApiConfigsManager,
|
||||
) : Interceptor {
|
||||
|
||||
@Throws(IOException::class)
|
||||
override fun intercept(chain: Interceptor.Chain): Response {
|
||||
var request = chain.request()
|
||||
val builder = request.newBuilder()
|
||||
|
||||
val environmentConfig = apiConfigsManager.getEnvironmentConfig(id)
|
||||
|
||||
request = builder
|
||||
.url(url = request.url.adjustBaseUrl(environmentConfig.baseUrl))
|
||||
.addHeaders(headers = environmentConfig.headers)
|
||||
.build()
|
||||
|
||||
return chain.proceed(request)
|
||||
}
|
||||
|
||||
private fun HttpUrl.adjustBaseUrl(url: String): HttpUrl {
|
||||
return this.newBuilder()
|
||||
.host(host = url.toHttpUrl().host)
|
||||
.build()
|
||||
}
|
||||
|
||||
private fun Request.Builder.addHeaders(headers: Map<String, ProviderSuspend<String>>): Request.Builder {
|
||||
runBlocking {
|
||||
headers.forEach { (name, valueProvider) ->
|
||||
val value = valueProvider()
|
||||
|
||||
if (value.isNotBlank()) addHeader(name = name, value = value)
|
||||
}
|
||||
}
|
||||
|
||||
return this
|
||||
}
|
||||
}
|
||||
|
|
@ -1,4 +1,4 @@
|
|||
package com.tangem.datasource.api.common
|
||||
package com.tangem.datasource.api.common.adapter
|
||||
|
||||
import com.squareup.moshi.FromJson
|
||||
import com.squareup.moshi.ToJson
|
||||
|
|
@ -7,7 +7,7 @@ import java.math.BigDecimal
|
|||
/**
|
||||
[REDACTED_AUTHOR]
|
||||
*/
|
||||
class BigDecimalAdapter {
|
||||
internal class BigDecimalAdapter {
|
||||
@FromJson
|
||||
fun fromJson(value: String) = BigDecimal(value)
|
||||
|
||||
|
|
@ -0,0 +1,26 @@
|
|||
package com.tangem.datasource.api.common.adapter
|
||||
|
||||
import com.squareup.moshi.*
|
||||
import org.joda.time.DateTime
|
||||
|
||||
internal class DateTimeAdapter : JsonAdapter<DateTime>() {
|
||||
|
||||
@FromJson
|
||||
override fun fromJson(reader: JsonReader): DateTime? {
|
||||
return if (reader.peek() == JsonReader.Token.NULL) {
|
||||
reader.nextNull<DateTime>()
|
||||
} else {
|
||||
val dateString = reader.nextString()
|
||||
DateTime.parse(dateString)
|
||||
}
|
||||
}
|
||||
|
||||
@ToJson
|
||||
override fun toJson(writer: JsonWriter, value: DateTime?) {
|
||||
if (value != null) {
|
||||
writer.value(value.toString())
|
||||
} else {
|
||||
writer.nullValue()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,29 @@
|
|||
package com.tangem.datasource.api.common.adapter
|
||||
|
||||
import com.squareup.moshi.*
|
||||
import org.joda.time.LocalDate
|
||||
import org.joda.time.format.DateTimeFormat
|
||||
|
||||
internal class LocalDateAdapter : JsonAdapter<LocalDate>() {
|
||||
|
||||
private val formatter = DateTimeFormat.forPattern("yyyy-MM-dd")
|
||||
|
||||
@FromJson
|
||||
override fun fromJson(reader: JsonReader): LocalDate? {
|
||||
return if (reader.peek() == JsonReader.Token.NULL) {
|
||||
reader.nextNull<LocalDate>()
|
||||
} else {
|
||||
val dateString = reader.nextString()
|
||||
return LocalDate.parse(dateString, formatter)
|
||||
}
|
||||
}
|
||||
|
||||
@ToJson
|
||||
override fun toJson(writer: JsonWriter, value: LocalDate?) {
|
||||
if (value != null) {
|
||||
writer.value(formatter.print(value))
|
||||
} else {
|
||||
writer.nullValue()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,50 @@
|
|||
package com.tangem.datasource.api.common.adapter
|
||||
|
||||
import com.squareup.moshi.JsonAdapter
|
||||
import com.squareup.moshi.Moshi
|
||||
import com.squareup.moshi.adapters.EnumJsonAdapter
|
||||
import com.tangem.datasource.api.stakekit.models.response.model.BalanceDTO.BalanceTypeDTO
|
||||
import com.tangem.datasource.api.stakekit.models.response.model.NetworkTypeDTO
|
||||
import com.tangem.datasource.api.stakekit.models.response.model.YieldDTO.MetadataDTO.RewardClaimingDTO
|
||||
import com.tangem.datasource.api.stakekit.models.response.model.YieldDTO.MetadataDTO.RewardScheduleDTO
|
||||
import com.tangem.datasource.api.stakekit.models.response.model.YieldDTO.RewardTypeDTO
|
||||
import com.tangem.datasource.api.stakekit.models.response.model.YieldDTO.ValidatorDTO.ValidatorStatusDTO
|
||||
import com.tangem.datasource.api.stakekit.models.response.model.action.StakingActionStatusDTO
|
||||
import com.tangem.datasource.api.stakekit.models.response.model.action.StakingActionTypeDTO
|
||||
import com.tangem.datasource.api.stakekit.models.response.model.transaction.StakingTransactionStatusDTO
|
||||
import com.tangem.datasource.api.stakekit.models.response.model.transaction.StakingTransactionTypeDTO
|
||||
|
||||
/**
|
||||
* Object to create a adapter for enum types with support for unknown enum values.
|
||||
*/
|
||||
object UnknownEnumMoshiAdapter {
|
||||
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
fun <T : Enum<T>> create(enumType: Class<out Enum<*>>, defaultValue: Enum<*>): JsonAdapter<out Enum<*>> {
|
||||
return EnumJsonAdapter.create(enumType as Class<T>).withUnknownFallback(defaultValue as T)
|
||||
}
|
||||
}
|
||||
|
||||
fun Moshi.Builder.addStakeKitEnumFallbackAdapters(): Moshi.Builder {
|
||||
val map = mapOf(
|
||||
// valid response enums
|
||||
BalanceTypeDTO::class.java to BalanceTypeDTO.UNKNOWN,
|
||||
NetworkTypeDTO::class.java to NetworkTypeDTO.UNKNOWN,
|
||||
RewardClaimingDTO::class.java to RewardClaimingDTO.UNKNOWN,
|
||||
RewardScheduleDTO::class.java to RewardScheduleDTO.UNKNOWN,
|
||||
RewardTypeDTO::class.java to RewardTypeDTO.UNKNOWN,
|
||||
StakingActionStatusDTO::class.java to StakingActionStatusDTO.UNKNOWN,
|
||||
StakingActionTypeDTO::class.java to StakingActionTypeDTO.UNKNOWN,
|
||||
StakingTransactionStatusDTO::class.java to StakingTransactionStatusDTO.UNKNOWN,
|
||||
StakingTransactionTypeDTO::class.java to StakingTransactionTypeDTO.UNKNOWN,
|
||||
ValidatorStatusDTO::class.java to ValidatorStatusDTO.UNKNOWN,
|
||||
)
|
||||
|
||||
return apply {
|
||||
map.forEach { entry ->
|
||||
val enumClass = entry.key
|
||||
val unknownValue = entry.value
|
||||
add(enumClass, UnknownEnumMoshiAdapter.create(enumClass, unknownValue))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,49 @@
|
|||
package com.tangem.datasource.api.common.config
|
||||
|
||||
typealias ApiConfigs = Set<@JvmSuppressWildcards ApiConfig>
|
||||
|
||||
/**
|
||||
* Api config
|
||||
*
|
||||
* @see <a href="https://www.notion.so/tangem/API-eacb264e7daf420a88b419a8a26f5b26?pvs=4">API configuration</a>
|
||||
*
|
||||
[REDACTED_AUTHOR]
|
||||
*/
|
||||
sealed class ApiConfig {
|
||||
|
||||
/** Default environment */
|
||||
abstract val defaultEnvironment: ApiEnvironment
|
||||
|
||||
/** Available environments */
|
||||
abstract val environmentConfigs: List<ApiEnvironmentConfig>
|
||||
|
||||
/** Unique id */
|
||||
val id: ID = initializeId()
|
||||
|
||||
enum class ID {
|
||||
Express,
|
||||
TangemTech,
|
||||
StakeKit,
|
||||
TangemVisaAuth,
|
||||
TangemVisa,
|
||||
}
|
||||
|
||||
private fun initializeId(): ID {
|
||||
return when (this) {
|
||||
is Express -> ID.Express
|
||||
is TangemTech -> ID.TangemTech
|
||||
is StakeKit -> ID.StakeKit
|
||||
is TangemVisaAuth -> ID.TangemVisaAuth
|
||||
is TangemVisa -> ID.TangemVisa
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
internal const val DEBUG_BUILD_TYPE = "debug"
|
||||
internal const val DEBUG_PG_BUILD_TYPE = "debugPG"
|
||||
internal const val INTERNAL_BUILD_TYPE = "internal"
|
||||
internal const val MOCKED_BUILD_TYPE = "mocked"
|
||||
internal const val EXTERNAL_BUILD_TYPE = "external"
|
||||
internal const val RELEASE_BUILD_TYPE = "release"
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,21 @@
|
|||
package com.tangem.datasource.api.common.config
|
||||
|
||||
import com.squareup.moshi.Json
|
||||
import com.squareup.moshi.JsonClass
|
||||
|
||||
/**
|
||||
* Api environment
|
||||
*
|
||||
[REDACTED_AUTHOR]
|
||||
*/
|
||||
@JsonClass(generateAdapter = false)
|
||||
enum class ApiEnvironment {
|
||||
@Json(name = "DEV")
|
||||
DEV,
|
||||
|
||||
@Json(name = "STAGE")
|
||||
STAGE,
|
||||
|
||||
@Json(name = "PROD")
|
||||
PROD,
|
||||
}
|
||||
|
|
@ -0,0 +1,18 @@
|
|||
package com.tangem.datasource.api.common.config
|
||||
|
||||
import com.tangem.utils.ProviderSuspend
|
||||
|
||||
/**
|
||||
* Api environment config
|
||||
*
|
||||
* @property environment environment
|
||||
* @property baseUrl base url
|
||||
* @property headers headers
|
||||
*
|
||||
[REDACTED_AUTHOR]
|
||||
*/
|
||||
data class ApiEnvironmentConfig(
|
||||
val environment: ApiEnvironment,
|
||||
val baseUrl: String,
|
||||
val headers: Map<String, ProviderSuspend<String>> = emptyMap(),
|
||||
)
|
||||
|
|
@ -0,0 +1,84 @@
|
|||
package com.tangem.datasource.api.common.config
|
||||
|
||||
import com.tangem.datasource.BuildConfig
|
||||
import com.tangem.datasource.local.config.environment.EnvironmentConfigStorage
|
||||
import com.tangem.datasource.utils.RequestHeader
|
||||
import com.tangem.lib.auth.ExpressAuthProvider
|
||||
import com.tangem.utils.ProviderSuspend
|
||||
import com.tangem.utils.version.AppVersionProvider
|
||||
|
||||
/**
|
||||
* Express [ApiConfig]
|
||||
*
|
||||
* @property environmentConfigStorage environment config storage
|
||||
* @property expressAuthProvider express auth provider
|
||||
* @property appVersionProvider app version provider
|
||||
*/
|
||||
internal class Express(
|
||||
private val environmentConfigStorage: EnvironmentConfigStorage,
|
||||
private val expressAuthProvider: ExpressAuthProvider,
|
||||
private val appVersionProvider: AppVersionProvider,
|
||||
) : ApiConfig() {
|
||||
|
||||
override val defaultEnvironment: ApiEnvironment = getInitialEnvironment()
|
||||
|
||||
override val environmentConfigs: List<ApiEnvironmentConfig> = listOf(
|
||||
createDevEnvironment(),
|
||||
createStageEnvironment(),
|
||||
createProdEnvironment(),
|
||||
)
|
||||
|
||||
private fun createDevEnvironment(): ApiEnvironmentConfig = ApiEnvironmentConfig(
|
||||
environment = ApiEnvironment.DEV,
|
||||
baseUrl = "[REDACTED_ENV_URL]",
|
||||
headers = createHeaders(isProd = false),
|
||||
)
|
||||
|
||||
private fun createStageEnvironment(): ApiEnvironmentConfig = ApiEnvironmentConfig(
|
||||
environment = ApiEnvironment.STAGE,
|
||||
baseUrl = "[REDACTED_ENV_URL]",
|
||||
headers = createHeaders(isProd = false),
|
||||
)
|
||||
|
||||
private fun createProdEnvironment(): ApiEnvironmentConfig = ApiEnvironmentConfig(
|
||||
environment = ApiEnvironment.PROD,
|
||||
baseUrl = "https://express.tangem.com/v1/",
|
||||
headers = createHeaders(isProd = true),
|
||||
)
|
||||
|
||||
private fun createHeaders(isProd: Boolean) = buildMap {
|
||||
put(key = "api-key", value = ProviderSuspend { getApiKey(isProd) })
|
||||
put(key = "user-id", value = ProviderSuspend(expressAuthProvider::getUserId))
|
||||
put(key = "session-id", value = ProviderSuspend(expressAuthProvider::getSessionId))
|
||||
putAll(from = RequestHeader.AppVersionPlatformHeaders(appVersionProvider).values)
|
||||
put(key = "refcode", value = ProviderSuspend(expressAuthProvider::getRefCode))
|
||||
}
|
||||
|
||||
private fun getApiKey(isProd: Boolean): String {
|
||||
return if (isProd) {
|
||||
environmentConfigStorage.getConfigSync().express
|
||||
} else {
|
||||
environmentConfigStorage.getConfigSync().devExpress
|
||||
}
|
||||
?.apiKey
|
||||
?: error("No express config provided")
|
||||
}
|
||||
|
||||
private companion object {
|
||||
|
||||
fun getInitialEnvironment(): ApiEnvironment {
|
||||
return when (BuildConfig.BUILD_TYPE) {
|
||||
DEBUG_BUILD_TYPE,
|
||||
DEBUG_PG_BUILD_TYPE,
|
||||
-> ApiEnvironment.DEV
|
||||
INTERNAL_BUILD_TYPE,
|
||||
MOCKED_BUILD_TYPE,
|
||||
-> ApiEnvironment.STAGE
|
||||
EXTERNAL_BUILD_TYPE,
|
||||
RELEASE_BUILD_TYPE,
|
||||
-> ApiEnvironment.PROD
|
||||
else -> error("Unknown build type [${BuildConfig.BUILD_TYPE}]")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,33 @@
|
|||
package com.tangem.datasource.api.common.config
|
||||
|
||||
import com.tangem.lib.auth.StakeKitAuthProvider
|
||||
import com.tangem.utils.ProviderSuspend
|
||||
|
||||
/**
|
||||
* StakeKit [ApiConfig]
|
||||
*
|
||||
* @property stakeKitAuthProvider StakeKit auth provider
|
||||
*
|
||||
[REDACTED_AUTHOR]
|
||||
*/
|
||||
internal class StakeKit(
|
||||
private val stakeKitAuthProvider: StakeKitAuthProvider,
|
||||
) : ApiConfig() {
|
||||
|
||||
override val defaultEnvironment: ApiEnvironment = ApiEnvironment.PROD
|
||||
|
||||
override val environmentConfigs: List<ApiEnvironmentConfig> = listOf(
|
||||
createProdEnvironment(),
|
||||
)
|
||||
|
||||
private fun createProdEnvironment(): ApiEnvironmentConfig {
|
||||
return ApiEnvironmentConfig(
|
||||
environment = ApiEnvironment.PROD,
|
||||
baseUrl = "https://api.stakek.it/v1/",
|
||||
headers = mapOf(
|
||||
"X-API-KEY" to ProviderSuspend(stakeKitAuthProvider::getApiKey),
|
||||
"accept" to ProviderSuspend { "application/json" },
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,36 @@
|
|||
package com.tangem.datasource.api.common.config
|
||||
|
||||
import com.tangem.datasource.api.common.AuthProvider
|
||||
import com.tangem.datasource.utils.RequestHeader
|
||||
import com.tangem.utils.version.AppVersionProvider
|
||||
|
||||
/** TangemTech [ApiConfig] */
|
||||
internal class TangemTech(
|
||||
private val appVersionProvider: AppVersionProvider,
|
||||
private val authProvider: AuthProvider,
|
||||
) : ApiConfig() {
|
||||
|
||||
override val defaultEnvironment: ApiEnvironment = ApiEnvironment.PROD
|
||||
|
||||
override val environmentConfigs = listOf(
|
||||
createDevEnvironment(),
|
||||
createProdEnvironment(),
|
||||
)
|
||||
|
||||
private fun createProdEnvironment(): ApiEnvironmentConfig = ApiEnvironmentConfig(
|
||||
environment = ApiEnvironment.PROD,
|
||||
baseUrl = "https://api.tangem-tech.com/v1/",
|
||||
headers = createHeaders(),
|
||||
)
|
||||
|
||||
private fun createDevEnvironment(): ApiEnvironmentConfig = ApiEnvironmentConfig(
|
||||
environment = ApiEnvironment.DEV,
|
||||
baseUrl = "https://devapi.tangem-tech.com/v1/",
|
||||
headers = createHeaders(),
|
||||
)
|
||||
|
||||
private fun createHeaders() = buildMap {
|
||||
putAll(from = RequestHeader.AppVersionPlatformHeaders(appVersionProvider).values)
|
||||
putAll(from = RequestHeader.AuthenticationHeader(authProvider).values)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,26 @@
|
|||
package com.tangem.datasource.api.common.config
|
||||
|
||||
import com.tangem.utils.ProviderSuspend
|
||||
import com.tangem.utils.version.AppVersionProvider
|
||||
|
||||
internal class TangemVisa(
|
||||
private val appVersionProvider: AppVersionProvider,
|
||||
) : ApiConfig() {
|
||||
|
||||
override val defaultEnvironment: ApiEnvironment = ApiEnvironment.PROD
|
||||
|
||||
override val environmentConfigs = listOf(
|
||||
createProdEnvironment(),
|
||||
)
|
||||
|
||||
private fun createProdEnvironment(): ApiEnvironmentConfig = ApiEnvironmentConfig(
|
||||
environment = ApiEnvironment.PROD,
|
||||
baseUrl = "https://bff.tangem.com/",
|
||||
headers = createHeaders(),
|
||||
)
|
||||
|
||||
private fun createHeaders() = mapOf(
|
||||
"version" to ProviderSuspend { appVersionProvider.versionName },
|
||||
"platform" to ProviderSuspend { "Android" },
|
||||
)
|
||||
}
|
||||
|
|
@ -0,0 +1,26 @@
|
|||
package com.tangem.datasource.api.common.config
|
||||
|
||||
import com.tangem.utils.ProviderSuspend
|
||||
import com.tangem.utils.version.AppVersionProvider
|
||||
|
||||
internal class TangemVisaAuth(
|
||||
private val appVersionProvider: AppVersionProvider,
|
||||
) : ApiConfig() {
|
||||
|
||||
override val defaultEnvironment: ApiEnvironment = ApiEnvironment.STAGE
|
||||
|
||||
override val environmentConfigs = listOf(
|
||||
createStageEnvironment(),
|
||||
)
|
||||
|
||||
private fun createStageEnvironment(): ApiEnvironmentConfig = ApiEnvironmentConfig(
|
||||
environment = ApiEnvironment.STAGE,
|
||||
baseUrl = "https://api-s.tangem.org/",
|
||||
headers = createHeaders(),
|
||||
)
|
||||
|
||||
private fun createHeaders() = mapOf(
|
||||
"version" to ProviderSuspend { appVersionProvider.versionName },
|
||||
"platform" to ProviderSuspend { "Android" },
|
||||
)
|
||||
}
|
||||
|
|
@ -0,0 +1,18 @@
|
|||
package com.tangem.datasource.api.common.config.managers
|
||||
|
||||
import com.tangem.datasource.api.common.config.ApiConfig
|
||||
import com.tangem.datasource.api.common.config.ApiEnvironmentConfig
|
||||
|
||||
/**
|
||||
* Api configs manager
|
||||
*
|
||||
[REDACTED_AUTHOR]
|
||||
*/
|
||||
interface ApiConfigsManager {
|
||||
|
||||
/** Initialize resources */
|
||||
suspend fun initialize() {}
|
||||
|
||||
/** Get environment config by [id] */
|
||||
fun getEnvironmentConfig(id: ApiConfig.ID): ApiEnvironmentConfig
|
||||
}
|
||||
|
|
@ -0,0 +1,70 @@
|
|||
package com.tangem.datasource.api.common.config.managers
|
||||
|
||||
import com.tangem.datasource.api.common.config.ApiConfig
|
||||
import com.tangem.datasource.api.common.config.ApiConfigs
|
||||
import com.tangem.datasource.api.common.config.ApiEnvironment
|
||||
import com.tangem.datasource.api.common.config.ApiEnvironmentConfig
|
||||
import com.tangem.datasource.local.preferences.AppPreferencesStore
|
||||
import com.tangem.datasource.local.preferences.PreferencesKeys
|
||||
import com.tangem.datasource.local.preferences.utils.getObjectMap
|
||||
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.flow.*
|
||||
|
||||
/**
|
||||
* Implementation of [ApiConfigsManager] in DEV environment
|
||||
*
|
||||
* @param apiConfigs api configs
|
||||
* @property appPreferencesStore app preferences store
|
||||
* @property dispatchers coroutine dispatcher provider
|
||||
*/
|
||||
internal class DevApiConfigsManager(
|
||||
apiConfigs: ApiConfigs,
|
||||
private val appPreferencesStore: AppPreferencesStore,
|
||||
private val dispatchers: CoroutineDispatcherProvider,
|
||||
) : MutableApiConfigsManager {
|
||||
|
||||
override val configs: Flow<Map<ApiConfig, ApiEnvironment>> get() = _apiConfigs
|
||||
|
||||
private val _apiConfigs = MutableStateFlow(value = apiConfigs.associateWith { it.defaultEnvironment })
|
||||
|
||||
override suspend fun initialize() {
|
||||
// We can't use appPreferencesStore.getObjectMap as base flow,
|
||||
// because we should keep possibility to work with configs synchronous.
|
||||
// See [getBaseUrl]
|
||||
appPreferencesStore.getObjectMap<ApiEnvironment>(PreferencesKeys.apiConfigsEnvironmentKey)
|
||||
.onEach { savedEnvironments ->
|
||||
_apiConfigs.update { apiConfigs ->
|
||||
apiConfigs.mapValues {
|
||||
val (config, currentEnvironment) = it
|
||||
|
||||
savedEnvironments[config.id.name] ?: currentEnvironment
|
||||
}
|
||||
}
|
||||
}
|
||||
.launchIn(CoroutineScope(dispatchers.main))
|
||||
}
|
||||
|
||||
override fun getEnvironmentConfig(id: ApiConfig.ID): ApiEnvironmentConfig {
|
||||
val apiConfigs = _apiConfigs.value
|
||||
|
||||
val config = apiConfigs.map { it }.firstOrNull { it.key.id == id }?.key
|
||||
?: error("Api config with id [$id] not found")
|
||||
|
||||
val currentEnvironment = apiConfigs[config]
|
||||
?: error("Current environment of api config with id [$id] not found")
|
||||
|
||||
return config.environmentConfigs.firstOrNull { it.environment == currentEnvironment }
|
||||
?: error("Api config with id [$id] doesn't contain environment [$currentEnvironment]")
|
||||
}
|
||||
|
||||
override suspend fun changeEnvironment(id: String, environment: ApiEnvironment) {
|
||||
appPreferencesStore.editData { mutablePreferences ->
|
||||
val updatedMap = mutablePreferences.getObjectMap<ApiEnvironment>(PreferencesKeys.apiConfigsEnvironmentKey)
|
||||
.toMutableMap()
|
||||
.apply { put(id, environment) }
|
||||
|
||||
mutablePreferences.setObjectMap(PreferencesKeys.apiConfigsEnvironmentKey, updatedMap)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,19 @@
|
|||
package com.tangem.datasource.api.common.config.managers
|
||||
|
||||
import com.tangem.datasource.api.common.config.ApiConfig
|
||||
import com.tangem.datasource.api.common.config.ApiEnvironment
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
|
||||
/**
|
||||
* Mutable [ApiConfigsManager] for change information about the current api environment
|
||||
*
|
||||
[REDACTED_AUTHOR]
|
||||
*/
|
||||
interface MutableApiConfigsManager : ApiConfigsManager {
|
||||
|
||||
/** Api configs with current [ApiEnvironment] */
|
||||
val configs: Flow<Map<ApiConfig, ApiEnvironment>>
|
||||
|
||||
/** Change api environment [environment] by [id] */
|
||||
suspend fun changeEnvironment(id: String, environment: ApiEnvironment)
|
||||
}
|
||||
|
|
@ -0,0 +1,26 @@
|
|||
package com.tangem.datasource.api.common.config.managers
|
||||
|
||||
import com.tangem.datasource.api.common.config.ApiConfig
|
||||
import com.tangem.datasource.api.common.config.ApiConfigs
|
||||
import com.tangem.datasource.api.common.config.ApiEnvironmentConfig
|
||||
|
||||
/**
|
||||
* Implementation of [ApiConfigsManager] in PROD environment
|
||||
*
|
||||
* @property apiConfigs api configs
|
||||
*/
|
||||
internal class ProdApiConfigsManager(
|
||||
private val apiConfigs: ApiConfigs,
|
||||
) : ApiConfigsManager {
|
||||
|
||||
override fun getEnvironmentConfig(id: ApiConfig.ID): ApiEnvironmentConfig {
|
||||
val config = apiConfigs.firstOrNull { it.id == id }
|
||||
?: error("Api config with id [$id] not found. Check that ApiConfig with id [$id] was provided into DI")
|
||||
|
||||
return config.environmentConfigs.firstOrNull { it.environment == config.defaultEnvironment }
|
||||
?: error(
|
||||
"Api config with id [$id] doesn't contain environment [${config.defaultEnvironment}]. " +
|
||||
"Check ApiConfig's environments is included default environment",
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,40 @@
|
|||
package com.tangem.datasource.api.common.response
|
||||
|
||||
/**
|
||||
* Represents the possible responses from an API request.
|
||||
*
|
||||
* @param T The type of the data that is expected in a successful response.
|
||||
*/
|
||||
sealed class ApiResponse<T : Any> {
|
||||
|
||||
/**
|
||||
* Represents a successful response from the API.
|
||||
*
|
||||
* @property data The data returned by the API.
|
||||
*/
|
||||
data class Success<T : Any>(val data: T) : ApiResponse<T>()
|
||||
|
||||
/**
|
||||
* Represents an error response or failure from the API.
|
||||
*
|
||||
* @property cause The cause of the error.
|
||||
*/
|
||||
data class Error(val cause: ApiResponseError) : ApiResponse<Nothing>()
|
||||
}
|
||||
|
||||
/**
|
||||
* Wraps data in a [ApiResponse.Success] instance.
|
||||
*
|
||||
* @param data The data to wrap.
|
||||
* @return A [ApiResponse.Success] instance containing the provided data.
|
||||
*/
|
||||
internal fun <T : Any> apiSuccess(data: T): ApiResponse<T> = ApiResponse.Success(data)
|
||||
|
||||
/**
|
||||
* Wraps an [ApiResponseError] in a [ApiResponse.Error] instance.
|
||||
*
|
||||
* @param cause The error to wrap.
|
||||
* @return A [ApiResponse.Error] instance containing the provided error.
|
||||
*/
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
internal fun <T : Any> apiError(cause: ApiResponseError): ApiResponse<T> = ApiResponse.Error(cause) as ApiResponse<T>
|
||||
|
|
@ -0,0 +1,16 @@
|
|||
package com.tangem.datasource.api.common.response
|
||||
|
||||
import retrofit2.Call
|
||||
import retrofit2.CallAdapter
|
||||
import java.lang.reflect.Type
|
||||
|
||||
internal class ApiResponseCallAdapter(
|
||||
private val resultType: Type,
|
||||
) : CallAdapter<Type, Call<ApiResponse<Type>>> {
|
||||
|
||||
override fun responseType(): Type = resultType
|
||||
|
||||
override fun adapt(call: Call<Type>): Call<ApiResponse<Type>> {
|
||||
return ApiResponseCallDelegate(call)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,29 @@
|
|||
package com.tangem.datasource.api.common.response
|
||||
|
||||
import retrofit2.Call
|
||||
import retrofit2.CallAdapter
|
||||
import retrofit2.Retrofit
|
||||
import java.lang.reflect.ParameterizedType
|
||||
import java.lang.reflect.Type
|
||||
|
||||
class ApiResponseCallAdapterFactory private constructor() : CallAdapter.Factory() {
|
||||
|
||||
override fun get(returnType: Type, annotations: Array<out Annotation>, retrofit: Retrofit): CallAdapter<*, *>? {
|
||||
if (getRawType(returnType) != Call::class.java) {
|
||||
return null
|
||||
}
|
||||
|
||||
val callType = getParameterUpperBound(0, returnType as ParameterizedType)
|
||||
if (getRawType(callType) != ApiResponse::class.java) {
|
||||
return null
|
||||
}
|
||||
|
||||
val resultType = getParameterUpperBound(0, callType as ParameterizedType)
|
||||
return ApiResponseCallAdapter(resultType)
|
||||
}
|
||||
|
||||
companion object {
|
||||
|
||||
fun create() = ApiResponseCallAdapterFactory()
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,53 @@
|
|||
package com.tangem.datasource.api.common.response
|
||||
|
||||
import okhttp3.Request
|
||||
import okio.Timeout
|
||||
import retrofit2.Call
|
||||
import retrofit2.Callback
|
||||
import retrofit2.Response
|
||||
import timber.log.Timber
|
||||
|
||||
internal class ApiResponseCallDelegate<T : Any>(
|
||||
private val wrappedCall: Call<T>,
|
||||
) : Call<ApiResponse<T>> {
|
||||
|
||||
override fun enqueue(callback: Callback<ApiResponse<T>>) {
|
||||
wrappedCall.enqueue(ApiResponseCallback(callback))
|
||||
}
|
||||
|
||||
override fun execute(): Response<ApiResponse<T>> = throw NotImplementedError()
|
||||
override fun clone(): Call<ApiResponse<T>> = ApiResponseCallDelegate(wrappedCall.clone())
|
||||
override fun request(): Request = wrappedCall.request()
|
||||
override fun timeout(): Timeout = wrappedCall.timeout()
|
||||
override fun isExecuted(): Boolean = wrappedCall.isExecuted
|
||||
override fun isCanceled(): Boolean = wrappedCall.isCanceled
|
||||
override fun cancel() {
|
||||
wrappedCall.cancel()
|
||||
}
|
||||
|
||||
private inner class ApiResponseCallback(
|
||||
private val responseCallback: Callback<ApiResponse<T>>,
|
||||
) : Callback<T> {
|
||||
|
||||
override fun onResponse(call: Call<T>, response: Response<T>) {
|
||||
val safeResponse = response.toSafeApiResponse()
|
||||
|
||||
responseCallback.onResponse(this@ApiResponseCallDelegate, Response.success(safeResponse))
|
||||
}
|
||||
|
||||
override fun onFailure(call: Call<T>, t: Throwable) {
|
||||
val error = try {
|
||||
t.toApiError()
|
||||
} catch (e: ApiResponseError) {
|
||||
Timber.e(e, "error map toApiError")
|
||||
e
|
||||
} catch (e: Exception) {
|
||||
Timber.e(e, "onFailure UnknownException")
|
||||
ApiResponseError.UnknownException(e)
|
||||
}
|
||||
val safeResponse = apiError<T>(error)
|
||||
|
||||
responseCallback.onResponse(this@ApiResponseCallDelegate, Response.success(safeResponse))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,93 @@
|
|||
package com.tangem.datasource.api.common.response
|
||||
|
||||
/**
|
||||
* Represents the possible errors that can occur during an API request.
|
||||
*/
|
||||
sealed class ApiResponseError : Exception() {
|
||||
|
||||
/**
|
||||
* Represents an HTTP exception, which typically occurs when the server responds
|
||||
* with a non-2xx HTTP status code.
|
||||
*
|
||||
* @property code The HTTP status code.
|
||||
* @property message A human-readable message describing the error.
|
||||
*/
|
||||
data class HttpException(
|
||||
val code: Code,
|
||||
override val message: String?,
|
||||
val errorBody: String?,
|
||||
) : ApiResponseError() {
|
||||
|
||||
// region Error Codes
|
||||
enum class Code(val code: Int) {
|
||||
// 4xx Server Errors
|
||||
BAD_REQUEST(code = 400),
|
||||
UNAUTHORIZED(code = 401),
|
||||
PAYMENT_REQUIRED(code = 402),
|
||||
FORBIDDEN(code = 403),
|
||||
NOT_FOUND(code = 404),
|
||||
METHOD_NOT_ALLOWED(code = 405),
|
||||
NOT_ACCEPTABLE(code = 406),
|
||||
PROXY_AUTHENTICATION_REQUIRED(code = 407),
|
||||
REQUEST_TIMEOUT(code = 408),
|
||||
CONFLICT(code = 409),
|
||||
GONE(code = 410),
|
||||
LENGTH_REQUIRED(code = 411),
|
||||
PRECONDITION_FAILED(code = 412),
|
||||
PAYLOAD_TOO_LARGE(code = 413),
|
||||
URI_TOO_LONG(code = 414),
|
||||
UNSUPPORTED_MEDIA_TYPE(code = 415),
|
||||
RANGE_NOT_SATISFIABLE(code = 416),
|
||||
EXPECTATION_FAILED(code = 417),
|
||||
IM_A_TEAPOT(code = 418), // Not an error, but an April Fools' joke from RFC 2324
|
||||
UNPROCESSABLE_ENTITY(code = 422),
|
||||
LOCKED(code = 423),
|
||||
FAILED_DEPENDENCY(code = 424),
|
||||
TOO_EARLY(code = 425),
|
||||
UPGRADE_REQUIRED(code = 426),
|
||||
PRECONDITION_REQUIRED(code = 428),
|
||||
TOO_MANY_REQUESTS(code = 429),
|
||||
REQUEST_HEADER_FIELDS_TOO_LARGE(code = 431),
|
||||
UNAVAILABLE_FOR_LEGAL_REASONS(code = 451),
|
||||
// 5xx Server Errors
|
||||
INTERNAL_SERVER_ERROR(code = 500),
|
||||
NOT_IMPLEMENTED(code = 501),
|
||||
BAD_GATEWAY(code = 502),
|
||||
SERVICE_UNAVAILABLE(code = 503),
|
||||
GATEWAY_TIMEOUT(code = 504),
|
||||
HTTP_VERSION_NOT_SUPPORTED(code = 505),
|
||||
VARIANT_ALSO_NEGOTIATES(code = 506),
|
||||
INSUFFICIENT_STORAGE(code = 507),
|
||||
LOOP_DETECTED(code = 508),
|
||||
NOT_EXTENDED(code = 510),
|
||||
NETWORK_AUTHENTICATION_REQUIRED(code = 511),
|
||||
;
|
||||
|
||||
override fun toString(): String = "$code - $name"
|
||||
|
||||
companion object {
|
||||
val values = values()
|
||||
}
|
||||
}
|
||||
// endregion Error Codes
|
||||
}
|
||||
|
||||
/** Represents a network error, typically when there's no connectivity. */
|
||||
@Suppress("UnusedPrivateMember")
|
||||
data object NetworkException : ApiResponseError() {
|
||||
private fun readResolve(): Any = NetworkException
|
||||
}
|
||||
|
||||
/** Represents a timeout error, typically when the server takes too long to respond. */
|
||||
@Suppress("UnusedPrivateMember")
|
||||
data object TimeoutException : ApiResponseError() {
|
||||
private fun readResolve(): Any = TimeoutException
|
||||
}
|
||||
|
||||
/**
|
||||
* Represents an unexpected exception that doesn't fall into one of the other categories.
|
||||
*
|
||||
* @property cause The exception that caused this error.
|
||||
*/
|
||||
data class UnknownException(override val cause: Throwable) : ApiResponseError()
|
||||
}
|
||||
|
|
@ -0,0 +1,13 @@
|
|||
package com.tangem.datasource.api.common.response
|
||||
|
||||
fun <T : Any> ApiResponse<T>.getOrThrow(): T = when (this) {
|
||||
is ApiResponse.Error -> throw cause
|
||||
is ApiResponse.Success -> data
|
||||
}
|
||||
|
||||
fun <T : Any, R : Any> ApiResponse<T>.fold(onSuccess: (T) -> R, onError: (ApiResponseError) -> R): R {
|
||||
return when (this) {
|
||||
is ApiResponse.Error -> onError(cause)
|
||||
is ApiResponse.Success -> onSuccess(data)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,45 @@
|
|||
package com.tangem.datasource.api.common.response
|
||||
|
||||
import kotlinx.coroutines.TimeoutCancellationException
|
||||
import retrofit2.Response
|
||||
import timber.log.Timber
|
||||
import java.net.ConnectException
|
||||
import java.net.SocketTimeoutException
|
||||
import java.net.UnknownHostException
|
||||
import java.util.concurrent.TimeoutException
|
||||
import javax.net.ssl.SSLHandshakeException
|
||||
|
||||
internal fun <T : Any> Response<T>.toSafeApiResponse(): ApiResponse<T> {
|
||||
val body = body()
|
||||
|
||||
return if (isSuccessful && body != null) {
|
||||
apiSuccess(body)
|
||||
} else {
|
||||
val code = ApiResponseError.HttpException.Code.values
|
||||
.firstOrNull { it.code == code() }
|
||||
val e = try {
|
||||
if (code == null) {
|
||||
ApiResponseError.UnknownException(IllegalArgumentException("Unknown error status code: ${code()}"))
|
||||
} else {
|
||||
ApiResponseError.HttpException(code, message(), errorBody()?.string())
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
Timber.e(e, "UnknownException occured")
|
||||
ApiResponseError.UnknownException(e)
|
||||
}
|
||||
|
||||
apiError(e)
|
||||
}
|
||||
}
|
||||
|
||||
internal fun Throwable.toApiError(): ApiResponseError = when (this) {
|
||||
is ConnectException,
|
||||
is UnknownHostException,
|
||||
is SSLHandshakeException,
|
||||
-> ApiResponseError.NetworkException
|
||||
is TimeoutException,
|
||||
is TimeoutCancellationException,
|
||||
is SocketTimeoutException,
|
||||
-> ApiResponseError.TimeoutException
|
||||
else -> ApiResponseError.UnknownException(cause = this)
|
||||
}
|
||||
|
|
@ -0,0 +1,6 @@
|
|||
package com.tangem.datasource.api.common.visa
|
||||
|
||||
interface TangemVisaAuthProvider {
|
||||
|
||||
suspend fun getAuthHeader(cardId: String): String
|
||||
}
|
||||
|
|
@ -0,0 +1,64 @@
|
|||
package com.tangem.datasource.api.express
|
||||
|
||||
import com.tangem.datasource.api.common.response.ApiResponse
|
||||
import com.tangem.datasource.api.express.models.request.AssetsRequestBody
|
||||
import com.tangem.datasource.api.express.models.request.ExchangeSentRequestBody
|
||||
import com.tangem.datasource.api.express.models.request.PairsRequestBody
|
||||
import com.tangem.datasource.api.express.models.response.*
|
||||
import retrofit2.http.Body
|
||||
import retrofit2.http.GET
|
||||
import retrofit2.http.POST
|
||||
import retrofit2.http.Query
|
||||
|
||||
/**
|
||||
* Interface of Tangem Express API (new swap mechanism)
|
||||
*/
|
||||
@Suppress("LongParameterList")
|
||||
interface TangemExpressApi {
|
||||
|
||||
@POST("assets")
|
||||
suspend fun getAssets(@Body body: AssetsRequestBody): ApiResponse<List<Asset>>
|
||||
|
||||
@POST("pairs")
|
||||
suspend fun getPairs(@Body body: PairsRequestBody): ApiResponse<List<SwapPair>>
|
||||
|
||||
@GET("providers")
|
||||
suspend fun getProviders(): ApiResponse<List<ExchangeProvider>>
|
||||
|
||||
@GET("exchange-quote")
|
||||
suspend fun getExchangeQuote(
|
||||
@Query("fromContractAddress") fromContractAddress: String,
|
||||
@Query("fromNetwork") fromNetwork: String,
|
||||
@Query("toContractAddress") toContractAddress: String,
|
||||
@Query("toNetwork") toNetwork: String,
|
||||
@Query("fromAmount") fromAmount: String,
|
||||
@Query("fromDecimals") fromDecimals: Int,
|
||||
@Query("toDecimals") toDecimals: Int,
|
||||
@Query("providerId") providerId: String,
|
||||
@Query("rateType") rateType: String,
|
||||
): ApiResponse<ExchangeQuoteResponse>
|
||||
|
||||
@GET("exchange-data")
|
||||
suspend fun getExchangeData(
|
||||
@Query("fromContractAddress") fromContractAddress: String,
|
||||
@Query("fromNetwork") fromNetwork: String,
|
||||
@Query("toContractAddress") toContractAddress: String,
|
||||
@Query("fromAddress") fromAddress: String,
|
||||
@Query("toNetwork") toNetwork: String,
|
||||
@Query("fromAmount") fromAmount: String,
|
||||
@Query("fromDecimals") fromDecimals: Int,
|
||||
@Query("toDecimals") toDecimals: Int,
|
||||
@Query("providerId") providerId: String,
|
||||
@Query("rateType") rateType: String,
|
||||
@Query("toAddress") toAddress: String,
|
||||
@Query("requestId") requestId: String,
|
||||
@Query("refundAddress") refundAddress: String?, // for cex only
|
||||
@Query("refundExtraId") refundExtraId: String?, // for cex only
|
||||
): ApiResponse<ExchangeDataResponse>
|
||||
|
||||
@GET("exchange-status")
|
||||
suspend fun getExchangeStatus(@Query("txId") txId: String): ApiResponse<ExchangeStatusResponse>
|
||||
|
||||
@POST("exchange-sent")
|
||||
suspend fun exchangeSent(@Body body: ExchangeSentRequestBody): ApiResponse<ExchangeSentResponseBody>
|
||||
}
|
||||
|
|
@ -0,0 +1,5 @@
|
|||
package com.tangem.datasource.api.express.models
|
||||
|
||||
object TangemExpressValues {
|
||||
const val EMPTY_CONTRACT_ADDRESS_VALUE = "0"
|
||||
}
|
||||
|
|
@ -0,0 +1,9 @@
|
|||
package com.tangem.datasource.api.express.models.request
|
||||
|
||||
import com.squareup.moshi.Json
|
||||
import com.squareup.moshi.JsonClass
|
||||
|
||||
@JsonClass(generateAdapter = true)
|
||||
data class AssetsRequestBody(
|
||||
@Json(name = "tokensList") val tokensList: List<LeastTokenInfo>?,
|
||||
)
|
||||
|
|
@ -0,0 +1,20 @@
|
|||
package com.tangem.datasource.api.express.models.request
|
||||
|
||||
import com.squareup.moshi.Json
|
||||
import com.squareup.moshi.JsonClass
|
||||
|
||||
@JsonClass(generateAdapter = true)
|
||||
data class ExchangeSentRequestBody(
|
||||
@Json(name = "txId")
|
||||
val txId: String,
|
||||
@Json(name = "fromNetwork")
|
||||
val fromNetwork: String,
|
||||
@Json(name = "fromAddress")
|
||||
val fromAddress: String,
|
||||
@Json(name = "payinAddress")
|
||||
val payinAddress: String,
|
||||
@Json(name = "payinExtraId")
|
||||
val payinExtraId: String?,
|
||||
@Json(name = "txHash")
|
||||
val txHash: String,
|
||||
)
|
||||
|
|
@ -0,0 +1,13 @@
|
|||
package com.tangem.datasource.api.express.models.request
|
||||
|
||||
import com.squareup.moshi.Json
|
||||
import com.squareup.moshi.JsonClass
|
||||
|
||||
@JsonClass(generateAdapter = true)
|
||||
data class LeastTokenInfo(
|
||||
@Json(name = "contractAddress")
|
||||
val contractAddress: String,
|
||||
|
||||
@Json(name = "network")
|
||||
val network: String,
|
||||
)
|
||||
|
|
@ -0,0 +1,13 @@
|
|||
package com.tangem.datasource.api.express.models.request
|
||||
|
||||
import com.squareup.moshi.Json
|
||||
import com.squareup.moshi.JsonClass
|
||||
|
||||
@JsonClass(generateAdapter = true)
|
||||
data class PairsRequestBody(
|
||||
@Json(name = "from")
|
||||
val from: List<LeastTokenInfo>,
|
||||
|
||||
@Json(name = "to")
|
||||
val to: List<LeastTokenInfo>,
|
||||
)
|
||||
|
|
@ -0,0 +1,19 @@
|
|||
package com.tangem.datasource.api.express.models.response
|
||||
|
||||
import com.squareup.moshi.Json
|
||||
import com.squareup.moshi.JsonClass
|
||||
|
||||
@JsonClass(generateAdapter = true)
|
||||
data class Asset(
|
||||
@Json(name = "contractAddress")
|
||||
val contractAddress: String,
|
||||
|
||||
@Json(name = "network")
|
||||
val network: String,
|
||||
|
||||
@Json(name = "exchangeAvailable")
|
||||
val exchangeAvailable: Boolean,
|
||||
|
||||
@Json(name = "onrampAvailable")
|
||||
val onrampAvailable: Boolean?,
|
||||
)
|
||||
|
|
@ -0,0 +1,87 @@
|
|||
package com.tangem.datasource.api.express.models.response
|
||||
|
||||
import com.squareup.moshi.Json
|
||||
import com.squareup.moshi.JsonClass
|
||||
|
||||
@JsonClass(generateAdapter = true)
|
||||
data class ExchangeDataResponseWithTxDetails(
|
||||
@Json(name = "dataResponse")
|
||||
val dataResponse: ExchangeDataResponse,
|
||||
@Json(name = "txDetails")
|
||||
val txDetails: TxDetails,
|
||||
)
|
||||
|
||||
@JsonClass(generateAdapter = true)
|
||||
data class ExchangeDataResponse(
|
||||
@Json(name = "fromAmount")
|
||||
val fromAmount: String,
|
||||
|
||||
@Json(name = "fromDecimals")
|
||||
val fromDecimals: Int,
|
||||
|
||||
@Json(name = "toAmount")
|
||||
val toAmount: String,
|
||||
|
||||
@Json(name = "toDecimals")
|
||||
val toDecimals: Int,
|
||||
|
||||
@Json(name = "txId")
|
||||
val txId: String, // inner tangem-express transaction id
|
||||
|
||||
@Json(name = "txDetailsJson")
|
||||
val txDetailsJson: String,
|
||||
|
||||
@Json(name = "signature")
|
||||
val signature: String,
|
||||
)
|
||||
|
||||
@JsonClass(generateAdapter = true)
|
||||
data class TxDetails(
|
||||
@Json(name = "payoutAddress")
|
||||
val payoutAddress: String,
|
||||
|
||||
@Json(name = "requestId")
|
||||
val requestId: String,
|
||||
|
||||
@Json(name = "txType")
|
||||
val txType: TxType,
|
||||
|
||||
@Json(name = "txFrom")
|
||||
val txFrom: String?, // account for debiting tokens (same as toAddress) if DEX, null if CEX
|
||||
|
||||
@Json(name = "txTo")
|
||||
val txTo: String, // swap smart-contract address if DEX, address for sending transaction if CEX
|
||||
|
||||
@Json(name = "txData")
|
||||
val txData: String?, // transaction data if DEX, null if CEX
|
||||
|
||||
@Json(name = "txValue")
|
||||
val txValue: String, // amount (same as fromAmount for Coin, but for bridge equal to otherNativeFee)
|
||||
|
||||
@Json(name = "otherNativeFee")
|
||||
val otherNativeFee: String?,
|
||||
|
||||
@Json(name = "externalTxId")
|
||||
val externalTxId: String?, // null if DEX, provider transaction id if CEX
|
||||
|
||||
@Json(name = "externalTxUrl")
|
||||
val externalTxUrl: String?, // null if DEX, url of provider exchange status page if CEX
|
||||
|
||||
@Json(name = "txExtraIdName")
|
||||
val txExtraIdName: String?,
|
||||
|
||||
@Json(name = "txExtraId")
|
||||
val txExtraId: String?,
|
||||
|
||||
@Json(name = "gas")
|
||||
val gas: String?,
|
||||
)
|
||||
|
||||
@JsonClass(generateAdapter = false)
|
||||
enum class TxType {
|
||||
@Json(name = "send")
|
||||
SEND,
|
||||
|
||||
@Json(name = "swap")
|
||||
SWAP,
|
||||
}
|
||||
|
|
@ -0,0 +1,50 @@
|
|||
package com.tangem.datasource.api.express.models.response
|
||||
|
||||
import com.squareup.moshi.Json
|
||||
import com.squareup.moshi.JsonClass
|
||||
import java.math.BigDecimal
|
||||
|
||||
@JsonClass(generateAdapter = true)
|
||||
data class ExchangeProvider(
|
||||
@Json(name = "id")
|
||||
val id: String,
|
||||
|
||||
@Json(name = "name")
|
||||
val name: String,
|
||||
|
||||
@Json(name = "type")
|
||||
val type: ExchangeProviderType,
|
||||
|
||||
@Json(name = "imageLarge")
|
||||
val imageLargeUrl: String,
|
||||
|
||||
@Json(name = "imageSmall")
|
||||
val imageSmallUrl: String,
|
||||
|
||||
@Json(name = "termsOfUse")
|
||||
val termsOfUse: String?,
|
||||
|
||||
@Json(name = "privacyPolicy")
|
||||
val privacyPolicy: String?,
|
||||
|
||||
@Json(name = "recommended")
|
||||
val isRecommended: Boolean = false,
|
||||
|
||||
@Json(name = "slippage")
|
||||
val slippage: BigDecimal?,
|
||||
)
|
||||
|
||||
@JsonClass(generateAdapter = false)
|
||||
enum class ExchangeProviderType {
|
||||
@Json(name = "dex")
|
||||
DEX,
|
||||
|
||||
@Json(name = "cex")
|
||||
CEX,
|
||||
|
||||
@Json(name = "dex-bridge")
|
||||
DEX_BRIDGE,
|
||||
|
||||
@Json(name = "onramp")
|
||||
ONRAMP,
|
||||
}
|
||||
|
|
@ -0,0 +1,28 @@
|
|||
package com.tangem.datasource.api.express.models.response
|
||||
|
||||
import com.squareup.moshi.Json
|
||||
import com.squareup.moshi.JsonClass
|
||||
import java.math.BigDecimal
|
||||
|
||||
@JsonClass(generateAdapter = true)
|
||||
data class ExchangeQuoteResponse(
|
||||
|
||||
@Json(name = "fromAmount")
|
||||
val fromAmount: String,
|
||||
|
||||
@Json(name = "fromDecimals")
|
||||
val fromDecimals: Int,
|
||||
|
||||
@Json(name = "toAmount")
|
||||
val toAmount: String,
|
||||
|
||||
@Json(name = "toDecimals")
|
||||
val toDecimals: Int,
|
||||
|
||||
@Json(name = "allowanceContract")
|
||||
val allowanceContract: String?,
|
||||
|
||||
@Json(name = "minAmount")
|
||||
val minAmount: BigDecimal,
|
||||
|
||||
)
|
||||
|
|
@ -0,0 +1,12 @@
|
|||
package com.tangem.datasource.api.express.models.response
|
||||
|
||||
import com.squareup.moshi.Json
|
||||
import com.squareup.moshi.JsonClass
|
||||
|
||||
@JsonClass(generateAdapter = true)
|
||||
data class ExchangeSentResponseBody(
|
||||
@Json(name = "txId")
|
||||
val txId: String,
|
||||
@Json(name = "status")
|
||||
val status: String,
|
||||
)
|
||||
|
|
@ -0,0 +1,84 @@
|
|||
package com.tangem.datasource.api.express.models.response
|
||||
|
||||
import com.squareup.moshi.Json
|
||||
import com.squareup.moshi.JsonClass
|
||||
|
||||
@JsonClass(generateAdapter = true)
|
||||
data class ExchangeStatusResponse(
|
||||
|
||||
@Json(name = "providerId")
|
||||
val providerId: String,
|
||||
|
||||
@Json(name = "status")
|
||||
val status: ExchangeStatus,
|
||||
|
||||
@Json(name = "externalTxId")
|
||||
val externalTxId: String?,
|
||||
|
||||
@Json(name = "externalTxUrl")
|
||||
val externalTxUrl: String?,
|
||||
|
||||
@Json(name = "error")
|
||||
val error: ExchangeStatusError?,
|
||||
|
||||
@Json(name = "refundNetwork")
|
||||
val refundNetwork: String? = null,
|
||||
|
||||
@Json(name = "refundContractAddress")
|
||||
val refundContractAddress: String? = null,
|
||||
)
|
||||
|
||||
@JsonClass(generateAdapter = false)
|
||||
enum class ExchangeStatus {
|
||||
|
||||
@Json(name = "new")
|
||||
New,
|
||||
|
||||
@Json(name = "waiting")
|
||||
Waiting,
|
||||
|
||||
@Json(name = "confirming")
|
||||
Confirming,
|
||||
|
||||
@Json(name = "exchanging")
|
||||
Exchanging,
|
||||
|
||||
@Json(name = "sending")
|
||||
Sending,
|
||||
|
||||
@Json(name = "finished")
|
||||
Finished,
|
||||
|
||||
@Json(name = "failed")
|
||||
Failed,
|
||||
|
||||
@Json(name = "refunded")
|
||||
Refunded,
|
||||
|
||||
@Json(name = "verifying")
|
||||
Verifying,
|
||||
|
||||
@Json(name = "expired")
|
||||
Cancelled,
|
||||
|
||||
@Json(name = "waiting-tx-hash")
|
||||
WaitingTxHash,
|
||||
|
||||
@Json(name = "tx-failed")
|
||||
TxFailed,
|
||||
|
||||
@Json(name = "paused")
|
||||
Paused,
|
||||
|
||||
@Json(name = "unknown")
|
||||
Unknown,
|
||||
}
|
||||
|
||||
@JsonClass(generateAdapter = true)
|
||||
data class ExchangeStatusError(
|
||||
@Json(name = "code")
|
||||
val code: Int,
|
||||
|
||||
@Json(name = "description")
|
||||
val description: String,
|
||||
)
|
||||
|
|
@ -0,0 +1,50 @@
|
|||
package com.tangem.datasource.api.express.models.response
|
||||
|
||||
import com.squareup.moshi.Json
|
||||
import com.squareup.moshi.JsonClass
|
||||
import java.math.BigDecimal
|
||||
|
||||
@JsonClass(generateAdapter = true)
|
||||
data class ExpressErrorResponse(
|
||||
@Json(name = "error")
|
||||
val error: ExpressError,
|
||||
)
|
||||
|
||||
@JsonClass(generateAdapter = true)
|
||||
data class ExpressError(
|
||||
@Json(name = "code")
|
||||
val code: Int,
|
||||
|
||||
@Json(name = "description")
|
||||
val description: String?,
|
||||
|
||||
@Json(name = "value")
|
||||
val value: ExpressErrorValue?,
|
||||
)
|
||||
|
||||
@JsonClass(generateAdapter = true)
|
||||
data class ExpressErrorValue(
|
||||
@Json(name = "minAmount")
|
||||
val minAmount: String?,
|
||||
|
||||
@Json(name = "maxAmount")
|
||||
val maxAmount: String?,
|
||||
|
||||
@Json(name = "decimals")
|
||||
val decimals: Int?,
|
||||
|
||||
@Json(name = "currentAllowance")
|
||||
val currentAllowance: BigDecimal?,
|
||||
|
||||
@Json(name = "receivedFromDecimals")
|
||||
val receivedFromDecimals: Int?,
|
||||
|
||||
@Json(name = "expressFromDecimals")
|
||||
val expressFromDecimals: Int?,
|
||||
|
||||
@Json(name = "fromAmount")
|
||||
val fromAmount: String?,
|
||||
|
||||
@Json(name = "fromAmountProvider")
|
||||
val fromAmountProvider: String?,
|
||||
)
|
||||
|
|
@ -0,0 +1,36 @@
|
|||
package com.tangem.datasource.api.express.models.response
|
||||
|
||||
import com.squareup.moshi.Json
|
||||
import com.squareup.moshi.JsonClass
|
||||
import com.tangem.datasource.api.express.models.request.LeastTokenInfo
|
||||
|
||||
@JsonClass(generateAdapter = true)
|
||||
data class SwapPair(
|
||||
@Json(name = "from")
|
||||
val from: LeastTokenInfo,
|
||||
|
||||
@Json(name = "to")
|
||||
val to: LeastTokenInfo,
|
||||
|
||||
@Json(name = "providers")
|
||||
val providers: List<SwapPairProvider>,
|
||||
|
||||
)
|
||||
|
||||
@JsonClass(generateAdapter = true)
|
||||
data class SwapPairProvider(
|
||||
@Json(name = "providerId")
|
||||
val providerId: String,
|
||||
|
||||
@Json(name = "rateTypes")
|
||||
val rateTypes: List<RateType>,
|
||||
)
|
||||
|
||||
@JsonClass(generateAdapter = false)
|
||||
enum class RateType {
|
||||
@Json(name = "float")
|
||||
FLOAT,
|
||||
|
||||
@Json(name = "fixed")
|
||||
FIXED,
|
||||
}
|
||||
|
|
@ -0,0 +1,6 @@
|
|||
package com.tangem.datasource.api.express.models.response
|
||||
|
||||
class SwapPairsWithProviders(
|
||||
val swapPair: List<SwapPair>,
|
||||
val providers: List<ExchangeProvider>,
|
||||
)
|
||||
|
|
@ -0,0 +1,46 @@
|
|||
package com.tangem.datasource.api.markets
|
||||
|
||||
import com.tangem.datasource.api.common.response.ApiResponse
|
||||
import com.tangem.datasource.api.markets.models.response.*
|
||||
import retrofit2.http.GET
|
||||
import retrofit2.http.Path
|
||||
import retrofit2.http.Query
|
||||
|
||||
interface TangemTechMarketsApi {
|
||||
|
||||
@Suppress("LongParameterList")
|
||||
@GET("coins/list")
|
||||
suspend fun getCoinsList(
|
||||
@Query("currency") currency: String,
|
||||
@Query("interval") interval: String,
|
||||
@Query("offset") offset: Int,
|
||||
@Query("limit") limit: Int,
|
||||
@Query("order") order: String,
|
||||
@Query("search") search: String?,
|
||||
@Query("timestamp") timestamp: Long?,
|
||||
): ApiResponse<TokenMarketListResponse>
|
||||
|
||||
@GET("coins/{coin_id}")
|
||||
suspend fun getCoinMarketData(
|
||||
@Path("coin_id") coinId: String,
|
||||
@Query("currency") currency: String,
|
||||
@Query("language") language: String,
|
||||
): ApiResponse<TokenMarketInfoResponse>
|
||||
|
||||
@GET("coins/{coin_id}/history")
|
||||
suspend fun getCoinChart(
|
||||
@Path("coin_id") coinId: String,
|
||||
@Query("currency") currency: String,
|
||||
@Query("interval") interval: String,
|
||||
): ApiResponse<TokenMarketChartResponse>
|
||||
|
||||
@GET("coins/{coin_id}/exchanges")
|
||||
suspend fun getCoinExchanges(@Path("coin_id") coinId: String): ApiResponse<TokenMarketExchangesResponse>
|
||||
|
||||
@GET("coins/history_preview")
|
||||
suspend fun getCoinsListCharts(
|
||||
@Query("coin_ids") coinIds: String,
|
||||
@Query("currency") currency: String,
|
||||
@Query("interval") interval: String,
|
||||
): ApiResponse<TokenMarketChartListResponse>
|
||||
}
|
||||
|
|
@ -0,0 +1,3 @@
|
|||
package com.tangem.datasource.api.markets.models.response
|
||||
|
||||
typealias TokenMarketChartListResponse = Map<String, TokenMarketChartResponse>
|
||||
|
|
@ -0,0 +1,13 @@
|
|||
package com.tangem.datasource.api.markets.models.response
|
||||
|
||||
import com.squareup.moshi.Json
|
||||
import com.squareup.moshi.JsonClass
|
||||
import java.math.BigDecimal
|
||||
|
||||
@JsonClass(generateAdapter = true)
|
||||
data class TokenMarketChartResponse(
|
||||
// There is a bug in the API, it returns null values.
|
||||
// We need to filter them out.
|
||||
@Json(name = "prices")
|
||||
val prices: Map<Long, BigDecimal?>,
|
||||
)
|
||||
|
|
@ -0,0 +1,38 @@
|
|||
package com.tangem.datasource.api.markets.models.response
|
||||
|
||||
import com.squareup.moshi.Json
|
||||
import com.squareup.moshi.JsonClass
|
||||
import java.math.BigDecimal
|
||||
|
||||
/**
|
||||
* Token market exchanges response
|
||||
*
|
||||
* @property exchanges list of exchanges
|
||||
*
|
||||
[REDACTED_AUTHOR]
|
||||
*/
|
||||
@JsonClass(generateAdapter = true)
|
||||
data class TokenMarketExchangesResponse(
|
||||
@Json(name = "exchanges") val exchanges: List<Exchange>,
|
||||
) {
|
||||
|
||||
/**
|
||||
* Exchange
|
||||
*
|
||||
* @property id id
|
||||
* @property name name
|
||||
* @property imageUrl image url
|
||||
* @property isCentralized CEX (true), DEX (false)
|
||||
* @property volumeInUsd aggregated volume in USD
|
||||
* @property trustScore trust score
|
||||
*/
|
||||
@JsonClass(generateAdapter = true)
|
||||
data class Exchange(
|
||||
@Json(name = "exchange_id") val id: String,
|
||||
@Json(name = "name") val name: String,
|
||||
@Json(name = "image") val imageUrl: String?,
|
||||
@Json(name = "centralized") val isCentralized: Boolean,
|
||||
@Json(name = "volume_usd") val volumeInUsd: BigDecimal,
|
||||
@Json(name = "trust_score") val trustScore: Int?,
|
||||
)
|
||||
}
|
||||
|
|
@ -0,0 +1,179 @@
|
|||
package com.tangem.datasource.api.markets.models.response
|
||||
|
||||
import com.squareup.moshi.Json
|
||||
import com.squareup.moshi.JsonClass
|
||||
import org.joda.time.DateTime
|
||||
import java.math.BigDecimal
|
||||
|
||||
@JsonClass(generateAdapter = true)
|
||||
data class TokenMarketInfoResponse(
|
||||
@Json(name = "id")
|
||||
val id: String,
|
||||
@Json(name = "name")
|
||||
val name: String,
|
||||
@Json(name = "symbol")
|
||||
val symbol: String,
|
||||
@Json(name = "current_price")
|
||||
val currentPrice: BigDecimal,
|
||||
@Json(name = "price_change_percentage")
|
||||
val priceChangePercentage: PriceChangePercentage?,
|
||||
@Json(name = "networks")
|
||||
val networks: List<Network>?,
|
||||
@Json(name = "short_description")
|
||||
val shortDescription: String?,
|
||||
@Json(name = "full_description")
|
||||
val fullDescription: String?,
|
||||
@Json(name = "insights")
|
||||
val insights: Insights?,
|
||||
@Json(name = "metrics")
|
||||
val metrics: Metrics?,
|
||||
@Json(name = "security_data")
|
||||
val securityData: SecurityData?,
|
||||
@Json(name = "links")
|
||||
val links: Links?,
|
||||
@Json(name = "price_performance")
|
||||
val pricePerformance: PricePerformance?,
|
||||
@Json(name = "exchanges_amount")
|
||||
val exchangesAmount: Int?,
|
||||
) {
|
||||
|
||||
@JsonClass(generateAdapter = true)
|
||||
data class PriceChangePercentage(
|
||||
@Json(name = "24h")
|
||||
val day: BigDecimal?,
|
||||
@Json(name = "1w")
|
||||
val week: BigDecimal?,
|
||||
@Json(name = "1m")
|
||||
val month: BigDecimal?,
|
||||
@Json(name = "3m")
|
||||
val threeMonths: BigDecimal?,
|
||||
@Json(name = "6m")
|
||||
val sixMonths: BigDecimal?,
|
||||
@Json(name = "1y")
|
||||
val year: BigDecimal?,
|
||||
@Json(name = "all_time")
|
||||
val allTime: BigDecimal?,
|
||||
)
|
||||
|
||||
@JsonClass(generateAdapter = true)
|
||||
data class Network(
|
||||
@Json(name = "network_id")
|
||||
val networkId: String,
|
||||
@Json(name = "exchangeable")
|
||||
val exchangeable: Boolean = false,
|
||||
@Json(name = "contract_address")
|
||||
val contractAddress: String?,
|
||||
@Json(name = "decimal_count")
|
||||
val decimalCount: Int?,
|
||||
)
|
||||
|
||||
@JsonClass(generateAdapter = true)
|
||||
data class Insights(
|
||||
@Json(name = "holders_change")
|
||||
val holdersChange: Change?,
|
||||
@Json(name = "liquidity_change")
|
||||
val liquidityChange: Change?,
|
||||
@Json(name = "buy_pressure_change")
|
||||
val buyPressureChange: Change?,
|
||||
@Json(name = "experienced_buyer_change")
|
||||
val experiencedBuyerChange: Change?,
|
||||
@Json(name = "networks")
|
||||
val sourceNetworks: List<SourceNetwork>?,
|
||||
) {
|
||||
@JsonClass(generateAdapter = true)
|
||||
data class SourceNetwork(
|
||||
@Json(name = "network_id")
|
||||
val id: String,
|
||||
@Json(name = "network_name")
|
||||
val name: String,
|
||||
)
|
||||
}
|
||||
|
||||
@JsonClass(generateAdapter = true)
|
||||
data class Change(
|
||||
@Json(name = "24h")
|
||||
val day: BigDecimal?,
|
||||
@Json(name = "1w")
|
||||
val week: BigDecimal?,
|
||||
@Json(name = "1m")
|
||||
val month: BigDecimal?,
|
||||
)
|
||||
|
||||
@JsonClass(generateAdapter = true)
|
||||
data class Metrics(
|
||||
@Json(name = "market_rating")
|
||||
val marketRating: Int?,
|
||||
@Json(name = "circulating_supply")
|
||||
val circulatingSupply: BigDecimal?,
|
||||
@Json(name = "market_cap")
|
||||
val marketCap: BigDecimal?,
|
||||
@Json(name = "volume_24h")
|
||||
val volume24h: BigDecimal?,
|
||||
@Json(name = "max_supply")
|
||||
val maxSupply: BigDecimal?,
|
||||
@Json(name = "fully_diluted_valuation")
|
||||
val fullyDilutedValuation: BigDecimal?,
|
||||
)
|
||||
|
||||
@JsonClass(generateAdapter = true)
|
||||
data class Links(
|
||||
@Json(name = "official_links")
|
||||
val officialLinks: List<Link>? = null,
|
||||
@Json(name = "social")
|
||||
val social: List<Link>? = null,
|
||||
@Json(name = "repository")
|
||||
val repository: List<Link>? = null,
|
||||
@Json(name = "blockchain_site")
|
||||
val blockchainSite: List<Link>? = null,
|
||||
)
|
||||
|
||||
@JsonClass(generateAdapter = true)
|
||||
data class Link(
|
||||
@Json(name = "title")
|
||||
val title: String,
|
||||
@Json(name = "id")
|
||||
val id: String?,
|
||||
@Json(name = "link")
|
||||
val link: String,
|
||||
)
|
||||
|
||||
@JsonClass(generateAdapter = true)
|
||||
data class PricePerformance(
|
||||
@Json(name = "24h")
|
||||
val day: Range?,
|
||||
@Json(name = "1m")
|
||||
val month: Range?,
|
||||
@Json(name = "all_time")
|
||||
val allTime: Range?,
|
||||
)
|
||||
|
||||
@JsonClass(generateAdapter = true)
|
||||
data class SecurityData(
|
||||
@Json(name = "total_security_score")
|
||||
val totalSecurityScore: Float,
|
||||
@Json(name = "provider_data")
|
||||
val providerData: List<ProviderData>,
|
||||
)
|
||||
|
||||
@JsonClass(generateAdapter = true)
|
||||
data class ProviderData(
|
||||
@Json(name = "provider_id")
|
||||
val providerId: String,
|
||||
@Json(name = "provider_name")
|
||||
val providerName: String,
|
||||
@Json(name = "link")
|
||||
val link: String?,
|
||||
@Json(name = "security_score")
|
||||
val securityScore: Float,
|
||||
@Json(name = "last_audit_date")
|
||||
val lastAuditDate: DateTime?,
|
||||
)
|
||||
|
||||
@JsonClass(generateAdapter = true)
|
||||
data class Range(
|
||||
@Json(name = "low_price")
|
||||
val low: BigDecimal?,
|
||||
@Json(name = "high_price")
|
||||
val high: BigDecimal?,
|
||||
)
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Add a link
Reference in a new issue