Updated on 2026-08-14

This commit is contained in:
Tangem 2025-08-20 11:43:43 +04:00
parent eb07388729
commit 3f6bcd3a0e
23 changed files with 226 additions and 101 deletions

View file

@ -227,8 +227,8 @@ dependencies {
implementation(projects.features.usedesk.impl)
implementation(projects.features.hotWallet.api)
implementation(projects.features.hotWallet.impl)
implementation(projects.features.kyc.api)
//TODO disable for release because of the permissions
// implementation(projects.features.kyc.api)
// implementation(projects.features.kyc.impl)
implementation(projects.features.welcome.api)
implementation(projects.features.welcome.impl)

View file

@ -24,9 +24,7 @@ import com.tangem.domain.wallets.derivations.derivationStyleProvider
import com.tangem.domain.models.scan.CardDTO
import com.tangem.domain.models.scan.ScanResponse
import com.tangem.domain.models.wallet.UserWalletId
import com.tangem.domain.visa.model.VisaActivationInput
import com.tangem.domain.visa.model.VisaDataForApprove
import com.tangem.domain.visa.model.VisaSignedDataByCustomerWallet
import com.tangem.domain.visa.model.*
import com.tangem.features.onboarding.v2.OnboardingV2FeatureToggles
import com.tangem.operations.ScanTask
import com.tangem.operations.derivation.DerivationTaskResponse
@ -501,7 +499,12 @@ internal class DefaultTangemSdkManager(
): CompletionResult<VisaSignedDataByCustomerWallet> {
return runTaskAsyncReturnOnMain(
runnable = VisaCustomerWalletApproveTask(
visaDataForApprove = visaDataForApprove,
VisaCustomerWalletApproveTask.Input(
cardId = visaDataForApprove.customerWalletCardId,
targetAddress = visaDataForApprove.targetAddress,
hashToSign = visaDataForApprove.dataToSign.hashToSign,
sign = visaDataForApprove.dataToSign::sign,
),
),
cardId = visaDataForApprove.customerWalletCardId,
initialMessage = Message(resources.getStringSafe(R.string.initial_message_tap_header)),

View file

@ -18,9 +18,7 @@ import com.tangem.crypto.hdWallet.bip32.ExtendedPublicKey
import com.tangem.domain.models.scan.CardDTO
import com.tangem.domain.models.scan.ScanResponse
import com.tangem.domain.models.wallet.UserWalletId
import com.tangem.domain.visa.model.VisaActivationInput
import com.tangem.domain.visa.model.VisaDataForApprove
import com.tangem.domain.visa.model.VisaSignedDataByCustomerWallet
import com.tangem.domain.visa.model.*
import com.tangem.operations.derivation.DerivationTaskResponse
import com.tangem.operations.preflightread.PreflightReadFilter
import com.tangem.operations.wallet.CreateWalletResponse

View file

@ -1,6 +1,7 @@
package com.tangem.tap.domain.tasks.visa
import arrow.core.getOrElse
import com.tangem.blockchain.blockchains.ethereum.EthereumUtils.toKeccak
import com.tangem.blockchain.common.UnmarshalHelper
import com.tangem.common.CompletionResult
import com.tangem.common.card.Card
@ -10,7 +11,6 @@ import com.tangem.common.core.CardSession
import com.tangem.common.core.CardSessionRunnable
import com.tangem.common.core.CompletionCallback
import com.tangem.common.core.TangemSdkError
import com.tangem.common.extensions.hexToBytes
import com.tangem.common.extensions.toDecompressedPublicKey
import com.tangem.common.extensions.toHexString
import com.tangem.core.error.ext.tangemError
@ -22,15 +22,13 @@ import com.tangem.domain.card.common.visa.VisaWalletPublicKeyUtility
import com.tangem.domain.card.common.visa.VisaWalletPublicKeyUtility.findKeyWithoutDerivation
import com.tangem.domain.models.scan.CardDTO
import com.tangem.domain.visa.error.VisaActivationError
import com.tangem.domain.visa.model.VisaDataForApprove
import com.tangem.domain.visa.model.VisaSignedDataByCustomerWallet
import com.tangem.domain.visa.model.sign
import com.tangem.operations.ScanTask
import com.tangem.operations.derivation.DeriveWalletPublicKeyTask
import com.tangem.operations.sign.SignHashCommand
class VisaCustomerWalletApproveTask(
private val visaDataForApprove: VisaDataForApprove,
private val visaDataForApprove: Input,
) : CardSessionRunnable<VisaSignedDataByCustomerWallet> {
override fun run(session: CardSession, callback: CompletionCallback<VisaSignedDataByCustomerWallet>) {
@ -44,7 +42,7 @@ class VisaCustomerWalletApproveTask(
return
}
if (visaDataForApprove.customerWalletCardId != null && card.cardId != visaDataForApprove.customerWalletCardId) {
if (visaDataForApprove.cardId != null && card.cardId != visaDataForApprove.cardId) {
callback(CompletionResult.Failure(VisaActivationError.CardIdNotMatched.tangemError))
return
}
@ -153,6 +151,12 @@ class VisaCustomerWalletApproveTask(
)
}
// TODO: [REDACTED_TASK_KEY] - Get this public function from Blockchain SDK
private fun hashPersonalMessage(message: ByteArray): ByteArray {
val prefix = "\u0019Ethereum Signed Message:\n${message.size}".toByteArray()
return (prefix + message).toKeccak()
}
private fun signApproveData(
targetWalletPublicKey: ByteArray,
derivationPath: DerivationPath?,
@ -160,10 +164,11 @@ class VisaCustomerWalletApproveTask(
session: CardSession,
callback: CompletionCallback<VisaSignedDataByCustomerWallet>,
) {
val hashToSign = visaDataForApprove.dataToSign.hashToSign.hexToBytes()
val content = "Tangem Pay wants to sign in with your account. Nonce: ${visaDataForApprove.hashToSign}"
val hash = hashPersonalMessage(content.toByteArray(Charsets.UTF_8))
val signTask = SignHashCommand(
hash = hashToSign,
hash = hash,
walletPublicKey = targetWalletPublicKey,
derivationPath = derivationPath,
)
@ -173,7 +178,7 @@ class VisaCustomerWalletApproveTask(
is CompletionResult.Success -> {
val rsvSignature = UnmarshalHelper.unmarshalSignatureExtended(
signature = result.data.signature,
hash = hashToSign,
hash = hash,
publicKey = extendedPublicKey?.publicKey?.toDecompressedPublicKey()
?: targetWalletPublicKey.toDecompressedPublicKey(),
).asRSVLegacyEVM().toHexString().lowercase()
@ -181,10 +186,7 @@ class VisaCustomerWalletApproveTask(
scanCard(
session = session,
callback = callback,
signedData = visaDataForApprove.dataToSign.sign(
signature = rsvSignature,
customerWalletAddress = visaDataForApprove.targetAddress,
),
signedData = visaDataForApprove.sign(rsvSignature, visaDataForApprove.targetAddress),
)
}
is CompletionResult.Failure -> {
@ -211,4 +213,11 @@ class VisaCustomerWalletApproveTask(
}
}
}
data class Input(
val cardId: String? = null,
val targetAddress: String,
val hashToSign: String,
val sign: (signature: String, customerWalletAddress: String) -> VisaSignedDataByCustomerWallet,
)
}

View file

@ -1,19 +1,7 @@
package com.tangem.datasource.api.pay
import com.tangem.datasource.api.common.response.ApiResponse
import com.tangem.datasource.api.pay.models.request.ActivationByCardWalletRequest
import com.tangem.datasource.api.pay.models.request.ActivationByCustomerWalletRequest
import com.tangem.datasource.api.pay.models.request.ActivationStatusRequest
import com.tangem.datasource.api.pay.models.request.ExchangeAccessTokenRequest
import com.tangem.datasource.api.pay.models.request.GenerateNoneByCardIdRequest
import com.tangem.datasource.api.pay.models.request.GenerateNoneByCardWalletRequest
import com.tangem.datasource.api.pay.models.request.GetAccessTokenByCardIdRequest
import com.tangem.datasource.api.pay.models.request.GetAccessTokenByCardWalletRequest
import com.tangem.datasource.api.pay.models.request.GetCardWalletAcceptanceRequest
import com.tangem.datasource.api.pay.models.request.GetCustomerWalletAcceptanceRequest
import com.tangem.datasource.api.pay.models.request.RefreshTokenByCardIdRequest
import com.tangem.datasource.api.pay.models.request.RefreshTokenByCardWalletRequest
import com.tangem.datasource.api.pay.models.request.SetPinCodeRequest
import com.tangem.datasource.api.pay.models.request.*
import com.tangem.datasource.api.pay.models.response.*
import retrofit2.http.Body
import retrofit2.http.GET
@ -33,9 +21,17 @@ interface TangemPayApi {
@Body request: GenerateNoneByCardWalletRequest,
): ApiResponse<GenerateNonceResponse>
@POST("v1/auth/challenge")
suspend fun generateNonceByCustomerWallet(
@Body request: GenerateNonceByCustomerWalletRequest,
): ApiResponse<GenerateNonceResponse>
@POST("v1/auth/token")
suspend fun getAccessTokenByCardId(@Body request: GetAccessTokenByCardIdRequest): ApiResponse<JWTResponse>
@POST("v1/auth/token")
suspend fun getTokenByCustomerWallet(@Body request: GetTokenByCustomerWalletRequest): ApiResponse<JWTResponse>
@POST("v1/auth/token")
suspend fun getAccessTokenByCardWallet(@Body request: GetAccessTokenByCardWalletRequest): ApiResponse<JWTResponse>

View file

@ -0,0 +1,10 @@
package com.tangem.datasource.api.pay.models.request
import com.squareup.moshi.Json
import com.squareup.moshi.JsonClass
@JsonClass(generateAdapter = true)
data class GenerateNonceByCustomerWalletRequest(
@Json(name = "auth_type") val authType: String = "customer_wallet",
@Json(name = "customer_wallet_address") val customerWalletAddress: String,
)

View file

@ -0,0 +1,12 @@
package com.tangem.datasource.api.pay.models.request
import com.squareup.moshi.Json
import com.squareup.moshi.JsonClass
@JsonClass(generateAdapter = true)
data class GetTokenByCustomerWalletRequest(
@Json(name = "auth_type") val authType: String = "customer_wallet",
@Json(name = "session_id") val sessionId: String,
@Json(name = "signature") val signature: String,
@Json(name = "message_format") val messageFormat: String,
)

View file

@ -50,6 +50,7 @@ dependencies {
/** Libs - Tangem */
implementation(tangemDeps.blockchain)
implementation(tangemDeps.card.core)
implementation(projects.libs.tangemSdkApi)
/** DI */
implementation(deps.hilt.core)

View file

@ -2,39 +2,52 @@ package com.tangem.data.pay
import arrow.core.Either
import com.squareup.moshi.Moshi
import com.tangem.common.map
import com.tangem.core.error.UniversalError
import com.tangem.datasource.api.common.response.ApiResponseError
import com.tangem.datasource.api.common.response.getOrThrow
import com.tangem.datasource.api.pay.TangemPayApi
import com.tangem.datasource.api.pay.models.response.VisaErrorResponseJsonAdapter
import com.tangem.datasource.di.NetworkMoshi
import com.tangem.domain.models.wallet.UserWalletId
import com.tangem.domain.pay.KycStartInfo
import com.tangem.domain.pay.repository.KycRepository
import com.tangem.domain.visa.error.VisaApiError
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
import dagger.assisted.Assisted
import com.tangem.domain.visa.model.VisaDataForApprove
import com.tangem.domain.visa.model.VisaDataToSignByCustomerWallet
import com.tangem.domain.visa.repository.VisaAuthRepository
import com.tangem.sdk.api.TangemSdkManager
import dagger.assisted.AssistedFactory
import dagger.assisted.AssistedInject
import kotlinx.coroutines.withContext
@Suppress("UnusedPrivateMember")
class DefaultKycRepository @AssistedInject constructor(
@Assisted userWalletId: UserWalletId,
@NetworkMoshi moshi: Moshi,
private val tangemPayApi: TangemPayApi,
private val dispatcherProvider: CoroutineDispatcherProvider,
private val visaAuthRepository: VisaAuthRepository,
private val tangemSdkManager: TangemSdkManager,
) : KycRepository {
private val visaErrorAdapter = VisaErrorResponseJsonAdapter(moshi)
override suspend fun getKycStartInfo(): Either<UniversalError, KycStartInfo> = withContext(dispatcherProvider.io) {
val authTokenForSpecificWallet = "get from userWalletId"
request {
tangemPayApi.getKycAccess(
authHeader = authTokenForSpecificWallet,
).getOrThrow().result
override suspend fun getKycStartInfo(address: String, cardId: String): Either<UniversalError, KycStartInfo> {
var authHeader = ""
visaAuthRepository.getCustomerWalletAuthChallenge(address).getOrNull()?.let { result ->
tangemSdkManager.visaCustomerWalletApprove(
VisaDataForApprove(
customerWalletCardId = cardId,
targetAddress = address,
dataToSign = VisaDataToSignByCustomerWallet(hashToSign = result.challenge),
),
).map { signResult ->
visaAuthRepository.getTokenWithCustomerWallet(
sessionId = result.session.sessionId,
signature = signResult.signature,
nonce = signResult.dataToSign.hashToSign,
).getOrNull()?.let { authHeader = it }
}
}
return request {
authHeader.ifEmpty { error("Cannot get auth header for KYC") }
tangemPayApi.getKycAccess(authHeader = authHeader).getOrThrow().result
}.map {
KycStartInfo(
token = it.token,
@ -65,6 +78,6 @@ class DefaultKycRepository @AssistedInject constructor(
@AssistedFactory
interface Factory : KycRepository.Factory {
override fun create(userWalletId: UserWalletId): DefaultKycRepository
override fun create(): DefaultKycRepository
}
}

View file

@ -131,16 +131,18 @@ internal class DefaultVisaActivationRepository @AssistedInject constructor(
val authTokens =
checkNotNull(visaAuthTokenStorage.get(visaCardId.cardId)) { "Visa auth tokens are not stored" }
visaApi.activateByCustomerWallet(
authHeader = authTokens.getAuthHeader(),
body = ActivationByCustomerWalletRequest(
orderId = signedData.dataToSign.request.orderId,
customerWallet = ActivationByCustomerWalletRequest.CustomerWallet(
deployAcceptanceSignature = signedData.signature,
customerWalletAddress = signedData.customerWalletAddress,
signedData.dataToSign.request?.orderId?.let { orderId ->
visaApi.activateByCustomerWallet(
authHeader = authTokens.getAuthHeader(),
body = ActivationByCustomerWalletRequest(
orderId = orderId,
customerWallet = ActivationByCustomerWalletRequest.CustomerWallet(
deployAcceptanceSignature = signedData.signature,
customerWalletAddress = signedData.customerWalletAddress,
),
),
),
).getOrThrow()
).getOrThrow()
} ?: error("Order Id cannot be null")
}
}

View file

@ -65,6 +65,39 @@ internal class DefaultVisaAuthRepository @Inject constructor(
}
}
override suspend fun getCustomerWalletAuthChallenge(
customerWalletAddress: String,
): Either<VisaApiError, VisaAuthChallenge.Wallet> = withContext(dispatchers.io) {
request {
visaAuthApi.generateNonceByCustomerWallet(
GenerateNonceByCustomerWalletRequest(customerWalletAddress = customerWalletAddress),
).getOrThrow()
}.map { response ->
VisaAuthChallenge.Wallet(
challenge = response.result.nonce,
session = VisaAuthSession(response.result.sessionId),
)
}
}
override suspend fun getTokenWithCustomerWallet(
sessionId: String,
signature: String,
nonce: String,
): Either<VisaApiError, String> = withContext(dispatchers.io) {
request {
visaAuthApi.getTokenByCustomerWallet(
GetTokenByCustomerWalletRequest(
sessionId = sessionId,
signature = signature,
messageFormat = "Tangem Pay wants to sign in with your account. Nonce: $nonce",
),
).getOrThrow()
}.map { response ->
"Bearer ${response.result.accessToken}"
}
}
override suspend fun getAccessTokens(
signedChallenge: VisaAuthSignedChallenge,
): Either<VisaApiError, VisaAuthTokens> = withContext(dispatchers.io) {

View file

@ -4,8 +4,8 @@ import kotlinx.serialization.Serializable
@Serializable
data class VisaDataToSignByCustomerWallet(
val request: VisaCustomerWalletDataToSignRequest,
val hashToSign: String,
val request: VisaCustomerWalletDataToSignRequest? = null,
)
fun VisaDataToSignByCustomerWallet.sign(signature: String, customerWalletAddress: String) =

View file

@ -0,0 +1,6 @@
package com.tangem.domain.visa.model
data class VisaSignedChallengeByCustomerWallet(
val challenge: String,
val signature: String,
)

View file

@ -3,13 +3,12 @@ package com.tangem.domain.pay.repository
import arrow.core.Either
import com.tangem.core.error.UniversalError
import com.tangem.domain.pay.KycStartInfo
import com.tangem.domain.models.wallet.UserWalletId
interface KycRepository {
suspend fun getKycStartInfo(): Either<UniversalError, KycStartInfo>
suspend fun getKycStartInfo(address: String, cardId: String): Either<UniversalError, KycStartInfo>
interface Factory {
fun create(userWalletId: UserWalletId): KycRepository
fun create(): KycRepository
}
}

View file

@ -18,6 +18,16 @@ interface VisaAuthRepository {
cardWalletAddress: String,
): Either<VisaApiError, VisaAuthChallenge.Wallet>
suspend fun getCustomerWalletAuthChallenge(
customerWalletAddress: String,
): Either<VisaApiError, VisaAuthChallenge.Wallet>
suspend fun getTokenWithCustomerWallet(
sessionId: String,
signature: String,
nonce: String,
): Either<VisaApiError, String>
suspend fun getAccessTokens(signedChallenge: VisaAuthSignedChallenge): Either<VisaApiError, VisaAuthTokens>
suspend fun refreshAccessTokens(refreshToken: VisaAuthTokens.RefreshToken): Either<VisaApiError, VisaAuthTokens>

View file

@ -4,9 +4,14 @@ import com.tangem.core.decompose.context.AppComponentContext
interface KycComponent {
fun launch()
fun launch(params: Params)
interface Factory {
fun create(appComponentContext: AppComponentContext): KycComponent
}
data class Params(
val targetAddress: String,
val cardId: String,
)
}

View file

@ -13,8 +13,7 @@ android {
dependencies {
/** Api */
//TODO disable for release because of the permissions
// implementation(projects.features.kyc.api)
implementation(projects.features.kyc.api)
/** Domain */
implementation(projects.domain.visa)

View file

@ -1,57 +1,41 @@
package com.tangem.features.kyc
import com.sumsub.sns.core.SNSMobileSDK
import com.sumsub.sns.core.data.listener.SNSCompleteHandler
import com.sumsub.sns.core.data.listener.TokenExpirationHandler
import com.sumsub.sns.core.data.model.SNSCompletionResult
import com.sumsub.sns.core.data.model.SNSInitConfig
import com.sumsub.sns.core.data.model.SNSSDKState
import com.tangem.core.decompose.context.AppComponentContext
import com.tangem.domain.pay.repository.KycRepository
import com.tangem.domain.models.wallet.UserWalletId
import com.tangem.features.kyc.theme.TangemSNSTheme
import com.tangem.core.decompose.model.getOrCreateModel
import com.tangem.features.kyc.theme.TangemSNSIconHandler
import com.tangem.features.kyc.theme.TangemSNSTheme
import dagger.assisted.Assisted
import dagger.assisted.AssistedFactory
import dagger.assisted.AssistedInject
import kotlinx.coroutines.launch
import kotlinx.coroutines.runBlocking
import java.util.Locale
class DefaultKycComponent @AssistedInject constructor(
@Assisted appComponentContext: AppComponentContext,
private val kycRepositoryFactory: KycRepository.Factory,
) : KycComponent, AppComponentContext by appComponentContext {
private val kycRepository = kycRepositoryFactory.create(UserWalletId("0FFFFF"))
private val model: DefaultKycModel = getOrCreateModel()
override fun launch() {
override fun launch(params: KycComponent.Params) {
componentScope.launch {
val startInfo = kycRepository.getKycStartInfo().getOrNull() ?: return@launch
val tokenExpirationHandler = object : TokenExpirationHandler {
override fun onTokenExpired(): String? {
val newToken = runBlocking { kycRepository.getKycStartInfo().getOrNull()?.token }
return newToken
model.uiState.collect {
it?.let { startInfo ->
val tokenExpirationHandler = object : TokenExpirationHandler {
override fun onTokenExpired() = ""
}
val snsSdk = SNSMobileSDK.Builder(activity)
.withAccessToken(accessToken = startInfo.token, onTokenExpiration = tokenExpirationHandler)
.withTheme(TangemSNSTheme.theme(activity))
.withIconHandler(TangemSNSIconHandler())
.withLocale(Locale("en"))
.build()
snsSdk.launch()
}
}
val snsSdk = SNSMobileSDK.Builder(activity)
.withAccessToken(accessToken = startInfo.token, onTokenExpiration = tokenExpirationHandler)
.withConf(SNSInitConfig(strings = mapOf()))
.withTheme(TangemSNSTheme.theme(activity))
.withIconHandler(TangemSNSIconHandler())
.withLocale(Locale("en"))
.withCompleteHandler(
object : SNSCompleteHandler {
override fun onComplete(result: SNSCompletionResult, state: SNSSDKState) {
}
},
)
.build()
snsSdk.launch()
}
model.getKycToken(params)
}
@AssistedFactory

View file

@ -0,0 +1,32 @@
package com.tangem.features.kyc
import androidx.compose.runtime.Stable
import com.tangem.core.decompose.di.ModelScoped
import com.tangem.core.decompose.model.Model
import com.tangem.domain.pay.KycStartInfo
import com.tangem.domain.pay.repository.KycRepository
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.launch
import javax.inject.Inject
@Stable
@ModelScoped
class DefaultKycModel @Inject constructor(
override val dispatchers: CoroutineDispatcherProvider,
kycRepositoryFactory: KycRepository.Factory,
) : Model() {
private val kycRepository = kycRepositoryFactory.create()
private val _uiState: MutableStateFlow<KycStartInfo?> = MutableStateFlow(null)
val uiState = _uiState.asStateFlow()
fun getKycToken(params: KycComponent.Params) {
modelScope.launch {
kycRepository.getKycStartInfo(address = params.targetAddress, cardId = params.cardId).getOrNull()
?.let { _uiState.emit(it) }
}
}
}

View file

@ -1,11 +1,16 @@
package com.tangem.features.kyc.di
import com.tangem.core.decompose.di.ModelComponent
import com.tangem.core.decompose.model.Model
import com.tangem.features.kyc.DefaultKycComponent
import com.tangem.features.kyc.DefaultKycModel
import com.tangem.features.kyc.KycComponent
import dagger.Binds
import dagger.Module
import dagger.hilt.InstallIn
import dagger.hilt.components.SingletonComponent
import dagger.multibindings.ClassKey
import dagger.multibindings.IntoMap
@Module
@InstallIn(SingletonComponent::class)
@ -13,4 +18,13 @@ internal interface FeatureModule {
@Binds
fun bindComponentFactory(impl: DefaultKycComponent.Factory): KycComponent.Factory
}
@Module
@InstallIn(ModelComponent::class)
internal interface ModelModule {
@Binds
@IntoMap
@ClassKey(DefaultKycModel::class)
fun provideModel(model: DefaultKycModel): Model
}

View file

@ -113,6 +113,7 @@ dependencies {
implementation(projects.features.biometry.api)
implementation(projects.features.nft.api)
implementation(projects.features.sendV2.api)
implementation(projects.features.kyc.api)
/** Common modules */
implementation(projects.common)

View file

@ -15,10 +15,8 @@ import com.tangem.crypto.hdWallet.DerivationPath
import com.tangem.crypto.hdWallet.bip32.ExtendedPublicKey
import com.tangem.domain.models.scan.CardDTO
import com.tangem.domain.models.scan.ScanResponse
import com.tangem.domain.visa.model.VisaActivationInput
import com.tangem.domain.visa.model.VisaDataForApprove
import com.tangem.domain.visa.model.VisaSignedDataByCustomerWallet
import com.tangem.domain.models.wallet.UserWalletId
import com.tangem.domain.visa.model.*
import com.tangem.operations.derivation.DerivationTaskResponse
import com.tangem.operations.preflightread.PreflightReadFilter
import com.tangem.operations.wallet.CreateWalletResponse

View file

@ -260,8 +260,8 @@ include(":features:walletconnect:impl")
include(":features:hot-wallet:api")
include(":features:hot-wallet:impl")
include(":features:kyc:api")
//TODO disable for release because of the permissions
// include(":features:kyc:api")
// include(":features:kyc:impl")
include(":features:create-wallet-selection:api")