Updated on 2026-08-14

This commit is contained in:
Tangem 2022-11-21 15:59:37 +03:00
parent ee722c5f46
commit 67a245a19c
18 changed files with 296 additions and 60 deletions

View file

@ -111,6 +111,7 @@ dependencies {
implementation(project(":common"))
implementation(project(":core:ui"))
implementation(project(":libs:crypto"))
implementation(project(":libs:auth"))
/** Features */
implementation(project(":features:referral:presentation"))

View file

@ -26,6 +26,7 @@ import com.tangem.tap.common.shop.GooglePayService.Companion.LOAD_PAYMENT_DATA_R
import com.tangem.tap.common.shop.googlepay.GooglePayUtil.createPaymentsClient
import com.tangem.tap.domain.TangemSdkManager
import com.tangem.tap.features.shop.redux.ShopAction
import com.tangem.tap.proxy.AppStateHolder
import com.tangem.wallet.R
import com.tangem.wallet.databinding.ActivityMainBinding
import dagger.hilt.android.AndroidEntryPoint
@ -33,6 +34,7 @@ import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job
import java.lang.ref.WeakReference
import javax.inject.Inject
import kotlin.coroutines.CoroutineContext
lateinit var tangemSdk: TangemSdk
@ -51,6 +53,9 @@ val mainScope = CoroutineScope(mainCoroutineContext)
@AndroidEntryPoint
class MainActivity : AppCompatActivity(), SnackbarHandler, ActivityResultCallbackHolder {
@Inject
lateinit var appStateHolder: AppStateHolder
private var snackbar: Snackbar? = null
private val dialogManager = DialogManager()
private val intentHandler = IntentHandler()
@ -66,6 +71,7 @@ class MainActivity : AppCompatActivity(), SnackbarHandler, ActivityResultCallbac
tangemSdk = TangemSdk.init(this, TangemSdkManager.config)
tangemSdkManager = TangemSdkManager(tangemSdk, this)
appStateHolder.tangemSdkManager = tangemSdkManager
backupService = BackupService.init(tangemSdk, this)
store.dispatch(

View file

@ -41,18 +41,16 @@ import com.tangem.tap.domain.walletconnect.WalletConnectRepository
import com.tangem.tap.network.NetworkConnectivity
import com.tangem.tap.persistence.CardBalanceStateAdapter
import com.tangem.tap.persistence.PreferencesStorage
import com.tangem.tap.proxy.AppStateHolder
import com.tangem.wallet.BuildConfig
import com.zendesk.logger.Logger
import dagger.hilt.android.HiltAndroidApp
import org.rekotlin.Store
import timber.log.Timber
import zendesk.chat.Chat
import javax.inject.Inject
val store = Store(
reducer = ::appReducer,
middleware = AppState.getMiddleware(),
state = AppState(),
)
lateinit var store: Store<AppState>
lateinit var foregroundActivityObserver: ForegroundActivityObserver
lateinit var preferencesStorage: PreferencesStorage
@ -64,9 +62,21 @@ lateinit var userTokensRepository: UserTokensRepository
@HiltAndroidApp
class TapApplication : Application(), ImageLoaderFactory {
@Inject
lateinit var appStateHolder: AppStateHolder
override fun onCreate() {
super.onCreate()
store = Store(
reducer = { action, state ->
appReducer(action, state, appStateHolder)
},
middleware = AppState.getMiddleware(),
state = AppState(),
)
appStateHolder.mainStore = store
if (BuildConfig.DEBUG) {
Timber.plant(Timber.DebugTree())
}
@ -91,6 +101,7 @@ class TapApplication : Application(), ImageLoaderFactory {
context = this,
tangemTechService = store.state.domainNetworks.tangemTechService,
)
appStateHolder.userTokensRepository = userTokensRepository
}
private fun initMoshiConverter() {

View file

@ -14,20 +14,21 @@ import com.tangem.tap.features.send.redux.reducers.SendScreenReducer
import com.tangem.tap.features.shop.redux.ShopReducer
import com.tangem.tap.features.tokens.redux.TokensReducer
import com.tangem.tap.features.wallet.redux.reducers.WalletReducer
import com.tangem.tap.proxy.AppStateHolder
import org.rekotlin.Action
fun appReducer(action: Action, state: AppState?): AppState {
fun appReducer(action: Action, state: AppState?, appStateHolder: AppStateHolder): AppState {
requireNotNull(state)
if (action is AppAction.RestoreState) return action.state
return AppState(
navigationState = NavigationReducer.reduce(action, state),
globalState = globalReducer(action, state),
globalState = globalReducer(action, state, appStateHolder),
homeState = HomeReducer.reduce(action, state),
onboardingNoteState = OnboardingNoteReducer.reduce(action, state),
onboardingWalletState = OnboardingWalletReducer.reduce(action, state),
onboardingOtherCardsState = OnboardingOtherCardsReducer.reduce(action, state),
walletState = WalletReducer.reduce(action, state),
walletState = WalletReducer.reduce(action, state, appStateHolder),
twinCardsState = TwinCardsReducer.reduce(action, state),
sendState = SendScreenReducer.reduce(action, state.sendState),
detailsState = DetailsReducer.reduce(action, state),

View file

@ -5,9 +5,10 @@ import com.tangem.domain.redux.global.DomainGlobalAction
import com.tangem.tap.common.redux.AppState
import com.tangem.tap.features.onboarding.OnboardingManager
import com.tangem.tap.preferencesStorage
import com.tangem.tap.proxy.AppStateHolder
import org.rekotlin.Action
fun globalReducer(action: Action, state: AppState): GlobalState {
fun globalReducer(action: Action, state: AppState, appStateHolder: AppStateHolder): GlobalState {
if (action !is GlobalAction) return state.globalState
@ -32,6 +33,7 @@ fun globalReducer(action: Action, state: AppState): GlobalState {
globalState.copy(scanCardFailsCounter = 0)
}
is GlobalAction.SaveScanNoteResponse -> {
appStateHolder.scanResponse = action.scanResponse
domainStore.dispatch(DomainGlobalAction.SaveScanNoteResponse(action.scanResponse))
globalState.copy(scanResponse = action.scanResponse)
}
@ -70,7 +72,9 @@ fun globalReducer(action: Action, state: AppState): GlobalState {
}
is GlobalAction.SetIfCardVerifiedOnline ->
globalState.copy(cardVerifiedOnline = action.verified)
is GlobalAction.FetchUserCountry.Success -> globalState.copy(userCountryCode = action.countryCode)
is GlobalAction.FetchUserCountry.Success -> globalState.copy(
userCountryCode = action.countryCode,
)
else -> globalState
}
}

View file

@ -30,17 +30,19 @@ import com.tangem.tap.features.wallet.redux.WalletState
import com.tangem.tap.features.wallet.redux.WalletStore
import com.tangem.tap.features.wallet.ui.BalanceStatus
import com.tangem.tap.features.wallet.ui.BalanceWidgetData
import com.tangem.tap.proxy.AppStateHolder
import org.rekotlin.Action
import timber.log.Timber
import java.math.BigDecimal
class WalletReducer {
companion object {
fun reduce(action: Action, state: AppState): WalletState = internalReduce(action, state)
fun reduce(action: Action, state: AppState, appStateHolder: AppStateHolder): WalletState =
internalReduce(action, state, appStateHolder)
}
}
private fun internalReduce(action: Action, state: AppState): WalletState {
private fun internalReduce(action: Action, state: AppState, appStateHolder: AppStateHolder): WalletState {
val multiWalletReducer = MultiWalletReducer()
val onWalletLoadedReducer = OnWalletLoadedReducer()
@ -65,17 +67,17 @@ private fun internalReduce(action: Action, state: AppState): WalletState {
blockchainNetwork = BlockchainNetwork(
Blockchain.Unknown,
null,
emptyList()
emptyList(),
),
walletsData = listOf(
WalletData(
currencyData = BalanceWidgetData(BalanceStatus.EmptyCard),
mainButton = WalletMainButton.CreateWalletButton(true),
currency = Currency.Blockchain(Blockchain.Unknown, null)
)
)
)
)
currency = Currency.Blockchain(Blockchain.Unknown, null),
),
),
),
),
)
}
is WalletAction.LoadData.Failure -> {
@ -87,18 +89,18 @@ private fun internalReduce(action: Action, state: AppState): WalletState {
walletsData = store.walletsData.map {
it.copy(
currencyData = it.currencyData.copy(
status = BalanceStatus.Unreachable
)
status = BalanceStatus.Unreachable,
),
)
}
},
)
)
}
newState = newState.copy(
state = ProgressState.Error,
error = ErrorType.NoInternetConnection,
wallets = wallets
wallets = wallets,
)
}
is TapError.UnknownBlockchain -> {
@ -110,16 +112,16 @@ private fun internalReduce(action: Action, state: AppState): WalletState {
blockchainNetwork = BlockchainNetwork(
Blockchain.Unknown,
null,
emptyList()
emptyList(),
),
walletsData = listOf(
WalletData(
currencyData = BalanceWidgetData(BalanceStatus.UnknownBlockchain),
currency = Currency.Blockchain(Blockchain.Unknown, null)
)
)
)
)
currency = Currency.Blockchain(Blockchain.Unknown, null),
),
),
),
),
)
}
else -> { /* no-op */
@ -190,7 +192,7 @@ private fun internalReduce(action: Action, state: AppState): WalletState {
is WalletAction.LoadWallet.Success -> newState = onWalletLoadedReducer.reduce(
wallet = action.wallet,
blockchainNetwork = action.blockchain,
walletState = newState
walletState = newState,
)
is WalletAction.LoadWallet.NoAccount -> {
val amount = BigDecimal.ZERO
@ -209,10 +211,10 @@ private fun internalReduce(action: Action, state: AppState): WalletState {
),
fiatAmount = fiatAmount,
fiatAmountFormatted = fiatAmount.toFormattedFiatValue(
fiatCurrencyName = state.globalState.appCurrency.symbol
fiatCurrencyName = state.globalState.appCurrency.symbol,
),
amountToCreateAccount = action.amountToCreateAccount,
)
),
)
}
val updatedWalletStore = newState.getWalletStore(action.blockchain)
@ -232,7 +234,7 @@ private fun internalReduce(action: Action, state: AppState): WalletState {
val newWalletData = walletData?.copy(
currencyData = walletData.currencyData.copy(
status = BalanceStatus.Unreachable,
errorMessage = message
errorMessage = message,
),
)
val tokenWallets = action.wallet.getTokens()
@ -244,8 +246,8 @@ private fun internalReduce(action: Action, state: AppState): WalletState {
.map {
it.copy(
currencyData = it.currencyData.copy(
status = BalanceStatus.Unreachable, errorMessage = message
)
status = BalanceStatus.Unreachable, errorMessage = message,
),
)
}
val updatedWallets = walletStore!!.updateWallets(listOfNotNull(newWalletData) + tokenWallets).walletsData
@ -296,9 +298,9 @@ private fun internalReduce(action: Action, state: AppState): WalletState {
selectedWalletData?.copy(
walletAddresses = WalletAddresses(
address,
walletAddresses.list
)
)
walletAddresses.list,
),
),
)
}
is WalletAction.SetWalletRent -> {
@ -320,6 +322,7 @@ private fun internalReduce(action: Action, state: AppState): WalletState {
else -> { /* no-op */
}
}
appStateHolder.walletState = newState
return newState
}
@ -345,7 +348,7 @@ fun Wallet.createAddressesData(): List<AddressData> {
it.value,
it.type,
getShareUri(it.value),
getExploreUrl(it.value)
getExploreUrl(it.value),
)
if (it.type == blockchain.defaultAddressType()) {
listOfAddressData.add(0, addressData)
@ -358,27 +361,26 @@ fun Wallet.createAddressesData(): List<AddressData> {
private fun handleCheckSignedHashesActions(
action: WalletAction.Warnings,
state: WalletState
state: WalletState,
): WalletState {
return when (action) {
WalletAction.Warnings.CheckHashesCount.ConfirmHashesCount -> state.copy(hashesCountVerified = true)
WalletAction.Warnings.CheckHashesCount.NeedToCheckHashesCountOnline -> state.copy(
hashesCountVerified = false
hashesCountVerified = false,
)
is WalletAction.Warnings.Set -> state.copy(mainWarningsList = action.warningList)
else -> state
}
}
private fun setNewFiatRate(
fiatRates: Map<Currency, BigDecimal?>,
appCurrency: FiatCurrency,
state: WalletState
state: WalletState,
): WalletState {
val rateFormatter: (BigDecimal) -> String = { rate: BigDecimal ->
rate.toFiatRateString(
fiatCurrencyName = appCurrency.symbol
fiatCurrencyName = appCurrency.symbol,
)
}
@ -387,7 +389,7 @@ private fun setNewFiatRate(
fiatRates = fiatRates.mapNotNullValues { it.value },
rateFormatter = rateFormatter,
appCurrency = appCurrency,
state = state
state = state,
)
} else {
setSingleWalletFiatRates(
@ -403,7 +405,7 @@ private fun setMultiWalletFiatRate(
fiatRates: Map<Currency, BigDecimal>,
rateFormatter: (BigDecimal) -> String,
appCurrency: FiatCurrency,
state: WalletState
state: WalletState,
): WalletState {
val newWalletsData = fiatRates.mapNotNull { (currency, rate) ->
val walletStore = state.getWalletStore(currency) ?: return@mapNotNull null
@ -488,10 +490,9 @@ private fun setSingleWalletFiatRate(
val walletData = state.primaryWallet.copy(
currencyData = state.primaryWallet.currencyData.copy(fiatAmountFormatted = fiatAmount),
fiatRate = rate,
fiatRateString = rateFormatted
fiatRateString = rateFormatted,
)
return state.updateWalletData(walletData)
} else if (currency is Currency.Token && currency.token == token) {
Timber.e("Working with token fiat rate")

View file

@ -0,0 +1,12 @@
package com.tangem.tap.network.auth
import com.tangem.common.extensions.toHexString
import com.tangem.lib.auth.AuthProvider
import com.tangem.tap.proxy.AppStateHolder
class AuthProviderImpl(private val appStateHolder: AppStateHolder) : AuthProvider {
override fun getAuthToken(): String {
return appStateHolder.scanResponse?.card?.cardPublicKey?.toHexString() ?: ""
}
}

View file

@ -0,0 +1,21 @@
package com.tangem.tap.network.auth.di
import com.tangem.lib.auth.AuthProvider
import com.tangem.tap.network.auth.AuthProviderImpl
import com.tangem.tap.proxy.AppStateHolder
import dagger.Module
import dagger.Provides
import dagger.hilt.InstallIn
import dagger.hilt.components.SingletonComponent
import javax.inject.Singleton
@Module
@InstallIn(SingletonComponent::class)
class AuthModule {
@Provides
@Singleton
fun provideAuthProvider(appStateHolder: AppStateHolder): AuthProvider {
return AuthProviderImpl(appStateHolder)
}
}

View file

@ -3,8 +3,8 @@ package com.tangem.tap.proxy
import com.tangem.common.card.Card
import com.tangem.domain.common.ScanResponse
import com.tangem.tap.common.redux.AppState
import com.tangem.tap.domain.TangemSdkManager
import com.tangem.tap.domain.tokens.UserTokensRepository
import com.tangem.tap.features.tokens.redux.TokensMiddleware
import com.tangem.tap.features.wallet.redux.WalletState
import org.rekotlin.Store
@ -14,11 +14,11 @@ import org.rekotlin.Store
*/
class AppStateHolder {
val scanResponse: ScanResponse? = null
val walletState: WalletState? = null
val userTokensRepository: UserTokensRepository? = null
val mainStore: Store<AppState>? = null
val tokesMiddleware: TokensMiddleware? = null
var scanResponse: ScanResponse? = null
var walletState: WalletState? = null
var userTokensRepository: UserTokensRepository? = null
var mainStore: Store<AppState>? = null
var tangemSdkManager: TangemSdkManager? = null
fun getActualCard(): Card? {
return scanResponse?.card

View file

@ -2,12 +2,27 @@ package com.tangem.tap.proxy
import com.tangem.blockchain.common.Blockchain
import com.tangem.blockchain.common.Token
import com.tangem.common.CompletionResult
import com.tangem.common.card.EllipticCurve
import com.tangem.common.extensions.ByteArrayKey
import com.tangem.common.extensions.toMapKey
import com.tangem.common.hdWallet.DerivationPath
import com.tangem.domain.common.ScanResponse
import com.tangem.domain.common.TapWorkarounds.derivationStyle
import com.tangem.domain.common.extensions.fromNetworkId
import com.tangem.lib.crypto.DerivationManager
import com.tangem.lib.crypto.models.Currency
import com.tangem.lib.crypto.models.NonNativeToken
import com.tangem.operations.derivation.ExtendedPublicKeysMap
import com.tangem.tap.DELAY_SDK_DIALOG_CLOSE
import com.tangem.tap.common.extensions.dispatchDebugErrorNotification
import com.tangem.tap.common.extensions.dispatchOnMain
import com.tangem.tap.common.redux.global.GlobalAction
import com.tangem.tap.domain.TapError
import com.tangem.tap.domain.tokens.models.BlockchainNetwork
import com.tangem.tap.scope
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
import kotlin.coroutines.suspendCoroutine
class DerivationManagerImpl(
@ -15,7 +30,6 @@ class DerivationManagerImpl(
) : DerivationManager {
override suspend fun deriveMissingBlockchains(currency: Currency) = suspendCoroutine<Boolean> { continuation ->
val tokesMiddleware = requireNotNull(appStateHolder.tokesMiddleware) { "tokesMiddleware is null" }
val blockchain = Blockchain.fromNetworkId(currency.networkId)
val card = appStateHolder.getActualCard()
if (blockchain != null && card != null) {
@ -31,9 +45,10 @@ class DerivationManagerImpl(
blockchainNetwork,
appToken,
)
if (appStateHolder.scanResponse != null) {
tokesMiddleware.deriveMissingBlockchains(
scanResponse = appStateHolder.scanResponse,
val scanResponse = appStateHolder.scanResponse
if (scanResponse != null) {
deriveMissingBlockchains(
scanResponse = scanResponse,
listOf(appCurrency),
) {
continuation.resumeWith(Result.success(true))
@ -57,4 +72,99 @@ class DerivationManagerImpl(
}
return false
}
private fun deriveMissingBlockchains(
scanResponse: ScanResponse,
currencyList: List<com.tangem.tap.features.wallet.models.Currency>,
onSuccess: (ScanResponse) -> Unit,
) {
val derivationDataList = listOfNotNull(
getDerivations(EllipticCurve.Secp256k1, scanResponse, currencyList),
getDerivations(EllipticCurve.Ed25519, scanResponse, currencyList),
)
val derivations = derivationDataList.associate { it.derivations }
if (derivations.isEmpty()) {
onSuccess(scanResponse)
return
}
scope.launch {
val result = appStateHolder.tangemSdkManager?.derivePublicKeys(
scanResponse.card.cardId,
derivations,
)
when (result) {
is CompletionResult.Success -> {
val newDerivedKeys = result.data.entries
val oldDerivedKeys = scanResponse.derivedKeys
val walletKeys = (newDerivedKeys.keys + oldDerivedKeys.keys).toSet()
val updatedDerivedKeys = walletKeys.associateWith { walletKey ->
val oldDerivations = ExtendedPublicKeysMap(oldDerivedKeys[walletKey] ?: emptyMap())
val newDerivations = newDerivedKeys[walletKey] ?: ExtendedPublicKeysMap(emptyMap())
ExtendedPublicKeysMap(oldDerivations + newDerivations)
}
val updatedScanResponse = scanResponse.copy(
derivedKeys = updatedDerivedKeys,
)
appStateHolder.mainStore?.dispatchOnMain(GlobalAction.SaveScanNoteResponse(updatedScanResponse))
delay(DELAY_SDK_DIALOG_CLOSE)
onSuccess(updatedScanResponse)
}
is CompletionResult.Failure -> {
appStateHolder.mainStore?.dispatchDebugErrorNotification(
TapError.CustomError(
"Error adding " +
"tokens",
),
)
}
else -> {
throw IllegalStateException("result result is null")
}
}
}
}
private fun getDerivations(
curve: EllipticCurve,
scanResponse: ScanResponse,
currencyList: List<com.tangem.tap.features.wallet.models.Currency>,
): DerivationData? {
val wallet = scanResponse.card.wallets.firstOrNull { it.curve == curve } ?: return null
val manageTokensCandidates = currencyList.map { it.blockchain }.distinct().filter {
it.getSupportedCurves().contains(curve)
}.mapNotNull {
it.derivationPath(scanResponse.card.derivationStyle)
}
val customTokensCandidates = currencyList.filter {
it.blockchain.getSupportedCurves().contains(curve)
}.mapNotNull { it.derivationPath }.map { DerivationPath(it) }
val bothCandidates = (manageTokensCandidates + customTokensCandidates).distinct()
if (bothCandidates.isEmpty()) return null
val mapKeyOfWalletPublicKey = wallet.publicKey.toMapKey()
val alreadyDerivedKeys: ExtendedPublicKeysMap =
scanResponse.derivedKeys[mapKeyOfWalletPublicKey] ?: ExtendedPublicKeysMap(emptyMap())
val alreadyDerivedPaths = alreadyDerivedKeys.keys.toList()
val toDerive = bothCandidates.filterNot { alreadyDerivedPaths.contains(it) }
if (toDerive.isEmpty()) return null
return DerivationData(
derivations = mapKeyOfWalletPublicKey to toDerive,
alreadyDerivedKeys = alreadyDerivedKeys,
mapKeyOfWalletPublicKey = mapKeyOfWalletPublicKey,
)
}
private class DerivationData(
val derivations: Pair<ByteArrayKey, List<DerivationPath>>,
val alreadyDerivedKeys: ExtendedPublicKeysMap,
val mapKeyOfWalletPublicKey: ByteArrayKey,
)
}

View file

@ -25,6 +25,9 @@ android {
dependencies {
/** Project */
implementation(project(":libs:auth"))
/** DI */
implementation(Library.hilt)
kapt(Library.hiltKapt)

View file

@ -0,0 +1,24 @@
package com.tangem.datasource.api
import com.tangem.lib.auth.AuthProvider
import okhttp3.Interceptor
import okhttp3.Response
class AuthHeaderInterceptor(
private val authProvider: AuthProvider,
) : Interceptor {
override fun intercept(chain: Interceptor.Chain): Response {
val request = chain.request().newBuilder().apply {
addHeader(
HEADER_CARD_KEY, authProvider.getAuthToken(),
)
}.build()
return chain.proceed(request)
}
companion object {
const val HEADER_CARD_KEY = "card_public_key"
}
}

View file

@ -1,6 +1,8 @@
package com.tangem.datasource.di
import com.tangem.datasource.api.AuthHeaderInterceptor
import com.tangem.datasource.api.referral.ReferralApi
import com.tangem.lib.auth.AuthProvider
import dagger.Module
import dagger.Provides
import dagger.hilt.InstallIn
@ -30,15 +32,16 @@ class NetworkModule {
@Provides
@Singleton
fun provideOkHttpClient(): OkHttpClient {
fun provideOkHttpClient(authProvider: AuthProvider): OkHttpClient {
return OkHttpClient.Builder()
.addInterceptor(
HttpLoggingInterceptor().setLevel(HttpLoggingInterceptor.Level.BASIC),
)
.addInterceptor(AuthHeaderInterceptor(authProvider))
.build()
}
private companion object {
const val PROD_REFERRAL_BASE_URL = ""
const val PROD_REFERRAL_BASE_URL = "https://api.tangem-tech.com/v1/"
}
}

1
libs/auth/.gitignore vendored Normal file
View file

@ -0,0 +1 @@
/build

View file

@ -0,0 +1,23 @@
plugins {
id("com.android.library")
kotlin("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
}
}

View file

@ -0,0 +1,2 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest package="com.tangem.lib.auth" />

View file

@ -0,0 +1,12 @@
package com.tangem.lib.auth
/**
* Provides auth for tangemTech API
*/
interface AuthProvider {
/**
* Returns authToken for tangem tech api
*/
fun getAuthToken(): String
}

View file

@ -11,6 +11,7 @@ include(":core:ui")
// region Libs modules
include(":libs:crypto")
include(":libs:auth")
// endregion Libs modules
// region Feature modules