Updated on 2026-08-14
This commit is contained in:
parent
84e5496689
commit
123f8b2e34
25 changed files with 411 additions and 296 deletions
|
|
@ -13,8 +13,10 @@ import com.tangem.TangemSdk
|
|||
import com.tangem.domain.common.FeatureCoroutineExceptionHandler
|
||||
import com.tangem.operations.backup.BackupService
|
||||
import com.tangem.tangem_sdk_new.extensions.init
|
||||
import com.tangem.tap.common.ActivityResultCallbackHolder
|
||||
import com.tangem.tap.common.DialogManager
|
||||
import com.tangem.tap.common.IntentHandler
|
||||
import com.tangem.tap.common.OnActivityResultCallback
|
||||
import com.tangem.tap.common.SnackbarHandler
|
||||
import com.tangem.tap.common.redux.NotificationsHandler
|
||||
import com.tangem.tap.common.redux.global.AndroidResources
|
||||
|
|
@ -47,13 +49,15 @@ private val mainCoroutineContext: CoroutineContext
|
|||
get() = Job() + Dispatchers.Main + FeatureCoroutineExceptionHandler.create("mainScope")
|
||||
val mainScope = CoroutineScope(mainCoroutineContext)
|
||||
|
||||
class MainActivity : AppCompatActivity(), SnackbarHandler {
|
||||
class MainActivity : AppCompatActivity(), SnackbarHandler, ActivityResultCallbackHolder {
|
||||
|
||||
private var snackbar: Snackbar? = null
|
||||
private val dialogManager = DialogManager()
|
||||
private val intentHandler = IntentHandler()
|
||||
private val binding: ActivityMainBinding by viewBinding(ActivityMainBinding::bind)
|
||||
|
||||
private val onActivityResultCallbacks = mutableListOf<OnActivityResultCallback>()
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
setContentView(R.layout.activity_main)
|
||||
|
|
@ -67,18 +71,17 @@ class MainActivity : AppCompatActivity(), SnackbarHandler {
|
|||
store.dispatch(GlobalAction.SetResources(getAndroidResources()))
|
||||
store.dispatch(
|
||||
ShopAction.CheckIfGooglePayAvailable(
|
||||
GooglePayService(createPaymentsClient(this), this)
|
||||
)
|
||||
GooglePayService(createPaymentsClient(this), this),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
private fun getAndroidResources(): AndroidResources {
|
||||
return AndroidResources(
|
||||
AndroidResources.RString(
|
||||
R.string.copy_toast_msg,
|
||||
R.string.details_notification_erase_wallet_not_possible
|
||||
)
|
||||
R.string.details_notification_erase_wallet_not_possible,
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
|
|
@ -92,7 +95,7 @@ class MainActivity : AppCompatActivity(), SnackbarHandler {
|
|||
|
||||
supportFragmentManager.registerFragmentLifecycleCallbacks(
|
||||
NavBarInsetsFragmentLifecycleCallback(),
|
||||
true
|
||||
true,
|
||||
)
|
||||
|
||||
requestedOrientation = ActivityInfo.SCREEN_ORIENTATION_PORTRAIT
|
||||
|
|
@ -137,7 +140,7 @@ class MainActivity : AppCompatActivity(), SnackbarHandler {
|
|||
if (snackbar != null) return
|
||||
|
||||
snackbar = Snackbar.make(
|
||||
binding.fragmentContainer, getString(text), Snackbar.LENGTH_INDEFINITE
|
||||
binding.fragmentContainer, getString(text), Snackbar.LENGTH_INDEFINITE,
|
||||
)
|
||||
if (buttonTitle != null && action != null) {
|
||||
snackbar?.setAction(getString(buttonTitle), action)
|
||||
|
|
@ -152,12 +155,23 @@ class MainActivity : AppCompatActivity(), SnackbarHandler {
|
|||
|
||||
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
|
||||
super.onActivityResult(requestCode, resultCode, data)
|
||||
|
||||
onActivityResultCallbacks.forEach { it(requestCode, resultCode, data) }
|
||||
when (requestCode) {
|
||||
LOAD_PAYMENT_DATA_REQUEST_CODE -> {
|
||||
store.dispatch(
|
||||
ShopAction.BuyWithGooglePay.HandleGooglePayResponse(resultCode, data)
|
||||
ShopAction.BuyWithGooglePay.HandleGooglePayResponse(resultCode, data),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun addOnActivityResultCallback(callback: OnActivityResultCallback) {
|
||||
onActivityResultCallbacks.remove(callback)
|
||||
onActivityResultCallbacks.add(callback)
|
||||
}
|
||||
|
||||
override fun removeOnActivityResultCallback(callback: OnActivityResultCallback) {
|
||||
onActivityResultCallbacks.remove(callback)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,13 @@
|
|||
package com.tangem.tap.common
|
||||
|
||||
import android.content.Intent
|
||||
|
||||
/**
|
||||
[REDACTED_AUTHOR]
|
||||
*/
|
||||
interface ActivityResultCallbackHolder {
|
||||
fun addOnActivityResultCallback(callback: OnActivityResultCallback)
|
||||
fun removeOnActivityResultCallback(callback: OnActivityResultCallback)
|
||||
}
|
||||
|
||||
typealias OnActivityResultCallback = (Int, Int, Intent?) -> Unit
|
||||
8
common/src/main/java/com/tangem/common/Filter.kt
Normal file
8
common/src/main/java/com/tangem/common/Filter.kt
Normal file
|
|
@ -0,0 +1,8 @@
|
|||
package com.tangem.common
|
||||
|
||||
/**
|
||||
[REDACTED_AUTHOR]
|
||||
*/
|
||||
interface Filter<T> {
|
||||
fun filter(value: T): Boolean
|
||||
}
|
||||
|
|
@ -0,0 +1,13 @@
|
|||
package com.tangem.common.module
|
||||
|
||||
/**
|
||||
[REDACTED_AUTHOR]
|
||||
*/
|
||||
object ModuleErrorCode {
|
||||
const val APP = 100000
|
||||
|
||||
const val COMMON = 10000
|
||||
const val NETWORK = 20000
|
||||
const val DOMAIN = 30000
|
||||
const val SALT_PAY = 40000
|
||||
}
|
||||
|
|
@ -1,14 +0,0 @@
|
|||
package com.tangem.common.module
|
||||
|
||||
/**
|
||||
[REDACTED_AUTHOR]
|
||||
* A module exception
|
||||
*/
|
||||
interface ModuleException {
|
||||
val message: String
|
||||
}
|
||||
|
||||
/**
|
||||
* An exception marked as FbConsumeException should be submitted to Firebase.Crashlytics as a non-fatal issue.
|
||||
*/
|
||||
interface FbConsumeException
|
||||
|
|
@ -6,17 +6,22 @@ package com.tangem.common.module
|
|||
*/
|
||||
interface ModuleMessage
|
||||
|
||||
interface ModuleMessageConverter<ModuleMessage, R> {
|
||||
fun convert(message: ModuleMessage): R
|
||||
}
|
||||
|
||||
/**
|
||||
* @property code describes what feature is the error coming from
|
||||
* @property message the error description
|
||||
* @property data any data that can help in the part where this error is being handled
|
||||
*/
|
||||
interface ModuleError : ModuleMessage {
|
||||
val code: Int
|
||||
val message: String
|
||||
val data: Any?
|
||||
abstract class ModuleError() : Throwable(), ModuleMessage {
|
||||
abstract val code: Int
|
||||
abstract override val message: String
|
||||
abstract val data: Any?
|
||||
}
|
||||
|
||||
/**
|
||||
* An exception marked as FbConsumeException should be submitted to Firebase.Crashlytics as a non-fatal issue.
|
||||
*/
|
||||
interface FbConsumeException
|
||||
|
||||
interface ModuleMessageConverter<ModuleMessage, R> {
|
||||
fun convert(message: ModuleMessage): R
|
||||
}
|
||||
|
|
@ -8,7 +8,7 @@ import com.tangem.network.api.tangemTech.CoinsResponse
|
|||
*/
|
||||
sealed interface DomainDialog {
|
||||
|
||||
data class DialogError(val error: DomainError) : DomainDialog
|
||||
data class DialogError(val error: DomainModuleError) : DomainDialog
|
||||
|
||||
data class SelectTokenDialog(
|
||||
val items: List<CoinsResponse.Coin.Network>,
|
||||
|
|
|
|||
|
|
@ -1,47 +0,0 @@
|
|||
package com.tangem.domain
|
||||
|
||||
import com.tangem.common.module.ModuleError
|
||||
import com.tangem.common.module.ModuleMessage
|
||||
|
||||
/**
|
||||
[REDACTED_AUTHOR]
|
||||
* All DomainError descendants must use their own range of codes, but no more than 999 error codes for each.
|
||||
*/
|
||||
sealed interface DomainModuleMessage : ModuleMessage
|
||||
|
||||
sealed class DomainError(
|
||||
subCode: Int,
|
||||
override val message: String,
|
||||
override val data: Any?,
|
||||
) : DomainModuleMessage, ModuleError {
|
||||
override val code: Int = ERROR_CODE_DOMAIN + subCode
|
||||
|
||||
companion object {
|
||||
// base code used for all error in the module
|
||||
const val ERROR_CODE_DOMAIN = 10000
|
||||
const val ERROR_CODE_ADD_CUSTOM_TOKEN = 100
|
||||
// const val CODE_ANY_OTHER = 200..299
|
||||
}
|
||||
}
|
||||
|
||||
sealed class AddCustomTokenError(
|
||||
subCode: Int = 0
|
||||
) : DomainError(ERROR_CODE_ADD_CUSTOM_TOKEN + subCode, this::class.java.simpleName, null) {
|
||||
|
||||
object FieldIsEmpty : AddCustomTokenError()
|
||||
object FieldIsNotEmpty : AddCustomTokenError()
|
||||
object InvalidContractAddress : AddCustomTokenError()
|
||||
object NetworkIsNotSelected : AddCustomTokenError()
|
||||
object InvalidDecimalsCount : AddCustomTokenError()
|
||||
object InvalidDerivationPath : AddCustomTokenError()
|
||||
|
||||
sealed class Network : AddCustomTokenError() {
|
||||
object CheckAddressRequestError : Network()
|
||||
}
|
||||
|
||||
sealed class Warning : AddCustomTokenError() {
|
||||
object PotentialScamToken : Warning()
|
||||
object TokenAlreadyAdded : Warning()
|
||||
object UnsupportedSolanaToken: Warning()
|
||||
}
|
||||
}
|
||||
|
|
@ -1,21 +0,0 @@
|
|||
package com.tangem.domain
|
||||
|
||||
import com.tangem.common.module.FbConsumeException
|
||||
import com.tangem.common.module.ModuleException
|
||||
|
||||
/**
|
||||
[REDACTED_AUTHOR]
|
||||
*/
|
||||
sealed class DomainException(override val message: String) : Throwable(message), ModuleException
|
||||
|
||||
sealed class AddCustomTokenException(message: String) : DomainException(message) {
|
||||
|
||||
data class SelectTokeNetworkException(val networkId: String) : AddCustomTokenException(
|
||||
"Unknown network [$networkId] should not be included in the network selection dialog."
|
||||
), FbConsumeException
|
||||
|
||||
data class UnAppropriateInitializationException(
|
||||
val of: String,
|
||||
val info: String? = null
|
||||
) : AddCustomTokenException("The [$of], must be properly initialized. Info [$info]")
|
||||
}
|
||||
|
|
@ -10,7 +10,7 @@ import com.tangem.domain.redux.state.ActionStateLoggerImpl
|
|||
object DomainLayer {
|
||||
internal val actionStateLogger = ActionStateLoggerImpl()
|
||||
|
||||
var onInitComplete: ((DomainException?) -> Unit)? = null
|
||||
var onInitComplete: ((DomainModuleError?) -> Unit)? = null
|
||||
|
||||
fun init(data: Map<String, Any?>? = null) {
|
||||
initActionStateLogger()
|
||||
|
|
|
|||
|
|
@ -0,0 +1,65 @@
|
|||
package com.tangem.domain
|
||||
|
||||
import com.tangem.common.module.FbConsumeException
|
||||
import com.tangem.common.module.ModuleError
|
||||
import com.tangem.common.module.ModuleErrorCode
|
||||
import com.tangem.common.module.ModuleMessage
|
||||
|
||||
/**
|
||||
[REDACTED_AUTHOR]
|
||||
* All DomainError descendants must use their own range of codes, but no more than 999 error codes for each.
|
||||
*/
|
||||
sealed interface DomainModuleMessage : ModuleMessage
|
||||
|
||||
sealed class DomainModuleError(
|
||||
subCode: Int,
|
||||
override val message: String,
|
||||
override val data: Any?,
|
||||
) : DomainModuleMessage, ModuleError() {
|
||||
override val code: Int = ModuleErrorCode.DOMAIN + subCode
|
||||
|
||||
companion object {
|
||||
// base code used for all errors in the module
|
||||
internal const val ERROR_CODE_ADD_CUSTOM_TOKEN = 100
|
||||
// const val CODE_ANY_OTHER = 200..299, 300..399 etc
|
||||
}
|
||||
}
|
||||
|
||||
sealed class AddCustomTokenError(
|
||||
subCode: Int = 0,
|
||||
message: String? = null,
|
||||
data: Any? = null,
|
||||
) : DomainModuleError(
|
||||
subCode = ERROR_CODE_ADD_CUSTOM_TOKEN + subCode,
|
||||
message = message ?: this::class.java.simpleName,
|
||||
data = data,
|
||||
) {
|
||||
|
||||
object FieldIsEmpty : AddCustomTokenError()
|
||||
object FieldIsNotEmpty : AddCustomTokenError()
|
||||
object InvalidContractAddress : AddCustomTokenError()
|
||||
object NetworkIsNotSelected : AddCustomTokenError()
|
||||
object InvalidDecimalsCount : AddCustomTokenError()
|
||||
object InvalidDerivationPath : AddCustomTokenError()
|
||||
|
||||
sealed class Network : AddCustomTokenError() {
|
||||
object CheckAddressRequestError : Network()
|
||||
}
|
||||
|
||||
sealed class Warning : AddCustomTokenError() {
|
||||
object PotentialScamToken : Warning()
|
||||
object TokenAlreadyAdded : Warning()
|
||||
object UnsupportedSolanaToken : Warning()
|
||||
}
|
||||
|
||||
data class SelectTokeNetworkError(val networkId: String) : AddCustomTokenError(
|
||||
message = "Unknown network [$networkId] should not be included in the network selection dialog.",
|
||||
), FbConsumeException
|
||||
|
||||
data class UnAppropriateInitialization(
|
||||
val of: String,
|
||||
val info: String? = null,
|
||||
) : AddCustomTokenError(
|
||||
message = "The [$of], must be properly initialized. Info [$info]",
|
||||
)
|
||||
}
|
||||
|
|
@ -0,0 +1,93 @@
|
|||
package com.tangem.domain.common
|
||||
|
||||
import com.tangem.blockchain.common.Blockchain
|
||||
import com.tangem.blockchain.common.Token
|
||||
import com.tangem.common.CardIdRange
|
||||
import com.tangem.common.contains
|
||||
import com.tangem.domain.features.BuildConfig
|
||||
|
||||
object SaltPayWorkaround {
|
||||
fun tokenFrom(blockchain: Blockchain): Token {
|
||||
return when (blockchain) {
|
||||
Blockchain.SaltPay -> Token(
|
||||
name = "WXDAI",
|
||||
symbol = "WXDAI",
|
||||
contractAddress = "0x4346186e7461cB4DF06bCFCB4cD591423022e417",
|
||||
decimals = 18,
|
||||
id = "xdai",
|
||||
)
|
||||
Blockchain.SaltPayTestnet -> Token(
|
||||
name = "WXDAI Test",
|
||||
symbol = "MyERC20",
|
||||
contractAddress = "0x69cca8D8295de046C7c14019D9029Ccc77987A48",
|
||||
decimals = 0,
|
||||
id = "xdai",
|
||||
)
|
||||
else -> throw IllegalArgumentException()
|
||||
}
|
||||
}
|
||||
|
||||
val visaBatches = listOf(
|
||||
"AE02",
|
||||
"AE03",
|
||||
) + attachDebugVisaBatches()
|
||||
|
||||
val tangemWalletCardIds = listOf(
|
||||
"AC01000000033503",
|
||||
"AC01000000033594",
|
||||
"AC01000000033586",
|
||||
"AC01000000034477",
|
||||
"AC01000000032760",
|
||||
"AC01000000033867",
|
||||
"AC01000000032653",
|
||||
"AC01000000032752",
|
||||
"AC01000000034485",
|
||||
"AC01000000033644",
|
||||
"AC01000000037454",
|
||||
"AC01000000037462",
|
||||
"AC03000000076070",
|
||||
"AC03000000076088",
|
||||
"AC03000000076096",
|
||||
"AC03000000076104",
|
||||
"AC03000000076112",
|
||||
"AC03000000076120",
|
||||
"AC03000000076138",
|
||||
"AC03000000076146",
|
||||
"AC03000000076153",
|
||||
"AC03000000076161",
|
||||
"AC03000000076179",
|
||||
"AC03000000076187",
|
||||
"AC03000000076195",
|
||||
"AC03000000076203",
|
||||
"AC03000000076211",
|
||||
"AC03000000076229",
|
||||
) + attachDebugTangemCardIds()
|
||||
|
||||
val tangemWalletCardIdRanges = listOf(
|
||||
CardIdRange("AC05000000000003", "AC05000000023997")!!,
|
||||
) + attachDebugTangemCardIdRanges()
|
||||
|
||||
fun isVisaBatchId(batchId: String): Boolean = visaBatches.contains(batchId)
|
||||
|
||||
fun isTangemWalletCardId(cardId: String): Boolean {
|
||||
return if (tangemWalletCardIds.contains(cardId)) true else tangemWalletCardIdRanges.contains(cardId)
|
||||
}
|
||||
|
||||
private fun attachDebugVisaBatches(): List<String> {
|
||||
return if (BuildConfig.DEBUG) listOf(
|
||||
"FF03",
|
||||
) else listOf()
|
||||
}
|
||||
|
||||
private fun attachDebugTangemCardIds(): List<String> {
|
||||
return if (BuildConfig.DEBUG) listOf(
|
||||
"FF04000000000232",
|
||||
) else listOf()
|
||||
}
|
||||
|
||||
private fun attachDebugTangemCardIdRanges(): List<CardIdRange> {
|
||||
return if (BuildConfig.DEBUG) listOf(
|
||||
CardIdRange("FF04000000000000", "FF04999999999999")!!,
|
||||
) else listOf()
|
||||
}
|
||||
}
|
||||
|
|
@ -8,10 +8,10 @@ import com.tangem.common.card.WalletData
|
|||
import com.tangem.common.extensions.ByteArrayKey
|
||||
import com.tangem.common.extensions.toMapKey
|
||||
import com.tangem.common.hdWallet.DerivationPath
|
||||
import com.tangem.domain.common.TapWorkarounds.getSaltPayBlockchain
|
||||
import com.tangem.domain.common.TapWorkarounds.getTangemNoteBlockchain
|
||||
import com.tangem.domain.common.TapWorkarounds.isSaltPay
|
||||
import com.tangem.domain.common.TapWorkarounds.isTangemNote
|
||||
import com.tangem.domain.common.TapWorkarounds.isTangemTwins
|
||||
import com.tangem.domain.common.TapWorkarounds.isTestCard
|
||||
import com.tangem.operations.CommandResponse
|
||||
import com.tangem.operations.backup.PrimaryCard
|
||||
|
|
@ -30,12 +30,8 @@ data class ScanResponse(
|
|||
) : CommandResponse {
|
||||
fun getBlockchain(): Blockchain {
|
||||
return when (productType) {
|
||||
ProductType.SaltPay -> {
|
||||
card.getSaltPayBlockchain()
|
||||
}
|
||||
ProductType.Note -> {
|
||||
card.getTangemNoteBlockchain() ?: Blockchain.Unknown
|
||||
}
|
||||
ProductType.SaltPay -> if (card.isTestCard) Blockchain.SaltPayTestnet else Blockchain.SaltPay
|
||||
ProductType.Note -> card.getTangemNoteBlockchain() ?: Blockchain.Unknown
|
||||
else -> {
|
||||
val blockchainName: String = walletData?.blockchain ?: return Blockchain.Unknown
|
||||
Blockchain.fromId(blockchainName)
|
||||
|
|
@ -44,7 +40,7 @@ data class ScanResponse(
|
|||
}
|
||||
|
||||
fun getPrimaryToken(): Token? {
|
||||
if (card.isSaltPay) return TapWorkarounds.saltPayToken
|
||||
if (card.isSaltPay) return SaltPayWorkaround.tokenFrom(getBlockchain())
|
||||
|
||||
val cardToken = walletData?.token ?: return null
|
||||
return Token(
|
||||
|
|
@ -57,11 +53,11 @@ data class ScanResponse(
|
|||
|
||||
fun isTangemNote(): Boolean = productType == ProductType.Note
|
||||
fun isTangemWallet(): Boolean = productType == ProductType.Wallet
|
||||
fun isSaltPay(): Boolean = productType == ProductType.SaltPay
|
||||
fun isTangemTwins(): Boolean = productType == ProductType.Twins
|
||||
fun twinsIsTwinned(): Boolean = card.isTangemTwins && walletData != null && secondTwinPublicKey != null
|
||||
fun supportsHdWallet(): Boolean = card.settings.isHDWalletAllowed
|
||||
fun supportsBackup(): Boolean = card.settings.isBackupAllowed
|
||||
fun twinsIsTwinned(): Boolean =
|
||||
card.isTangemTwins() && walletData != null && secondTwinPublicKey != null
|
||||
|
||||
fun hasDerivation(blockchain: Blockchain, rawDerivationPath: String): Boolean {
|
||||
return hasDerivation(blockchain, DerivationPath(rawDerivationPath))
|
||||
|
|
@ -95,12 +91,10 @@ enum class ProductType {
|
|||
|
||||
val Card.productType: ProductType
|
||||
get() = when {
|
||||
isTangemTwins() -> ProductType.Twins
|
||||
isTangemNote() -> ProductType.Note
|
||||
isTangemTwins -> ProductType.Twins
|
||||
isTangemNote -> ProductType.Note
|
||||
isSaltPay -> ProductType.SaltPay
|
||||
else -> ProductType.Wallet
|
||||
}
|
||||
|
||||
typealias KeyWalletPublicKey = ByteArrayKey
|
||||
|
||||
fun Card.isTangemTwins(): Boolean = TwinsHelper.getTwinCardNumber(cardId) != null
|
||||
typealias KeyWalletPublicKey = ByteArrayKey
|
||||
|
|
@ -2,7 +2,6 @@ package com.tangem.domain.common
|
|||
|
||||
import com.tangem.blockchain.common.Blockchain
|
||||
import com.tangem.blockchain.common.DerivationStyle
|
||||
import com.tangem.blockchain.common.Token
|
||||
import com.tangem.common.card.Card
|
||||
import java.util.*
|
||||
|
||||
|
|
@ -10,6 +9,10 @@ import java.util.*
|
|||
[REDACTED_AUTHOR]
|
||||
*/
|
||||
object TapWorkarounds {
|
||||
private const val START_2_COIN_ISSUER = "start2coin"
|
||||
private const val TEST_CARD_BATCH = "99FF"
|
||||
private const val TEST_CARD_ID_STARTS_WITH = "FF99"
|
||||
|
||||
fun isStart2CoinIssuer(cardIssuer: String?): Boolean {
|
||||
return cardIssuer?.lowercase(Locale.US) == START_2_COIN_ISSUER
|
||||
}
|
||||
|
|
@ -17,21 +20,15 @@ object TapWorkarounds {
|
|||
val Card.isStart2Coin: Boolean
|
||||
get() = isStart2CoinIssuer(issuer.name)
|
||||
|
||||
val Card.isSaltPay: Boolean
|
||||
get() = isSaltPayVisa || isSaltPayTangem
|
||||
|
||||
val Card.isSaltPayVisa: Boolean
|
||||
get() = saltPayVisaBatches.contains(batchId)
|
||||
|
||||
val Card.isSaltPayTangem: Boolean
|
||||
get() = saltPayTangemCardIds.contains(cardId)
|
||||
|
||||
val Card.isTestCard: Boolean
|
||||
get() = batchId == TEST_CARD_BATCH && cardId.startsWith(TEST_CARD_ID_STARTS_WITH)
|
||||
|
||||
val Card.useOldStyleDerivation: Boolean
|
||||
get() = batchId == "AC01" || batchId == "AC02" || batchId == "CB95"
|
||||
|
||||
val Card.derivationStyle: DerivationStyle?
|
||||
get() = if (!settings.isHDWalletAllowed) {
|
||||
Int.MAX_VALUE
|
||||
null
|
||||
} else if (useOldStyleDerivation) {
|
||||
DerivationStyle.LEGACY
|
||||
|
|
@ -39,38 +36,15 @@ object TapWorkarounds {
|
|||
DerivationStyle.NEW
|
||||
}
|
||||
|
||||
fun Card.isExcluded(): Boolean {
|
||||
val excludedBatch = excludedBatches.contains(batchId)
|
||||
val excludedIssuerName = excludedIssuers.contains(issuer.name.uppercase(Locale.ROOT))
|
||||
return excludedBatch || excludedIssuerName
|
||||
}
|
||||
val Card.isNotSupportedInThatRelease: Boolean
|
||||
get() = false
|
||||
|
||||
fun Card.isNotSupportedInThatRelease(): Boolean {
|
||||
return false
|
||||
}
|
||||
val Card.isTangemTwins: Boolean
|
||||
get() = TwinsHelper.getTwinCardNumber(cardId) != null
|
||||
|
||||
fun Card.isTangemNote(): Boolean = tangemNoteBatches.contains(batchId)
|
||||
fun isTangemWalletBatch(card: Card): Boolean = tangemWalletBatches.contains(card.batchId)
|
||||
fun Card.getTangemNoteBlockchain(): Blockchain? =
|
||||
tangemNoteBatches[batchId] ?: null
|
||||
|
||||
fun Card.getSaltPayBlockchain(): Blockchain = Blockchain.SaltPay
|
||||
|
||||
private const val START_2_COIN_ISSUER = "start2coin"
|
||||
private const val TEST_CARD_BATCH = "99FF"
|
||||
private const val TEST_CARD_ID_STARTS_WITH = "FF99"
|
||||
private val excludedBatches = listOf(
|
||||
"0027",
|
||||
"0030",
|
||||
"0031",
|
||||
"0035",
|
||||
)
|
||||
|
||||
private val excludedIssuers = listOf(
|
||||
"TTM BANK",
|
||||
)
|
||||
|
||||
private val tangemWalletBatches = listOf("AC01")
|
||||
//TODO: replace by reading files from a card
|
||||
val Card.isTangemNote: Boolean
|
||||
get() = tangemNoteBatches.contains(batchId)
|
||||
|
||||
private val tangemNoteBatches = mapOf(
|
||||
"AB01" to Blockchain.Bitcoin,
|
||||
|
|
@ -87,55 +61,34 @@ object TapWorkarounds {
|
|||
"AB12" to Blockchain.Ethereum,
|
||||
)
|
||||
|
||||
private val tangemWalletBatchesWithStandardDerivationType = listOf(
|
||||
"AC01", "AC02", "CB95",
|
||||
fun Card.getTangemNoteBlockchain(): Blockchain? = tangemNoteBatches[batchId] ?: null
|
||||
|
||||
|
||||
val Card.isSaltPay: Boolean
|
||||
get() = isSaltPayVisa || isSaltPayTangem
|
||||
|
||||
val Card.isSaltPayVisa: Boolean
|
||||
get() = SaltPayWorkaround.isVisaBatchId(batchId)
|
||||
|
||||
val Card.isSaltPayTangem: Boolean
|
||||
get() = SaltPayWorkaround.isTangemWalletCardId(cardId)
|
||||
|
||||
|
||||
val Card.isExcluded: Boolean
|
||||
get() {
|
||||
val excludedBatch = excludedBatches.contains(batchId)
|
||||
val excludedIssuerName = excludedIssuers.contains(issuer.name.uppercase(Locale.ROOT))
|
||||
return excludedBatch || excludedIssuerName
|
||||
}
|
||||
|
||||
private val excludedBatches = listOf(
|
||||
"0027",
|
||||
"0030",
|
||||
"0031",
|
||||
"0035",
|
||||
)
|
||||
|
||||
val saltPayTangemCardIds = listOf(
|
||||
"AC01000000000015", // TODO: remove testing CIDs
|
||||
"AC03000000000088", // TODO: remove testing CIDs
|
||||
"AC03000000076070",
|
||||
"AC03000000076088",
|
||||
"AC03000000076096",
|
||||
"AC03000000076104",
|
||||
"AC03000000076112",
|
||||
"AC03000000076120",
|
||||
"AC03000000076138",
|
||||
"AC03000000076146",
|
||||
"AC03000000076153",
|
||||
"AC03000000076161",
|
||||
"AC03000000076179",
|
||||
"AC03000000076187",
|
||||
"AC03000000076195",
|
||||
"AC03000000076203",
|
||||
"AC03000000076211",
|
||||
"AC03000000076229",
|
||||
// added 01.10.2022
|
||||
"AC01000000033503",
|
||||
"AC01000000033594",
|
||||
"AC01000000033586",
|
||||
"AC01000000034477",
|
||||
"AC01000000032760",
|
||||
"AC01000000033867",
|
||||
"AC01000000032653",
|
||||
"AC01000000032752",
|
||||
"AC01000000034485",
|
||||
"AC01000000033644",
|
||||
"AC01000000037454",
|
||||
"AC01000000037462",
|
||||
private val excludedIssuers = listOf(
|
||||
"TTM BANK",
|
||||
)
|
||||
|
||||
private val saltPayVisaBatches = listOf(
|
||||
"AE02",
|
||||
"AE03",
|
||||
)
|
||||
|
||||
val saltPayToken: Token
|
||||
get() = Token(
|
||||
name = "Wrapped xDAI",
|
||||
symbol = "WxDAI",
|
||||
contractAddress = "0x4346186e7461cB4DF06bCFCB4cD591423022e417",
|
||||
decimals = 18,
|
||||
id = "xdai",
|
||||
)
|
||||
}
|
||||
|
|
@ -1,36 +0,0 @@
|
|||
package com.tangem.domain.common.card
|
||||
|
||||
import timber.log.Timber
|
||||
|
||||
/**
|
||||
[REDACTED_AUTHOR]
|
||||
*/
|
||||
class CardIdRange(
|
||||
val cardIdStart: String,
|
||||
val cardIdEnd: String,
|
||||
) {
|
||||
val range: LongRange
|
||||
|
||||
init {
|
||||
checkCardId(cardIdStart)
|
||||
checkCardId(cardIdEnd)
|
||||
range = toLong(cardIdStart)..toLong(cardIdEnd)
|
||||
}
|
||||
|
||||
fun contains(cardId: String): Boolean = try {
|
||||
checkCardId(cardId)
|
||||
range.contains(toLong(cardId))
|
||||
} catch (ex: Exception) {
|
||||
Timber.e(ex, "CardIdRange: check for the cardId: [$cardId] in range failed.")
|
||||
false
|
||||
}
|
||||
|
||||
private fun stripBatchPrefix(cardId: String): String = cardId.drop(4)
|
||||
|
||||
@Throws(NumberFormatException::class)
|
||||
private fun toLong(cardId: String): Long = stripBatchPrefix(cardId).toLong()
|
||||
|
||||
private fun checkCardId(cardId: String) {
|
||||
if (cardId.length != 16) throw IllegalArgumentException("CardId must be 16 characters long.")
|
||||
}
|
||||
}
|
||||
|
|
@ -48,6 +48,7 @@ fun Blockchain.Companion.fromNetworkId(networkId: String): Blockchain? {
|
|||
"optimistic-ethereum/test" -> Blockchain.OptimismTestnet
|
||||
"dash" -> Blockchain.Dash
|
||||
"wxdai" -> Blockchain.SaltPay
|
||||
"wxdai/test" -> Blockchain.SaltPayTestnet
|
||||
else -> null
|
||||
}
|
||||
}
|
||||
|
|
@ -100,6 +101,7 @@ fun Blockchain.toNetworkId(): String {
|
|||
Blockchain.OptimismTestnet -> "optimistic-ethereum/test"
|
||||
Blockchain.Dash -> "dash"
|
||||
Blockchain.SaltPay -> "wxdai"
|
||||
Blockchain.SaltPayTestnet -> "wxdai/test"
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -131,7 +133,7 @@ fun Blockchain.toCoinId(): String {
|
|||
Blockchain.Kusama -> "kusama"
|
||||
Blockchain.Optimism, Blockchain.OptimismTestnet -> "ethereum"
|
||||
Blockchain.Dash -> "dash"
|
||||
Blockchain.SaltPay -> "xdai"
|
||||
Blockchain.SaltPay, Blockchain.SaltPayTestnet -> "xdai"
|
||||
Blockchain.Unknown -> "unknown"
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,22 @@
|
|||
package com.tangem.domain.common.extensions
|
||||
|
||||
import com.tangem.common.CompletionResult
|
||||
import com.tangem.common.services.Result
|
||||
|
||||
/**
|
||||
[REDACTED_AUTHOR]
|
||||
*/
|
||||
inline fun <T> Result<T>.successOr(failureClause: (Result.Failure) -> T): T {
|
||||
return when (this) {
|
||||
is Result.Success -> this.data
|
||||
is Result.Failure -> failureClause(this)
|
||||
}
|
||||
}
|
||||
|
||||
inline fun <T> CompletionResult<T>.successOr(failureClause: (CompletionResult.Failure<T>) -> T): T {
|
||||
return when (this) {
|
||||
is CompletionResult.Success -> this.data
|
||||
is CompletionResult.Failure -> failureClause(this)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -8,7 +8,6 @@ import com.tangem.domain.AddCustomTokenError
|
|||
import com.tangem.domain.AddCustomTokenError.Warning.PotentialScamToken
|
||||
import com.tangem.domain.AddCustomTokenError.Warning.TokenAlreadyAdded
|
||||
import com.tangem.domain.AddCustomTokenError.Warning.UnsupportedSolanaToken
|
||||
import com.tangem.domain.AddCustomTokenException
|
||||
import com.tangem.domain.DomainDialog
|
||||
import com.tangem.domain.DomainWrapped
|
||||
import com.tangem.domain.common.TapWorkarounds.derivationStyle
|
||||
|
|
@ -200,7 +199,7 @@ internal class AddCustomTokenHub : BaseStoreHub<AddCustomTokenState>("AddCustomT
|
|||
networkIdConverter = { networkId ->
|
||||
val blockchain = Blockchain.fromNetworkId(networkId)
|
||||
if (blockchain == null || blockchain == Blockchain.Unknown) {
|
||||
throw AddCustomTokenException.SelectTokeNetworkException(networkId)
|
||||
throw AddCustomTokenError.SelectTokeNetworkError(networkId)
|
||||
}
|
||||
hubState.blockchainToName(blockchain) ?: ""
|
||||
},
|
||||
|
|
@ -488,7 +487,7 @@ internal class AddCustomTokenHub : BaseStoreHub<AddCustomTokenState>("AddCustomT
|
|||
|
||||
@Throws
|
||||
private fun throwUnAppropriateInitialization(objName: String) {
|
||||
throw AddCustomTokenException.UnAppropriateInitializationException(
|
||||
throw AddCustomTokenError.UnAppropriateInitialization(
|
||||
"AddCustomTokenHub", "$objName must be not NULL"
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@ package com.tangem.domain.redux.global
|
|||
import com.tangem.domain.DomainDialog
|
||||
import com.tangem.domain.common.LogConfig
|
||||
import com.tangem.domain.common.ScanResponse
|
||||
import com.tangem.network.api.paymentology.PaymentologyApiService
|
||||
import com.tangem.network.api.tangemTech.TangemTechService
|
||||
|
||||
/**
|
||||
|
|
@ -18,5 +19,6 @@ data class DomainGlobalState(
|
|||
)
|
||||
|
||||
data class NetworkServices(
|
||||
val tangemTechService: TangemTechService = TangemTechService(LogConfig.network.tangemTechService)
|
||||
val tangemTechService: TangemTechService = TangemTechService(LogConfig.network.tangemTechService),
|
||||
val paymentologyService: PaymentologyApiService = PaymentologyApiService(LogConfig.network.paymentologyApiService),
|
||||
)
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
package com.tangem.network.api.paymentology
|
||||
|
||||
import retrofit2.http.Body
|
||||
import retrofit2.http.Headers
|
||||
import retrofit2.http.POST
|
||||
|
||||
/**
|
||||
|
|
@ -8,21 +9,30 @@ import retrofit2.http.POST
|
|||
*/
|
||||
interface PaymentologyApi {
|
||||
|
||||
@Headers("Content-Type: application/json")
|
||||
@POST("card/verify")
|
||||
fun checkRegistration(
|
||||
suspend fun checkRegistration(
|
||||
@Body request: CheckRegistrationRequests,
|
||||
): RegistrationResponse.Item
|
||||
): RegistrationResponse
|
||||
|
||||
@Headers("Content-Type: application/json")
|
||||
@POST("card/get_challenge")
|
||||
fun requestAttestationChallenge(
|
||||
suspend fun requestAttestationChallenge(
|
||||
@Body request: CheckRegistrationRequests.Item,
|
||||
): AttestationResponse
|
||||
|
||||
@Headers("Content-Type: application/json")
|
||||
@POST("card/set_pin")
|
||||
fun registerWallet(
|
||||
suspend fun registerWallet(
|
||||
@Body request: RegisterWalletRequest,
|
||||
): RegisterWalletResponse
|
||||
|
||||
@Headers("Content-Type: application/json")
|
||||
@POST("card/kyc")
|
||||
suspend fun registerKYC(
|
||||
@Body request: RegisterKYCRequest,
|
||||
): RegisterWalletResponse
|
||||
|
||||
companion object {
|
||||
val baseUrl: String = "https://paymentologygate.oa.r.appspot.com/"
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@ package com.tangem.network.api.paymentology
|
|||
import com.tangem.common.extensions.toHexString
|
||||
import com.tangem.common.services.Result
|
||||
import com.tangem.common.services.performRequest
|
||||
import com.tangem.network.common.MoshiConverter
|
||||
import com.tangem.network.common.createRetrofitInstance
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.withContext
|
||||
|
|
@ -15,17 +16,19 @@ class PaymentologyApiService(
|
|||
) {
|
||||
private val api = createRetrofitInstance(
|
||||
baseUrl = PaymentologyApi.baseUrl,
|
||||
converterFactory = MoshiConverter.createFactory(MoshiConverter.sdkMoshi()),
|
||||
logEnabled = logEnabled,
|
||||
).create(PaymentologyApi::class.java)
|
||||
|
||||
suspend fun checkRegistration(
|
||||
cardId: String,
|
||||
publicKey: ByteArray,
|
||||
): Result<RegistrationResponse.Item> = withContext(Dispatchers.IO) {
|
||||
// later convert response to SaltPayRegistrator.State
|
||||
): Result<RegistrationResponse> = withContext(Dispatchers.IO) {
|
||||
val requestItem = CheckRegistrationRequests.Item(cardId, publicKey.toHexString())
|
||||
val requests = listOf(requestItem)
|
||||
performRequest { api.checkRegistration(CheckRegistrationRequests(requests)) }
|
||||
val request = CheckRegistrationRequests(listOf(requestItem))
|
||||
performRequest {
|
||||
api.checkRegistration(request)
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun requestAttestationChallenge(
|
||||
|
|
@ -33,12 +36,28 @@ class PaymentologyApiService(
|
|||
publicKey: ByteArray,
|
||||
): Result<AttestationResponse> = withContext(Dispatchers.IO) {
|
||||
val requestItem = CheckRegistrationRequests.Item(cardId, publicKey.toHexString())
|
||||
performRequest { api.requestAttestationChallenge(requestItem) }
|
||||
performRequest {
|
||||
api.requestAttestationChallenge(requestItem)
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun registerWallet(
|
||||
request: RegisterWalletRequest,
|
||||
): Result<RegisterWalletResponse> = withContext(Dispatchers.IO) {
|
||||
performRequest { api.registerWallet(request) }
|
||||
performRequest {
|
||||
api.registerWallet(request)
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun registerKYC(
|
||||
request: RegisterKYCRequest,
|
||||
): Result<RegisterWalletResponse> = withContext(Dispatchers.IO) {
|
||||
performRequest {
|
||||
api.registerKYC(request)
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
fun stub(): PaymentologyApiService = PaymentologyApiService(false)
|
||||
}
|
||||
}
|
||||
|
|
@ -1,6 +1,7 @@
|
|||
package com.tangem.network.api.paymentology
|
||||
|
||||
import com.squareup.moshi.Json
|
||||
import com.squareup.moshi.JsonClass
|
||||
import com.tangem.common.extensions.calculateHashCode
|
||||
|
||||
/**
|
||||
|
|
@ -9,10 +10,11 @@ import com.tangem.common.extensions.calculateHashCode
|
|||
data class CheckRegistrationRequests(
|
||||
val requests: List<Item>,
|
||||
) {
|
||||
@JsonClass(generateAdapter = true)
|
||||
data class Item(
|
||||
@Json(name = "CID")
|
||||
var cardId: String = "",
|
||||
var publicKey: String = "",
|
||||
val cardId: String = "",
|
||||
val publicKey: String = "",
|
||||
)
|
||||
}
|
||||
|
||||
|
|
@ -35,12 +37,12 @@ data class RegisterWalletRequest(
|
|||
other as RegisterWalletRequest
|
||||
|
||||
if (cardId != other.cardId) return false
|
||||
if (publicKey.contentEquals(other.publicKey)) return false
|
||||
if (walletPublicKey.contentEquals(other.walletPublicKey)) return false
|
||||
if (walletSalt.contentEquals(other.walletSalt)) return false
|
||||
if (walletSignature.contentEquals(other.walletSignature)) return false
|
||||
if (cardSalt.contentEquals(other.cardSalt)) return false
|
||||
if (cardSignature.contentEquals(other.cardSignature)) return false
|
||||
if (!publicKey.contentEquals(other.publicKey)) return false
|
||||
if (!walletPublicKey.contentEquals(other.walletPublicKey)) return false
|
||||
if (!walletSalt.contentEquals(other.walletSalt)) return false
|
||||
if (!walletSignature.contentEquals(other.walletSignature)) return false
|
||||
if (!cardSalt.contentEquals(other.cardSalt)) return false
|
||||
if (!cardSignature.contentEquals(other.cardSignature)) return false
|
||||
if (pin != other.pin) return false
|
||||
|
||||
return true
|
||||
|
|
@ -56,4 +58,34 @@ data class RegisterWalletRequest(
|
|||
cardSignature.contentHashCode(),
|
||||
pin.hashCode(),
|
||||
)
|
||||
}
|
||||
|
||||
data class RegisterKYCRequest(
|
||||
@Json(name = "CID")
|
||||
val cardId: String,
|
||||
val publicKey: ByteArray,
|
||||
val kycProvider: String,
|
||||
val kycRefId: String,
|
||||
) {
|
||||
|
||||
override fun equals(other: Any?): Boolean {
|
||||
if (this === other) return true
|
||||
if (javaClass != other?.javaClass) return false
|
||||
|
||||
other as RegisterKYCRequest
|
||||
|
||||
if (cardId != other.cardId) return false
|
||||
if (!publicKey.contentEquals(other.publicKey)) return false
|
||||
if (kycProvider != other.kycProvider) return false
|
||||
if (kycRefId != other.kycRefId) return false
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
override fun hashCode(): Int = calculateHashCode(
|
||||
cardId.hashCode(),
|
||||
publicKey.contentHashCode(),
|
||||
kycProvider.hashCode(),
|
||||
kycRefId.hashCode(),
|
||||
)
|
||||
}
|
||||
|
|
@ -6,16 +6,12 @@ import com.tangem.common.extensions.calculateHashCode
|
|||
/**
|
||||
[REDACTED_AUTHOR]
|
||||
*/
|
||||
interface ErrorContainer {
|
||||
val error: String?
|
||||
}
|
||||
|
||||
data class RegistrationResponse(
|
||||
val results: List<Item>,
|
||||
val results: List<Item> = listOf(),
|
||||
val success: Boolean,
|
||||
override val error: String?,
|
||||
val errorCode: String?,
|
||||
) : ErrorContainer {
|
||||
val error: String?,
|
||||
val errorCode: Int?,
|
||||
) {
|
||||
|
||||
data class Item(
|
||||
@Json(name = "CID")
|
||||
|
|
@ -28,20 +24,22 @@ data class RegistrationResponse(
|
|||
val blockchainInit: Boolean?,
|
||||
@Json(name = "kyc_passed")
|
||||
val kycPassed: Boolean?,
|
||||
@Json(name = "kyc_waiting")
|
||||
val kycWaiting: Boolean?,
|
||||
@Json(name = "kyc_provider")
|
||||
val kycProvider: String?,
|
||||
@Json(name = "kyc_date")
|
||||
val kycDate: String?,
|
||||
@Json(name = "disabled_by_admin")
|
||||
val disabledByAdmin: Boolean?,
|
||||
override val error: String?,
|
||||
) : ErrorContainer
|
||||
val error: String?,
|
||||
)
|
||||
}
|
||||
|
||||
data class AttestationResponse(
|
||||
val challenge: ByteArray,
|
||||
val challenge: ByteArray?,
|
||||
val success: Boolean,
|
||||
override val error: String?,
|
||||
val errorCode: String?,
|
||||
) : ErrorContainer {
|
||||
val error: String?,
|
||||
val errorCode: Int?,
|
||||
) {
|
||||
|
||||
override fun equals(other: Any?): Boolean {
|
||||
if (this === other) return true
|
||||
|
|
@ -49,7 +47,7 @@ data class AttestationResponse(
|
|||
|
||||
other as AttestationResponse
|
||||
|
||||
if (challenge.contentEquals(other.challenge)) return false
|
||||
if (!challenge.contentEquals(other.challenge)) return false
|
||||
if (success != other.success) return false
|
||||
if (error != other.error) return false
|
||||
if (errorCode != other.errorCode) return false
|
||||
|
|
@ -67,6 +65,6 @@ data class AttestationResponse(
|
|||
|
||||
data class RegisterWalletResponse(
|
||||
val success: Boolean,
|
||||
override val error: String?,
|
||||
val errorCode: String?,
|
||||
) : ErrorContainer
|
||||
val error: String?,
|
||||
val errorCode: Int?,
|
||||
)
|
||||
|
|
@ -1,6 +1,7 @@
|
|||
package com.tangem.network.common
|
||||
|
||||
import com.tangem.common.module.ModuleError
|
||||
import com.tangem.common.module.ModuleErrorCode
|
||||
import com.tangem.common.module.ModuleMessage
|
||||
|
||||
/**
|
||||
|
|
@ -12,12 +13,11 @@ sealed class NetworkError(
|
|||
subCode: Int,
|
||||
override val message: String,
|
||||
override val data: Any?,
|
||||
) : NetworkModuleMessage, ModuleError {
|
||||
override val code: Int = ERROR_CODE_NETWORK + subCode
|
||||
) : NetworkModuleMessage, ModuleError() {
|
||||
override val code: Int = ModuleErrorCode.NETWORK + subCode
|
||||
|
||||
companion object {
|
||||
// base code used for all error in the module
|
||||
const val ERROR_CODE_NETWORK = 20000
|
||||
// const val CODE_ANY_OTHER = 100..199
|
||||
// const val CODE_ANY_OTHER = 100..199, 200..299
|
||||
}
|
||||
}
|
||||
|
|
@ -1,9 +0,0 @@
|
|||
package com.tangem.network.common
|
||||
|
||||
/**
|
||||
[REDACTED_AUTHOR]
|
||||
*/
|
||||
interface NetworkInternalException
|
||||
|
||||
sealed class NetworkException(message: String?) : Throwable(message), NetworkInternalException {
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue