Updated on 2026-08-14
This commit is contained in:
parent
72ddf5ae12
commit
084d4282f7
13 changed files with 608 additions and 0 deletions
|
|
@ -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
1
common/routing/.gitignore
vendored
Normal file
|
|
@ -0,0 +1 @@
|
|||
/build
|
||||
25
common/routing/build.gradle.kts
Normal file
25
common/routing/build.gradle.kts
Normal 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)
|
||||
}
|
||||
|
|
@ -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")
|
||||
}
|
||||
|
|
@ -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 = {})
|
||||
}
|
||||
|
|
@ -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"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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 */
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
|
|
@ -0,0 +1,8 @@
|
|||
package com.tangem.common.routing.bundle
|
||||
|
||||
import android.os.Bundle
|
||||
|
||||
interface RouteBundleParams {
|
||||
|
||||
fun getBundle(): Bundle
|
||||
}
|
||||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
@ -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)
|
||||
}
|
||||
|
|
@ -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")
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue