Updated on 2026-08-14
This commit is contained in:
parent
ff282a1d7d
commit
65a55cb5e3
23 changed files with 222 additions and 144 deletions
|
|
@ -69,8 +69,8 @@ repositories {
|
||||||
dependencies {
|
dependencies {
|
||||||
implementation fileTree(include: ['*.aar'], dir: 'libs')
|
implementation fileTree(include: ['*.aar'], dir: 'libs')
|
||||||
implementation implementation(project(path: ':domain'))
|
implementation implementation(project(path: ':domain'))
|
||||||
// TODO: refactoring: only for backwards compatibility with non-relocated services to the network module
|
|
||||||
implementation implementation(project(path: ':network'))
|
implementation implementation(project(path: ':network'))
|
||||||
|
implementation implementation(project(path: ':common'))
|
||||||
|
|
||||||
implementation 'androidx.core:core-ktx:1.7.0'
|
implementation 'androidx.core:core-ktx:1.7.0'
|
||||||
implementation 'androidx.appcompat:appcompat:1.4.1'
|
implementation 'androidx.appcompat:appcompat:1.4.1'
|
||||||
|
|
|
||||||
|
|
@ -20,7 +20,7 @@ import com.tangem.domain.DomainDialog
|
||||||
import com.tangem.domain.redux.domainStore
|
import com.tangem.domain.redux.domainStore
|
||||||
import com.tangem.domain.redux.global.DomainGlobalAction
|
import com.tangem.domain.redux.global.DomainGlobalAction
|
||||||
import com.tangem.domain.redux.global.DomainGlobalState
|
import com.tangem.domain.redux.global.DomainGlobalState
|
||||||
import com.tangem.tap.features.tokens.addCustomToken.DomainErrorConverter
|
import com.tangem.tap.common.moduleMessage.ModuleMessageConverter
|
||||||
import com.tangem.tap.features.tokens.addCustomToken.compose.SelectTokenNetworkDialog
|
import com.tangem.tap.features.tokens.addCustomToken.compose.SelectTokenNetworkDialog
|
||||||
import com.tangem.wallet.R
|
import com.tangem.wallet.R
|
||||||
import org.rekotlin.StoreSubscriber
|
import org.rekotlin.StoreSubscriber
|
||||||
|
|
@ -55,13 +55,13 @@ private fun ShowTheDialog(dialogState: MutableState<DomainDialog?>) {
|
||||||
if (dialogState.value == null) return
|
if (dialogState.value == null) return
|
||||||
|
|
||||||
val context = LocalContext.current
|
val context = LocalContext.current
|
||||||
val errorConverter = remember { DomainErrorConverter(context) }
|
val errorConverter = remember { ModuleMessageConverter(context) }
|
||||||
val onDismissRequest = { domainStore.dispatch(DomainGlobalAction.ShowDialog(null)) }
|
val onDismissRequest = { domainStore.dispatch(DomainGlobalAction.ShowDialog(null)) }
|
||||||
|
|
||||||
when (val dialog = dialogState.value) {
|
when (val dialog = dialogState.value) {
|
||||||
is DomainDialog.DialogError -> ErrorDialog(
|
is DomainDialog.DialogError -> ErrorDialog(
|
||||||
title = stringResource(id = R.string.common_error),
|
title = stringResource(id = R.string.common_error),
|
||||||
body = errorConverter.convertError(dialog.error),
|
body = errorConverter.convert(dialog.error),
|
||||||
onDismissRequest
|
onDismissRequest
|
||||||
)
|
)
|
||||||
is DomainDialog.SelectTokenDialog -> SelectTokenNetworkDialog(dialog, onDismissRequest)
|
is DomainDialog.SelectTokenDialog -> SelectTokenNetworkDialog(dialog, onDismissRequest)
|
||||||
|
|
|
||||||
|
|
@ -15,16 +15,17 @@ import androidx.compose.runtime.mutableStateOf
|
||||||
import androidx.compose.runtime.remember
|
import androidx.compose.runtime.remember
|
||||||
import androidx.compose.ui.Alignment
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.platform.LocalContext
|
||||||
import androidx.compose.ui.text.TextStyle
|
import androidx.compose.ui.text.TextStyle
|
||||||
import androidx.compose.ui.text.input.VisualTransformation
|
import androidx.compose.ui.text.input.VisualTransformation
|
||||||
import androidx.compose.ui.text.style.TextOverflow
|
import androidx.compose.ui.text.style.TextOverflow
|
||||||
import androidx.compose.ui.tooling.preview.Preview
|
import androidx.compose.ui.tooling.preview.Preview
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import androidx.compose.ui.unit.sp
|
import androidx.compose.ui.unit.sp
|
||||||
import com.tangem.domain.DomainError
|
import com.tangem.common.module.ModuleError
|
||||||
import com.tangem.domain.ErrorConverter
|
|
||||||
import com.tangem.domain.common.form.Field
|
import com.tangem.domain.common.form.Field
|
||||||
import com.tangem.tap.common.compose.extensions.stringResourceDefault
|
import com.tangem.tap.common.compose.extensions.stringResourceDefault
|
||||||
|
import com.tangem.tap.common.moduleMessage.ModuleMessageConverter
|
||||||
|
|
||||||
/**
|
/**
|
||||||
[REDACTED_AUTHOR]
|
[REDACTED_AUTHOR]
|
||||||
|
|
@ -41,8 +42,8 @@ fun OutlinedTextFieldWidget(
|
||||||
isEnabled: Boolean = true,
|
isEnabled: Boolean = true,
|
||||||
isVisible: Boolean = true,
|
isVisible: Boolean = true,
|
||||||
isLoading: Boolean = false,
|
isLoading: Boolean = false,
|
||||||
error: DomainError? = null,
|
error: ModuleError? = null,
|
||||||
errorConverter: ErrorConverter<String>? = null,
|
errorConverter: ModuleMessageConverter? = null,
|
||||||
debounceTextChanges: Long = 400,
|
debounceTextChanges: Long = 400,
|
||||||
visualTransformation: VisualTransformation = VisualTransformation.None,
|
visualTransformation: VisualTransformation = VisualTransformation.None,
|
||||||
keyboardOptions: KeyboardOptions = KeyboardOptions.Default,
|
keyboardOptions: KeyboardOptions = KeyboardOptions.Default,
|
||||||
|
|
@ -79,7 +80,7 @@ private fun OutlinedProgressTextField(
|
||||||
placeholder: String = "",
|
placeholder: String = "",
|
||||||
isEnabled: Boolean = true,
|
isEnabled: Boolean = true,
|
||||||
isLoading: Boolean = false,
|
isLoading: Boolean = false,
|
||||||
error: DomainError? = null,
|
error: ModuleError? = null,
|
||||||
debounce: Long = 400,
|
debounce: Long = 400,
|
||||||
visualTransformation: VisualTransformation = VisualTransformation.None,
|
visualTransformation: VisualTransformation = VisualTransformation.None,
|
||||||
keyboardOptions: KeyboardOptions = KeyboardOptions.Default,
|
keyboardOptions: KeyboardOptions = KeyboardOptions.Default,
|
||||||
|
|
@ -132,8 +133,8 @@ private fun OutlinedProgressTextField(
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
private fun AnimatedErrorView(
|
private fun AnimatedErrorView(
|
||||||
error: DomainError? = null,
|
error: ModuleError? = null,
|
||||||
errorConverter: ErrorConverter<String>,
|
errorConverter: ModuleMessageConverter,
|
||||||
) {
|
) {
|
||||||
AnimatedVisibility(
|
AnimatedVisibility(
|
||||||
visible = error != null,
|
visible = error != null,
|
||||||
|
|
@ -141,7 +142,7 @@ private fun AnimatedErrorView(
|
||||||
exit = slideOutVertically() + fadeOut(),
|
exit = slideOutVertically() + fadeOut(),
|
||||||
) {
|
) {
|
||||||
error?.let {
|
error?.let {
|
||||||
ErrorView(errorConverter.convertError(it), style = TextStyle(fontSize = 14.sp))
|
ErrorView(errorConverter.convert(it), style = TextStyle(fontSize = 14.sp))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -149,20 +150,14 @@ private fun AnimatedErrorView(
|
||||||
@Preview
|
@Preview
|
||||||
@Composable
|
@Composable
|
||||||
fun OutlinedTextFieldWithErrorTest() {
|
fun OutlinedTextFieldWithErrorTest() {
|
||||||
val converter = remember {
|
val context = LocalContext.current
|
||||||
object : ErrorConverter<String> {
|
val converter = remember { ModuleMessageConverter(context) }
|
||||||
override fun convertError(error: DomainError): String {
|
|
||||||
return "Hello, i'am the error: ${error::class.java.simpleName}"
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
class SimpleError(
|
class SimpleError(
|
||||||
override val code: Int = 1,
|
override val code: Int = 1,
|
||||||
override val message: String = "Error message",
|
override val message: String = "Error message",
|
||||||
override val data: Any? = null,
|
override val data: Any? = null,
|
||||||
) : DomainError
|
) : ModuleError
|
||||||
|
|
||||||
val modifier = Modifier
|
val modifier = Modifier
|
||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,24 @@
|
||||||
|
package com.tangem.tap.common.moduleMessage
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import com.tangem.common.module.ModuleMessage
|
||||||
|
import com.tangem.common.module.ModuleMessageConverter
|
||||||
|
import com.tangem.domain.DomainModuleMessage
|
||||||
|
import com.tangem.tap.common.moduleMessage.domain.DomainMessageConverter
|
||||||
|
|
||||||
|
class ModuleMessageConverter(
|
||||||
|
private val context: Context
|
||||||
|
) : ModuleMessageConverter<ModuleMessage, String> {
|
||||||
|
|
||||||
|
override fun convert(message: ModuleMessage): String {
|
||||||
|
val convertedMessage = when (message) {
|
||||||
|
is DomainModuleMessage -> DomainMessageConverter(context).convert(message)
|
||||||
|
else -> null
|
||||||
|
}
|
||||||
|
return convertedMessage ?: convertUnknownMessage(message)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun convertUnknownMessage(message: ModuleMessage): String {
|
||||||
|
return "Unknown message: ${message::class.java.simpleName}"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -1,10 +1,9 @@
|
||||||
package com.tangem.tap.features.tokens.addCustomToken
|
package com.tangem.tap.common.moduleMessage.domain
|
||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
|
import com.tangem.common.module.ModuleMessageConverter
|
||||||
|
import com.tangem.domain.AddCustomTokenError
|
||||||
import com.tangem.domain.DomainError
|
import com.tangem.domain.DomainError
|
||||||
import com.tangem.domain.ErrorConverter
|
|
||||||
import com.tangem.domain.features.addCustomToken.AddCustomTokenError
|
|
||||||
import com.tangem.domain.features.addCustomToken.AddCustomTokenWarning
|
|
||||||
import com.tangem.wallet.R
|
import com.tangem.wallet.R
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -12,27 +11,23 @@ import com.tangem.wallet.R
|
||||||
*/
|
*/
|
||||||
class DomainErrorConverter(
|
class DomainErrorConverter(
|
||||||
private val context: Context
|
private val context: Context
|
||||||
) : ErrorConverter<String> {
|
) : ModuleMessageConverter<DomainError, String?> {
|
||||||
|
override fun convert(message: DomainError): String? = when (message) {
|
||||||
override fun convertError(error: DomainError): String {
|
is AddCustomTokenError -> AddCustomTokenConverter(context).convert(message)
|
||||||
val errorMessage = when (error) {
|
else -> null
|
||||||
is AddCustomTokenError -> AddCustomTokenConverter(context).convertError(error)
|
|
||||||
else -> null
|
|
||||||
}
|
|
||||||
return errorMessage?.let { it } ?: "Unknown error: ${error::class.java.simpleName}"
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private class AddCustomTokenConverter(
|
private class AddCustomTokenConverter(
|
||||||
private val context: Context
|
private val context: Context
|
||||||
) : ErrorConverter<String> {
|
) : ModuleMessageConverter<DomainError, String?> {
|
||||||
|
|
||||||
override fun convertError(error: DomainError): String {
|
override fun convert(message: DomainError): String? {
|
||||||
val customTokenError = (error as? AddCustomTokenError) ?: throw UnsupportedOperationException()
|
val customTokenError = (message as? AddCustomTokenError) ?: throw UnsupportedOperationException()
|
||||||
|
|
||||||
val rawMessage = when (customTokenError) {
|
val rawMessage = when (customTokenError) {
|
||||||
AddCustomTokenWarning.PotentialScamToken -> R.string.custom_token_validation_error_not_found
|
AddCustomTokenError.Warning.PotentialScamToken -> R.string.custom_token_validation_error_not_found
|
||||||
AddCustomTokenWarning.TokenAlreadyAdded -> R.string.custom_token_validation_error_already_added
|
AddCustomTokenError.Warning.TokenAlreadyAdded -> R.string.custom_token_validation_error_already_added
|
||||||
AddCustomTokenError.InvalidContractAddress -> R.string.custom_token_creation_error_invalid_contract_address
|
AddCustomTokenError.InvalidContractAddress -> R.string.custom_token_creation_error_invalid_contract_address
|
||||||
AddCustomTokenError.NetworkIsNotSelected -> R.string.custom_token_creation_error_network_not_selected
|
AddCustomTokenError.NetworkIsNotSelected -> R.string.custom_token_creation_error_network_not_selected
|
||||||
AddCustomTokenError.InvalidDerivationPath -> R.string.custom_token_creation_error_invalid_derivation_path
|
AddCustomTokenError.InvalidDerivationPath -> R.string.custom_token_creation_error_invalid_derivation_path
|
||||||
|
|
@ -42,10 +37,11 @@ private class AddCustomTokenConverter(
|
||||||
AddCustomTokenError.FieldIsEmpty -> R.string.custom_token_creation_error_required_field
|
AddCustomTokenError.FieldIsEmpty -> R.string.custom_token_creation_error_required_field
|
||||||
else -> null
|
else -> null
|
||||||
}
|
}
|
||||||
|
|
||||||
return when (rawMessage) {
|
return when (rawMessage) {
|
||||||
is Int -> context.getString(rawMessage)
|
is Int -> context.getString(rawMessage)
|
||||||
is String -> rawMessage
|
is String -> rawMessage
|
||||||
else -> "Unknown error: ${customTokenError::class.java.simpleName}"
|
else -> null
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -0,0 +1,20 @@
|
||||||
|
package com.tangem.tap.common.moduleMessage.domain
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import com.tangem.common.module.ModuleMessageConverter
|
||||||
|
import com.tangem.domain.DomainError
|
||||||
|
import com.tangem.domain.DomainModuleMessage
|
||||||
|
|
||||||
|
/**
|
||||||
|
[REDACTED_AUTHOR]
|
||||||
|
*/
|
||||||
|
class DomainMessageConverter(
|
||||||
|
private val context: Context
|
||||||
|
) : ModuleMessageConverter<DomainModuleMessage, String?> {
|
||||||
|
override fun convert(message: DomainModuleMessage): String? {
|
||||||
|
return when (message) {
|
||||||
|
is DomainError -> DomainErrorConverter(context).convert(message)
|
||||||
|
else -> null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -13,11 +13,9 @@ import androidx.compose.ui.res.colorResource
|
||||||
import androidx.compose.ui.res.stringResource
|
import androidx.compose.ui.res.stringResource
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import androidx.compose.ui.unit.sp
|
import androidx.compose.ui.unit.sp
|
||||||
import com.tangem.domain.ErrorConverter
|
import com.tangem.domain.AddCustomTokenError
|
||||||
import com.tangem.domain.common.form.DataField
|
import com.tangem.domain.common.form.DataField
|
||||||
import com.tangem.domain.common.form.FieldId
|
import com.tangem.domain.common.form.FieldId
|
||||||
import com.tangem.domain.features.addCustomToken.AddCustomTokenError
|
|
||||||
import com.tangem.domain.features.addCustomToken.AddCustomTokenWarning
|
|
||||||
import com.tangem.domain.features.addCustomToken.CustomTokenFieldId.*
|
import com.tangem.domain.features.addCustomToken.CustomTokenFieldId.*
|
||||||
import com.tangem.domain.features.addCustomToken.redux.AddCustomTokenAction
|
import com.tangem.domain.features.addCustomToken.redux.AddCustomTokenAction
|
||||||
import com.tangem.domain.features.addCustomToken.redux.AddCustomTokenState
|
import com.tangem.domain.features.addCustomToken.redux.AddCustomTokenState
|
||||||
|
|
@ -27,7 +25,7 @@ import com.tangem.domain.redux.domainStore
|
||||||
import com.tangem.tap.common.compose.ComposeDialogManager
|
import com.tangem.tap.common.compose.ComposeDialogManager
|
||||||
import com.tangem.tap.common.compose.ToggledRippleTheme
|
import com.tangem.tap.common.compose.ToggledRippleTheme
|
||||||
import com.tangem.tap.common.compose.keyboardAsState
|
import com.tangem.tap.common.compose.keyboardAsState
|
||||||
import com.tangem.tap.features.tokens.addCustomToken.DomainErrorConverter
|
import com.tangem.tap.common.moduleMessage.ModuleMessageConverter
|
||||||
import com.tangem.wallet.R
|
import com.tangem.wallet.R
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -82,7 +80,7 @@ fun AddCustomTokenScreen(state: MutableState<AddCustomTokenState>) {
|
||||||
@Composable
|
@Composable
|
||||||
private fun FormFields(state: MutableState<AddCustomTokenState>) {
|
private fun FormFields(state: MutableState<AddCustomTokenState>) {
|
||||||
val context = LocalContext.current
|
val context = LocalContext.current
|
||||||
val errorConverter = remember { DomainErrorConverter(context) }
|
val errorConverter = remember { ModuleMessageConverter(context) }
|
||||||
|
|
||||||
val stateValue = state.value
|
val stateValue = state.value
|
||||||
stateValue.form.fieldList.forEach { field ->
|
stateValue.form.fieldList.forEach { field ->
|
||||||
|
|
@ -99,11 +97,11 @@ private fun FormFields(state: MutableState<AddCustomTokenState>) {
|
||||||
}
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun Warnings(warnings: List<AddCustomTokenWarning>) {
|
fun Warnings(warnings: List<AddCustomTokenError.Warning>) {
|
||||||
if (warnings.isEmpty()) return
|
if (warnings.isEmpty()) return
|
||||||
|
|
||||||
val context = LocalContext.current
|
val context = LocalContext.current
|
||||||
val warningConverter = remember { DomainErrorConverter(context) }
|
val warningConverter = remember { ModuleMessageConverter(context) }
|
||||||
|
|
||||||
Column {
|
Column {
|
||||||
warnings.forEachIndexed { index, item ->
|
warnings.forEachIndexed { index, item ->
|
||||||
|
|
@ -120,7 +118,7 @@ fun Warnings(warnings: List<AddCustomTokenWarning>) {
|
||||||
) {
|
) {
|
||||||
Text(
|
Text(
|
||||||
modifier = Modifier.padding(16.dp),
|
modifier = Modifier.padding(16.dp),
|
||||||
text = warningConverter.convertError(item),
|
text = warningConverter.convert(item),
|
||||||
color = colorResource(id = R.color.white),
|
color = colorResource(id = R.color.white),
|
||||||
fontSize = 14.sp
|
fontSize = 14.sp
|
||||||
)
|
)
|
||||||
|
|
@ -172,14 +170,14 @@ private fun AddCustomTokenFab(
|
||||||
data class ScreenFieldData(
|
data class ScreenFieldData(
|
||||||
val field: DataField<*>,
|
val field: DataField<*>,
|
||||||
val error: AddCustomTokenError?,
|
val error: AddCustomTokenError?,
|
||||||
val errorConverter: ErrorConverter<String>,
|
val errorConverter: ModuleMessageConverter,
|
||||||
val viewState: ViewStates.TokenField
|
val viewState: ViewStates.TokenField
|
||||||
) {
|
) {
|
||||||
companion object {
|
companion object {
|
||||||
fun fromState(
|
fun fromState(
|
||||||
field: DataField<*>,
|
field: DataField<*>,
|
||||||
state: AddCustomTokenState,
|
state: AddCustomTokenState,
|
||||||
errorConverter: DomainErrorConverter
|
errorConverter: ModuleMessageConverter
|
||||||
): ScreenFieldData {
|
): ScreenFieldData {
|
||||||
return ScreenFieldData(
|
return ScreenFieldData(
|
||||||
field = field,
|
field = field,
|
||||||
|
|
|
||||||
1
common/.gitignore
vendored
Normal file
1
common/.gitignore
vendored
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
/build
|
||||||
9
common/build.gradle
Normal file
9
common/build.gradle
Normal file
|
|
@ -0,0 +1,9 @@
|
||||||
|
plugins {
|
||||||
|
id 'java-library'
|
||||||
|
id 'org.jetbrains.kotlin.jvm'
|
||||||
|
}
|
||||||
|
|
||||||
|
java {
|
||||||
|
sourceCompatibility = JavaVersion.VERSION_1_8
|
||||||
|
targetCompatibility = JavaVersion.VERSION_1_8
|
||||||
|
}
|
||||||
8
common/src/main/java/com/tangem/common/Validator.kt
Normal file
8
common/src/main/java/com/tangem/common/Validator.kt
Normal file
|
|
@ -0,0 +1,8 @@
|
||||||
|
package com.tangem.common
|
||||||
|
|
||||||
|
/**
|
||||||
|
[REDACTED_AUTHOR]
|
||||||
|
*/
|
||||||
|
interface Validator<Data, Error> {
|
||||||
|
fun validate(data: Data? = null): Error?
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,14 @@
|
||||||
|
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
|
||||||
|
|
@ -0,0 +1,22 @@
|
||||||
|
package com.tangem.common.module
|
||||||
|
|
||||||
|
/**
|
||||||
|
[REDACTED_AUTHOR]
|
||||||
|
* The base object for communication between modules
|
||||||
|
*/
|
||||||
|
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?
|
||||||
|
}
|
||||||
|
|
@ -47,13 +47,14 @@ android {
|
||||||
|
|
||||||
dependencies {
|
dependencies {
|
||||||
implementation implementation(project(path: ':network'))
|
implementation implementation(project(path: ':network'))
|
||||||
|
implementation implementation(project(path: ':common'))
|
||||||
|
|
||||||
// Tangem sdk's
|
// Tangem sdk's
|
||||||
implementation 'com.tangem:blockchain:develop-71'
|
implementation 'com.tangem:blockchain:develop-71'
|
||||||
implementation 'com.tangem.tangem-sdk-kotlin:core:develop-142'
|
implementation 'com.tangem.tangem-sdk-kotlin:core:develop-142'
|
||||||
implementation 'com.tangem.tangem-sdk-kotlin:android:develop-142'
|
implementation 'com.tangem.tangem-sdk-kotlin:android:develop-142'
|
||||||
|
|
||||||
// Kotlin
|
// Kotlin coroutines
|
||||||
implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-core:1.5.2'
|
implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-core:1.5.2'
|
||||||
|
|
||||||
// State management
|
// State management
|
||||||
|
|
|
||||||
|
|
@ -1,29 +1,42 @@
|
||||||
package com.tangem.domain
|
package com.tangem.domain
|
||||||
|
|
||||||
|
import com.tangem.common.module.ModuleError
|
||||||
|
import com.tangem.common.module.ModuleMessage
|
||||||
|
|
||||||
/**
|
/**
|
||||||
[REDACTED_AUTHOR]
|
[REDACTED_AUTHOR]
|
||||||
* @property code describes what feature is the error coming from
|
* All DomainError descendants must use their own range of codes, but no more than 999 error codes for each.
|
||||||
* @property message the error description
|
|
||||||
* @property data any data that can help in the part where this error is being handled
|
|
||||||
*/
|
*/
|
||||||
interface DomainError : DomainMessage {
|
sealed interface DomainModuleMessage : ModuleMessage
|
||||||
val code: Int
|
|
||||||
val message: String
|
|
||||||
val data: Any?
|
|
||||||
}
|
|
||||||
|
|
||||||
open class AnError(
|
sealed class DomainError(
|
||||||
override val code: Int,
|
override val code: Int,
|
||||||
override val message: String,
|
override val message: String,
|
||||||
override val data: Any? = null,
|
override val data: Any?,
|
||||||
) : DomainError
|
) : DomainModuleMessage, ModuleError {
|
||||||
|
|
||||||
interface ErrorConverter<T> {
|
companion object {
|
||||||
fun convertError(error: DomainError): T
|
const val CODE_ADD_CUSTOM_TOKEN = 1000
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
interface Validator<Data, Error> {
|
sealed class AddCustomTokenError(
|
||||||
fun validate(data: Data? = null): Error?
|
subCode: Int = 0
|
||||||
}
|
) : DomainError(CODE_ADD_CUSTOM_TOKEN + subCode, this::class.java.simpleName, null) {
|
||||||
|
|
||||||
const val ERROR_CODE_ADD_CUSTOM_TOKEN = 100
|
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()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -1,17 +1,19 @@
|
||||||
package com.tangem.domain
|
package com.tangem.domain
|
||||||
|
|
||||||
|
import com.tangem.common.module.FbConsumeException
|
||||||
|
import com.tangem.common.module.ModuleException
|
||||||
|
|
||||||
/**
|
/**
|
||||||
[REDACTED_AUTHOR]
|
[REDACTED_AUTHOR]
|
||||||
* Must be handled by the module or sent to Crashlytics
|
|
||||||
*/
|
*/
|
||||||
interface DomainInternalException
|
sealed class AddCustomTokenException(override val message: String) : Throwable(message), ModuleException {
|
||||||
|
|
||||||
sealed class DomainException(message: String?) : Throwable(message), DomainInternalException {
|
data class SelectTokeNetworkException(val networkId: String) : AddCustomTokenException(
|
||||||
data class SelectTokeNetworkException(val networkId: String) : DomainException(
|
|
||||||
"Unknown network [$networkId] should not be included in the network selection dialog."
|
"Unknown network [$networkId] should not be included in the network selection dialog."
|
||||||
)
|
), FbConsumeException
|
||||||
|
|
||||||
data class UnAppropriateInitializationException(val of: String, val info: String? = null) : DomainException(
|
data class UnAppropriateInitializationException(
|
||||||
"The [$of], must be properly initialized. Info []"
|
val of: String,
|
||||||
)
|
val info: String? = null
|
||||||
|
) : AddCustomTokenException("The [$of], must be properly initialized. Info [$info]")
|
||||||
}
|
}
|
||||||
|
|
@ -1,15 +0,0 @@
|
||||||
package com.tangem.domain
|
|
||||||
|
|
||||||
/**
|
|
||||||
[REDACTED_AUTHOR]
|
|
||||||
*/
|
|
||||||
sealed interface DomainMessage
|
|
||||||
|
|
||||||
sealed interface DomainNotification : DomainMessage {
|
|
||||||
interface Toast : DomainNotification {}
|
|
||||||
|
|
||||||
interface Snackbar : DomainNotification {}
|
|
||||||
|
|
||||||
interface Dialog : DomainNotification {}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
@ -2,8 +2,8 @@ package com.tangem.domain.common.form
|
||||||
|
|
||||||
import com.tangem.blockchain.blockchains.ethereum.EthereumAddressService
|
import com.tangem.blockchain.blockchains.ethereum.EthereumAddressService
|
||||||
import com.tangem.blockchain.common.Blockchain
|
import com.tangem.blockchain.common.Blockchain
|
||||||
import com.tangem.domain.Validator
|
import com.tangem.common.Validator
|
||||||
import com.tangem.domain.features.addCustomToken.AddCustomTokenError
|
import com.tangem.domain.AddCustomTokenError
|
||||||
|
|
||||||
/**
|
/**
|
||||||
[REDACTED_AUTHOR]
|
[REDACTED_AUTHOR]
|
||||||
|
|
|
||||||
|
|
@ -1,25 +0,0 @@
|
||||||
package com.tangem.domain.features.addCustomToken
|
|
||||||
|
|
||||||
import com.tangem.domain.AnError
|
|
||||||
import com.tangem.domain.ERROR_CODE_ADD_CUSTOM_TOKEN
|
|
||||||
|
|
||||||
/**
|
|
||||||
[REDACTED_AUTHOR]
|
|
||||||
*/
|
|
||||||
sealed class AddCustomTokenError : AnError(ERROR_CODE_ADD_CUSTOM_TOKEN, "Add custom token - error") {
|
|
||||||
object FieldIsEmpty : AddCustomTokenError()
|
|
||||||
object FieldIsNotEmpty : AddCustomTokenError()
|
|
||||||
object InvalidContractAddress : AddCustomTokenError()
|
|
||||||
object NetworkIsNotSelected : AddCustomTokenError()
|
|
||||||
object InvalidDecimalsCount : AddCustomTokenError()
|
|
||||||
object InvalidDerivationPath : AddCustomTokenError()
|
|
||||||
|
|
||||||
sealed class Network : AddCustomTokenWarning() {
|
|
||||||
object CheckAddressRequestError : Network()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
sealed class AddCustomTokenWarning : AddCustomTokenError() {
|
|
||||||
object PotentialScamToken : AddCustomTokenWarning()
|
|
||||||
object TokenAlreadyAdded : AddCustomTokenWarning()
|
|
||||||
}
|
|
||||||
|
|
@ -2,11 +2,10 @@ package com.tangem.domain.features.addCustomToken.redux
|
||||||
|
|
||||||
import com.tangem.blockchain.common.Blockchain
|
import com.tangem.blockchain.common.Blockchain
|
||||||
import com.tangem.blockchain.common.DerivationStyle
|
import com.tangem.blockchain.common.DerivationStyle
|
||||||
|
import com.tangem.domain.AddCustomTokenError
|
||||||
import com.tangem.domain.DomainWrapped
|
import com.tangem.domain.DomainWrapped
|
||||||
import com.tangem.domain.common.form.Field
|
import com.tangem.domain.common.form.Field
|
||||||
import com.tangem.domain.common.form.FieldId
|
import com.tangem.domain.common.form.FieldId
|
||||||
import com.tangem.domain.features.addCustomToken.AddCustomTokenError
|
|
||||||
import com.tangem.domain.features.addCustomToken.AddCustomTokenWarning
|
|
||||||
import com.tangem.domain.features.addCustomToken.CustomCurrency
|
import com.tangem.domain.features.addCustomToken.CustomCurrency
|
||||||
import com.tangem.domain.features.addCustomToken.CustomTokenFieldId
|
import com.tangem.domain.features.addCustomToken.CustomTokenFieldId
|
||||||
import org.rekotlin.Action
|
import org.rekotlin.Action
|
||||||
|
|
@ -47,9 +46,12 @@ sealed class AddCustomTokenAction : Action {
|
||||||
|
|
||||||
// warnings
|
// warnings
|
||||||
sealed class Warning : AddCustomTokenAction() {
|
sealed class Warning : AddCustomTokenAction() {
|
||||||
data class Add(val warnings: Set<AddCustomTokenWarning>) : Warning()
|
data class Add(val warnings: Set<AddCustomTokenError.Warning>) : Warning()
|
||||||
data class Remove(val warnings: Set<AddCustomTokenWarning>) : Warning()
|
data class Remove(val warnings: Set<AddCustomTokenError.Warning>) : Warning()
|
||||||
data class Replace(val remove: Set<AddCustomTokenWarning>, val add: Set<AddCustomTokenWarning>) : Warning()
|
data class Replace(
|
||||||
|
val remove: Set<AddCustomTokenError.Warning>,
|
||||||
|
val add: Set<AddCustomTokenError.Warning>
|
||||||
|
) : Warning()
|
||||||
}
|
}
|
||||||
|
|
||||||
// To change the screenState
|
// To change the screenState
|
||||||
|
|
|
||||||
|
|
@ -5,8 +5,11 @@ import com.tangem.blockchain.common.Blockchain
|
||||||
import com.tangem.blockchain.common.DerivationStyle
|
import com.tangem.blockchain.common.DerivationStyle
|
||||||
import com.tangem.common.extensions.guard
|
import com.tangem.common.extensions.guard
|
||||||
import com.tangem.common.services.Result
|
import com.tangem.common.services.Result
|
||||||
|
import com.tangem.domain.AddCustomTokenError
|
||||||
|
import com.tangem.domain.AddCustomTokenError.Warning.PotentialScamToken
|
||||||
|
import com.tangem.domain.AddCustomTokenError.Warning.TokenAlreadyAdded
|
||||||
|
import com.tangem.domain.AddCustomTokenException
|
||||||
import com.tangem.domain.DomainDialog
|
import com.tangem.domain.DomainDialog
|
||||||
import com.tangem.domain.DomainException
|
|
||||||
import com.tangem.domain.DomainWrapped
|
import com.tangem.domain.DomainWrapped
|
||||||
import com.tangem.domain.common.TapWorkarounds.derivationStyle
|
import com.tangem.domain.common.TapWorkarounds.derivationStyle
|
||||||
import com.tangem.domain.common.extensions.fromNetworkId
|
import com.tangem.domain.common.extensions.fromNetworkId
|
||||||
|
|
@ -145,9 +148,9 @@ internal class AddCustomTokenHub : BaseStoreHub<AddCustomTokenState>("AddCustomT
|
||||||
|
|
||||||
private suspend fun updateWarningAlreadyAdded(isInAppSavedList: Boolean) {
|
private suspend fun updateWarningAlreadyAdded(isInAppSavedList: Boolean) {
|
||||||
if (isInAppSavedList) {
|
if (isInAppSavedList) {
|
||||||
AddCustomTokenWarning.TokenAlreadyAdded.add()
|
TokenAlreadyAdded.add()
|
||||||
} else {
|
} else {
|
||||||
AddCustomTokenWarning.TokenAlreadyAdded.remove()
|
TokenAlreadyAdded.remove()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -170,7 +173,7 @@ internal class AddCustomTokenHub : BaseStoreHub<AddCustomTokenState>("AddCustomT
|
||||||
val result = when (foundTokensResult) {
|
val result = when (foundTokensResult) {
|
||||||
is Result.Success -> foundTokensResult.data
|
is Result.Success -> foundTokensResult.data
|
||||||
is Result.Failure -> {
|
is Result.Failure -> {
|
||||||
// val warning = AddCustomTokenWarning.Network.CheckAddressRequestError
|
// val warning = Warning.Network.CheckAddressRequestError
|
||||||
// dispatchOnMain(Warning.Add(setOf(warning)))
|
// dispatchOnMain(Warning.Add(setOf(warning)))
|
||||||
emptyList()
|
emptyList()
|
||||||
}
|
}
|
||||||
|
|
@ -182,8 +185,8 @@ internal class AddCustomTokenHub : BaseStoreHub<AddCustomTokenState>("AddCustomT
|
||||||
private suspend fun manageFoundTokenChanges(foundTokens: List<Coins.CheckAddressResponse.Token>) {
|
private suspend fun manageFoundTokenChanges(foundTokens: List<Coins.CheckAddressResponse.Token>) {
|
||||||
if (foundTokens.isEmpty()) {
|
if (foundTokens.isEmpty()) {
|
||||||
// token not found - it's completely custom
|
// token not found - it's completely custom
|
||||||
AddCustomTokenWarning.TokenAlreadyAdded.remove()
|
TokenAlreadyAdded.remove()
|
||||||
AddCustomTokenWarning.PotentialScamToken.add()
|
PotentialScamToken.add()
|
||||||
|
|
||||||
dispatchOnMain(SetFoundTokenId(null))
|
dispatchOnMain(SetFoundTokenId(null))
|
||||||
clearTokenFields()
|
clearTokenFields()
|
||||||
|
|
@ -208,33 +211,33 @@ internal class AddCustomTokenHub : BaseStoreHub<AddCustomTokenState>("AddCustomT
|
||||||
if (isInAppSavedTokens) {
|
if (isInAppSavedTokens) {
|
||||||
lockTokenFields()
|
lockTokenFields()
|
||||||
lockAddButton()
|
lockAddButton()
|
||||||
AddCustomTokenWarning.PotentialScamToken.replace(AddCustomTokenWarning.TokenAlreadyAdded)
|
PotentialScamToken.replace(TokenAlreadyAdded)
|
||||||
} else {
|
} else {
|
||||||
// not in the saved tokens list
|
// not in the saved tokens list
|
||||||
if (singleTokenContract.active) {
|
if (singleTokenContract.active) {
|
||||||
lockTokenFields()
|
lockTokenFields()
|
||||||
unlockAddButton()
|
unlockAddButton()
|
||||||
if (hubState.derivationPathIsSelected()) {
|
if (hubState.derivationPathIsSelected()) {
|
||||||
AddCustomTokenWarning.PotentialScamToken.add()
|
PotentialScamToken.add()
|
||||||
} else {
|
} else {
|
||||||
AddCustomTokenWarning.TokenAlreadyAdded.remove()
|
TokenAlreadyAdded.remove()
|
||||||
AddCustomTokenWarning.PotentialScamToken.remove()
|
PotentialScamToken.remove()
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
unlockAddButton()
|
unlockAddButton()
|
||||||
AddCustomTokenWarning.PotentialScamToken.add()
|
PotentialScamToken.add()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
else -> {
|
else -> {
|
||||||
AddCustomTokenWarning.PotentialScamToken.replace(AddCustomTokenWarning.TokenAlreadyAdded)
|
PotentialScamToken.replace(TokenAlreadyAdded)
|
||||||
|
|
||||||
val dialog = DomainDialog.SelectTokenDialog(
|
val dialog = DomainDialog.SelectTokenDialog(
|
||||||
items = foundToken.contracts,
|
items = foundToken.contracts,
|
||||||
networkIdConverter = { networkId ->
|
networkIdConverter = { networkId ->
|
||||||
val blockchain = Blockchain.fromNetworkId(networkId)
|
val blockchain = Blockchain.fromNetworkId(networkId)
|
||||||
if (blockchain == null || blockchain == Blockchain.Unknown) {
|
if (blockchain == null || blockchain == Blockchain.Unknown) {
|
||||||
throw DomainException.SelectTokeNetworkException(networkId)
|
throw AddCustomTokenException.SelectTokeNetworkException(networkId)
|
||||||
}
|
}
|
||||||
hubState.blockchainToName(blockchain) ?: ""
|
hubState.blockchainToName(blockchain) ?: ""
|
||||||
},
|
},
|
||||||
|
|
@ -253,8 +256,8 @@ internal class AddCustomTokenHub : BaseStoreHub<AddCustomTokenState>("AddCustomT
|
||||||
}
|
}
|
||||||
|
|
||||||
private suspend fun replaceWarnings(
|
private suspend fun replaceWarnings(
|
||||||
warningsAdd: MutableSet<AddCustomTokenWarning> = mutableSetOf(),
|
warningsAdd: MutableSet<AddCustomTokenError.Warning> = mutableSetOf(),
|
||||||
warningsRemove: MutableSet<AddCustomTokenWarning> = mutableSetOf(),
|
warningsRemove: MutableSet<AddCustomTokenError.Warning> = mutableSetOf(),
|
||||||
) {
|
) {
|
||||||
if (warningsAdd.isNotEmpty() || warningsRemove.isNotEmpty()) {
|
if (warningsAdd.isNotEmpty() || warningsRemove.isNotEmpty()) {
|
||||||
dispatchOnMain(Warning.Replace(warningsRemove.toSet(), warningsAdd.toSet()))
|
dispatchOnMain(Warning.Replace(warningsRemove.toSet(), warningsAdd.toSet()))
|
||||||
|
|
@ -263,7 +266,7 @@ internal class AddCustomTokenHub : BaseStoreHub<AddCustomTokenState>("AddCustomT
|
||||||
|
|
||||||
private suspend fun updateAddButton() {
|
private suspend fun updateAddButton() {
|
||||||
val state = hubState
|
val state = hubState
|
||||||
if (state.warnings.contains(AddCustomTokenWarning.TokenAlreadyAdded)) {
|
if (state.warnings.contains(TokenAlreadyAdded)) {
|
||||||
lockAddButton()
|
lockAddButton()
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
@ -483,19 +486,19 @@ internal class AddCustomTokenHub : BaseStoreHub<AddCustomTokenState>("AddCustomT
|
||||||
dispatchOnMain(action)
|
dispatchOnMain(action)
|
||||||
}
|
}
|
||||||
|
|
||||||
private suspend fun AddCustomTokenWarning.add() {
|
private suspend fun AddCustomTokenError.Warning.add() {
|
||||||
dispatchOnMain(Warning.Add(setOf(this)))
|
dispatchOnMain(Warning.Add(setOf(this)))
|
||||||
}
|
}
|
||||||
|
|
||||||
private suspend fun AddCustomTokenWarning.remove() {
|
private suspend fun AddCustomTokenError.Warning.remove() {
|
||||||
dispatchOnMain(Warning.Remove(setOf(this)))
|
dispatchOnMain(Warning.Remove(setOf(this)))
|
||||||
}
|
}
|
||||||
|
|
||||||
private suspend fun AddCustomTokenWarning.replace(to: AddCustomTokenWarning) {
|
private suspend fun AddCustomTokenError.Warning.replace(to: AddCustomTokenError.Warning) {
|
||||||
dispatchOnMain(Warning.Replace(setOf(this), setOf(to)))
|
dispatchOnMain(Warning.Replace(setOf(this), setOf(to)))
|
||||||
}
|
}
|
||||||
|
|
||||||
// private suspend fun AddCustomTokenWarning.replace(replace: Boolean, to: AddCustomTokenWarning) {
|
// private suspend fun Warning.replace(replace: Boolean, to: Warning) {
|
||||||
// if (replace) dispatchOnMain(Warning.Replace(setOf(this), setOf(to)))
|
// if (replace) dispatchOnMain(Warning.Replace(setOf(this), setOf(to)))
|
||||||
// }
|
// }
|
||||||
|
|
||||||
|
|
@ -653,7 +656,7 @@ internal class AddCustomTokenHub : BaseStoreHub<AddCustomTokenState>("AddCustomT
|
||||||
|
|
||||||
@Throws
|
@Throws
|
||||||
private fun throwUnAppropriateInitialization(objName: String) {
|
private fun throwUnAppropriateInitialization(objName: String) {
|
||||||
throw DomainException.UnAppropriateInitializationException(
|
throw AddCustomTokenException.UnAppropriateInitializationException(
|
||||||
"AddCustomTokenHub", "$objName must be not NULL"
|
"AddCustomTokenHub", "$objName must be not NULL"
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,7 @@ package com.tangem.domain.features.addCustomToken.redux
|
||||||
|
|
||||||
import com.tangem.blockchain.common.Blockchain
|
import com.tangem.blockchain.common.Blockchain
|
||||||
import com.tangem.blockchain.common.DerivationStyle
|
import com.tangem.blockchain.common.DerivationStyle
|
||||||
|
import com.tangem.domain.AddCustomTokenError
|
||||||
import com.tangem.domain.DomainWrapped
|
import com.tangem.domain.DomainWrapped
|
||||||
import com.tangem.domain.common.form.*
|
import com.tangem.domain.common.form.*
|
||||||
import com.tangem.domain.features.addCustomToken.*
|
import com.tangem.domain.features.addCustomToken.*
|
||||||
|
|
@ -16,7 +17,7 @@ data class AddCustomTokenState(
|
||||||
val formValidators: Map<CustomTokenFieldId, CustomTokenValidator<out Any>> = createFormValidators(),
|
val formValidators: Map<CustomTokenFieldId, CustomTokenValidator<out Any>> = createFormValidators(),
|
||||||
val formErrors: Map<CustomTokenFieldId, AddCustomTokenError> = emptyMap(),
|
val formErrors: Map<CustomTokenFieldId, AddCustomTokenError> = emptyMap(),
|
||||||
val tokenId: String? = null,
|
val tokenId: String? = null,
|
||||||
val warnings: Set<AddCustomTokenWarning> = emptySet(),
|
val warnings: Set<AddCustomTokenError.Warning> = emptySet(),
|
||||||
val screenState: ScreenState = createInitialScreenState(),
|
val screenState: ScreenState = createInitialScreenState(),
|
||||||
val tangemTechServiceManager: AddCustomTokenService? = null
|
val tangemTechServiceManager: AddCustomTokenService? = null
|
||||||
) : StateType {
|
) : StateType {
|
||||||
|
|
|
||||||
|
|
@ -9,9 +9,17 @@ java {
|
||||||
}
|
}
|
||||||
|
|
||||||
dependencies {
|
dependencies {
|
||||||
|
implementation implementation(project(path: ':common'))
|
||||||
|
|
||||||
// Tangem sdk's
|
// Tangem sdk's
|
||||||
implementation 'com.tangem.tangem-sdk-kotlin:core:develop-142'
|
implementation 'com.tangem.tangem-sdk-kotlin:core:develop-142'
|
||||||
|
|
||||||
|
// Kotlin coroutines
|
||||||
|
implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-core:1.5.2'
|
||||||
|
|
||||||
|
// Logs
|
||||||
|
implementation 'com.jakewharton.timber:timber:4.7.1'
|
||||||
|
|
||||||
// Network
|
// Network
|
||||||
implementation(platform("com.squareup.okhttp3:okhttp-bom:4.9.3"))
|
implementation(platform("com.squareup.okhttp3:okhttp-bom:4.9.3"))
|
||||||
implementation("com.squareup.okhttp3:okhttp")
|
implementation("com.squareup.okhttp3:okhttp")
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,4 @@
|
||||||
include ':app'
|
include ':app'
|
||||||
include ':domain'
|
include ':domain'
|
||||||
include ':network'
|
include ':network'
|
||||||
|
include ':common'
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue