Updated on 2026-08-14

This commit is contained in:
Tangem 2024-06-11 15:40:27 +04:00
parent 72ddf5ae12
commit 084d4282f7
13 changed files with 608 additions and 0 deletions

View file

@ -76,6 +76,7 @@ dependencies {
implementation(projects.domain.walletConnect)
implementation(projects.common)
implementation(projects.common.routing)
implementation(projects.core.analytics)
implementation(projects.core.analytics.models)
implementation(projects.core.navigation)

1
common/routing/.gitignore vendored Normal file
View file

@ -0,0 +1 @@
/build

View file

@ -0,0 +1,25 @@
plugins {
alias(deps.plugins.android.library)
alias(deps.plugins.kotlin.android)
alias(deps.plugins.kotlin.serialization)
id("configuration")
}
android {
namespace = "com.tangem.common.routing"
}
dependencies {
/* Core */
implementation(projects.core.decompose)
/* Domain */
implementation(projects.domain.qrScanning.models)
implementation(projects.domain.tokens.models)
implementation(projects.domain.wallets.models)
/* Libs - Other */
implementation(deps.androidx.core.ktx)
implementation(deps.kotlin.serialization)
implementation(deps.timber)
}

View file

@ -0,0 +1,164 @@
package com.tangem.common.routing
import android.os.Bundle
import com.tangem.common.routing.bundle.RouteBundleParams
import com.tangem.common.routing.bundle.bundle
import com.tangem.common.routing.entity.SerializableIntent
import com.tangem.core.decompose.navigation.Route
import com.tangem.domain.qrscanning.models.SourceType
import com.tangem.domain.tokens.model.CryptoCurrency
import com.tangem.domain.wallets.models.UserWalletId
import kotlinx.serialization.Serializable
@Serializable
sealed class AppRoute(val path: String) : Route, RouteBundleParams {
override fun getBundle(): Bundle = bundle(serializer())
@Serializable
data object Initial : AppRoute(path = "/initial")
@Serializable
data object Home : AppRoute(path = "/home")
@Serializable
data class Welcome(
val intent: SerializableIntent? = null,
) : AppRoute(path = "/welcome") {
companion object {
const val INITIAL_INTENT_KEY = "intent"
}
}
@Serializable
data object Disclaimer : AppRoute(path = "/disclaimer")
@Serializable
data object OnboardingNote : AppRoute(path = "/onboarding/note")
@Serializable
data class OnboardingWallet(
val canSkipBackup: Boolean = true,
) : AppRoute(path = "/onboarding/wallet${if (canSkipBackup) "/skippable" else ""}") {
companion object {
const val CAN_SKIP_BACKUP_KEY = "canSkipBackup"
}
}
@Serializable
data object OnboardingTwins : AppRoute(path = "/onboarding/twins")
@Serializable
data object OnboardingOther : AppRoute(path = "/onboarding/other")
@Serializable
data object Wallet : AppRoute(path = "/wallet")
@Serializable
data class CurrencyDetails(
val userWalletId: UserWalletId,
val currency: CryptoCurrency,
) : AppRoute(path = "/currency_details/${userWalletId.stringValue}/${currency.id.value}") {
companion object {
const val USER_WALLET_ID_KEY = "userWalletId"
const val CRYPTO_CURRENCY_KEY = "currency"
}
}
@Serializable
data class Send(
val userWalletId: UserWalletId,
val currency: CryptoCurrency,
val transactionId: String? = null,
val amount: String? = null,
val tag: String? = null,
val destinationAddress: String? = null,
) : AppRoute(
path = "/send/${userWalletId.stringValue}/${currency.id.value}?" +
"&$transactionId" +
"&$amount" +
"&$tag" +
"&$destinationAddress",
) {
companion object {
const val USER_WALLET_ID_KEY = "userWalletId"
const val CRYPTO_CURRENCY_KEY = "currency"
const val TRANSACTION_ID_KEY = "transactionId"
const val AMOUNT_KEY = "amount"
const val TAG_KEY = "tag"
const val DESTINATION_ADDRESS_KEY = "destinationAddress"
}
}
@Serializable
data class Details(
val userWalletId: UserWalletId,
) : AppRoute(path = "/details/${userWalletId.stringValue}")
@Serializable
data object DetailsSecurity : AppRoute(path = "/details/security")
@Serializable
data object CardSettings : AppRoute(path = "/card_settings")
@Serializable
data object AppSettings : AppRoute(path = "/app_settings")
@Serializable
data object ResetToFactory : AppRoute(path = "/reset_to_factory")
@Serializable
data object AccessCodeRecovery : AppRoute(path = "/access_code_recovery")
@Serializable
data object ManageTokens : AppRoute(path = "/manage_tokens")
@Serializable
data object AddCustomToken : AppRoute(path = "/add_custom_token")
@Serializable
data object WalletConnectSessions : AppRoute(path = "/wallet_connect_sessions")
@Serializable
data class QrScanning(
val source: SourceType,
val networkName: String? = null,
) : AppRoute(path = "/qr_scanning") {
companion object {
const val SOURCE_KEY = "source"
const val NETWORK_KEY = "networkName"
}
}
@Serializable
data object ReferralProgram : AppRoute(path = "/referral_program")
@Serializable
data class Swap(
val currency: CryptoCurrency,
) : AppRoute(path = "/swap") {
companion object {
const val CURRENCY_BUNDLE_KEY = "currency"
}
}
@Serializable
data object TesterMenu : AppRoute(path = "/tester_menu")
@Serializable
data object SaveWallet : AppRoute(path = "/save_wallet")
@Serializable
data object AppCurrencySelector : AppRoute(path = "/app_currency_selector")
@Serializable
data object ModalNotification : AppRoute(path = "/modal_notification")
@Serializable
data object Staking : AppRoute(path = "/staking")
}

View file

@ -0,0 +1,42 @@
package com.tangem.common.routing
import kotlin.reflect.KClass
/**
* Interface for a router in the application.
* It provides methods for navigating through the application.
*
* Same as [com.tangem.core.decompose.navigation.Router] but without Decompose dependency.
*
* ***Must be removed after Decompose migration.***
*/
interface AppRouter {
/**
* The current navigation stack.
*/
val backStack: List<AppRoute>
/**
* 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: AppRoute, 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 class is found.
*
* @param routeClass The route class to pop to.
* @param onComplete The callback to be invoked when the operation is complete.
*/
fun popTo(routeClass: KClass<out AppRoute>, onComplete: (isSuccess: Boolean) -> Unit = {})
}

View file

@ -0,0 +1,108 @@
package com.tangem.common.routing.bundle
import android.os.Bundle
import kotlinx.serialization.ExperimentalSerializationApi
import kotlinx.serialization.descriptors.SerialDescriptor
import kotlinx.serialization.descriptors.StructureKind
import kotlinx.serialization.encoding.AbstractDecoder
import kotlinx.serialization.encoding.CompositeDecoder
import kotlinx.serialization.modules.SerializersModule
@ExperimentalSerializationApi
internal class BundleDecoder(
private val bundle: Bundle,
private val elementsCount: Int = -1,
private val isInitializer: Boolean = true,
override val serializersModule: SerializersModule,
) : AbstractDecoder() {
private var index = -1
private var elementKey: String? = null
override fun decodeElementIndex(descriptor: SerialDescriptor): Int {
if (++index >= elementsCount) {
return CompositeDecoder.DECODE_DONE
}
elementKey = descriptor.getElementName(index)
return index
}
override fun beginStructure(descriptor: SerialDescriptor): CompositeDecoder {
val b = if (isInitializer) {
bundle
} else {
requireNotNull(bundle.getBundle(elementKey)) {
"Bundle is missing for key $elementKey while decoding"
}
}
val count = when (descriptor.kind) {
StructureKind.MAP,
StructureKind.LIST,
-> b.getInt("\$size")
else -> descriptor.elementsCount
}
return BundleDecoder(
bundle = b,
elementsCount = count,
isInitializer = false,
serializersModule = serializersModule,
)
}
override fun endStructure(descriptor: SerialDescriptor) {
/* no-op */
}
override fun decodeBoolean(): Boolean {
return bundle.getBoolean(elementKey)
}
override fun decodeByte(): Byte {
return bundle.getByte(elementKey)
}
override fun decodeChar(): Char {
return bundle.getChar(elementKey)
}
override fun decodeDouble(): Double {
return bundle.getDouble(elementKey)
}
override fun decodeEnum(enumDescriptor: SerialDescriptor): Int {
return bundle.getInt(elementKey)
}
override fun decodeFloat(): Float {
return bundle.getFloat(elementKey)
}
override fun decodeInt(): Int {
return bundle.getInt(elementKey)
}
override fun decodeLong(): Long {
return bundle.getLong(elementKey)
}
override fun decodeNotNullMark(): Boolean {
return bundle.containsKey(elementKey)
}
override fun decodeNull(): Nothing? {
return null
}
override fun decodeShort(): Short {
return bundle.getShort(elementKey)
}
override fun decodeString(): String {
return requireNotNull(bundle.getString(elementKey)) {
"String is missing for key $elementKey while decoding"
}
}
}

View file

@ -0,0 +1,107 @@
package com.tangem.common.routing.bundle
import android.os.Bundle
import kotlinx.serialization.ExperimentalSerializationApi
import kotlinx.serialization.descriptors.SerialDescriptor
import kotlinx.serialization.descriptors.StructureKind
import kotlinx.serialization.encoding.AbstractEncoder
import kotlinx.serialization.encoding.CompositeEncoder
import kotlinx.serialization.modules.SerializersModule
@ExperimentalSerializationApi
internal class BundleEncoder(
private val bundle: Bundle,
private val parentBundle: Bundle? = null,
private val keyInParent: String? = null,
private val isInitializer: Boolean = true,
override val serializersModule: SerializersModule,
) : AbstractEncoder() {
private var elementKey: String? = null
override fun encodeElement(descriptor: SerialDescriptor, index: Int): Boolean {
elementKey = descriptor.getElementName(index)
return super.encodeElement(descriptor, index)
}
override fun beginStructure(descriptor: SerialDescriptor): CompositeEncoder {
return if (isInitializer) {
BundleEncoder(
bundle = bundle,
parentBundle = null,
keyInParent = elementKey,
isInitializer = false,
serializersModule = serializersModule,
)
} else {
BundleEncoder(
bundle = Bundle(),
parentBundle = bundle,
keyInParent = elementKey,
isInitializer = false,
serializersModule = serializersModule,
)
}
}
override fun endStructure(descriptor: SerialDescriptor) {
if (descriptor.kind in arrayOf(StructureKind.LIST, StructureKind.MAP)) {
val size = elementKey?.toIntOrNull()?.let { it + 1 } ?: 0
bundle.putInt("\$size", size)
}
if (keyInParent.isNullOrBlank()) {
return
}
parentBundle?.putBundle(keyInParent, bundle)
}
override fun encodeBoolean(value: Boolean) {
bundle.putBoolean(elementKey, value)
}
override fun encodeByte(value: Byte) {
bundle.putByte(elementKey, value)
}
override fun encodeChar(value: Char) {
bundle.putChar(elementKey, value)
}
override fun encodeDouble(value: Double) {
bundle.putDouble(elementKey, value)
}
override fun encodeEnum(enumDescriptor: SerialDescriptor, index: Int) {
bundle.putInt(elementKey, index)
}
override fun encodeFloat(value: Float) {
bundle.putFloat(elementKey, value)
}
override fun encodeInt(value: Int) {
bundle.putInt(elementKey, value)
}
override fun encodeLong(value: Long) {
bundle.putLong(elementKey, value)
}
override fun encodeNull() {
/* no-op */
}
override fun encodeShort(value: Short) {
bundle.putShort(elementKey, value)
}
override fun encodeString(value: String) {
bundle.putString(elementKey, value)
}
override fun encodeNotNullMark() {
/* no-op */
}
}

View file

@ -0,0 +1,60 @@
package com.tangem.common.routing.bundle
import android.os.Bundle
import kotlinx.serialization.DeserializationStrategy
import kotlinx.serialization.ExperimentalSerializationApi
import kotlinx.serialization.SerializationStrategy
import kotlinx.serialization.modules.EmptySerializersModule
import kotlinx.serialization.modules.SerializersModule
val defaultSerializersModule: SerializersModule = EmptySerializersModule()
/**
* Deserialize this bundle into an object of type [T].
*
* @receiver [Bundle] to deserialize.
* @param deserializer [DeserializationStrategy] of the [T] class.
*
* @return Object of type T deserialized from bundle.
*/
@OptIn(ExperimentalSerializationApi::class)
fun <T> Bundle.unbundle(
deserializer: DeserializationStrategy<T>,
serializersModule: SerializersModule = defaultSerializersModule,
): T {
val decoder = BundleDecoder(
bundle = this,
elementsCount = -1,
isInitializer = true,
serializersModule = serializersModule,
)
return deserializer.deserialize(decoder)
}
/**
* Serialize [T] into a bundle.
*
* @receiver Object to serialize.
* @param serializer [SerializationStrategy] of the [T] class.
*
* @return bundle serialized from value
*/
@OptIn(ExperimentalSerializationApi::class)
fun <T> T.bundle(
serializer: SerializationStrategy<T>,
serializersModule: SerializersModule = defaultSerializersModule,
): Bundle {
val bundle = Bundle(serializer.descriptor.elementsCount)
val encoder = BundleEncoder(
bundle = bundle,
parentBundle = null,
keyInParent = null,
isInitializer = true,
serializersModule = serializersModule,
)
serializer.serialize(encoder, value = this)
return bundle
}

View file

@ -0,0 +1,8 @@
package com.tangem.common.routing.bundle
import android.os.Bundle
interface RouteBundleParams {
fun getBundle(): Bundle
}

View file

@ -0,0 +1,24 @@
package com.tangem.common.routing.entity
import android.os.Bundle
import kotlinx.serialization.Serializable
@Serializable
data class SerializableBundle(
val map: Map<String, String>,
) {
constructor(bundle: Bundle) : this(
map = bundle.keySet().mapNotNull { key ->
bundle.getString(key)?.let { key to it }
}.toMap(),
)
fun toBundle(): Bundle {
return Bundle().apply {
map.forEach { (key, value) ->
putString(key, value)
}
}
}
}

View file

@ -0,0 +1,51 @@
package com.tangem.common.routing.entity
import android.content.ComponentName
import android.content.Intent
import android.net.Uri
import kotlinx.serialization.Serializable
@Serializable
data class SerializableIntent(
val action: String?,
val dataString: String?,
val categories: Set<String>?,
val type: String?,
val packageValue: String?,
val component: String?,
val flags: Int,
val extras: SerializableBundle?,
) {
constructor(intent: Intent) : this(
action = intent.action,
dataString = intent.dataString,
categories = intent.categories,
type = intent.type,
packageValue = intent.`package`,
component = intent.component?.flattenToString(),
flags = intent.flags,
extras = intent.extras?.let(::SerializableBundle),
)
fun toIntent(): Intent {
val intent = Intent()
intent.action = action
intent.setDataAndType(
dataString?.let { Uri.parse(it) },
type,
)
categories?.let { categories ->
for (category in categories) {
intent.addCategory(category)
}
}
intent.`package` = packageValue
intent.component = component?.let { ComponentName.unflattenFromString(it) }
intent.flags = flags
extras?.let { intent.putExtras(it.toBundle()) }
return intent
}
}

View file

@ -0,0 +1,16 @@
package com.tangem.common.routing.utils
import com.tangem.common.routing.AppRoute
import com.tangem.common.routing.AppRouter
/**
* Pops routes from the navigation stack until the specified route [R] is found.
*
* ***Must be removed after Decompose migration.***
*
* @param R The route to pop to.
* @param onComplete The callback to be invoked when the operation is complete.
*/
inline fun <reified R : AppRoute> AppRouter.popTo(noinline onComplete: (isSuccess: Boolean) -> Unit = {}) {
popTo(R::class, onComplete)
}

View file

@ -113,6 +113,7 @@ enableFeaturePreview("TYPESAFE_PROJECT_ACCESSORS")
include(":app")
include(":common")
include(":common:ui-charts")
include(":common:routing")
// region Core modules
include(":core:analytics")