Updated on 2026-08-14
This commit is contained in:
commit
e8ccd67098
295 changed files with 4414 additions and 2971 deletions
|
|
@ -39,6 +39,7 @@ dependencies {
|
|||
/** Network */
|
||||
implementation(deps.moshi)
|
||||
implementation(deps.moshi.kotlin)
|
||||
implementation(deps.moshi.adapters)
|
||||
implementation(deps.okHttp)
|
||||
implementation(deps.okHttp.prettyLogging)
|
||||
implementation(deps.retrofit)
|
||||
|
|
@ -53,6 +54,7 @@ dependencies {
|
|||
|
||||
/** Chucker */
|
||||
debugImplementation(deps.chucker)
|
||||
mockedImplementation(deps.chuckerStub)
|
||||
externalImplementation(deps.chuckerStub)
|
||||
internalImplementation(deps.chuckerStub)
|
||||
releaseImplementation(deps.chuckerStub)
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
@ -0,0 +1,19 @@
|
|||
package com.tangem.datasource.api.tangemTech
|
||||
|
||||
import com.tangem.datasource.config.models.ProviderModel
|
||||
import retrofit2.http.GET
|
||||
import retrofit2.http.Header
|
||||
|
||||
/**
|
||||
* Tangem Tech API for app services
|
||||
*
|
||||
[REDACTED_AUTHOR]
|
||||
*/
|
||||
interface TangemTechServiceApi {
|
||||
|
||||
@GET("v1/networks")
|
||||
suspend fun getBlockchainProviders(
|
||||
@Header("card_public_key") cardPublicKey: String,
|
||||
@Header("card_id") cardId: String,
|
||||
): Map<String, List<ProviderModel>>
|
||||
}
|
||||
|
|
@ -0,0 +1,87 @@
|
|||
package com.tangem.datasource.asset.loader
|
||||
|
||||
import com.squareup.moshi.Moshi
|
||||
import com.squareup.moshi.Types
|
||||
import com.squareup.moshi.adapter
|
||||
import com.tangem.datasource.asset.reader.AssetReader
|
||||
import com.tangem.datasource.di.NetworkMoshi
|
||||
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
|
||||
import com.tangem.utils.coroutines.runCatching
|
||||
import timber.log.Timber
|
||||
import javax.inject.Inject
|
||||
|
||||
/**
|
||||
* Asset file loader
|
||||
*
|
||||
[REDACTED_AUTHOR]
|
||||
*/
|
||||
class AssetLoader @Inject constructor(
|
||||
val assetReader: AssetReader,
|
||||
@NetworkMoshi val moshi: Moshi,
|
||||
val dispatchers: CoroutineDispatcherProvider,
|
||||
) {
|
||||
|
||||
/** Load content [Content] of asset file [fileName] */
|
||||
@OptIn(ExperimentalStdlibApi::class)
|
||||
suspend inline fun <reified Content> load(fileName: String): Content? {
|
||||
return runCatching(dispatchers.io) {
|
||||
val json = assetReader.readJson(fileName = fileName)
|
||||
|
||||
moshi.adapter<Content>().fromJson(json)
|
||||
}
|
||||
.fold(
|
||||
onSuccess = { parsedConfig ->
|
||||
if (parsedConfig == null) Timber.e(IllegalStateException("Parsed config is null"))
|
||||
parsedConfig
|
||||
},
|
||||
onFailure = {
|
||||
Timber.e(it, "Failed to load config from assets")
|
||||
null
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
/** Load list [V] values of asset file [fileName] */
|
||||
suspend inline fun <reified V> loadList(fileName: String): List<V> {
|
||||
return runCatching(dispatchers.io) {
|
||||
val json = assetReader.readJson(fileName = fileName)
|
||||
|
||||
val type = Types.newParameterizedType(List::class.java, V::class.java)
|
||||
val adapter = moshi.adapter<List<V>>(type)
|
||||
|
||||
adapter.fromJson(json)
|
||||
}
|
||||
.fold(
|
||||
onSuccess = { parsedConfig ->
|
||||
if (parsedConfig == null) Timber.e(IllegalStateException("Parsed config is null"))
|
||||
parsedConfig.orEmpty()
|
||||
},
|
||||
onFailure = {
|
||||
Timber.e(it, "Failed to load config from assets")
|
||||
emptyList()
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
/** Load map [String] keys and [V] values of asset file [fileName] */
|
||||
suspend inline fun <reified V> loadMap(fileName: String): Map<String, V> {
|
||||
return runCatching(dispatchers.io) {
|
||||
val json = assetReader.readJson(fileName = fileName)
|
||||
|
||||
val type = Types.newParameterizedType(Map::class.java, String::class.java, V::class.java)
|
||||
val adapter = moshi.adapter<Map<String, V>>(type)
|
||||
|
||||
adapter.fromJson(json)
|
||||
}
|
||||
.fold(
|
||||
onSuccess = { parsedConfig ->
|
||||
if (parsedConfig == null) Timber.e(IllegalStateException("Parsed config is null"))
|
||||
parsedConfig.orEmpty()
|
||||
},
|
||||
onFailure = {
|
||||
Timber.e(it, "Failed to load config from assets")
|
||||
emptyMap()
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -1,4 +1,4 @@
|
|||
package com.tangem.datasource.asset
|
||||
package com.tangem.datasource.asset.reader
|
||||
|
||||
import android.content.Context
|
||||
import dagger.hilt.android.qualifiers.ApplicationContext
|
||||
|
|
@ -21,8 +21,7 @@ internal class AndroidAssetReader @Inject constructor(
|
|||
.use(BufferedReader::readText)
|
||||
}
|
||||
|
||||
override fun openFile(fileName: String): InputStream {
|
||||
return context.assets
|
||||
.open(fileName)
|
||||
override fun openFile(file: String): InputStream {
|
||||
return context.assets.open(file)
|
||||
}
|
||||
}
|
||||
|
|
@ -1,4 +1,4 @@
|
|||
package com.tangem.datasource.asset
|
||||
package com.tangem.datasource.asset.reader
|
||||
|
||||
import java.io.InputStream
|
||||
|
||||
|
|
@ -146,8 +146,8 @@ internal class ConfigManagerImpl @Inject constructor() : ConfigManager {
|
|||
blockBookRest = accessTokens.bitcoin?.blockBookRest,
|
||||
),
|
||||
algorand = GetBlockAccessToken(rest = accessTokens.algorand?.rest),
|
||||
zkSync = GetBlockAccessToken(rest = accessTokens.zksync?.jsonRPC),
|
||||
polygonZkevm = GetBlockAccessToken(rest = accessTokens.polygonZkevm?.jsonRPC),
|
||||
zkSyncEra = GetBlockAccessToken(rest = accessTokens.zksync?.jsonRPC),
|
||||
polygonZkEvm = GetBlockAccessToken(rest = accessTokens.polygonZkevm?.jsonRPC),
|
||||
base = GetBlockAccessToken(rest = accessTokens.base?.jsonRPC),
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@ package com.tangem.datasource.config
|
|||
|
||||
import com.squareup.moshi.JsonAdapter
|
||||
import com.squareup.moshi.Moshi
|
||||
import com.tangem.datasource.asset.AssetReader
|
||||
import com.tangem.datasource.asset.reader.AssetReader
|
||||
import com.tangem.datasource.config.models.ConfigModel
|
||||
import com.tangem.datasource.config.models.ConfigValueModel
|
||||
import com.tangem.datasource.config.models.FeatureModel
|
||||
|
|
@ -11,6 +11,7 @@ import timber.log.Timber
|
|||
/**
|
||||
[REDACTED_AUTHOR]
|
||||
*/
|
||||
@Deprecated(message = "Use AssetReader instead")
|
||||
class FeaturesLocalLoader(
|
||||
private val assetReader: AssetReader,
|
||||
private val moshi: Moshi,
|
||||
|
|
|
|||
|
|
@ -7,6 +7,7 @@ import com.squareup.moshi.JsonClass
|
|||
[REDACTED_AUTHOR]
|
||||
*/
|
||||
|
||||
// TODO remove
|
||||
class FeatureModel(
|
||||
val isTopUpEnabled: Boolean,
|
||||
val isCreatingTwinCardsAllowed: Boolean,
|
||||
|
|
|
|||
|
|
@ -0,0 +1,32 @@
|
|||
package com.tangem.datasource.config.models
|
||||
|
||||
import com.squareup.moshi.Json
|
||||
|
||||
/** Config provider model */
|
||||
sealed class ProviderModel {
|
||||
|
||||
/**
|
||||
* Example,
|
||||
* {
|
||||
* "type": "public",
|
||||
* "url": "https://example.com"
|
||||
* }
|
||||
*/
|
||||
data class Public(
|
||||
@Json(name = "url") val url: String,
|
||||
) : ProviderModel()
|
||||
|
||||
/**
|
||||
* Example,
|
||||
* {
|
||||
* "type": "private",
|
||||
* "name": "nownodes"
|
||||
* }
|
||||
*/
|
||||
data class Private(
|
||||
@Json(name = "name") val name: String,
|
||||
) : ProviderModel()
|
||||
|
||||
/** Unsupported type */
|
||||
data object UnsupportedType : ProviderModel()
|
||||
}
|
||||
|
|
@ -1,22 +0,0 @@
|
|||
package com.tangem.datasource.di
|
||||
|
||||
import com.tangem.blockchain.common.AccountCreator
|
||||
import com.tangem.datasource.api.tangemTech.TangemTechApi
|
||||
import com.tangem.datasource.local.blockchain.DefaultAccountCreator
|
||||
import com.tangem.lib.auth.AuthProvider
|
||||
import dagger.Module
|
||||
import dagger.Provides
|
||||
import dagger.hilt.InstallIn
|
||||
import dagger.hilt.components.SingletonComponent
|
||||
import javax.inject.Singleton
|
||||
|
||||
@Module
|
||||
@InstallIn(SingletonComponent::class)
|
||||
internal object AccountCreatorModule {
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
fun provideAccountCreator(authProvider: AuthProvider, tangemTechApi: TangemTechApi): AccountCreator {
|
||||
return DefaultAccountCreator(authProvider, tangemTechApi)
|
||||
}
|
||||
}
|
||||
|
|
@ -1,7 +1,7 @@
|
|||
package com.tangem.datasource.di
|
||||
|
||||
import com.tangem.datasource.asset.AndroidAssetReader
|
||||
import com.tangem.datasource.asset.AssetReader
|
||||
import com.tangem.datasource.asset.reader.AndroidAssetReader
|
||||
import com.tangem.datasource.asset.reader.AssetReader
|
||||
import dagger.Binds
|
||||
import dagger.Module
|
||||
import dagger.hilt.InstallIn
|
||||
|
|
|
|||
|
|
@ -1,21 +0,0 @@
|
|||
package com.tangem.datasource.di
|
||||
|
||||
import com.tangem.blockchain.common.datastorage.BlockchainDataStorage
|
||||
import com.tangem.datasource.local.blockchain.DefaultBlockchainDataStorage
|
||||
import com.tangem.datasource.local.preferences.AppPreferencesStore
|
||||
import dagger.Module
|
||||
import dagger.Provides
|
||||
import dagger.hilt.InstallIn
|
||||
import dagger.hilt.components.SingletonComponent
|
||||
import javax.inject.Singleton
|
||||
|
||||
@Module
|
||||
@InstallIn(SingletonComponent::class)
|
||||
internal object BlockchainDataStorageModule {
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
fun provideBlockchainDataStorage(appPreferencesStore: AppPreferencesStore): BlockchainDataStorage {
|
||||
return DefaultBlockchainDataStorage(appPreferencesStore)
|
||||
}
|
||||
}
|
||||
|
|
@ -1,11 +1,13 @@
|
|||
package com.tangem.datasource.di
|
||||
|
||||
import com.squareup.moshi.Moshi
|
||||
import com.squareup.moshi.adapters.PolymorphicJsonAdapterFactory
|
||||
import com.squareup.moshi.kotlin.reflect.KotlinJsonAdapterFactory
|
||||
import com.tangem.common.json.MoshiJsonConverter
|
||||
import com.tangem.datasource.api.common.BigDecimalAdapter
|
||||
import com.tangem.datasource.api.common.DateTimeAdapter
|
||||
import com.tangem.datasource.api.common.LocalDateAdapter
|
||||
import com.tangem.datasource.config.models.ProviderModel
|
||||
import dagger.Module
|
||||
import dagger.Provides
|
||||
import dagger.hilt.InstallIn
|
||||
|
|
@ -21,6 +23,12 @@ class MoshiModule {
|
|||
@NetworkMoshi
|
||||
fun provideNetworkMoshi(): Moshi {
|
||||
return Moshi.Builder()
|
||||
.add(
|
||||
PolymorphicJsonAdapterFactory.of(ProviderModel::class.java, "type")
|
||||
.withSubtype(ProviderModel.Public::class.java, "public")
|
||||
.withSubtype(ProviderModel.Private::class.java, "private")
|
||||
.withDefaultValue(ProviderModel.UnsupportedType),
|
||||
)
|
||||
.add(BigDecimalAdapter())
|
||||
.add(LocalDateAdapter())
|
||||
.add(DateTimeAdapter())
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@ import com.tangem.datasource.BuildConfig
|
|||
import com.tangem.datasource.api.common.response.ApiResponseCallAdapterFactory
|
||||
import com.tangem.datasource.api.express.TangemExpressApi
|
||||
import com.tangem.datasource.api.tangemTech.TangemTechApi
|
||||
import com.tangem.datasource.api.tangemTech.TangemTechServiceApi
|
||||
import com.tangem.datasource.utils.RequestHeader.*
|
||||
import com.tangem.datasource.utils.addHeaders
|
||||
import com.tangem.datasource.utils.addLoggers
|
||||
|
|
@ -19,6 +20,7 @@ import dagger.hilt.components.SingletonComponent
|
|||
import okhttp3.OkHttpClient
|
||||
import retrofit2.Retrofit
|
||||
import retrofit2.converter.moshi.MoshiConverterFactory
|
||||
import java.util.concurrent.TimeUnit
|
||||
import javax.inject.Singleton
|
||||
|
||||
@Module
|
||||
|
|
@ -96,6 +98,33 @@ class NetworkModule {
|
|||
.create(TangemTechApi::class.java)
|
||||
}
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
fun provideTangemTechServiceApi(
|
||||
@NetworkMoshi moshi: Moshi,
|
||||
@ApplicationContext context: Context,
|
||||
appVersionProvider: AppVersionProvider,
|
||||
): TangemTechServiceApi {
|
||||
return Retrofit.Builder()
|
||||
.addConverterFactory(MoshiConverterFactory.create(moshi))
|
||||
.addCallAdapterFactory(ApiResponseCallAdapterFactory.create())
|
||||
.baseUrl(PROD_TANGEM_TECH_BASE_URL)
|
||||
.client(
|
||||
OkHttpClient.Builder()
|
||||
.callTimeout(timeout = 5, unit = TimeUnit.SECONDS)
|
||||
.addHeaders(
|
||||
CacheControlHeader,
|
||||
AppVersionPlatformHeaders(appVersionProvider),
|
||||
// TODO("refactor header init") get auth data after biometric auth to avoid race condition
|
||||
// AuthenticationHeader(authProvider),
|
||||
)
|
||||
.addLoggers(context)
|
||||
.build(),
|
||||
)
|
||||
.build()
|
||||
.create(TangemTechServiceApi::class.java)
|
||||
}
|
||||
|
||||
private companion object {
|
||||
const val PROD_EXPRESS_BASE_URL = "https://express.tangem.com/v1/"
|
||||
const val DEV_EXPRESS_BASE_URL = "[REDACTED_ENV_URL]"
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@ package com.tangem.datasource.di
|
|||
|
||||
import com.squareup.moshi.Moshi
|
||||
import com.squareup.moshi.adapter
|
||||
import com.tangem.datasource.asset.AssetReader
|
||||
import com.tangem.datasource.asset.reader.AssetReader
|
||||
import com.tangem.datasource.local.testnet.DefaultTestnetTokensStorage
|
||||
import com.tangem.datasource.local.testnet.TestnetTokensStorage
|
||||
import dagger.Module
|
||||
|
|
|
|||
|
|
@ -1,31 +0,0 @@
|
|||
package com.tangem.datasource.local.blockchain
|
||||
|
||||
import com.tangem.blockchain.common.AccountCreator
|
||||
import com.tangem.blockchain.common.Blockchain
|
||||
import com.tangem.blockchain.common.BlockchainSdkError
|
||||
import com.tangem.blockchain.extensions.Result
|
||||
import com.tangem.common.extensions.toHexString
|
||||
import com.tangem.datasource.api.common.response.getOrThrow
|
||||
import com.tangem.datasource.api.tangemTech.TangemTechApi
|
||||
import com.tangem.datasource.api.tangemTech.models.CreateUserNetworkAccountBody
|
||||
import com.tangem.lib.auth.AuthProvider
|
||||
|
||||
internal class DefaultAccountCreator(
|
||||
private val authProvider: AuthProvider,
|
||||
private val tangemTechApi: TangemTechApi,
|
||||
) : AccountCreator {
|
||||
|
||||
override suspend fun createAccount(blockchain: Blockchain, walletPublicKey: ByteArray): Result<String> {
|
||||
val request = CreateUserNetworkAccountBody(blockchain.id.removeSuffix("/test"), walletPublicKey.toHexString())
|
||||
return try {
|
||||
val response = tangemTechApi.createUserNetworkAccount(
|
||||
cardPublicKey = authProvider.getCardPublicKey(),
|
||||
cardId = authProvider.getCardId(),
|
||||
body = request,
|
||||
).getOrThrow()
|
||||
Result.Success(response.data.accountId)
|
||||
} catch (e: Exception) {
|
||||
Result.Failure(BlockchainSdkError.FailedToCreateAccount)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,29 +0,0 @@
|
|||
package com.tangem.datasource.local.blockchain
|
||||
|
||||
import androidx.datastore.preferences.core.edit
|
||||
import androidx.datastore.preferences.core.stringPreferencesKey
|
||||
import com.tangem.blockchain.common.datastorage.BlockchainDataStorage
|
||||
import com.tangem.datasource.local.preferences.AppPreferencesStore
|
||||
import com.tangem.datasource.local.preferences.utils.getSyncOrNull
|
||||
|
||||
/**
|
||||
* [BlockchainDataStorage] implementation
|
||||
*
|
||||
* @property appPreferencesStore app preferences store
|
||||
*
|
||||
[REDACTED_AUTHOR]
|
||||
*/
|
||||
internal class DefaultBlockchainDataStorage(
|
||||
private val appPreferencesStore: AppPreferencesStore,
|
||||
) : BlockchainDataStorage {
|
||||
|
||||
override suspend fun getOrNull(key: String): String? {
|
||||
return appPreferencesStore.getSyncOrNull(key = stringPreferencesKey(name = key))
|
||||
}
|
||||
|
||||
override suspend fun store(key: String, value: String) {
|
||||
appPreferencesStore.edit {
|
||||
it[stringPreferencesKey(key)] = value
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -5,9 +5,13 @@ import com.tangem.datasource.local.preferences.PreferencesKeys.APP_LAUNCH_COUNT_
|
|||
import com.tangem.datasource.local.preferences.PreferencesKeys.FUNDS_FOUND_DATE_KEY
|
||||
import com.tangem.datasource.local.preferences.PreferencesKeys.IS_TANGEM_TOS_ACCEPTED_KEY
|
||||
import com.tangem.datasource.local.preferences.PreferencesKeys.SAVE_USER_WALLETS_KEY
|
||||
import com.tangem.datasource.local.preferences.PreferencesKeys.SHOULD_OPEN_WELCOME_ON_RESUME_KEY
|
||||
import com.tangem.datasource.local.preferences.PreferencesKeys.SHOULD_SAVE_ACCESS_CODES_KEY
|
||||
import com.tangem.datasource.local.preferences.PreferencesKeys.SHOULD_SHOW_SAVE_USER_WALLET_SCREEN_KEY
|
||||
import com.tangem.datasource.local.preferences.PreferencesKeys.SHOW_RATING_DIALOG_AT_LAUNCH_COUNT_KEY
|
||||
import com.tangem.datasource.local.preferences.PreferencesKeys.USED_CARDS_INFO_KEY
|
||||
import com.tangem.datasource.local.preferences.PreferencesKeys.USER_WAS_INTERACT_WITH_RATING_KEY
|
||||
import com.tangem.datasource.local.preferences.PreferencesKeys.WAS_APPLICATION_STOPPED_KEY
|
||||
import com.tangem.datasource.local.preferences.PreferencesKeys.WAS_TWINS_ONBOARDING_SHOWN
|
||||
|
||||
/**
|
||||
|
|
@ -19,6 +23,8 @@ object PreferencesKeys {
|
|||
|
||||
val SAVE_USER_WALLETS_KEY by lazy { booleanPreferencesKey(name = "saveUserWallets") }
|
||||
|
||||
val SHOULD_SHOW_SAVE_USER_WALLET_SCREEN_KEY by lazy { booleanPreferencesKey("saveUserWalletShown") }
|
||||
|
||||
val APP_LAUNCH_COUNT_KEY by lazy { intPreferencesKey(name = "launchCount") }
|
||||
|
||||
val SHOW_RATING_DIALOG_AT_LAUNCH_COUNT_KEY by lazy { intPreferencesKey(name = "showRatingDialogAtLaunchCount") }
|
||||
|
|
@ -75,6 +81,12 @@ object PreferencesKeys {
|
|||
|
||||
val SEND_TAP_HELP_PREVIEW_KEY by lazy { booleanPreferencesKey(name = "sendTapHelpPreview") }
|
||||
|
||||
val WAS_APPLICATION_STOPPED_KEY by lazy { booleanPreferencesKey(name = "applicationStopped") }
|
||||
|
||||
val SHOULD_OPEN_WELCOME_ON_RESUME_KEY by lazy { booleanPreferencesKey(name = "openWelcomeOnResume") }
|
||||
|
||||
val SHOULD_SAVE_ACCESS_CODES_KEY by lazy { booleanPreferencesKey(name = "saveAccessCodes") }
|
||||
|
||||
fun getStart2CoinTOSAcceptedKey(region: String?) = booleanPreferencesKey(name = "start2Coin_tos_accepted_$region")
|
||||
}
|
||||
|
||||
|
|
@ -82,6 +94,7 @@ object PreferencesKeys {
|
|||
internal fun getTapPrefKeysToMigrate(): Set<String> {
|
||||
return setOf(
|
||||
SAVE_USER_WALLETS_KEY,
|
||||
SHOULD_SHOW_SAVE_USER_WALLET_SCREEN_KEY,
|
||||
APP_LAUNCH_COUNT_KEY,
|
||||
SHOW_RATING_DIALOG_AT_LAUNCH_COUNT_KEY,
|
||||
FUNDS_FOUND_DATE_KEY,
|
||||
|
|
@ -89,6 +102,9 @@ internal fun getTapPrefKeysToMigrate(): Set<String> {
|
|||
USED_CARDS_INFO_KEY,
|
||||
WAS_TWINS_ONBOARDING_SHOWN,
|
||||
IS_TANGEM_TOS_ACCEPTED_KEY,
|
||||
WAS_APPLICATION_STOPPED_KEY,
|
||||
SHOULD_OPEN_WELCOME_ON_RESUME_KEY,
|
||||
SHOULD_SAVE_ACCESS_CODES_KEY,
|
||||
)
|
||||
.map(Preferences.Key<*>::name)
|
||||
.toSet()
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
package com.tangem.datasource.local.testnet
|
||||
|
||||
import com.squareup.moshi.JsonAdapter
|
||||
import com.tangem.datasource.asset.AssetReader
|
||||
import com.tangem.datasource.asset.reader.AssetReader
|
||||
import com.tangem.datasource.local.testnet.models.TestnetTokensConfig
|
||||
|
||||
/**
|
||||
|
|
@ -17,6 +17,7 @@ internal class DefaultTestnetTokensStorage(
|
|||
private val adapter: JsonAdapter<TestnetTokensConfig>,
|
||||
) : TestnetTokensStorage {
|
||||
|
||||
@Deprecated(message = "Use AssetReader instead")
|
||||
override fun getConfig(): TestnetTokensConfig {
|
||||
return requireNotNull(
|
||||
value = adapter.fromJson(
|
||||
|
|
|
|||
|
|
@ -1,8 +1,7 @@
|
|||
package com.tangem.datasource.utils
|
||||
|
||||
import com.tangem.datasource.api.common.AuthProvider
|
||||
import com.tangem.lib.auth.AppVersionProvider
|
||||
import com.tangem.lib.auth.AuthBearerProvider
|
||||
import com.tangem.lib.auth.AuthProvider
|
||||
import com.tangem.lib.auth.ExpressAuthProvider
|
||||
|
||||
/**
|
||||
|
|
@ -15,7 +14,7 @@ sealed class RequestHeader(vararg pairs: Pair<String, () -> String>) {
|
|||
/** Header list */
|
||||
val values: List<Pair<String, () -> String>> = pairs.toList()
|
||||
|
||||
object CacheControlHeader : RequestHeader("Cache-Control" to { "max-age=600" })
|
||||
data object CacheControlHeader : RequestHeader("Cache-Control" to { "max-age=600" })
|
||||
|
||||
class AuthenticationHeader(authProvider: AuthProvider) : RequestHeader(
|
||||
"card_id" to { authProvider.getCardId() },
|
||||
|
|
@ -28,10 +27,6 @@ sealed class RequestHeader(vararg pairs: Pair<String, () -> String>) {
|
|||
"session-id" to { expressAuthProvider.getSessionId() },
|
||||
)
|
||||
|
||||
class AuthBearerHeader(authBearerProvider: AuthBearerProvider) : RequestHeader(
|
||||
"Authorization" to { "Bearer " + authBearerProvider.getApiKey() },
|
||||
)
|
||||
|
||||
class AppVersionPlatformHeaders(appVersionProvider: AppVersionProvider) : RequestHeader(
|
||||
"version" to { appVersionProvider.getAppVersion() },
|
||||
"platform" to { "android" },
|
||||
|
|
|
|||
1
core/decompose/.gitignore
vendored
Normal file
1
core/decompose/.gitignore
vendored
Normal file
|
|
@ -0,0 +1 @@
|
|||
/build
|
||||
15
core/decompose/build.gradle.kts
Normal file
15
core/decompose/build.gradle.kts
Normal file
|
|
@ -0,0 +1,15 @@
|
|||
plugins {
|
||||
alias(deps.plugins.kotlin.jvm)
|
||||
alias(deps.plugins.kotlin.kapt)
|
||||
id("configuration")
|
||||
}
|
||||
|
||||
dependencies {
|
||||
implementation(projects.core.utils)
|
||||
|
||||
api(deps.decompose)
|
||||
implementation(deps.kotlin.coroutines)
|
||||
|
||||
implementation(deps.hilt.core)
|
||||
kapt(deps.hilt.kapt)
|
||||
}
|
||||
|
|
@ -0,0 +1,23 @@
|
|||
package com.tangem.core.decompose.context
|
||||
|
||||
import com.arkivanov.decompose.ComponentContext
|
||||
import com.tangem.core.decompose.di.HiltComponentBuilderOwner
|
||||
import com.tangem.core.decompose.navigation.NavigationOwner
|
||||
import com.tangem.core.decompose.ui.UiMessageSenderOwner
|
||||
import com.tangem.core.decompose.utils.ComponentScopeOwner
|
||||
import com.tangem.core.decompose.utils.DispatchersOwner
|
||||
import com.tangem.core.decompose.utils.TagsOwner
|
||||
|
||||
/**
|
||||
* Interface for the application component context.
|
||||
*
|
||||
* It combines several other interfaces related to navigation, dispatching, UI messaging, etc.
|
||||
*/
|
||||
interface AppComponentContext :
|
||||
ComponentContext,
|
||||
NavigationOwner,
|
||||
ComponentScopeOwner,
|
||||
DispatchersOwner,
|
||||
UiMessageSenderOwner,
|
||||
HiltComponentBuilderOwner,
|
||||
TagsOwner
|
||||
|
|
@ -0,0 +1,73 @@
|
|||
package com.tangem.core.decompose.context
|
||||
|
||||
import com.arkivanov.decompose.ComponentContext
|
||||
import com.arkivanov.decompose.childContext
|
||||
import com.arkivanov.essenty.lifecycle.Lifecycle
|
||||
import com.tangem.core.decompose.di.HiltComponentBuilderOwner
|
||||
import com.tangem.core.decompose.navigation.NavigationOwner
|
||||
import com.tangem.core.decompose.navigation.Router
|
||||
import com.tangem.core.decompose.ui.DefaultUiMessageSender
|
||||
import com.tangem.core.decompose.ui.UiMessageHandler
|
||||
import com.tangem.core.decompose.ui.UiMessageSender
|
||||
import com.tangem.core.decompose.ui.UiMessageSenderOwner
|
||||
import com.tangem.core.decompose.utils.ComponentCoroutineScope
|
||||
import com.tangem.core.decompose.utils.DispatchersOwner
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
|
||||
/**
|
||||
* Creates a new child [AppComponentContext] with the provided [key] and optional [lifecycle].
|
||||
*
|
||||
* @param key The key to use.
|
||||
* @param lifecycle The [Lifecycle] to use. If not provided, the parent's lifecycle will be used.
|
||||
* @param router The [Router] to use in the child. If not provided, the parent's router will be used.
|
||||
* @param messageHandler The [UiMessageHandler] to use in the child. If not provided, the parent's message sender will
|
||||
* be used.
|
||||
|
||||
*
|
||||
* @see childByContext
|
||||
* */
|
||||
fun AppComponentContext.child(
|
||||
key: String,
|
||||
lifecycle: Lifecycle? = null,
|
||||
router: Router? = null,
|
||||
messageHandler: UiMessageHandler? = null,
|
||||
): AppComponentContext = childByContext(
|
||||
componentContext = childContext(key, lifecycle),
|
||||
router = router,
|
||||
messageHandler = messageHandler,
|
||||
)
|
||||
|
||||
/**
|
||||
* Creates a new child [AppComponentContext] with the provided [componentContext].
|
||||
*
|
||||
* @param componentContext The [ComponentContext] to use.
|
||||
* @param router The [Router] to use in the child. If not provided, the parent's router will be used.
|
||||
* @param messageHandler The [UiMessageHandler] to use in the child. If not provided, the parent's message sender will
|
||||
* be used.
|
||||
|
||||
*
|
||||
* @see child
|
||||
* */
|
||||
fun AppComponentContext.childByContext(
|
||||
componentContext: ComponentContext,
|
||||
router: Router? = null,
|
||||
messageHandler: UiMessageHandler? = null,
|
||||
): AppComponentContext = object :
|
||||
AppComponentContext,
|
||||
ComponentContext by componentContext,
|
||||
NavigationOwner by this@childByContext,
|
||||
UiMessageSenderOwner by this@childByContext,
|
||||
DispatchersOwner by this@childByContext,
|
||||
HiltComponentBuilderOwner by this@childByContext {
|
||||
|
||||
override val tags: HashMap<String, Any> = HashMap()
|
||||
|
||||
override val componentScope: CoroutineScope = ComponentCoroutineScope(lifecycle, dispatchers)
|
||||
|
||||
override val messageSender: UiMessageSender = messageHandler
|
||||
?.let(::DefaultUiMessageSender)
|
||||
?: this@childByContext.messageSender
|
||||
|
||||
override val router: Router
|
||||
get() = router ?: this@childByContext.router
|
||||
}
|
||||
|
|
@ -0,0 +1,35 @@
|
|||
package com.tangem.core.decompose.context
|
||||
|
||||
import com.arkivanov.decompose.ComponentContext
|
||||
import com.arkivanov.essenty.instancekeeper.getOrCreate
|
||||
import com.tangem.core.decompose.di.DecomposeComponent
|
||||
import com.tangem.core.decompose.navigation.AppNavigationProvider
|
||||
import com.tangem.core.decompose.navigation.DefaultAppNavigationProvider
|
||||
import com.tangem.core.decompose.navigation.DefaultRouter
|
||||
import com.tangem.core.decompose.navigation.Router
|
||||
import com.tangem.core.decompose.ui.DefaultUiMessageSender
|
||||
import com.tangem.core.decompose.ui.UiMessageHandler
|
||||
import com.tangem.core.decompose.ui.UiMessageSender
|
||||
import com.tangem.core.decompose.utils.ComponentCoroutineScope
|
||||
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
|
||||
class DefaultAppComponentContext(
|
||||
componentContext: ComponentContext,
|
||||
messageHandler: UiMessageHandler,
|
||||
override val dispatchers: CoroutineDispatcherProvider,
|
||||
override val hiltComponentBuilder: DecomposeComponent.Builder,
|
||||
) : AppComponentContext, ComponentContext by componentContext {
|
||||
|
||||
override val tags: HashMap<String, Any> = HashMap()
|
||||
|
||||
override val componentScope: CoroutineScope = ComponentCoroutineScope(lifecycle, dispatchers)
|
||||
|
||||
override val messageSender: UiMessageSender = DefaultUiMessageSender(messageHandler)
|
||||
|
||||
override val navigationProvider: AppNavigationProvider
|
||||
get() = instanceKeeper.getOrCreate { DefaultAppNavigationProvider() }
|
||||
|
||||
override val router: Router
|
||||
get() = instanceKeeper.getOrCreate { DefaultRouter(navigationProvider) }
|
||||
}
|
||||
|
|
@ -0,0 +1,9 @@
|
|||
package com.tangem.core.decompose.di
|
||||
|
||||
/**
|
||||
* Annotation for marking a dependency as a component scoped.
|
||||
*
|
||||
* This means that the lifecycle of the dependency is limited to the lifecycle of the component it is attached to.
|
||||
*/
|
||||
@Retention(AnnotationRetention.SOURCE)
|
||||
annotation class ComponentScoped
|
||||
|
|
@ -0,0 +1,47 @@
|
|||
package com.tangem.core.decompose.di
|
||||
|
||||
import com.tangem.core.decompose.navigation.Router
|
||||
import com.tangem.core.decompose.ui.UiMessageSender
|
||||
import dagger.BindsInstance
|
||||
import dagger.hilt.DefineComponent
|
||||
import dagger.hilt.components.SingletonComponent
|
||||
|
||||
/**
|
||||
* Interface for the Decompose component.
|
||||
*
|
||||
* It is annotated as [ComponentScoped], meaning it has a lifecycle that is scoped to the component.
|
||||
*/
|
||||
@ComponentScoped
|
||||
@DefineComponent(parent = SingletonComponent::class)
|
||||
interface DecomposeComponent {
|
||||
|
||||
/**
|
||||
* Builder interface for the component.
|
||||
*/
|
||||
@DefineComponent.Builder
|
||||
interface Builder {
|
||||
|
||||
/**
|
||||
* Sets the router for the component.
|
||||
*
|
||||
* @param router The router to set.
|
||||
* @return The builder instance.
|
||||
*/
|
||||
fun router(@BindsInstance router: Router): Builder
|
||||
|
||||
/**
|
||||
* Sets the UI message sender for the component.
|
||||
*
|
||||
* @param uiMessageSender The UI message sender to set.
|
||||
* @return The builder instance.
|
||||
*/
|
||||
fun uiMessageSender(@BindsInstance uiMessageSender: UiMessageSender): Builder
|
||||
|
||||
/**
|
||||
* Builds the Decompose component.
|
||||
*
|
||||
* @return The built Decompose component.
|
||||
*/
|
||||
fun build(): DecomposeComponent
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,12 @@
|
|||
package com.tangem.core.decompose.di
|
||||
|
||||
/**
|
||||
* Interface for owning a Hilt component builder.
|
||||
*/
|
||||
interface HiltComponentBuilderOwner {
|
||||
|
||||
/**
|
||||
* Provides access to the Hilt component builder instance.
|
||||
*/
|
||||
val hiltComponentBuilder: DecomposeComponent.Builder
|
||||
}
|
||||
|
|
@ -0,0 +1,36 @@
|
|||
package com.tangem.core.decompose.model
|
||||
|
||||
import com.arkivanov.essenty.instancekeeper.InstanceKeeper
|
||||
import com.tangem.core.decompose.navigation.Router
|
||||
import com.tangem.core.decompose.ui.UiMessageSender
|
||||
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.SupervisorJob
|
||||
import kotlinx.coroutines.cancel
|
||||
|
||||
/**
|
||||
* Abstract class for a component's model.
|
||||
*
|
||||
* It provides access to the coroutine dispatchers and a coroutine scope which will survive re-creation of component
|
||||
* and will be destroyed when the component is destroyed.
|
||||
*
|
||||
* Also, it can inject and use some component features like [Router] and [UiMessageSender].
|
||||
*/
|
||||
abstract class Model : InstanceKeeper.Instance {
|
||||
|
||||
/**
|
||||
* Provides access to the coroutine dispatchers.
|
||||
*/
|
||||
protected abstract val dispatchers: CoroutineDispatcherProvider
|
||||
|
||||
/**
|
||||
* The coroutine scope for the model. That will be cancelled when the model is destroyed.
|
||||
*/
|
||||
protected val modelScope by lazy {
|
||||
CoroutineScope(context = dispatchers.mainImmediate + SupervisorJob())
|
||||
}
|
||||
|
||||
override fun onDestroy() {
|
||||
runCatching { modelScope.cancel() }
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,51 @@
|
|||
package com.tangem.core.decompose.model
|
||||
|
||||
import com.arkivanov.essenty.instancekeeper.getOrCreate
|
||||
import com.arkivanov.essenty.instancekeeper.getOrCreateSimple
|
||||
import com.tangem.core.decompose.context.AppComponentContext
|
||||
import com.tangem.core.decompose.di.DecomposeComponent
|
||||
import dagger.hilt.EntryPoint
|
||||
import dagger.hilt.EntryPoints
|
||||
import dagger.hilt.InstallIn
|
||||
import javax.inject.Provider
|
||||
|
||||
/**
|
||||
* Entry point for the models in the application.
|
||||
*
|
||||
* It provides a map of model providers.
|
||||
*/
|
||||
@EntryPoint
|
||||
@InstallIn(DecomposeComponent::class)
|
||||
interface ModelsEntryPoint {
|
||||
|
||||
fun models(): Map<Class<*>, Provider<Model>>
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets or creates a component's [Model].
|
||||
*/
|
||||
inline fun <reified M : Model> AppComponentContext.getOrCreateModel(): M {
|
||||
val modelKey = "model_${M::class.simpleName}"
|
||||
|
||||
val entryPoint = instanceKeeper.getOrCreateSimple(key = "modelsEntryPoint") {
|
||||
val hiltComponent = hiltComponentBuilder
|
||||
.router(router)
|
||||
.uiMessageSender(messageSender)
|
||||
.build()
|
||||
|
||||
EntryPoints.get(hiltComponent, ModelsEntryPoint::class.java)
|
||||
}
|
||||
|
||||
val model = instanceKeeper.getOrCreate(modelKey) {
|
||||
requireNotNull(entryPoint.models()[M::class.java]?.get()) {
|
||||
"Model ${M::class.simpleName} is not provided"
|
||||
}
|
||||
}
|
||||
|
||||
val isModelExist = tags.getOrElse(modelKey) { false } as Boolean
|
||||
if (!isModelExist) {
|
||||
tags[modelKey] = true
|
||||
}
|
||||
|
||||
return model as M
|
||||
}
|
||||
|
|
@ -0,0 +1,28 @@
|
|||
@file:Suppress("UNCHECKED_CAST")
|
||||
|
||||
package com.tangem.core.decompose.navigation
|
||||
|
||||
import com.arkivanov.decompose.router.stack.StackNavigation
|
||||
|
||||
/**
|
||||
* Interface for providing application navigation.
|
||||
* It provides or creates a StackNavigation instance for the application.
|
||||
*/
|
||||
interface AppNavigationProvider {
|
||||
|
||||
/**
|
||||
* Gets or creates a StackNavigation instance.
|
||||
*
|
||||
* @return The StackNavigation instance.
|
||||
*/
|
||||
fun getOrCreate(): StackNavigation<Route>
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets or creates a [StackNavigation] instance of a specific type.
|
||||
*
|
||||
* @return The [StackNavigation] instance.
|
||||
*/
|
||||
fun <R : Route> AppNavigationProvider.getOrCreateTyped(): StackNavigation<R> {
|
||||
return getOrCreate() as StackNavigation<R>
|
||||
}
|
||||
|
|
@ -0,0 +1,13 @@
|
|||
package com.tangem.core.decompose.navigation
|
||||
|
||||
import com.arkivanov.decompose.router.stack.StackNavigation
|
||||
import com.arkivanov.essenty.instancekeeper.InstanceKeeper
|
||||
|
||||
internal class DefaultAppNavigationProvider : AppNavigationProvider, InstanceKeeper.Instance {
|
||||
|
||||
private var navigation: StackNavigation<Route>? = null
|
||||
|
||||
override fun getOrCreate(): StackNavigation<Route> {
|
||||
return navigation ?: StackNavigation<Route>().also { navigation = it }
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,32 @@
|
|||
package com.tangem.core.decompose.navigation
|
||||
|
||||
import com.arkivanov.decompose.ExperimentalDecomposeApi
|
||||
import com.arkivanov.decompose.router.stack.StackNavigation
|
||||
import com.arkivanov.decompose.router.stack.pop
|
||||
import com.arkivanov.decompose.router.stack.popWhile
|
||||
import com.arkivanov.decompose.router.stack.pushNew
|
||||
import com.arkivanov.essenty.instancekeeper.InstanceKeeper
|
||||
|
||||
internal class DefaultRouter(
|
||||
private val navigationProvider: AppNavigationProvider,
|
||||
) : Router, InstanceKeeper.Instance {
|
||||
|
||||
private val navigation: StackNavigation<Route>
|
||||
get() = navigationProvider.getOrCreate()
|
||||
|
||||
@OptIn(ExperimentalDecomposeApi::class)
|
||||
override fun push(route: Route, onComplete: (isSuccess: Boolean) -> Unit) {
|
||||
navigation.pushNew(route, onComplete)
|
||||
}
|
||||
|
||||
override fun pop(onComplete: (isSuccess: Boolean) -> Unit) {
|
||||
navigation.pop(onComplete)
|
||||
}
|
||||
|
||||
override fun popTo(route: Route, onComplete: (isSuccess: Boolean) -> Unit) {
|
||||
navigation.popWhile(
|
||||
predicate = { it != route },
|
||||
onComplete = onComplete,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,17 @@
|
|||
package com.tangem.core.decompose.navigation
|
||||
|
||||
/**
|
||||
* Interface for owning navigation-related properties.
|
||||
*/
|
||||
interface NavigationOwner {
|
||||
|
||||
/**
|
||||
* The [Router] instance.
|
||||
*/
|
||||
val router: Router
|
||||
|
||||
/**
|
||||
* The [AppNavigationProvider] instance.
|
||||
*/
|
||||
val navigationProvider: AppNavigationProvider
|
||||
}
|
||||
|
|
@ -0,0 +1,6 @@
|
|||
package com.tangem.core.decompose.navigation
|
||||
|
||||
/**
|
||||
* Interface for a route in the application.
|
||||
*/
|
||||
interface Route
|
||||
|
|
@ -0,0 +1,31 @@
|
|||
package com.tangem.core.decompose.navigation
|
||||
|
||||
/**
|
||||
* Interface for a router in the application.
|
||||
* It provides methods for navigating through the application.
|
||||
*/
|
||||
interface Router {
|
||||
|
||||
/**
|
||||
* Pushes a new route to the navigation stack.
|
||||
*
|
||||
* @param route The route to push.
|
||||
* @param onComplete The callback to be invoked when the operation is complete.
|
||||
*/
|
||||
fun push(route: Route, onComplete: (isSuccess: Boolean) -> Unit = {})
|
||||
|
||||
/**
|
||||
* Pops the top route from the navigation stack.
|
||||
*
|
||||
* @param onComplete The callback to be invoked when the operation is complete.
|
||||
*/
|
||||
fun pop(onComplete: (isSuccess: Boolean) -> Unit = {})
|
||||
|
||||
/**
|
||||
* Pops routes from the navigation stack until the specified route is found.
|
||||
*
|
||||
* @param route The route to pop to.
|
||||
* @param onComplete The callback to be invoked when the operation is complete.
|
||||
*/
|
||||
fun popTo(route: Route, onComplete: (isSuccess: Boolean) -> Unit = {})
|
||||
}
|
||||
|
|
@ -0,0 +1,10 @@
|
|||
package com.tangem.core.decompose.ui
|
||||
|
||||
internal class DefaultUiMessageSender(
|
||||
private val handler: UiMessageHandler,
|
||||
) : UiMessageSender {
|
||||
|
||||
override fun send(message: UiMessage) {
|
||||
handler.handleMessage(message)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,9 @@
|
|||
package com.tangem.core.decompose.ui
|
||||
|
||||
/**
|
||||
* Interface for a message that can be sent to a [UiMessageSender] and handled by a [UiMessageHandler].
|
||||
*
|
||||
* @see UiMessageSender
|
||||
* @see UiMessageHandler
|
||||
*/
|
||||
interface UiMessage
|
||||
|
|
@ -0,0 +1,17 @@
|
|||
package com.tangem.core.decompose.ui
|
||||
|
||||
/**
|
||||
* Interface for handling UI messages.
|
||||
*
|
||||
* @see UiMessage
|
||||
* @see UiMessageSender
|
||||
*/
|
||||
interface UiMessageHandler {
|
||||
|
||||
/**
|
||||
* Handles the given UI message.
|
||||
*
|
||||
* @param message The UI message to handle.
|
||||
*/
|
||||
fun handleMessage(message: UiMessage)
|
||||
}
|
||||
|
|
@ -0,0 +1,17 @@
|
|||
package com.tangem.core.decompose.ui
|
||||
|
||||
/**
|
||||
* Interface for sending messages to UI.
|
||||
*
|
||||
* @see UiMessage
|
||||
* @see UiMessageHandler
|
||||
* */
|
||||
interface UiMessageSender {
|
||||
|
||||
/**
|
||||
* Sends the given UI message.
|
||||
*
|
||||
* @param message The UI message to send.
|
||||
* */
|
||||
fun send(message: UiMessage)
|
||||
}
|
||||
|
|
@ -0,0 +1,12 @@
|
|||
package com.tangem.core.decompose.ui
|
||||
|
||||
/**
|
||||
* Interface for owning a [UiMessageSender].
|
||||
* */
|
||||
interface UiMessageSenderOwner {
|
||||
|
||||
/**
|
||||
* The [UiMessageSender] instance.
|
||||
* */
|
||||
val messageSender: UiMessageSender
|
||||
}
|
||||
|
|
@ -0,0 +1,20 @@
|
|||
package com.tangem.core.decompose.utils
|
||||
|
||||
import com.arkivanov.essenty.lifecycle.Lifecycle
|
||||
import com.arkivanov.essenty.lifecycle.doOnDestroy
|
||||
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.SupervisorJob
|
||||
import kotlinx.coroutines.cancel
|
||||
|
||||
/**
|
||||
|
||||
* [CoroutineDispatcherProvider.mainImmediate] dispatcher.
|
||||
* */
|
||||
@Suppress("FunctionName")
|
||||
internal fun ComponentCoroutineScope(lifecycle: Lifecycle, dispatchers: CoroutineDispatcherProvider): CoroutineScope {
|
||||
val scope = CoroutineScope(context = dispatchers.mainImmediate + SupervisorJob())
|
||||
lifecycle.doOnDestroy(scope::cancel)
|
||||
|
||||
return scope
|
||||
}
|
||||
|
|
@ -0,0 +1,16 @@
|
|||
package com.tangem.core.decompose.utils
|
||||
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
|
||||
/**
|
||||
* Interface for owning a component scope.
|
||||
*/
|
||||
interface ComponentScopeOwner {
|
||||
|
||||
/**
|
||||
* Provides access to the component's [CoroutineScope] instance.
|
||||
*
|
||||
* This scope is used for launching coroutines that are bound to the component's lifecycle.
|
||||
*/
|
||||
val componentScope: CoroutineScope
|
||||
}
|
||||
|
|
@ -0,0 +1,14 @@
|
|||
package com.tangem.core.decompose.utils
|
||||
|
||||
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
|
||||
|
||||
/**
|
||||
* Interface for owning a [CoroutineDispatcherProvider].
|
||||
*/
|
||||
interface DispatchersOwner {
|
||||
|
||||
/**
|
||||
* Provides access to the [CoroutineDispatcherProvider] instance.
|
||||
*/
|
||||
val dispatchers: CoroutineDispatcherProvider
|
||||
}
|
||||
|
|
@ -0,0 +1,12 @@
|
|||
package com.tangem.core.decompose.utils
|
||||
|
||||
/**
|
||||
* Interface for owning tags.
|
||||
*/
|
||||
interface TagsOwner {
|
||||
|
||||
/**
|
||||
* Provides access to the tags map instance.
|
||||
*/
|
||||
val tags: HashMap<String, Any>
|
||||
}
|
||||
|
|
@ -11,10 +11,6 @@
|
|||
"name": "REDESIGNED_SEND_SCREEN_ENABLED",
|
||||
"version": "5.9.0"
|
||||
},
|
||||
{
|
||||
"name": "GENERAL_USER_WALLETS_LIST_MANAGER_ENABLED",
|
||||
"version": "5.8.0"
|
||||
},
|
||||
{
|
||||
"name": "LOCAL_USER_LOGS_ENABLED",
|
||||
"version": "5.8.0"
|
||||
|
|
@ -26,5 +22,9 @@
|
|||
{
|
||||
"name": "WC_SOLANA_TX_SIGN_ENABLED",
|
||||
"version": "5.11.0"
|
||||
},
|
||||
{
|
||||
"name": "TOKEN_LIST_LCE_ENABLED",
|
||||
"version": "5.10.0"
|
||||
}
|
||||
]
|
||||
|
|
|
|||
|
|
@ -10,7 +10,7 @@ import com.tangem.core.featuretoggle.manager.ProdFeatureTogglesManager
|
|||
import com.tangem.core.featuretoggle.storage.LocalFeatureTogglesStorage
|
||||
import com.tangem.core.featuretoggle.version.DefaultVersionProvider
|
||||
import com.tangem.core.featuretoggles.BuildConfig
|
||||
import com.tangem.datasource.asset.AssetReader
|
||||
import com.tangem.datasource.asset.reader.AssetReader
|
||||
import com.tangem.datasource.local.preferences.AppPreferencesStore
|
||||
import dagger.Module
|
||||
import dagger.Provides
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@ package com.tangem.core.featuretoggle.storage
|
|||
import androidx.annotation.VisibleForTesting
|
||||
import com.squareup.moshi.JsonAdapter
|
||||
import com.tangem.core.featuretoggle.storage.LocalFeatureTogglesStorage.Companion.LOCAL_CONFIG_PATH
|
||||
import com.tangem.datasource.asset.AssetReader
|
||||
import com.tangem.datasource.asset.reader.AssetReader
|
||||
import timber.log.Timber
|
||||
import kotlin.properties.Delegates
|
||||
|
||||
|
|
@ -24,6 +24,7 @@ internal class LocalFeatureTogglesStorage(
|
|||
override var featureToggles: List<FeatureToggle> by Delegates.notNull()
|
||||
private set
|
||||
|
||||
@Deprecated(message = "Use AssetReader instead")
|
||||
override suspend fun init() {
|
||||
runCatching { requireNotNull(jsonAdapter.fromJson(assetReader.readJson(LOCAL_CONFIG_PATH))) }
|
||||
.onSuccess { featureToggles = it }
|
||||
|
|
|
|||
|
|
@ -3,12 +3,11 @@ package com.tangem.core.featuretoggle.storage
|
|||
import android.annotation.SuppressLint
|
||||
import com.google.common.truth.Truth
|
||||
import com.squareup.moshi.JsonAdapter
|
||||
import com.tangem.datasource.asset.AssetReader
|
||||
import com.tangem.datasource.asset.reader.AssetReader
|
||||
import io.mockk.coEvery
|
||||
import io.mockk.mockk
|
||||
import io.mockk.verifyAll
|
||||
import io.mockk.verifyOrder
|
||||
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||
import kotlinx.coroutines.test.runTest
|
||||
import org.junit.Test
|
||||
import java.io.IOException
|
||||
|
|
@ -16,7 +15,6 @@ import java.io.IOException
|
|||
/**
|
||||
[REDACTED_AUTHOR]
|
||||
*/
|
||||
@OptIn(ExperimentalCoroutinesApi::class)
|
||||
@SuppressLint("CheckResult")
|
||||
internal class LocalFeatureTogglesStorageTest {
|
||||
|
||||
|
|
|
|||
|
|
@ -540,6 +540,7 @@
|
|||
<string name="token_button_unavailability_generic_description">Выбранная операция в данный момент недоступна. Попробуйте позже.</string>
|
||||
<string name="token_button_unavailability_reason_buy_unavailable">В данный момент покупка монеты %s недоступна. Но мы работаем над её добавлением.</string>
|
||||
<string name="token_button_unavailability_reason_empty_balance">У вас нет средств для отправки. Пополните счет, чтобы иметь возможность отправить с него средства.</string>
|
||||
<string name="token_button_unavailability_reason_no_quotes">Выбранная операция в данный момент недоступна. Попробуйте позже.</string>
|
||||
<string name="token_button_unavailability_reason_not_exchangeable">Обмен %s не доступен. Но мы работаем над его добавлением.</string>
|
||||
<string name="token_button_unavailability_reason_sell_unavailable">В данный момент продажа монеты %s недоступна. Но мы работаем над её добавлением.</string>
|
||||
<string name="token_details_generate_xpub">Сгенерировать XPUB</string>
|
||||
|
|
|
|||
|
|
@ -537,6 +537,7 @@
|
|||
<string name="token_button_unavailability_generic_description">This operation is currently unavailable. Please try again later.</string>
|
||||
<string name="token_button_unavailability_reason_buy_unavailable">The purchase of the %s is currently unavailable. But we are working on adding it.</string>
|
||||
<string name="token_button_unavailability_reason_empty_balance">You do not have funds to send. Top up your account to be able to send funds from it.</string>
|
||||
<string name="token_button_unavailability_reason_no_quotes">This operation is currently unavailable. Please try again later.</string>
|
||||
<string name="token_button_unavailability_reason_not_exchangeable">%s swap is not available. But we are working on adding it.</string>
|
||||
<string name="token_button_unavailability_reason_sell_unavailable">Sell of the %s coin is currently unavailable. But we are working on adding it.</string>
|
||||
<string name="token_details_generate_xpub">Generate XPUB</string>
|
||||
|
|
|
|||
|
|
@ -19,6 +19,7 @@ dependencies {
|
|||
/** Project - Core */
|
||||
implementation(projects.core.res)
|
||||
implementation(projects.core.utils)
|
||||
implementation(projects.core.decompose)
|
||||
|
||||
/** AndroidX libraries */
|
||||
implementation(deps.androidx.fragment.ktx)
|
||||
|
|
|
|||
|
|
@ -0,0 +1,47 @@
|
|||
package com.tangem.core.ui.message
|
||||
|
||||
import androidx.compose.material.SnackbarDuration
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.Immutable
|
||||
import androidx.compose.runtime.Stable
|
||||
import com.tangem.core.decompose.ui.UiMessage
|
||||
import com.tangem.core.ui.extensions.TextReference
|
||||
|
||||
/**
|
||||
* Event that is used to show a message in the UI.
|
||||
*
|
||||
* @see EventMessageHandler
|
||||
* */
|
||||
@Immutable
|
||||
sealed interface EventMessage : UiMessage
|
||||
|
||||
/**
|
||||
* Shows a snackbar.
|
||||
*
|
||||
* @param message The message to show.
|
||||
* @param duration The duration of the snackbar.
|
||||
* @param actionLabel The label of the action button.
|
||||
* @param action The action to perform when the action button is clicked.
|
||||
* */
|
||||
data class SnackbarMessage(
|
||||
val message: TextReference,
|
||||
val duration: SnackbarDuration = SnackbarDuration.Short,
|
||||
val actionLabel: TextReference? = null,
|
||||
val action: (() -> Unit)? = null,
|
||||
) : EventMessage
|
||||
|
||||
/**
|
||||
* Shows a [content] in the UI.
|
||||
*
|
||||
* @param content The content to show.
|
||||
* */
|
||||
data class ContentMessage(val content: Content) : EventMessage {
|
||||
|
||||
@Stable
|
||||
fun interface Content {
|
||||
|
||||
@Suppress("ComposableFunctionName", "TopLevelComposableFunctions")
|
||||
@Composable
|
||||
operator fun invoke(onDismiss: () -> Unit)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,43 @@
|
|||
package com.tangem.core.ui.message
|
||||
|
||||
import android.content.Context
|
||||
import androidx.compose.material.SnackbarHostState
|
||||
import androidx.compose.material.SnackbarResult
|
||||
import androidx.compose.runtime.*
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import com.tangem.core.ui.event.EventEffect
|
||||
import com.tangem.core.ui.extensions.resolveReference
|
||||
|
||||
@Composable
|
||||
fun EventMessageEffect(messageHandler: EventMessageHandler, snackbarHostState: SnackbarHostState) {
|
||||
val messageEvent by messageHandler.collectAsState()
|
||||
val context = LocalContext.current
|
||||
var contentMessage: ContentMessage? by remember { mutableStateOf(value = null) }
|
||||
|
||||
EventEffect(event = messageEvent) { message ->
|
||||
when (message) {
|
||||
is ContentMessage -> {
|
||||
contentMessage = message
|
||||
}
|
||||
is SnackbarMessage -> {
|
||||
showSnackbar(snackbarHostState, message, context)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
contentMessage?.content?.invoke(
|
||||
onDismiss = { contentMessage = null },
|
||||
)
|
||||
}
|
||||
|
||||
private suspend fun showSnackbar(snackbarHostState: SnackbarHostState, message: SnackbarMessage, context: Context) {
|
||||
val result = snackbarHostState.showSnackbar(
|
||||
message = message.message.resolveReference(context.resources),
|
||||
actionLabel = message.actionLabel?.resolveReference(context.resources),
|
||||
duration = message.duration,
|
||||
)
|
||||
|
||||
if (result == SnackbarResult.ActionPerformed) {
|
||||
message.action?.invoke()
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,27 @@
|
|||
package com.tangem.core.ui.message
|
||||
|
||||
import com.tangem.core.decompose.ui.UiMessage
|
||||
import com.tangem.core.decompose.ui.UiMessageHandler
|
||||
import com.tangem.core.ui.event.StateEvent
|
||||
import com.tangem.core.ui.event.consumedEvent
|
||||
import com.tangem.core.ui.event.triggeredEvent
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
|
||||
/**
|
||||
* Message handler that is used to show or remove an [EventMessage] in the UI.
|
||||
*/
|
||||
class EventMessageHandler(
|
||||
private val events: MutableStateFlow<StateEvent<EventMessage>>,
|
||||
) : UiMessageHandler, StateFlow<StateEvent<EventMessage>> by events {
|
||||
|
||||
override fun handleMessage(message: UiMessage) {
|
||||
if (message !is EventMessage) return
|
||||
|
||||
events.value = triggeredEvent(message, ::consumeEvent)
|
||||
}
|
||||
|
||||
private fun consumeEvent() {
|
||||
events.value = consumedEvent()
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,9 @@
|
|||
package com.tangem.core.ui.test
|
||||
|
||||
object TestTags {
|
||||
const val STORIES_SCREEN = "STORIES_SCREEN_CONTAINER"
|
||||
const val STORIES_SCREEN_SCAN_BUTTON = "STORIES_SCREEN_SCAN_BUTTON"
|
||||
const val STORIES_SCREEN_ORDER_BUTTON = "STORIES_SCREEN_ORDER_BUTTON"
|
||||
|
||||
const val WALLET_SCREEN = "WALLET_SCREEN_CONTAINER"
|
||||
}
|
||||
|
|
@ -8,6 +8,7 @@ import javax.inject.Inject
|
|||
|
||||
interface CoroutineDispatcherProvider {
|
||||
val main: CoroutineDispatcher
|
||||
val mainImmediate: CoroutineDispatcher
|
||||
val io: CoroutineDispatcher
|
||||
val default: CoroutineDispatcher
|
||||
val single: CoroutineDispatcher
|
||||
|
|
@ -15,6 +16,7 @@ interface CoroutineDispatcherProvider {
|
|||
|
||||
class AppCoroutineDispatcherProvider @Inject constructor() : CoroutineDispatcherProvider {
|
||||
override val main: CoroutineDispatcher = Dispatchers.Main
|
||||
override val mainImmediate: CoroutineDispatcher = Dispatchers.Main.immediate
|
||||
override val io: CoroutineDispatcher = Dispatchers.IO
|
||||
override val default: CoroutineDispatcher = Dispatchers.Default
|
||||
override val single: CoroutineDispatcher = Executors.newFixedThreadPool(1).asCoroutineDispatcher()
|
||||
|
|
@ -22,6 +24,7 @@ class AppCoroutineDispatcherProvider @Inject constructor() : CoroutineDispatcher
|
|||
|
||||
class TestingCoroutineDispatcherProvider(
|
||||
override val main: CoroutineDispatcher = Dispatchers.Unconfined,
|
||||
override val mainImmediate: CoroutineDispatcher = Dispatchers.Unconfined,
|
||||
override val io: CoroutineDispatcher = Dispatchers.Unconfined,
|
||||
override val default: CoroutineDispatcher = Dispatchers.Unconfined,
|
||||
override val single: CoroutineDispatcher = Executors.newFixedThreadPool(1).asCoroutineDispatcher(),
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue