diff --git a/app/build.gradle b/app/build.gradle index 575478d236..6cc910a506 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -68,6 +68,9 @@ repositories { dependencies { implementation fileTree(include: ['*.aar'], dir: 'libs') + 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 'androidx.core:core-ktx:1.7.0' implementation 'androidx.appcompat:appcompat:1.4.1' @@ -119,6 +122,7 @@ dependencies { implementation 'com.github.salomonbrys.kotson:kotson:2.5.0' // Network and Json + //TODO: refactoring: remove it when all network services moved to the network module implementation 'com.squareup.retrofit2:retrofit:2.8.1' implementation 'com.squareup.retrofit2:converter-moshi:2.6.0' implementation 'com.squareup.moshi:moshi:1.12.0' diff --git a/app/src/main/java/com/tangem/tap/MainActivity.kt b/app/src/main/java/com/tangem/tap/MainActivity.kt index ed0df5d1f8..a663e66e7a 100644 --- a/app/src/main/java/com/tangem/tap/MainActivity.kt +++ b/app/src/main/java/com/tangem/tap/MainActivity.kt @@ -3,12 +3,12 @@ package com.tangem.tap import android.content.Intent import android.content.pm.ActivityInfo import android.os.Bundle -import android.util.Log import android.view.View import androidx.appcompat.app.AppCompatActivity import by.kirich1409.viewbindingdelegate.viewBinding import com.google.android.material.snackbar.Snackbar 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.DialogManager @@ -27,12 +27,9 @@ import com.tangem.tap.features.details.redux.walletconnect.WalletConnectAction import com.tangem.tap.features.shop.redux.ShopAction import com.tangem.wallet.R import com.tangem.wallet.databinding.ActivityMainBinding -import kotlinx.coroutines.CoroutineExceptionHandler import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Job -import java.io.PrintWriter -import java.io.StringWriter import java.lang.ref.WeakReference import kotlin.coroutines.CoroutineContext @@ -42,23 +39,13 @@ lateinit var backupService: BackupService var notificationsHandler: NotificationsHandler? = null private val coroutineContext: CoroutineContext - get() = Job() + Dispatchers.IO + initCoroutineExceptionHandler() + get() = Job() + Dispatchers.IO + FeatureCoroutineExceptionHandler.create("scope") val scope = CoroutineScope(coroutineContext) private val mainCoroutineContext: CoroutineContext - get() = Job() + Dispatchers.Main + get() = Job() + Dispatchers.Main + FeatureCoroutineExceptionHandler.create("mainScope") val mainScope = CoroutineScope(mainCoroutineContext) -private fun initCoroutineExceptionHandler(): CoroutineExceptionHandler { - return CoroutineExceptionHandler { _, throwable -> - val sw = StringWriter() - throwable.printStackTrace(PrintWriter(sw)) - val exceptionAsString: String = sw.toString() - Log.e("Coroutine", exceptionAsString) - throw throwable - } -} - class MainActivity : AppCompatActivity(), SnackbarHandler { private var snackbar: Snackbar? = null diff --git a/app/src/main/java/com/tangem/tap/TapApplication.kt b/app/src/main/java/com/tangem/tap/TapApplication.kt index 7dbd7c7b3d..e4254151fc 100644 --- a/app/src/main/java/com/tangem/tap/TapApplication.kt +++ b/app/src/main/java/com/tangem/tap/TapApplication.kt @@ -7,6 +7,7 @@ import com.google.firebase.remoteconfig.ktx.remoteConfig import com.google.firebase.remoteconfig.ktx.remoteConfigSettings import com.tangem.Log import com.tangem.blockchain.network.BlockchainSdkRetrofitBuilder +import com.tangem.network.common.MoshiConverter import com.tangem.tap.common.analytics.GlobalAnalyticsHandler import com.tangem.tap.common.images.PicassoHelper import com.tangem.tap.common.redux.AppState @@ -24,7 +25,6 @@ import com.tangem.tap.features.feedback.AdditionalEmailInfo import com.tangem.tap.features.feedback.FeedbackManager import com.tangem.tap.features.feedback.TangemLogCollector import com.tangem.tap.network.NetworkConnectivity -import com.tangem.tap.network.createMoshi import com.tangem.tap.persistence.PreferencesStorage import com.tangem.wallet.BuildConfig import org.rekotlin.Store @@ -72,7 +72,7 @@ class TapApplication : Application() { } private fun loadConfigs() { - val moshi = createMoshi() + val moshi = MoshiConverter.defaultMoshi() val localLoader = FeaturesLocalLoader(this, moshi) val remoteLoader = FeaturesRemoteLoader(moshi) val configManager = ConfigManager(localLoader, remoteLoader) diff --git a/app/src/main/java/com/tangem/tap/WaitForMigration.kt b/app/src/main/java/com/tangem/tap/WaitForMigration.kt new file mode 100644 index 0000000000..536b9711eb --- /dev/null +++ b/app/src/main/java/com/tangem/tap/WaitForMigration.kt @@ -0,0 +1,7 @@ +package com.tangem.tap + +/** +[REDACTED_AUTHOR] + */ + +val DELAY_SDK_DIALOG_CLOSE = 1400L \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/common/compose/BlockchainSpinner.kt b/app/src/main/java/com/tangem/tap/common/compose/BlockchainSpinner.kt new file mode 100644 index 0000000000..5a67842827 --- /dev/null +++ b/app/src/main/java/com/tangem/tap/common/compose/BlockchainSpinner.kt @@ -0,0 +1,31 @@ +package com.tangem.tap.common.compose + +import androidx.annotation.StringRes +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.stringResource +import com.tangem.blockchain.common.Blockchain +import com.tangem.domain.common.form.Field + +@Composable +fun BlockchainSpinner( + @StringRes title: Int, + itemList: List, + selectedItem: Field.Data, + isEnabled: Boolean = true, + textFieldConverter: (Blockchain) -> String, + dropdownItemView: @Composable ((Blockchain) -> Unit)? = null, + onItemSelected: (Blockchain) -> Unit, +) { + OutlinedSpinner( + modifier = Modifier.fillMaxWidth(), + label = stringResource(id = title), + itemList = itemList, + selectedItem = selectedItem, + textFieldConverter = textFieldConverter, + dropdownItemView = dropdownItemView, + isEnabled = isEnabled, + onItemSelected = onItemSelected + ) +} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/common/compose/Button.kt b/app/src/main/java/com/tangem/tap/common/compose/Button.kt new file mode 100644 index 0000000000..992939febe --- /dev/null +++ b/app/src/main/java/com/tangem/tap/common/compose/Button.kt @@ -0,0 +1,115 @@ +package com.tangem.tap.common.compose + +import androidx.compose.foundation.layout.* +import androidx.compose.material.* +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.text.TextStyle +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.DpSize +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import com.tangem.tap.common.compose.extensions.stringResourceDefault +import com.tangem.wallet.R + +/** +[REDACTED_AUTHOR] + */ +@Composable +fun RectangleButton( + modifier: Modifier = Modifier, + text: String = "", + textId: Int? = null, + isEnabled: Boolean = true, + contentPadding: PaddingValues = ButtonDefaults.ContentPadding, + leadingView: @Composable RowScope.() -> Unit = {}, + middleView: @Composable RowScope.() -> Unit = { TextInButton(text = text, textId = textId) }, + trailingView: @Composable RowScope.() -> Unit = {}, + onClick: () -> Unit, +) { + Button( + modifier = modifier, + contentPadding = contentPadding, + enabled = isEnabled, + onClick = onClick, + ) { + leadingView() + middleView() + trailingView() + } +} + +@Composable +private fun TextInButton( + modifier: Modifier = Modifier, + text: String = "", + textId: Int? = null, +) { + Text( + modifier = modifier, + text = stringResourceDefault(textId, text), + maxLines = 1, + style = TextStyle( + fontSize = 16.sp, + lineHeight = 20.sp, + fontWeight = FontWeight.Medium, + ) + ) +} + +@Composable +fun PasteButton( + modifier: Modifier = Modifier, + enabled: Boolean = true, + dpSize: DpSize = DpSize(40.dp, 40.dp), + onClick: () -> Unit, + content: @Composable (() -> Unit)? = null +) { + IconButton( + modifier = modifier.size(dpSize), + enabled = enabled, + onClick = onClick, + ) { + when (content) { + null -> { + val icon = if (enabled) R.drawable.ic_paste else R.drawable.ic_paste_disabled + Icon(painterResource(id = icon), contentDescription = "Paste") + } + else -> content() + } + } +} + +@Preview +@Composable +fun ButtonTest() { + Scaffold( + ) { + Column(modifier = Modifier.padding(16.dp)) { + PreviewItem("Button") { + RectangleButton(text = "Some button") {} + } + PreviewItem("PasteButton") { + PasteButton(onClick = {}) + } + } + } +} + +@Composable +fun PreviewItem( + name: String, + content: @Composable RowScope.() -> Unit, +) { + Row(verticalAlignment = Alignment.CenterVertically) { + Text( + modifier = Modifier.weight(1f), + text = name, + ) + content() + } + SpacerH8() +} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/common/compose/ComposableTextDebouncer.kt b/app/src/main/java/com/tangem/tap/common/compose/ComposableTextDebouncer.kt new file mode 100644 index 0000000000..0e9040f220 --- /dev/null +++ b/app/src/main/java/com/tangem/tap/common/compose/ComposableTextDebouncer.kt @@ -0,0 +1,24 @@ +package com.tangem.tap.common.compose + +import androidx.compose.runtime.Composable +import androidx.compose.runtime.remember +import com.tangem.domain.common.util.ValueDebouncer + +/** +[REDACTED_AUTHOR] + * This is an empty compose view. It just remember the ValueDebouncer inside of itself. + */ +@Composable +fun valueDebouncerAsState( + debounce: Long = 400, + onValueChanged: (T) -> Unit +): ValueDebouncer { + return remember { + ValueDebouncer( + debounce = debounce, + onValueChanged = { changedValue -> + changedValue?.let { onValueChanged(it) } + }, + ) + } +} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/common/compose/ComposeDialogManager.kt b/app/src/main/java/com/tangem/tap/common/compose/ComposeDialogManager.kt new file mode 100644 index 0000000000..aa0e7e71d0 --- /dev/null +++ b/app/src/main/java/com/tangem/tap/common/compose/ComposeDialogManager.kt @@ -0,0 +1,144 @@ +package com.tangem.tap.common.compose + +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.* +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.items +import androidx.compose.material.* +import androidx.compose.runtime.* +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.TextStyle +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import androidx.compose.ui.window.Dialog +import androidx.compose.ui.window.DialogProperties +import com.tangem.domain.DomainDialog +import com.tangem.domain.DomainStateDialog +import com.tangem.domain.redux.domainStore +import com.tangem.domain.redux.global.DomainGlobalAction +import com.tangem.domain.redux.global.DomainGlobalState +import com.tangem.tap.features.tokens.addCustomToken.DomainErrorConverter +import com.tangem.tap.features.tokens.addCustomToken.compose.SelectTokenNetworkDialog +import com.tangem.wallet.R +import org.rekotlin.StoreSubscriber + +@Composable +fun ComposeDialogManager() { + val dialogSate = remember { mutableStateOf(null) } + val subscriber = remember { + object : StoreSubscriber { + override fun newState(state: DomainGlobalState) { + dialogSate.value = state.dialog + } + } + } + + ShowTheDialog(dialogSate) + + LaunchedEffect(key1 = Unit, block = { + domainStore.subscribe(subscriber) { state -> + state.skipRepeats { oldState, newState -> + oldState.globalState == newState.globalState + }.select { it.globalState } + } + }) + DisposableEffect(key1 = Unit, effect = { + onDispose { domainStore.unsubscribe(subscriber) } + }) +} + +@Composable +private fun ShowTheDialog(dialogState: MutableState) { + if (dialogState.value == null) return + + val context = LocalContext.current + val errorConverter = remember { DomainErrorConverter(context) } + val onDismissRequest = { domainStore.dispatch(DomainGlobalAction.ShowDialog(null)) } + + when (val dialog = dialogState.value) { + is DomainDialog.DialogError -> ErrorDialog( + title = stringResource(id = R.string.common_error), + body = errorConverter.convertError(dialog.error), + onDismissRequest + ) + is DomainDialog.SelectTokenDialog -> SelectTokenNetworkDialog(dialog, onDismissRequest) + } +} + +/** + * Dialog with single item selection + */ +@Composable +fun SimpleDialog( + title: String, + items: List, + onSelect: (T) -> Unit, + onDismissRequest: () -> Unit, + itemContent: @Composable (T) -> Unit, +) { + Dialog( + properties = DialogProperties(false, false), + onDismissRequest = { } + ) { + Surface( + modifier = Modifier.fillMaxWidth(), + shape = MaterialTheme.shapes.medium + ) { + Column( + modifier = Modifier.padding(22.dp) + ) { + DialogTitle(title = title) + LazyColumn() { + items(items) { item -> + Row( + modifier = Modifier + .fillMaxWidth() + .height(56.dp) + .clickable { + onSelect(item) + onDismissRequest() + }, + verticalAlignment = Alignment.CenterVertically, + ) { itemContent(item) } + } + } + } + } + } +} + +@Composable +private fun DialogTitle(title: String) { + Text( + text = title, + style = LocalTextStyle.provides( + TextStyle( + fontWeight = FontWeight.Bold, + fontSize = 20.sp + ) + ).value + ) + SpacerH16() +} + +@Composable +fun ErrorDialog( + title: String, + body: String, + onDismissRequest: () -> Unit, +) { + AlertDialog( + title = { DialogTitle(title) }, + text = { Text(body) }, + onDismissRequest = onDismissRequest, + confirmButton = { + Button(onClick = onDismissRequest) { + Text(text = stringResource(id = R.string.common_ok)) + } + } + ) +} diff --git a/app/src/main/java/com/tangem/tap/common/compose/ErrorViews.kt b/app/src/main/java/com/tangem/tap/common/compose/ErrorViews.kt new file mode 100644 index 0000000000..49b6625474 --- /dev/null +++ b/app/src/main/java/com/tangem/tap/common/compose/ErrorViews.kt @@ -0,0 +1,41 @@ +package com.tangem.tap.common.compose + +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.padding +import androidx.compose.material.LocalTextStyle +import androidx.compose.material.MaterialTheme +import androidx.compose.material.Scaffold +import androidx.compose.material.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.text.TextStyle +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp + +/** +[REDACTED_AUTHOR] + */ +@Composable +fun ErrorView( + text: String, + modifier: Modifier = Modifier, + style: TextStyle = LocalTextStyle.current +) { + Text( + text, + color = MaterialTheme.colors.error, + modifier = modifier, + style = style + ) +} + +@Preview +@Composable +fun ErrorViewTest() { + Scaffold( + ) { + Box(Modifier.padding(16.dp)) { + ErrorView(text = "Some error description") + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/common/compose/KeyboardState.kt b/app/src/main/java/com/tangem/tap/common/compose/KeyboardState.kt index 2c8fc377be..2f08f3ae15 100644 --- a/app/src/main/java/com/tangem/tap/common/compose/KeyboardState.kt +++ b/app/src/main/java/com/tangem/tap/common/compose/KeyboardState.kt @@ -6,7 +6,7 @@ import androidx.compose.runtime.* import androidx.compose.ui.platform.LocalView sealed class Keyboard { - data class Opened(val height: Int): Keyboard() + data class Opened(val height: Int) : Keyboard() object Closed : Keyboard() } @@ -14,12 +14,21 @@ sealed class Keyboard { fun keyboardAsState(): State { val keyboardState: MutableState = remember { mutableStateOf(Keyboard.Closed) } val view = LocalView.current + val discrepancy = remember { + mutableStateOf(0) + } DisposableEffect(view) { val onGlobalListener = ViewTreeObserver.OnGlobalLayoutListener { + val rect = Rect() view.getWindowVisibleDisplayFrame(rect) val screenHeight = view.rootView.height - val keypadHeight = screenHeight - rect.bottom + val keypadHeight: Int = screenHeight - (rect.bottom + rect.top) - discrepancy.value + if (discrepancy.value == 0) { + discrepancy.value = keypadHeight; + if (keypadHeight == 0) discrepancy.value = 1 + } + keyboardState.value = if (keypadHeight > screenHeight * 0.15) { Keyboard.Opened(keypadHeight) } else { diff --git a/app/src/main/java/com/tangem/tap/common/compose/OutlinedSpinner.kt b/app/src/main/java/com/tangem/tap/common/compose/OutlinedSpinner.kt new file mode 100644 index 0000000000..b7eb414062 --- /dev/null +++ b/app/src/main/java/com/tangem/tap/common/compose/OutlinedSpinner.kt @@ -0,0 +1,90 @@ +package com.tangem.tap.common.compose + +import androidx.compose.material.* +import androidx.compose.runtime.Composable +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.text.TextStyle +import androidx.compose.ui.tooling.preview.Preview +import com.tangem.blockchain.common.Blockchain +import com.tangem.common.extensions.VoidCallback +import com.tangem.domain.common.form.Field +import com.tangem.tap.common.extensions.ValueCallback + +/** +[REDACTED_AUTHOR] + */ +@OptIn(ExperimentalMaterialApi::class) +@Composable +fun OutlinedSpinner( + modifier: Modifier = Modifier, + label: String, + itemList: List, + selectedItem: Field.Data, + onItemSelected: ValueCallback, + textFieldConverter: (T) -> String = { it.toString() }, + dropdownItemView: @Composable ((T) -> Unit)? = null, + isEnabled: Boolean = true, + onClose: VoidCallback = {} +) { + val rIsExpanded = remember { mutableStateOf(false) } + val rSelectedItem = remember { mutableStateOf(selectedItem.value) } + if (!selectedItem.isUserInput) { + rSelectedItem.value = selectedItem.value + } + + val onItemSelectedInternal: (T) -> Unit = { + rSelectedItem.value = it + rIsExpanded.value = false + onItemSelected(it) + } + val onDismissRequest = { + rIsExpanded.value = false + onClose() + } + + ExposedDropdownMenuBox( + expanded = rIsExpanded.value, + onExpandedChange = { rIsExpanded.value = !rIsExpanded.value }, + ) { + ProvideTextStyle(value = TextStyle(color = Color.Blue)) { + OutlinedTextField( + modifier = modifier, + readOnly = true, + enabled = isEnabled, + value = textFieldConverter(rSelectedItem.value), + onValueChange = {}, + label = { Text(label) }, + trailingIcon = { ExposedDropdownMenuDefaults.TrailingIcon(expanded = rIsExpanded.value) }, + ) + } + ExposedDropdownMenu( + expanded = rIsExpanded.value, + onDismissRequest = onDismissRequest, + ) { + itemList.forEach { item -> + DropdownMenuItem(onClick = { onItemSelectedInternal(item) }) { + when (dropdownItemView) { + null -> Text(textFieldConverter(item)) + else -> dropdownItemView(item) + } + } + } + } + } +} + +@Preview +@Composable +fun TestSpinnerPreview() { + Scaffold() { + OutlinedSpinner( + label = "Blockchain name", + itemList = listOf(Blockchain.values()), + selectedItem = Field.Data(Blockchain.Avalanche), + onItemSelected = {}, + ) + } +} diff --git a/app/src/main/java/com/tangem/tap/common/compose/OutlinedTextFieldWidget.kt b/app/src/main/java/com/tangem/tap/common/compose/OutlinedTextFieldWidget.kt new file mode 100644 index 0000000000..d0819b0817 --- /dev/null +++ b/app/src/main/java/com/tangem/tap/common/compose/OutlinedTextFieldWidget.kt @@ -0,0 +1,202 @@ +package com.tangem.tap.common.compose + +import androidx.compose.animation.* +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.text.KeyboardOptions +import androidx.compose.material.LinearProgressIndicator +import androidx.compose.material.OutlinedTextField +import androidx.compose.material.Scaffold +import androidx.compose.material.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.text.TextStyle +import androidx.compose.ui.text.input.VisualTransformation +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import com.tangem.domain.DomainError +import com.tangem.domain.ErrorConverter +import com.tangem.domain.common.form.Field +import com.tangem.tap.common.compose.extensions.stringResourceDefault + +/** +[REDACTED_AUTHOR] + */ +@Composable +fun OutlinedTextFieldWidget( + modifier: Modifier = Modifier, + textFieldData: Field.Data, + labelId: Int? = null, + label: String = "", + placeholderId: Int? = null, + placeholder: String = "", + trailingIcon: @Composable (() -> Unit)? = null, + isEnabled: Boolean = true, + isVisible: Boolean = true, + isLoading: Boolean = false, + error: DomainError? = null, + errorConverter: ErrorConverter? = null, + debounceTextChanges: Long = 400, + visualTransformation: VisualTransformation = VisualTransformation.None, + keyboardOptions: KeyboardOptions = KeyboardOptions.Default, + onTextChanged: (String) -> Unit, +) { + if (!isVisible) return + + Column( + modifier = modifier.animateContentSize(), + ) { + OutlinedProgressTextField( + modifier = modifier, + textFieldData = textFieldData, + label = stringResourceDefault(labelId, label), + placeholder = stringResourceDefault(placeholderId, placeholder), + trailingIcon = trailingIcon, + isEnabled = isEnabled, + isLoading = isLoading, + error = error, + debounce = debounceTextChanges, + visualTransformation = visualTransformation, + keyboardOptions = keyboardOptions, + onTextChanged = onTextChanged + ) + errorConverter?.let { AnimatedErrorView(error, it) } + } +} + +@Composable +private fun OutlinedProgressTextField( + modifier: Modifier = Modifier, + textFieldData: Field.Data, + label: String = "", + placeholder: String = "", + isEnabled: Boolean = true, + isLoading: Boolean = false, + error: DomainError? = null, + debounce: Long = 400, + visualTransformation: VisualTransformation = VisualTransformation.None, + keyboardOptions: KeyboardOptions = KeyboardOptions.Default, + trailingIcon: @Composable (() -> Unit)? = null, + onTextChanged: (String) -> Unit, +) { + val rTextDebouncer = valueDebouncerAsState(debounce, onTextChanged) + val rText = remember { mutableStateOf(textFieldData.value) } + + fun updateFieldValueAndEmmit(value: String) { + rText.value = value + rTextDebouncer.emmit(value) + } + // This action came from redux. Update the field value and send a new event as if from the user + if (!textFieldData.isUserInput) { + updateFieldValueAndEmmit(textFieldData.value) + } + + Box { + OutlinedTextField( + modifier = Modifier + .fillMaxWidth(), + value = rText.value, + onValueChange = ::updateFieldValueAndEmmit, + keyboardOptions = keyboardOptions, + label = { Text(label) }, + placeholder = { Text(placeholder) }, + trailingIcon = trailingIcon, + singleLine = true, + enabled = isEnabled, + isError = error != null, + visualTransformation = visualTransformation, + ) + AnimatedVisibility( + modifier = modifier + .fillMaxWidth() + .align(Alignment.BottomCenter) + .padding(start = 6.dp, top = 0.dp, end = 6.dp, bottom = 6.dp), + visible = isLoading, + ) { LinearProgressIndicator() } + } + +} + +@Composable +private fun AnimatedErrorView( + error: DomainError? = null, + errorConverter: ErrorConverter, +) { + AnimatedVisibility( + visible = error != null, + enter = fadeIn() + slideInVertically(), + exit = slideOutVertically() + fadeOut(), + ) { + error?.let { + ErrorView(errorConverter.convertError(it), style = TextStyle(fontSize = 14.sp)) + } + } +} + +@Preview +@Composable +fun OutlinedTextFieldWithErrorTest() { + val converter = remember { + object : ErrorConverter { + override fun convertError(error: DomainError): String { + return "Hello, i'am the error: ${error::class.java.simpleName}" + } + + } + } + + class SimpleError( + override val code: Int = 1, + override val message: String = "Error message", + override val data: Any? = null, + ) : DomainError + + val modifier = Modifier + .fillMaxWidth() + .padding(16.dp) + Scaffold( + ) { + Column( + ) { + OutlinedTextFieldWidget( + modifier = modifier, + textFieldData = Field.Data(""), + label = "First label", + placeholder = "1 placeholder", + error = null, + errorConverter = converter, + ) {} + OutlinedTextFieldWidget( + modifier = modifier, + textFieldData = Field.Data("First"), + label = "First label", + placeholder = "1 placeholder", + error = null, + errorConverter = converter, + ) {} + OutlinedTextFieldWidget( + modifier = modifier, + textFieldData = Field.Data("First"), + label = "First label", + placeholder = "1 placeholder", + isLoading = true, + error = null, + errorConverter = converter, + ) {} + OutlinedTextFieldWidget( + modifier = modifier, + textFieldData = Field.Data("First"), + label = "First label", + placeholder = "1 placeholder", + error = SimpleError(), + errorConverter = converter, + ) {} + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/common/compose/Spacer.kt b/app/src/main/java/com/tangem/tap/common/compose/Spacer.kt new file mode 100644 index 0000000000..b31a15c4ae --- /dev/null +++ b/app/src/main/java/com/tangem/tap/common/compose/Spacer.kt @@ -0,0 +1,91 @@ +package com.tangem.tap.common.compose + +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.width +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.unit.Dp +import androidx.compose.ui.unit.dp + +/** +[REDACTED_AUTHOR] + */ +// ***************************** Horizontal +@Composable +fun SpacerH(height: Dp, modifier: Modifier = Modifier) { + Spacer(modifier = modifier.height(height)) +} + +@Composable +fun SpacerH4(modifier: Modifier = Modifier) { + SpacerH(4.dp, modifier) +} + +@Composable +fun SpacerH8(modifier: Modifier = Modifier) { + SpacerH(8.dp, modifier) +} + +@Composable +fun SpacerH16(modifier: Modifier = Modifier) { + SpacerH(16.dp, modifier) +} + +@Composable +fun SpacerH24(modifier: Modifier = Modifier) { + SpacerH(24.dp, modifier) +} + +// ***************************** Vertical +@Composable +fun SpacerW(width: Dp, modifier: Modifier = Modifier) { + Spacer(modifier = modifier.width(width)) +} + +@Composable +fun SpacerW4(modifier: Modifier = Modifier) { + SpacerW(4.dp, modifier) +} + +@Composable +fun SpacerW8(modifier: Modifier = Modifier) { + SpacerW(8.dp, modifier) +} + +@Composable +fun SpacerW16(modifier: Modifier = Modifier) { + SpacerW(16.dp, modifier) +} + +@Composable +fun SpacerW24(modifier: Modifier = Modifier) { + SpacerW(24.dp, modifier) +} + +// ***************************** Size +@Composable +fun SpacerS(size: Dp, modifier: Modifier = Modifier) { + Spacer(modifier = modifier.size(size)) +} + +@Composable +fun SpacerS4(modifier: Modifier = Modifier) { + SpacerS(4.dp, modifier) +} + +@Composable +fun SpacerS8(modifier: Modifier = Modifier) { + SpacerS(8.dp, modifier) +} + +@Composable +fun SpacerS16(modifier: Modifier = Modifier) { + SpacerS(16.dp, modifier) +} + +@Composable +fun SpacerS24(modifier: Modifier = Modifier) { + SpacerS(24.dp, modifier) +} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/common/compose/Undefined.kt b/app/src/main/java/com/tangem/tap/common/compose/Undefined.kt new file mode 100644 index 0000000000..6cd0b70d72 --- /dev/null +++ b/app/src/main/java/com/tangem/tap/common/compose/Undefined.kt @@ -0,0 +1,27 @@ +package com.tangem.tap.common.compose + +import androidx.compose.foundation.layout.Column +import androidx.compose.material.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.unit.sp + +/** +[REDACTED_AUTHOR] + * Compose views are not typically used as a main or base view. + */ + +@Composable +fun TitleSubtitle( + title: String, + subtitle: String +) { + Column() { + Text(text = title) + Text( + text = subtitle, + fontSize = 12.sp, + color = Color.Gray + ) + } +} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/common/extensions/compose/Color.kt b/app/src/main/java/com/tangem/tap/common/compose/extensions/Color.kt similarity index 65% rename from app/src/main/java/com/tangem/tap/common/extensions/compose/Color.kt rename to app/src/main/java/com/tangem/tap/common/compose/extensions/Color.kt index d78a72ead5..d25bc9e26b 100644 --- a/app/src/main/java/com/tangem/tap/common/extensions/compose/Color.kt +++ b/app/src/main/java/com/tangem/tap/common/compose/extensions/Color.kt @@ -1,4 +1,4 @@ -package com.tangem.tap.common.extensions.compose +package com.tangem.tap.common.compose.extensions import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.toArgb @@ -10,7 +10,11 @@ import androidx.core.graphics.red /** [REDACTED_AUTHOR] */ -fun Color.argb(): Int { +fun Color.toAndroidGraphicsColor(): Int { val argb = this.toArgb() return android.graphics.Color.argb(argb.alpha, argb.red, argb.green, argb.blue) +} + +fun Color.parse(hexColor: String): Color { + return Color(hexColor.removePrefix("#").toInt(16)) } \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/common/compose/extensions/Context.kt b/app/src/main/java/com/tangem/tap/common/compose/extensions/Context.kt new file mode 100644 index 0000000000..25ae94623c --- /dev/null +++ b/app/src/main/java/com/tangem/tap/common/compose/extensions/Context.kt @@ -0,0 +1,19 @@ +package com.tangem.tap.common.compose.extensions + +import androidx.compose.runtime.Composable +import androidx.compose.ui.platform.LocalContext +import com.tangem.tap.common.extensions.copyToClipboard +import com.tangem.tap.common.extensions.getFromClipboard + +/** +[REDACTED_AUTHOR] + */ +@Composable +fun copyToClipboard(value: Any, label: String = "") { + LocalContext.current.copyToClipboard(value, label) +} + +@Composable +fun getFromClipboard(default: CharSequence? = null): CharSequence? { + return LocalContext.current.getFromClipboard(default) +} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/common/compose/extensions/Resources.kt b/app/src/main/java/com/tangem/tap/common/compose/extensions/Resources.kt new file mode 100644 index 0000000000..5b8da92e24 --- /dev/null +++ b/app/src/main/java/com/tangem/tap/common/compose/extensions/Resources.kt @@ -0,0 +1,21 @@ +package com.tangem.tap.common.compose.extensions + +import android.content.res.Resources +import androidx.annotation.StringRes +import androidx.compose.runtime.Composable +import androidx.compose.ui.platform.LocalContext + +/** +[REDACTED_AUTHOR] + */ +@Composable +fun stringResourceDefault(@StringRes id: Int?, default: String = ""): String { + val resources = LocalContext.current.resources + return try { + resources.getString(requireNotNull(id)) + } catch (ex: Resources.NotFoundException) { + default + } catch (ex: IllegalArgumentException) { + default + } +} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/common/extensions/Any.kt b/app/src/main/java/com/tangem/tap/common/extensions/Any.kt new file mode 100644 index 0000000000..3dd4357bba --- /dev/null +++ b/app/src/main/java/com/tangem/tap/common/extensions/Any.kt @@ -0,0 +1,6 @@ +package com.tangem.tap.common.extensions + +/** +[REDACTED_AUTHOR] + */ +typealias ValueCallback = (T) -> Unit \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/common/extensions/Navigation.kt b/app/src/main/java/com/tangem/tap/common/extensions/Navigation.kt index 5fdd3a9279..86d8393672 100644 --- a/app/src/main/java/com/tangem/tap/common/extensions/Navigation.kt +++ b/app/src/main/java/com/tangem/tap/common/extensions/Navigation.kt @@ -20,6 +20,7 @@ import com.tangem.tap.features.onboarding.products.twins.ui.TwinsCardsFragment import com.tangem.tap.features.onboarding.products.wallet.ui.OnboardingWalletFragment import com.tangem.tap.features.send.ui.SendFragment import com.tangem.tap.features.shop.ui.ShopFragment +import com.tangem.tap.features.tokens.addCustomToken.AddCustomTokenFragment import com.tangem.tap.features.tokens.ui.AddTokensFragment import com.tangem.tap.features.wallet.ui.WalletDetailsFragment import com.tangem.tap.features.wallet.ui.WalletFragment @@ -82,6 +83,7 @@ private fun fragmentFactory(screen: AppScreen): Fragment { AppScreen.DetailsSecurity -> DetailsSecurityFragment() AppScreen.Disclaimer -> DisclaimerFragment() AppScreen.AddTokens -> AddTokensFragment() + AppScreen.AddCustomToken -> AddCustomTokenFragment() AppScreen.WalletDetails -> WalletDetailsFragment() AppScreen.WalletConnectSessions -> WalletConnectSessionsFragment() AppScreen.QrScan -> QrScanFragment() diff --git a/app/src/main/java/com/tangem/tap/common/extensions/Picasso.kt b/app/src/main/java/com/tangem/tap/common/extensions/Picasso.kt index 4799d802ad..dbeefadbe8 100644 --- a/app/src/main/java/com/tangem/tap/common/extensions/Picasso.kt +++ b/app/src/main/java/com/tangem/tap/common/extensions/Picasso.kt @@ -9,9 +9,9 @@ import com.squareup.picasso.Transformation import com.tangem.blockchain.common.Blockchain import com.tangem.blockchain.common.IconsUtil import com.tangem.blockchain.common.Token +import com.tangem.domain.common.extensions.toNetworkId import com.tangem.tap.domain.extensions.getCustomIconUrl import com.tangem.tap.domain.tokens.getIconUrl -import com.tangem.tap.domain.tokens.toNetworkId import com.tangem.wallet.R fun Picasso.loadCurrenciesIcon( diff --git a/app/src/main/java/com/tangem/tap/common/extensions/Store.kt b/app/src/main/java/com/tangem/tap/common/extensions/Store.kt index f4b2863aac..7a0488d1a5 100644 --- a/app/src/main/java/com/tangem/tap/common/extensions/Store.kt +++ b/app/src/main/java/com/tangem/tap/common/extensions/Store.kt @@ -1,10 +1,11 @@ package com.tangem.tap.common.extensions +import com.tangem.domain.common.ScanResponse +import com.tangem.domain.common.extensions.withMainContext import com.tangem.tap.common.redux.StateDialog import com.tangem.tap.common.redux.global.GlobalAction import com.tangem.tap.common.redux.navigation.NavigationAction import com.tangem.tap.domain.TapError -import com.tangem.tap.domain.tasks.product.ScanResponse import com.tangem.tap.scope import com.tangem.tap.store import kotlinx.coroutines.Dispatchers diff --git a/app/src/main/java/com/tangem/tap/common/redux/AppState.kt b/app/src/main/java/com/tangem/tap/common/redux/AppState.kt index 719386cad9..ac955b82a7 100644 --- a/app/src/main/java/com/tangem/tap/common/redux/AppState.kt +++ b/app/src/main/java/com/tangem/tap/common/redux/AppState.kt @@ -1,5 +1,7 @@ package com.tangem.tap.common.redux +import com.tangem.domain.redux.DomainState +import com.tangem.domain.redux.domainStore import com.tangem.tap.common.redux.global.GlobalMiddleware import com.tangem.tap.common.redux.global.GlobalState import com.tangem.tap.common.redux.navigation.NavigationState @@ -49,6 +51,9 @@ data class AppState( val shopState: ShopState = ShopState(), ) : StateType { + val domainState: DomainState + get() = domainStore.state + companion object { fun getMiddleware(): List> { return listOf( diff --git a/app/src/main/java/com/tangem/tap/common/redux/global/GlobalAction.kt b/app/src/main/java/com/tangem/tap/common/redux/global/GlobalAction.kt index 58684ac334..21fd824d71 100644 --- a/app/src/main/java/com/tangem/tap/common/redux/global/GlobalAction.kt +++ b/app/src/main/java/com/tangem/tap/common/redux/global/GlobalAction.kt @@ -4,13 +4,13 @@ import com.tangem.blockchain.common.Blockchain import com.tangem.blockchain.common.WalletManager import com.tangem.common.CompletionResult import com.tangem.common.core.TangemError +import com.tangem.domain.common.ScanResponse import com.tangem.tap.common.analytics.GlobalAnalyticsHandler import com.tangem.tap.common.redux.* import com.tangem.tap.domain.TapError import com.tangem.tap.domain.configurable.config.ConfigManager import com.tangem.tap.domain.configurable.warningMessage.WarningMessage import com.tangem.tap.domain.configurable.warningMessage.WarningMessagesManager -import com.tangem.tap.domain.tasks.product.ScanResponse import com.tangem.tap.features.details.redux.SecurityOption import com.tangem.tap.features.feedback.EmailData import com.tangem.tap.features.feedback.FeedbackManager diff --git a/app/src/main/java/com/tangem/tap/common/redux/global/GlobalMidlleware.kt b/app/src/main/java/com/tangem/tap/common/redux/global/GlobalMidlleware.kt index 7675e42b03..7e1cb3f0cc 100644 --- a/app/src/main/java/com/tangem/tap/common/redux/global/GlobalMidlleware.kt +++ b/app/src/main/java/com/tangem/tap/common/redux/global/GlobalMidlleware.kt @@ -3,10 +3,12 @@ package com.tangem.tap.common.redux.global import com.tangem.common.CompletionResult import com.tangem.common.core.TangemSdkError import com.tangem.common.extensions.ifNotNull +import com.tangem.domain.common.extensions.withMainContext +import com.tangem.domain.redux.domainStore +import com.tangem.domain.redux.global.DomainGlobalAction import com.tangem.tap.* import com.tangem.tap.common.extensions.dispatchDialogShow import com.tangem.tap.common.extensions.dispatchOnMain -import com.tangem.tap.common.extensions.withMainContext import com.tangem.tap.common.redux.AppDialog import com.tangem.tap.common.redux.AppState import com.tangem.tap.domain.configurable.warningMessage.WarningMessagesManager @@ -100,10 +102,14 @@ private val globalMiddlewareHandler: Middleware = { dispatch, appState store.dispatch(GlobalAction.ScanFailsCounter.ChooseBehavior(result)) when (result) { is CompletionResult.Success -> { + domainStore.dispatch(DomainGlobalAction.SetScanResponse(result.data)) tangemSdkManager.changeDisplayedCardIdNumbersCount(result.data) action.onSuccess?.invoke(result.data) } - is CompletionResult.Failure -> action.onFailure?.invoke(result.error) + is CompletionResult.Failure -> { + domainStore.dispatch(DomainGlobalAction.SetScanResponse(null)) + action.onFailure?.invoke(result.error) + } } } } diff --git a/app/src/main/java/com/tangem/tap/common/redux/global/GlobalState.kt b/app/src/main/java/com/tangem/tap/common/redux/global/GlobalState.kt index ace0a06ae7..ef6ffeb391 100644 --- a/app/src/main/java/com/tangem/tap/common/redux/global/GlobalState.kt +++ b/app/src/main/java/com/tangem/tap/common/redux/global/GlobalState.kt @@ -1,5 +1,6 @@ package com.tangem.tap.common.redux.global +import com.tangem.domain.common.ScanResponse import com.tangem.tap.common.analytics.AnalyticsHandler import com.tangem.tap.common.entities.TapCurrency.Companion.DEFAULT_FIAT_CURRENCY import com.tangem.tap.common.redux.StateDialog @@ -7,7 +8,6 @@ import com.tangem.tap.domain.PayIdManager import com.tangem.tap.domain.TapWalletManager import com.tangem.tap.domain.configurable.config.ConfigManager import com.tangem.tap.domain.configurable.warningMessage.WarningMessagesManager -import com.tangem.tap.domain.tasks.product.ScanResponse import com.tangem.tap.features.feedback.FeedbackManager import com.tangem.tap.features.onboarding.OnboardingManager import com.tangem.tap.network.coinmarketcap.CoinMarketCapService diff --git a/app/src/main/java/com/tangem/tap/common/redux/navigation/NavigationState.kt b/app/src/main/java/com/tangem/tap/common/redux/navigation/NavigationState.kt index 6f68439f61..1fc6832148 100644 --- a/app/src/main/java/com/tangem/tap/common/redux/navigation/NavigationState.kt +++ b/app/src/main/java/com/tangem/tap/common/redux/navigation/NavigationState.kt @@ -17,7 +17,7 @@ enum class AppScreen { Wallet, WalletDetails, Send, Details, DetailsConfirm, DetailsSecurity, - AddTokens, + AddTokens, AddCustomToken, WalletConnectSessions, QrScan } \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/domain/ProductType.kt b/app/src/main/java/com/tangem/tap/domain/ProductType.kt deleted file mode 100644 index 4f57e8949d..0000000000 --- a/app/src/main/java/com/tangem/tap/domain/ProductType.kt +++ /dev/null @@ -1,9 +0,0 @@ -package com.tangem.tap.domain - -/** -[REDACTED_AUTHOR] - */ -enum class ProductType { - Note, Twins, Wallet - -} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/domain/TangemSdkManager.kt b/app/src/main/java/com/tangem/tap/domain/TangemSdkManager.kt index d4ac60c730..61dbbe7f32 100644 --- a/app/src/main/java/com/tangem/tap/domain/TangemSdkManager.kt +++ b/app/src/main/java/com/tangem/tap/domain/TangemSdkManager.kt @@ -17,6 +17,7 @@ import com.tangem.common.core.Config import com.tangem.common.core.TangemSdkError import com.tangem.common.extensions.ByteArrayKey import com.tangem.common.hdWallet.DerivationPath +import com.tangem.domain.common.ScanResponse import com.tangem.operations.CommandResponse import com.tangem.operations.derivation.DerivationTaskResponse import com.tangem.operations.derivation.DeriveMultipleWalletPublicKeysTask @@ -30,7 +31,6 @@ import com.tangem.tap.common.analytics.AnalyticsParam import com.tangem.tap.domain.tasks.CreateWalletAndRescanTask import com.tangem.tap.domain.tasks.product.ResetToFactorySettingsTask import com.tangem.tap.domain.tasks.product.ScanProductTask -import com.tangem.tap.domain.tasks.product.ScanResponse import com.tangem.tap.domain.tokens.CurrenciesRepository import com.tangem.tap.features.demo.DemoHelper import com.tangem.wallet.R diff --git a/app/src/main/java/com/tangem/tap/domain/TapWalletManager.kt b/app/src/main/java/com/tangem/tap/domain/TapWalletManager.kt index cd4dbc367f..598c125413 100644 --- a/app/src/main/java/com/tangem/tap/domain/TapWalletManager.kt +++ b/app/src/main/java/com/tangem/tap/domain/TapWalletManager.kt @@ -3,21 +3,23 @@ package com.tangem.tap.domain import com.tangem.blockchain.blockchains.solana.RentProvider import com.tangem.blockchain.common.* import com.tangem.common.services.Result +import com.tangem.domain.common.ScanResponse +import com.tangem.domain.common.TapWorkarounds.isStart2Coin +import com.tangem.domain.common.TapWorkarounds.isTestCard +import com.tangem.domain.common.TapWorkarounds.derivationStyle +import com.tangem.domain.common.extensions.withMainContext import com.tangem.tap.common.ThrottlerWithValues import com.tangem.tap.common.extensions.dispatchOnMain import com.tangem.tap.common.extensions.safeUpdate import com.tangem.tap.common.extensions.stripZeroPlainString -import com.tangem.tap.common.extensions.withMainContext import com.tangem.tap.common.redux.global.FiatCurrencyName import com.tangem.tap.common.redux.global.GlobalAction import com.tangem.tap.currenciesRepository -import com.tangem.tap.domain.TapWorkarounds.derivationStyle -import com.tangem.tap.domain.TapWorkarounds.isStart2Coin -import com.tangem.tap.domain.TapWorkarounds.isTestCard import com.tangem.tap.domain.configurable.config.ConfigManager +import com.tangem.tap.domain.extensions.isMultiwalletAllowed import com.tangem.tap.domain.extensions.makePrimaryWalletManager import com.tangem.tap.domain.extensions.makeWalletManagersForApp -import com.tangem.tap.domain.tasks.product.ScanResponse +import com.tangem.tap.domain.tokens.CardCurrencies import com.tangem.tap.domain.tokens.BlockchainNetwork import com.tangem.tap.features.demo.isDemoCard import com.tangem.tap.features.wallet.models.PendingTransactionType @@ -34,7 +36,7 @@ import java.math.BigDecimal class TapWalletManager { val walletManagerFactory: WalletManagerFactory - by lazy { WalletManagerFactory(blockchainSdkConfig) } + by lazy { WalletManagerFactory(blockchainSdkConfig) } private val coinMarketCapService = CoinMarketCapService() private val blockchainSdkConfig by lazy { diff --git a/app/src/main/java/com/tangem/tap/domain/extensions/Card.kt b/app/src/main/java/com/tangem/tap/domain/extensions/Card.kt index 124ae27387..0dc1819018 100644 --- a/app/src/main/java/com/tangem/tap/domain/extensions/Card.kt +++ b/app/src/main/java/com/tangem/tap/domain/extensions/Card.kt @@ -2,14 +2,33 @@ package com.tangem.tap.domain.extensions import com.tangem.common.card.Card import com.tangem.common.card.CardWallet +import com.tangem.common.card.EllipticCurve +import com.tangem.common.card.FirmwareVersion import com.tangem.common.extensions.toHexString import com.tangem.common.services.Result +import com.tangem.domain.common.TapWorkarounds +import com.tangem.domain.common.TapWorkarounds.isStart2Coin +import com.tangem.domain.common.TwinCardNumber +import com.tangem.domain.common.getTwinCardNumber +import com.tangem.domain.common.isTangemTwin import com.tangem.operations.attestation.CardVerifyAndGetInfo import com.tangem.operations.attestation.OnlineCardVerifier -import com.tangem.tap.domain.twins.TwinCardNumber -import com.tangem.tap.domain.twins.getTwinCardNumber import com.tangem.tap.features.wallet.redux.Artwork + +val Card.remainingSignatures: Int? + get() = this.getSingleWallet()?.remainingSignatures + +val Card.isWalletDataSupported: Boolean + get() = this.firmwareVersion.major >= 4 + +val Card.isMultiwalletAllowed: Boolean + get() { + return !isTangemTwin() && !isStart2Coin && !TapWorkarounds.isTangemNote(this) + && (firmwareVersion >= FirmwareVersion.MultiWalletAvailable || + getSingleWallet()?.curve == EllipticCurve.Secp256k1) + } + fun Card.getSingleWallet(): CardWallet? { return wallets.firstOrNull() } @@ -66,10 +85,4 @@ fun Card.getArtworkUrl(artworkId: String?): String? { cardId.startsWith(Artwork.MARTA_CARD_ID) -> Artwork.MARTA_CARD_URL else -> null } -} - -val Card.remainingSignatures: Int? - get() = this.getSingleWallet()?.remainingSignatures - -val Card.isWalletDataSupported: Boolean - get() = this.firmwareVersion.major >= 4 \ No newline at end of file +} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/domain/extensions/WalletManagerFactory.kt b/app/src/main/java/com/tangem/tap/domain/extensions/WalletManagerFactory.kt index 44df57b86f..8053bb2959 100644 --- a/app/src/main/java/com/tangem/tap/domain/extensions/WalletManagerFactory.kt +++ b/app/src/main/java/com/tangem/tap/domain/extensions/WalletManagerFactory.kt @@ -7,14 +7,16 @@ import com.tangem.common.card.EllipticCurve import com.tangem.common.extensions.hexToBytes import com.tangem.common.extensions.toMapKey import com.tangem.common.hdWallet.DerivationPath -import com.tangem.tap.domain.TapWorkarounds.isTestCard -import com.tangem.tap.domain.TapWorkarounds.useOldStyleDerivation -import com.tangem.tap.domain.tasks.product.ScanResponse +import com.tangem.domain.common.ScanResponse +import com.tangem.domain.common.TapWorkarounds.isTestCard +import com.tangem.domain.common.TapWorkarounds.useOldStyleDerivation import com.tangem.tap.domain.tokens.BlockchainNetwork import com.tangem.tap.features.wallet.redux.Currency fun WalletManagerFactory.makeWalletManagerForApp( - scanResponse: ScanResponse, blockchain: Blockchain, derivationParams: DerivationParams? + scanResponse: ScanResponse, + blockchain: Blockchain, + derivationParams: DerivationParams? ): WalletManager? { val card = scanResponse.card if (card.isTestCard && blockchain.getTestnetVersion() == null) return null @@ -31,7 +33,7 @@ fun WalletManagerFactory.makeWalletManagerForApp( scanResponse.isTangemTwins() && scanResponse.secondTwinPublicKey != null -> { makeTwinWalletManager( card.cardId, - wallet.publicKey, scanResponse.secondTwinPublicKey.hexToBytes(), + wallet.publicKey, scanResponse.secondTwinPublicKey!!.hexToBytes(), environmentBlockchain, wallet.curve ) } diff --git a/app/src/main/java/com/tangem/tap/domain/tasks/product/CreateProductWalletTask.kt b/app/src/main/java/com/tangem/tap/domain/tasks/product/CreateProductWalletTask.kt index 66a582a762..06afdcbd3c 100644 --- a/app/src/main/java/com/tangem/tap/domain/tasks/product/CreateProductWalletTask.kt +++ b/app/src/main/java/com/tangem/tap/domain/tasks/product/CreateProductWalletTask.kt @@ -9,6 +9,11 @@ import com.tangem.common.extensions.ByteArrayKey import com.tangem.common.extensions.guard import com.tangem.common.extensions.toMapKey import com.tangem.common.hdWallet.DerivationPath +import com.tangem.domain.common.KeyWalletPublicKey +import com.tangem.domain.common.ProductType +import com.tangem.domain.common.TapWorkarounds.derivationStyle +import com.tangem.domain.common.TapWorkarounds.getTangemNoteBlockchain +import com.tangem.domain.common.TapWorkarounds.isTestCard import com.tangem.operations.CommandResponse import com.tangem.operations.backup.PrimaryCard import com.tangem.operations.backup.StartPrimaryCardLinkingTask @@ -16,12 +21,7 @@ import com.tangem.operations.derivation.DeriveMultipleWalletPublicKeysTask import com.tangem.operations.derivation.ExtendedPublicKeysMap import com.tangem.operations.wallet.CreateWalletResponse import com.tangem.operations.wallet.CreateWalletTask -import com.tangem.tap.domain.ProductType -import com.tangem.tap.domain.TapWorkarounds.derivationStyle -import com.tangem.tap.domain.TapWorkarounds.getTangemNoteBlockchain -import com.tangem.tap.domain.TapWorkarounds.isTestCard import com.tangem.tap.domain.tasks.product.CreateWalletsTask -import com.tangem.tap.domain.tasks.product.KeyWalletPublicKey import com.tangem.tap.domain.tasks.product.ProductCommandProcessor import com.tangem.tap.domain.tasks.product.getCurvesForNonCreatedWallets import com.tangem.tap.features.demo.DemoHelper @@ -79,7 +79,7 @@ private class CreateWalletTangemNote : ProductCommandProcessor = mapOf(), - val primaryCard: PrimaryCard? = null -) : CommandResponse { - - fun getBlockchain(): Blockchain { - if (productType == ProductType.Note) return getTangemNoteBlockchain(card) - ?: return Blockchain.Unknown - val blockchainName: String = walletData?.blockchain ?: return Blockchain.Unknown - return Blockchain.fromId(blockchainName) - } - - fun getPrimaryToken(): Token? { - val cardToken = walletData?.token ?: return null - return Token( - cardToken.name, - cardToken.symbol, - cardToken.contractAddress, - cardToken.decimals, - ) - } - - fun isTangemNote(): Boolean = productType == ProductType.Note - fun isTangemWallet(): Boolean = productType == ProductType.Wallet - fun isTangemTwins(): Boolean = productType == ProductType.Twins - - fun supportsHdWallet(): Boolean = card.settings.isHDWalletAllowed - fun supportsBackup(): Boolean = card.settings.isBackupAllowed - - fun twinsIsTwinned(): Boolean = - card.isTangemTwins() && walletData != null && secondTwinPublicKey != null -} - -typealias KeyWalletPublicKey = ByteArrayKey - class ScanProductTask( val card: Card? = null, private val currenciesRepository: CurrenciesRepository?, @@ -134,8 +88,6 @@ class ScanProductTask( } } -private fun Card.isTangemTwins(): Boolean = TwinsHelper.getTwinCardNumber(cardId) != null - private class ScanNoteProcessor : ProductCommandProcessor { override fun proceed( card: Card, @@ -299,12 +251,12 @@ private class ScanWalletProcessor( if (!card.useOldStyleDerivation) { blockchainsToDerive.removeAll( listOf( - Blockchain.BSC, Blockchain.BSCTestnet, - Blockchain.Polygon, Blockchain.PolygonTestnet, - Blockchain.RSK, - Blockchain.Fantom, Blockchain.FantomTestnet, - Blockchain.Avalanche, Blockchain.AvalancheTestnet, - ).map { BlockchainNetwork(it, card) } + Blockchain.BSC, Blockchain.BSCTestnet, + Blockchain.Polygon, Blockchain.PolygonTestnet, + Blockchain.RSK, + Blockchain.Fantom, Blockchain.FantomTestnet, + Blockchain.Avalanche, Blockchain.AvalancheTestnet, + ).map { BlockchainNetwork(it, card) } ) } return blockchainsToDerive.distinct() diff --git a/app/src/main/java/com/tangem/tap/domain/tokens/CurrenciesRepository.kt b/app/src/main/java/com/tangem/tap/domain/tokens/CurrenciesRepository.kt index cd7fee1260..640faa30fb 100644 --- a/app/src/main/java/com/tangem/tap/domain/tokens/CurrenciesRepository.kt +++ b/app/src/main/java/com/tangem/tap/domain/tokens/CurrenciesRepository.kt @@ -12,18 +12,18 @@ import com.tangem.blockchain.common.Token import com.tangem.blockchain.common.WalletManager import com.tangem.common.card.Card import com.tangem.common.card.FirmwareVersion +import com.tangem.domain.common.TapWorkarounds.derivationStyle +import com.tangem.network.common.MoshiConverter import com.tangem.tap.common.extensions.appendIf import com.tangem.tap.common.extensions.readJsonFileToString -import com.tangem.tap.domain.TapWorkarounds.derivationStyle import com.tangem.tap.domain.extensions.setCustomIconUrl import com.tangem.tap.features.demo.DemoHelper -import com.tangem.tap.network.createMoshi import timber.log.Timber import java.util.* class CurrenciesRepository(val context: Application) { - private val moshi = createMoshi() + private val moshi = MoshiConverter.defaultMoshi() private val blockchainsAdapter: JsonAdapter> = moshi.adapter( Types.newParameterizedType(List::class.java, Blockchain::class.java) ) diff --git a/app/src/main/java/com/tangem/tap/domain/tokens/Currency.kt b/app/src/main/java/com/tangem/tap/domain/tokens/Currency.kt index be3d1b4229..128190acd2 100644 --- a/app/src/main/java/com/tangem/tap/domain/tokens/Currency.kt +++ b/app/src/main/java/com/tangem/tap/domain/tokens/Currency.kt @@ -2,6 +2,7 @@ package com.tangem.tap.domain.tokens import com.squareup.moshi.JsonClass import com.tangem.blockchain.common.Blockchain +import com.tangem.domain.common.extensions.fromNetworkId @JsonClass(generateAdapter = true) data class CurrencyFromJson( @@ -74,62 +75,4 @@ data class Contract( fun getIconUrl(id: String): String { return "https://s3.eu-central-1.amazonaws.com/tangem.api/coins/large/$id.png" -} - - -fun Blockchain.Companion.fromNetworkId(networkId: String): Blockchain? { - return when (networkId) { - "avalanche" -> Blockchain.Avalanche - "binancecoin" -> Blockchain.Binance - "binance-smart-chain" -> Blockchain.BSC - "ethereum" -> Blockchain.Ethereum - "polygon-pos" -> Blockchain.Polygon - "solana" -> Blockchain.Solana - "fantom" -> Blockchain.Fantom - "bitcoin" -> Blockchain.Bitcoin - "bitcoin-cash" -> Blockchain.BitcoinCash - "cardano" -> Blockchain.CardanoShelley - "dogecoin" -> Blockchain.Dogecoin - "ducatus" -> Blockchain.Ducatus - "litecoin" -> Blockchain.Litecoin - "rsk" -> Blockchain.RSK - "stellar" -> Blockchain.Stellar - "tezos" -> Blockchain.Tezos - "ripple" -> Blockchain.XRP - else -> null - } -} - -fun Blockchain.toNetworkId(): String { - return when (this) { - Blockchain.Unknown -> "unknown" - Blockchain.Avalanche -> "avalanche" - Blockchain.AvalancheTestnet -> "avalaunche" - Blockchain.Binance -> "binancecoin" - Blockchain.BinanceTestnet -> "binancecoin" - Blockchain.BSC -> "binance-smart-chain" - Blockchain.BSCTestnet -> "binance-smart-chain" - Blockchain.Bitcoin -> "bitcoin" - Blockchain.BitcoinTestnet -> "bitcoin" - Blockchain.BitcoinCash -> "bitcoin-cash" - Blockchain.BitcoinCashTestnet -> "bitcoin-cash" - Blockchain.Cardano -> "cardano" - Blockchain.CardanoShelley -> "cardano" - Blockchain.Dogecoin -> "dogecoin" - Blockchain.Ducatus -> "ducatus" - Blockchain.Ethereum -> "ethereum" - Blockchain.EthereumTestnet -> "ethereum" - Blockchain.Fantom -> "fantom" - Blockchain.FantomTestnet -> "fantom" - Blockchain.Litecoin -> "litecoin" - Blockchain.Polygon -> "matic-network" - Blockchain.PolygonTestnet -> "matic-networks" - Blockchain.RSK -> "rootstock" - Blockchain.Stellar -> "stellar" - Blockchain.StellarTestnet -> "stellar" - Blockchain.Solana -> "solana" - Blockchain.SolanaTestnet -> "solana" - Blockchain.Tezos -> "tezos" - Blockchain.XRP -> "ripple" - } } \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/domain/twins/CreateSecondTwinWalletTask.kt b/app/src/main/java/com/tangem/tap/domain/twins/CreateSecondTwinWalletTask.kt index f7ef809d38..85c03925ff 100644 --- a/app/src/main/java/com/tangem/tap/domain/twins/CreateSecondTwinWalletTask.kt +++ b/app/src/main/java/com/tangem/tap/domain/twins/CreateSecondTwinWalletTask.kt @@ -8,6 +8,7 @@ import com.tangem.common.core.CardSession import com.tangem.common.core.CardSessionRunnable import com.tangem.common.core.TangemError import com.tangem.common.extensions.hexToBytes +import com.tangem.domain.common.TwinsHelper import com.tangem.operations.wallet.CreateWalletResponse import com.tangem.operations.wallet.CreateWalletTask import com.tangem.operations.wallet.PurgeWalletCommand diff --git a/app/src/main/java/com/tangem/tap/domain/twins/FinalizeTwinTask.kt b/app/src/main/java/com/tangem/tap/domain/twins/FinalizeTwinTask.kt index 51ade7b4c3..73b9e2f40a 100644 --- a/app/src/main/java/com/tangem/tap/domain/twins/FinalizeTwinTask.kt +++ b/app/src/main/java/com/tangem/tap/domain/twins/FinalizeTwinTask.kt @@ -4,10 +4,10 @@ import com.tangem.common.CompletionResult import com.tangem.common.KeyPair import com.tangem.common.core.CardSession import com.tangem.common.core.CardSessionRunnable +import com.tangem.domain.common.ScanResponse import com.tangem.operations.PreflightReadMode import com.tangem.operations.PreflightReadTask import com.tangem.tap.domain.tasks.product.ScanProductTask -import com.tangem.tap.domain.tasks.product.ScanResponse class FinalizeTwinTask( private val twinPublicKey: ByteArray, private val issuerKeys: KeyPair, diff --git a/app/src/main/java/com/tangem/tap/domain/twins/TwinCardsManager.kt b/app/src/main/java/com/tangem/tap/domain/twins/TwinCardsManager.kt index a8b65e58d9..43c91a54fb 100644 --- a/app/src/main/java/com/tangem/tap/domain/twins/TwinCardsManager.kt +++ b/app/src/main/java/com/tangem/tap/domain/twins/TwinCardsManager.kt @@ -10,11 +10,11 @@ import com.tangem.common.card.Card import com.tangem.common.core.TangemSdkError import com.tangem.common.extensions.hexToBytes import com.tangem.common.extensions.toHexString +import com.tangem.domain.common.ScanResponse +import com.tangem.network.common.MoshiConverter import com.tangem.operations.wallet.CreateWalletResponse import com.tangem.tap.common.analytics.Analytics import com.tangem.tap.common.analytics.AnalyticsHandler -import com.tangem.tap.domain.tasks.product.ScanResponse -import com.tangem.tap.network.createMoshi import com.tangem.tap.tangemSdkManager class TwinCardsManager( @@ -107,7 +107,7 @@ class TwinCardsManager( } private fun getAdapter(): JsonAdapter> { - return createMoshi().adapter( + return MoshiConverter.defaultMoshi().adapter( Types.newParameterizedType(List::class.java, Issuer::class.java) ) } diff --git a/app/src/main/java/com/tangem/tap/domain/twins/TwinCardsWidget.kt b/app/src/main/java/com/tangem/tap/domain/twins/TwinCardsWidget.kt index 3a163149e5..36d3b037ff 100644 --- a/app/src/main/java/com/tangem/tap/domain/twins/TwinCardsWidget.kt +++ b/app/src/main/java/com/tangem/tap/domain/twins/TwinCardsWidget.kt @@ -6,6 +6,7 @@ import android.animation.PropertyValuesHolder import android.view.View import androidx.core.animation.doOnEnd import com.tangem.common.extensions.VoidCallback +import com.tangem.domain.common.TwinCardNumber import com.tangem.tangem_sdk_new.ui.widget.leapfrogWidget.LeapView import com.tangem.tangem_sdk_new.ui.widget.leapfrogWidget.LeapViewState import com.tangem.tangem_sdk_new.ui.widget.leapfrogWidget.LeapfrogWidget diff --git a/app/src/main/java/com/tangem/tap/domain/walletconnect/WalletConnectRepository.kt b/app/src/main/java/com/tangem/tap/domain/walletconnect/WalletConnectRepository.kt index 67dab3645d..8180023cb9 100644 --- a/app/src/main/java/com/tangem/tap/domain/walletconnect/WalletConnectRepository.kt +++ b/app/src/main/java/com/tangem/tap/domain/walletconnect/WalletConnectRepository.kt @@ -5,14 +5,14 @@ import android.content.Context import com.squareup.moshi.JsonAdapter import com.squareup.moshi.JsonClass import com.squareup.moshi.Types +import com.tangem.network.common.MoshiConverter import com.tangem.tap.features.details.redux.walletconnect.WalletConnectSession import com.tangem.tap.features.details.redux.walletconnect.WalletForSession -import com.tangem.tap.network.createMoshi import com.trustwallet.walletconnect.models.WCPeerMeta import com.trustwallet.walletconnect.models.session.WCSession class WalletConnectRepository(val context: Application) { - private val moshi = createMoshi() + private val moshi = MoshiConverter.defaultMoshi() private val walletConnectAdapter: JsonAdapter> = moshi.adapter( Types.newParameterizedType(List::class.java, SessionDao::class.java) ) diff --git a/app/src/main/java/com/tangem/tap/features/demo/DemoHelper.kt b/app/src/main/java/com/tangem/tap/features/demo/DemoHelper.kt index 8754f68655..24e8671c33 100644 --- a/app/src/main/java/com/tangem/tap/features/demo/DemoHelper.kt +++ b/app/src/main/java/com/tangem/tap/features/demo/DemoHelper.kt @@ -5,9 +5,9 @@ import com.tangem.blockchain.common.* import com.tangem.blockchain.extensions.Result import com.tangem.blockchain.extensions.SimpleResult import com.tangem.common.CompletionResult +import com.tangem.domain.common.ScanResponse import com.tangem.tap.common.extensions.dispatchNotification import com.tangem.tap.common.redux.AppState -import com.tangem.tap.domain.tasks.product.ScanResponse import com.tangem.tap.features.details.redux.walletconnect.WalletConnectAction import com.tangem.tap.features.onboarding.products.wallet.redux.BackupAction import com.tangem.tap.features.wallet.redux.WalletAction diff --git a/app/src/main/java/com/tangem/tap/features/demo/DemoMiddlewares.kt b/app/src/main/java/com/tangem/tap/features/demo/DemoMiddlewares.kt index 4d5036d62f..92551d6411 100644 --- a/app/src/main/java/com/tangem/tap/features/demo/DemoMiddlewares.kt +++ b/app/src/main/java/com/tangem/tap/features/demo/DemoMiddlewares.kt @@ -1,9 +1,9 @@ package com.tangem.tap.features.demo import com.tangem.common.extensions.guard -import com.tangem.tap.common.extensions.withMainContext +import com.tangem.domain.common.ScanResponse +import com.tangem.domain.common.extensions.withMainContext import com.tangem.tap.domain.extensions.makePrimaryWalletManager -import com.tangem.tap.domain.tasks.product.ScanResponse import com.tangem.tap.features.onboarding.products.note.redux.OnboardingNoteAction import com.tangem.tap.features.wallet.redux.Currency import com.tangem.tap.features.wallet.redux.ProgressState diff --git a/app/src/main/java/com/tangem/tap/features/demo/Extentions.kt b/app/src/main/java/com/tangem/tap/features/demo/Extentions.kt index a863282f6d..99ecfbc227 100644 --- a/app/src/main/java/com/tangem/tap/features/demo/Extentions.kt +++ b/app/src/main/java/com/tangem/tap/features/demo/Extentions.kt @@ -1,7 +1,7 @@ package com.tangem.tap.features.demo import com.tangem.blockchain.common.WalletManager -import com.tangem.tap.domain.tasks.product.ScanResponse +import com.tangem.domain.common.ScanResponse /** [REDACTED_AUTHOR] diff --git a/app/src/main/java/com/tangem/tap/features/details/redux/DetailsAction.kt b/app/src/main/java/com/tangem/tap/features/details/redux/DetailsAction.kt index 4cf2a6fde3..1c087a518a 100644 --- a/app/src/main/java/com/tangem/tap/features/details/redux/DetailsAction.kt +++ b/app/src/main/java/com/tangem/tap/features/details/redux/DetailsAction.kt @@ -2,12 +2,12 @@ package com.tangem.tap.features.details.redux import com.tangem.blockchain.common.Wallet import com.tangem.common.card.Card +import com.tangem.domain.common.ScanResponse +import com.tangem.domain.common.TwinCardNumber import com.tangem.operations.pins.CheckUserCodesResponse import com.tangem.tap.common.redux.NotificationAction import com.tangem.tap.common.redux.global.FiatCurrencyName -import com.tangem.tap.domain.tasks.product.ScanResponse import com.tangem.tap.domain.termsOfUse.CardTou -import com.tangem.tap.domain.twins.TwinCardNumber import com.tangem.tap.network.coinmarketcap.FiatCurrency import com.tangem.wallet.R import org.rekotlin.Action diff --git a/app/src/main/java/com/tangem/tap/features/details/redux/DetailsReducer.kt b/app/src/main/java/com/tangem/tap/features/details/redux/DetailsReducer.kt index 7ea35d3284..848a5f4a8e 100644 --- a/app/src/main/java/com/tangem/tap/features/details/redux/DetailsReducer.kt +++ b/app/src/main/java/com/tangem/tap/features/details/redux/DetailsReducer.kt @@ -2,12 +2,12 @@ package com.tangem.tap.features.details.redux import com.tangem.common.card.Card +import com.tangem.domain.common.TapWorkarounds.isStart2Coin +import com.tangem.domain.common.isTangemTwin import com.tangem.tap.common.redux.AppState -import com.tangem.tap.domain.TapWorkarounds.isStart2Coin import com.tangem.tap.domain.extensions.isWalletDataSupported import com.tangem.tap.domain.extensions.signedHashesCount import com.tangem.tap.domain.extensions.toSendableAmounts -import com.tangem.tap.domain.twins.isTangemTwin import com.tangem.tap.features.wallet.models.hasPendingTransactions import org.rekotlin.Action import java.util.* @@ -64,8 +64,8 @@ private fun handleEraseWallet( val card = state.scanResponse?.card val notAllowedByAnyWallet = card?.wallets?.any { it.settings.isPermanent } ?: false val notAllowedByCard = notAllowedByAnyWallet || - (card?.isWalletDataSupported == true && - (!state.scanResponse.isTangemNote() && !state.scanResponse.supportsBackup())) + (card?.isWalletDataSupported == true && + (!state.scanResponse.isTangemNote() && !state.scanResponse.supportsBackup())) val notEmpty = state.wallets.any { it.hasPendingTransactions() || it.amounts.toSendableAmounts().isNotEmpty() @@ -136,7 +136,7 @@ private fun handleSecurityAction( is DetailsAction.ManageSecurity.OpenSecurity -> { val allowedSecurityOptions = when { state.scanResponse?.card?.isStart2Coin == true || - state.scanResponse?.isTangemNote() == true -> { + state.scanResponse?.isTangemNote() == true -> { EnumSet.of(SecurityOption.LongTap) } state.scanResponse?.supportsBackup() == true -> { diff --git a/app/src/main/java/com/tangem/tap/features/details/redux/DetailsState.kt b/app/src/main/java/com/tangem/tap/features/details/redux/DetailsState.kt index 8f55ff5901..48d8ded881 100644 --- a/app/src/main/java/com/tangem/tap/features/details/redux/DetailsState.kt +++ b/app/src/main/java/com/tangem/tap/features/details/redux/DetailsState.kt @@ -2,10 +2,10 @@ package com.tangem.tap.features.details.redux import android.net.Uri import com.tangem.blockchain.common.Wallet +import com.tangem.domain.common.ScanResponse import com.tangem.tap.common.entities.Button import com.tangem.tap.common.entities.TapCurrency.Companion.DEFAULT_FIAT_CURRENCY import com.tangem.tap.common.redux.global.FiatCurrencyName -import com.tangem.tap.domain.tasks.product.ScanResponse import com.tangem.tap.features.onboarding.products.twins.redux.TwinCardsState import com.tangem.tap.network.coinmarketcap.FiatCurrency import com.tangem.tap.store diff --git a/app/src/main/java/com/tangem/tap/features/details/redux/walletconnect/WalletConnectAction.kt b/app/src/main/java/com/tangem/tap/features/details/redux/walletconnect/WalletConnectAction.kt index 30b6af3508..a180d20d4a 100644 --- a/app/src/main/java/com/tangem/tap/features/details/redux/walletconnect/WalletConnectAction.kt +++ b/app/src/main/java/com/tangem/tap/features/details/redux/walletconnect/WalletConnectAction.kt @@ -1,8 +1,8 @@ package com.tangem.tap.features.details.redux.walletconnect import android.app.Activity +import com.tangem.domain.common.ScanResponse import com.tangem.tap.common.redux.NotificationAction -import com.tangem.tap.domain.tasks.product.ScanResponse import com.tangem.wallet.R import com.trustwallet.walletconnect.models.binance.WCBinanceTradeOrder import com.trustwallet.walletconnect.models.binance.WCBinanceTransferOrder diff --git a/app/src/main/java/com/tangem/tap/features/details/redux/walletconnect/WalletConnectMiddleware.kt b/app/src/main/java/com/tangem/tap/features/details/redux/walletconnect/WalletConnectMiddleware.kt index 362689ddc4..c5a6ae1c60 100644 --- a/app/src/main/java/com/tangem/tap/features/details/redux/walletconnect/WalletConnectMiddleware.kt +++ b/app/src/main/java/com/tangem/tap/features/details/redux/walletconnect/WalletConnectMiddleware.kt @@ -4,6 +4,9 @@ import com.tangem.blockchain.common.Blockchain import com.tangem.blockchain.common.DerivationParams import com.tangem.blockchain.common.WalletManager import com.tangem.common.extensions.guard +import com.tangem.domain.common.ScanResponse +import com.tangem.domain.common.TapWorkarounds.derivationStyle +import com.tangem.domain.common.TapWorkarounds.isTestCard import com.tangem.tap.common.extensions.dispatchOnMain import com.tangem.tap.common.extensions.getFromClipboard import com.tangem.tap.common.redux.AppState @@ -11,11 +14,8 @@ import com.tangem.tap.common.redux.global.GlobalAction import com.tangem.tap.common.redux.navigation.AppScreen import com.tangem.tap.common.redux.navigation.NavigationAction import com.tangem.tap.currenciesRepository -import com.tangem.tap.domain.TapWorkarounds.derivationStyle -import com.tangem.tap.domain.TapWorkarounds.isTestCard +import com.tangem.tap.domain.extensions.isMultiwalletAllowed import com.tangem.tap.domain.extensions.makeWalletManagerForApp -import com.tangem.tap.domain.isMultiwalletAllowed -import com.tangem.tap.domain.tasks.product.ScanResponse import com.tangem.tap.domain.tokens.BlockchainNetwork import com.tangem.tap.domain.walletconnect.BnbHelper import com.tangem.tap.domain.walletconnect.WalletConnectManager diff --git a/app/src/main/java/com/tangem/tap/features/details/redux/walletconnect/WalletConnectState.kt b/app/src/main/java/com/tangem/tap/features/details/redux/walletconnect/WalletConnectState.kt index fdecb27416..0039e247ee 100644 --- a/app/src/main/java/com/tangem/tap/features/details/redux/walletconnect/WalletConnectState.kt +++ b/app/src/main/java/com/tangem/tap/features/details/redux/walletconnect/WalletConnectState.kt @@ -5,8 +5,8 @@ import com.tangem.blockchain.common.Blockchain import com.tangem.blockchain.common.TransactionData import com.tangem.blockchain.common.WalletManager import com.tangem.common.hdWallet.DerivationPath +import com.tangem.domain.common.ScanResponse import com.tangem.tap.common.redux.StateDialog -import com.tangem.tap.domain.tasks.product.ScanResponse import com.tangem.tap.features.details.ui.walletconnect.dialogs.PersonalSignDialogData import com.tangem.tap.features.details.ui.walletconnect.dialogs.TransactionRequestDialogData import com.trustwallet.walletconnect.models.WCPeerMeta diff --git a/app/src/main/java/com/tangem/tap/features/details/ui/DetailsFragment.kt b/app/src/main/java/com/tangem/tap/features/details/ui/DetailsFragment.kt index 5df5d5969b..781ba05548 100644 --- a/app/src/main/java/com/tangem/tap/features/details/ui/DetailsFragment.kt +++ b/app/src/main/java/com/tangem/tap/features/details/ui/DetailsFragment.kt @@ -7,12 +7,12 @@ import androidx.activity.OnBackPressedCallback import androidx.fragment.app.Fragment import androidx.transition.TransitionInflater import by.kirich1409.viewbindingdelegate.viewBinding +import com.tangem.domain.common.getTwinCardIdForUser import com.tangem.tap.common.extensions.show import com.tangem.tap.common.redux.global.GlobalAction import com.tangem.tap.common.redux.navigation.AppScreen import com.tangem.tap.common.redux.navigation.NavigationAction -import com.tangem.tap.domain.isMultiwalletAllowed -import com.tangem.tap.domain.twins.getTwinCardIdForUser +import com.tangem.tap.domain.extensions.isMultiwalletAllowed import com.tangem.tap.features.details.redux.DetailsAction import com.tangem.tap.features.details.redux.DetailsState import com.tangem.tap.features.details.redux.SecurityOption diff --git a/app/src/main/java/com/tangem/tap/features/feedback/FeedbackManager.kt b/app/src/main/java/com/tangem/tap/features/feedback/FeedbackManager.kt index 11680f7d46..8406eb54e6 100644 --- a/app/src/main/java/com/tangem/tap/features/feedback/FeedbackManager.kt +++ b/app/src/main/java/com/tangem/tap/features/feedback/FeedbackManager.kt @@ -7,10 +7,10 @@ import android.os.Build import com.tangem.Log import com.tangem.TangemSdkLogger import com.tangem.blockchain.common.* +import com.tangem.domain.common.ScanResponse +import com.tangem.domain.common.TapWorkarounds import com.tangem.tap.common.extensions.sendEmail import com.tangem.tap.common.extensions.stripZeroPlainString -import com.tangem.tap.domain.TapWorkarounds -import com.tangem.tap.domain.tasks.product.ScanResponse import com.tangem.wallet.R import timber.log.Timber import java.io.File diff --git a/app/src/main/java/com/tangem/tap/features/home/compose/HomeButtons.kt b/app/src/main/java/com/tangem/tap/features/home/compose/HomeButtons.kt index 040c72557f..04fb1f2647 100644 --- a/app/src/main/java/com/tangem/tap/features/home/compose/HomeButtons.kt +++ b/app/src/main/java/com/tangem/tap/features/home/compose/HomeButtons.kt @@ -1,6 +1,8 @@ package com.tangem.tap.features.home.compose -import androidx.compose.foundation.layout.* +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.height import androidx.compose.material.Button import androidx.compose.material.ButtonDefaults import androidx.compose.material.Text @@ -12,6 +14,7 @@ import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp +import com.tangem.tap.common.compose.SpacerS8 import com.tangem.wallet.R @Composable @@ -45,7 +48,7 @@ fun HomeButtons( maxLines = 1 ) } - Spacer(modifier = Modifier.size(8.dp)) + SpacerS8() Button( modifier = Modifier .weight(1f) diff --git a/app/src/main/java/com/tangem/tap/features/home/compose/Stories.kt b/app/src/main/java/com/tangem/tap/features/home/compose/Stories.kt index de06dcaa66..4bfac1e8a0 100644 --- a/app/src/main/java/com/tangem/tap/features/home/compose/Stories.kt +++ b/app/src/main/java/com/tangem/tap/features/home/compose/Stories.kt @@ -24,6 +24,7 @@ import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp +import com.tangem.tap.common.compose.SpacerS24 import androidx.compose.ui.unit.sp import com.tangem.tap.features.home.redux.HomeState import com.tangem.tap.features.wallet.redux.ProgressState @@ -117,7 +118,7 @@ fun StoriesScreen( verticalArrangement = Arrangement.Center, modifier = Modifier.fillMaxWidth() ) { - Spacer(modifier = Modifier.size(24.dp)) + SpacerS24() StoriesProgressBar( steps = steps, currentStep = currentStep.value, diff --git a/app/src/main/java/com/tangem/tap/features/home/compose/StoriesGeneralContent.kt b/app/src/main/java/com/tangem/tap/features/home/compose/StoriesGeneralContent.kt index 8d2577fc01..410a8118aa 100644 --- a/app/src/main/java/com/tangem/tap/features/home/compose/StoriesGeneralContent.kt +++ b/app/src/main/java/com/tangem/tap/features/home/compose/StoriesGeneralContent.kt @@ -22,7 +22,9 @@ import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp import androidx.compose.ui.viewinterop.AndroidView import com.tangem.tangem_sdk_new.extensions.dpToPx -import com.tangem.tap.common.extensions.compose.argb +import com.tangem.tap.common.compose.SpacerS16 +import com.tangem.tap.common.compose.SpacerS24 +import com.tangem.tap.common.compose.extensions.toAndroidGraphicsColor import com.tangem.wallet.R @Composable @@ -53,11 +55,11 @@ fun StoriesGeneralContent( textAlign = TextAlign.Center ) - Spacer(modifier = Modifier.size(16.dp)) + SpacerS16() SubtitleText(subtitleText, subtitleTextId) - Spacer(modifier = Modifier.size(25.dp)) + SpacerS24() if (imageSource != null) { Image( @@ -93,7 +95,7 @@ fun SubtitleText(subtitleText: String, subtitleTextId: Int?) { textAlignment = View.TEXT_ALIGNMENT_CENTER typeface = Typeface.DEFAULT setTextSize(TypedValue.COMPLEX_UNIT_SP, 20f) - setTextColor(color.argb()) + setTextColor(color.toAndroidGraphicsColor()) } } } diff --git a/app/src/main/java/com/tangem/tap/features/home/compose/StoriesProgressBar.kt b/app/src/main/java/com/tangem/tap/features/home/compose/StoriesProgressBar.kt index b8dd83e070..d121949529 100644 --- a/app/src/main/java/com/tangem/tap/features/home/compose/StoriesProgressBar.kt +++ b/app/src/main/java/com/tangem/tap/features/home/compose/StoriesProgressBar.kt @@ -13,6 +13,7 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp +import com.tangem.tap.common.compose.SpacerW4 @Composable fun StoriesProgressBar( @@ -67,7 +68,7 @@ fun StoriesProgressBar( ) {} } if (index != steps) { - Spacer(modifier = Modifier.width(4.dp)) + SpacerW4() } } } diff --git a/app/src/main/java/com/tangem/tap/features/home/redux/HomeMiddleware.kt b/app/src/main/java/com/tangem/tap/features/home/redux/HomeMiddleware.kt index aa5f0f8c16..e4472de543 100644 --- a/app/src/main/java/com/tangem/tap/features/home/redux/HomeMiddleware.kt +++ b/app/src/main/java/com/tangem/tap/features/home/redux/HomeMiddleware.kt @@ -1,12 +1,13 @@ package com.tangem.tap.features.home.redux +import com.tangem.domain.common.extensions.withMainContext +import com.tangem.tap.DELAY_SDK_DIALOG_CLOSE import com.tangem.tap.common.analytics.AnalyticsEvent import com.tangem.tap.common.analytics.AnalyticsParam import com.tangem.tap.common.analytics.GetCardSourceParams import com.tangem.tap.common.entities.IndeterminateProgressButton import com.tangem.tap.common.extensions.dispatchOpenUrl import com.tangem.tap.common.extensions.onCardScanned -import com.tangem.tap.common.extensions.withMainContext import com.tangem.tap.common.post import com.tangem.tap.common.postUi import com.tangem.tap.common.redux.AppState @@ -14,7 +15,6 @@ import com.tangem.tap.common.redux.global.GlobalAction import com.tangem.tap.common.redux.navigation.AppScreen import com.tangem.tap.common.redux.navigation.FragmentShareTransition import com.tangem.tap.common.redux.navigation.NavigationAction -import com.tangem.tap.domain.DELAY_SDK_DIALOG_CLOSE import com.tangem.tap.features.home.redux.HomeMiddleware.Companion.BUY_WALLET_URL import com.tangem.tap.features.onboarding.OnboardingHelper import com.tangem.tap.features.onboarding.products.twins.redux.TwinCardsAction @@ -50,7 +50,10 @@ private val homeMiddleware: Middleware = { dispatch, state -> postUi(700) { store.dispatch(HomeAction.ReadCard) } } } - is HomeAction.ReadCard -> handleReadCard() + is HomeAction.ReadCard -> { + handleReadCard() +// store.dispatch(NavigationAction.NavigateTo(AppScreen.AddCustomTokens)) + } is HomeAction.GoToShop -> { when (action.regionProvider.getRegion()?.toLowerCase()) { "ru" -> store.dispatchOpenUrl(BUY_WALLET_URL) diff --git a/app/src/main/java/com/tangem/tap/features/onboarding/OnboardingHelper.kt b/app/src/main/java/com/tangem/tap/features/onboarding/OnboardingHelper.kt index 258701cabc..7efb84deb6 100644 --- a/app/src/main/java/com/tangem/tap/features/onboarding/OnboardingHelper.kt +++ b/app/src/main/java/com/tangem/tap/features/onboarding/OnboardingHelper.kt @@ -1,9 +1,9 @@ package com.tangem.tap.features.onboarding +import com.tangem.domain.common.ProductType +import com.tangem.domain.common.ScanResponse import com.tangem.tap.common.redux.navigation.AppScreen -import com.tangem.tap.domain.ProductType import com.tangem.tap.domain.extensions.hasWallets -import com.tangem.tap.domain.tasks.product.ScanResponse import com.tangem.tap.preferencesStorage /** diff --git a/app/src/main/java/com/tangem/tap/features/onboarding/OnboardingManager.kt b/app/src/main/java/com/tangem/tap/features/onboarding/OnboardingManager.kt index 9465a39cfd..7fefd24545 100644 --- a/app/src/main/java/com/tangem/tap/features/onboarding/OnboardingManager.kt +++ b/app/src/main/java/com/tangem/tap/features/onboarding/OnboardingManager.kt @@ -5,13 +5,13 @@ import com.tangem.blockchain.common.Blockchain import com.tangem.blockchain.common.WalletManager import com.tangem.common.extensions.isZero import com.tangem.common.services.Result +import com.tangem.domain.common.ScanResponse import com.tangem.operations.attestation.CardVerifyAndGetInfo import com.tangem.operations.attestation.OnlineCardVerifier import com.tangem.tap.common.extensions.isPositive import com.tangem.tap.common.extensions.safeUpdate import com.tangem.tap.domain.TapError import com.tangem.tap.domain.extensions.getOrLoadCardArtworkUrl -import com.tangem.tap.domain.tasks.product.ScanResponse import com.tangem.tap.features.wallet.models.hasPendingTransactions import com.tangem.tap.features.wallet.redux.Currency import com.tangem.tap.features.wallet.redux.ProgressState diff --git a/app/src/main/java/com/tangem/tap/features/onboarding/products/note/redux/OnboardingNoteMiddleware.kt b/app/src/main/java/com/tangem/tap/features/onboarding/products/note/redux/OnboardingNoteMiddleware.kt index d1aac99f7a..2c854ab5a0 100644 --- a/app/src/main/java/com/tangem/tap/features/onboarding/products/note/redux/OnboardingNoteMiddleware.kt +++ b/app/src/main/java/com/tangem/tap/features/onboarding/products/note/redux/OnboardingNoteMiddleware.kt @@ -2,6 +2,8 @@ package com.tangem.tap.features.onboarding.products.note.redux import com.tangem.common.CompletionResult import com.tangem.common.extensions.guard +import com.tangem.domain.common.extensions.withMainContext +import com.tangem.tap.DELAY_SDK_DIALOG_CLOSE import com.tangem.tap.common.extensions.* import com.tangem.tap.common.postUi import com.tangem.tap.common.redux.AppDialog @@ -9,7 +11,6 @@ import com.tangem.tap.common.redux.AppState import com.tangem.tap.common.redux.global.GlobalAction import com.tangem.tap.common.redux.navigation.AppScreen import com.tangem.tap.common.redux.navigation.NavigationAction -import com.tangem.tap.domain.DELAY_SDK_DIALOG_CLOSE import com.tangem.tap.domain.TapError import com.tangem.tap.domain.extensions.hasWallets import com.tangem.tap.domain.extensions.makePrimaryWalletManager diff --git a/app/src/main/java/com/tangem/tap/features/onboarding/products/otherCards/redux/OnboardingOtherCardsMiddleware.kt b/app/src/main/java/com/tangem/tap/features/onboarding/products/otherCards/redux/OnboardingOtherCardsMiddleware.kt index 29ca1fbb3b..0f854da103 100644 --- a/app/src/main/java/com/tangem/tap/features/onboarding/products/otherCards/redux/OnboardingOtherCardsMiddleware.kt +++ b/app/src/main/java/com/tangem/tap/features/onboarding/products/otherCards/redux/OnboardingOtherCardsMiddleware.kt @@ -1,14 +1,14 @@ package com.tangem.tap.features.onboarding.products.otherCards.redux import com.tangem.common.CompletionResult +import com.tangem.domain.common.extensions.withMainContext +import com.tangem.tap.DELAY_SDK_DIALOG_CLOSE import com.tangem.tap.common.extensions.onCardScanned -import com.tangem.tap.common.extensions.withMainContext import com.tangem.tap.common.postUi import com.tangem.tap.common.redux.AppState import com.tangem.tap.common.redux.global.GlobalAction import com.tangem.tap.common.redux.navigation.AppScreen import com.tangem.tap.common.redux.navigation.NavigationAction -import com.tangem.tap.domain.DELAY_SDK_DIALOG_CLOSE import com.tangem.tap.domain.extensions.hasWallets import com.tangem.tap.scope import com.tangem.tap.store diff --git a/app/src/main/java/com/tangem/tap/features/onboarding/products/twins/redux/TwinCardsAction.kt b/app/src/main/java/com/tangem/tap/features/onboarding/products/twins/redux/TwinCardsAction.kt index 766930141a..ee4bd5ddab 100644 --- a/app/src/main/java/com/tangem/tap/features/onboarding/products/twins/redux/TwinCardsAction.kt +++ b/app/src/main/java/com/tangem/tap/features/onboarding/products/twins/redux/TwinCardsAction.kt @@ -3,9 +3,9 @@ package com.tangem.tap.features.onboarding.products.twins.redux import com.tangem.Message import com.tangem.blockchain.common.WalletManager import com.tangem.common.extensions.VoidCallback +import com.tangem.domain.common.ScanResponse import com.tangem.tap.common.redux.StateDialog import com.tangem.tap.domain.TapError -import com.tangem.tap.domain.tasks.product.ScanResponse import com.tangem.tap.domain.twins.AssetReader import com.tangem.tap.domain.twins.TwinCardsManager import com.tangem.tap.features.onboarding.OnboardingWalletBalance diff --git a/app/src/main/java/com/tangem/tap/features/onboarding/products/twins/redux/TwinCardsMiddleware.kt b/app/src/main/java/com/tangem/tap/features/onboarding/products/twins/redux/TwinCardsMiddleware.kt index 9c523d369b..3ba1648a4b 100644 --- a/app/src/main/java/com/tangem/tap/features/onboarding/products/twins/redux/TwinCardsMiddleware.kt +++ b/app/src/main/java/com/tangem/tap/features/onboarding/products/twins/redux/TwinCardsMiddleware.kt @@ -3,6 +3,9 @@ package com.tangem.tap.features.onboarding.products.twins.redux import com.tangem.blockchain.extensions.Result import com.tangem.common.CompletionResult import com.tangem.common.extensions.guard +import com.tangem.domain.common.ScanResponse +import com.tangem.domain.common.extensions.withMainContext +import com.tangem.tap.DELAY_SDK_DIALOG_CLOSE import com.tangem.tap.common.extensions.* import com.tangem.tap.common.postUi import com.tangem.tap.common.redux.AppDialog @@ -10,10 +13,8 @@ import com.tangem.tap.common.redux.AppState import com.tangem.tap.common.redux.global.GlobalAction import com.tangem.tap.common.redux.navigation.AppScreen import com.tangem.tap.common.redux.navigation.NavigationAction -import com.tangem.tap.domain.DELAY_SDK_DIALOG_CLOSE import com.tangem.tap.domain.TapError import com.tangem.tap.domain.extensions.makePrimaryWalletManager -import com.tangem.tap.domain.tasks.product.ScanResponse import com.tangem.tap.domain.twins.TwinCardsManager import com.tangem.tap.features.wallet.redux.Currency import com.tangem.tap.features.wallet.redux.ProgressState diff --git a/app/src/main/java/com/tangem/tap/features/onboarding/products/twins/redux/TwinCardsReducer.kt b/app/src/main/java/com/tangem/tap/features/onboarding/products/twins/redux/TwinCardsReducer.kt index 437213c588..d482a5a647 100644 --- a/app/src/main/java/com/tangem/tap/features/onboarding/products/twins/redux/TwinCardsReducer.kt +++ b/app/src/main/java/com/tangem/tap/features/onboarding/products/twins/redux/TwinCardsReducer.kt @@ -1,7 +1,7 @@ package com.tangem.tap.features.onboarding.products.twins.redux +import com.tangem.domain.common.getTwinCardNumber import com.tangem.tap.common.redux.AppState -import com.tangem.tap.domain.twins.getTwinCardNumber import org.rekotlin.Action class TwinCardsReducer { diff --git a/app/src/main/java/com/tangem/tap/features/onboarding/products/twins/redux/TwinCardsState.kt b/app/src/main/java/com/tangem/tap/features/onboarding/products/twins/redux/TwinCardsState.kt index eb05dd7402..fc24dbd461 100644 --- a/app/src/main/java/com/tangem/tap/features/onboarding/products/twins/redux/TwinCardsState.kt +++ b/app/src/main/java/com/tangem/tap/features/onboarding/products/twins/redux/TwinCardsState.kt @@ -1,9 +1,9 @@ package com.tangem.tap.features.onboarding.products.twins.redux import com.tangem.blockchain.common.WalletManager +import com.tangem.domain.common.TwinCardNumber import com.tangem.tap.domain.TapError import com.tangem.tap.domain.extensions.buyIsAllowed -import com.tangem.tap.domain.twins.TwinCardNumber import com.tangem.tap.domain.twins.TwinCardsManager import com.tangem.tap.features.onboarding.OnboardingWalletBalance import com.tangem.tap.store diff --git a/app/src/main/java/com/tangem/tap/features/onboarding/products/twins/ui/TwinsCardsFragment.kt b/app/src/main/java/com/tangem/tap/features/onboarding/products/twins/ui/TwinsCardsFragment.kt index b1c97b64ee..38809f8d57 100644 --- a/app/src/main/java/com/tangem/tap/features/onboarding/products/twins/ui/TwinsCardsFragment.kt +++ b/app/src/main/java/com/tangem/tap/features/onboarding/products/twins/ui/TwinsCardsFragment.kt @@ -13,6 +13,7 @@ import com.squareup.picasso.Picasso import com.tangem.Message import com.tangem.blockchain.common.Blockchain import com.tangem.common.extensions.VoidCallback +import com.tangem.domain.common.TwinCardNumber import com.tangem.tangem_sdk_new.ui.widget.leapfrogWidget.LeapfrogWidget import com.tangem.tap.common.extensions.* import com.tangem.tap.common.redux.navigation.AppScreen @@ -21,7 +22,6 @@ import com.tangem.tap.common.redux.navigation.ShareElement import com.tangem.tap.common.toggleWidget.RefreshBalanceWidget import com.tangem.tap.common.transitions.InternalNoteLayoutTransition import com.tangem.tap.domain.twins.AssetReader -import com.tangem.tap.domain.twins.TwinCardNumber import com.tangem.tap.domain.twins.TwinsCardWidget import com.tangem.tap.features.addBackPressHandler import com.tangem.tap.features.onboarding.products.BaseOnboardingFragment diff --git a/app/src/main/java/com/tangem/tap/features/onboarding/products/wallet/redux/OnboardingWalletMiddleware.kt b/app/src/main/java/com/tangem/tap/features/onboarding/products/wallet/redux/OnboardingWalletMiddleware.kt index 4004c0f96b..4050cd2b20 100644 --- a/app/src/main/java/com/tangem/tap/features/onboarding/products/wallet/redux/OnboardingWalletMiddleware.kt +++ b/app/src/main/java/com/tangem/tap/features/onboarding/products/wallet/redux/OnboardingWalletMiddleware.kt @@ -2,16 +2,16 @@ package com.tangem.tap.features.onboarding.products.wallet.redux import com.tangem.common.CompletionResult import com.tangem.common.card.Card +import com.tangem.domain.common.ScanResponse +import com.tangem.domain.common.extensions.withMainContext import com.tangem.operations.backup.BackupService import com.tangem.tap.* import com.tangem.tap.common.extensions.dispatchOnMain -import com.tangem.tap.common.extensions.withMainContext import com.tangem.tap.common.redux.AppState import com.tangem.tap.common.redux.global.GlobalAction import com.tangem.tap.common.redux.navigation.AppScreen import com.tangem.tap.common.redux.navigation.NavigationAction import com.tangem.tap.domain.extensions.hasWallets -import com.tangem.tap.domain.tasks.product.ScanResponse import com.tangem.tap.features.demo.DemoHelper import com.tangem.tap.features.home.redux.HomeAction import com.tangem.tap.features.wallet.redux.Artwork diff --git a/app/src/main/java/com/tangem/tap/features/send/redux/middlewares/SendMiddleware.kt b/app/src/main/java/com/tangem/tap/features/send/redux/middlewares/SendMiddleware.kt index 5fa42b5cb2..795e331174 100644 --- a/app/src/main/java/com/tangem/tap/features/send/redux/middlewares/SendMiddleware.kt +++ b/app/src/main/java/com/tangem/tap/features/send/redux/middlewares/SendMiddleware.kt @@ -9,6 +9,9 @@ import com.tangem.blockchain.extensions.SimpleResult import com.tangem.common.card.Card import com.tangem.common.core.TangemSdkError import com.tangem.common.services.Result +import com.tangem.domain.common.TapWorkarounds.isStart2Coin +import com.tangem.domain.common.extensions.withMainContext +import com.tangem.tap.DELAY_SDK_DIALOG_CLOSE import com.tangem.tap.common.analytics.Analytics import com.tangem.tap.common.analytics.AnalyticsEvent import com.tangem.tap.common.analytics.AnalyticsParam @@ -17,10 +20,8 @@ import com.tangem.tap.common.redux.AppDialog import com.tangem.tap.common.redux.AppState import com.tangem.tap.common.redux.global.GlobalAction import com.tangem.tap.common.redux.navigation.NavigationAction -import com.tangem.tap.domain.DELAY_SDK_DIALOG_CLOSE import com.tangem.tap.domain.TangemSigner import com.tangem.tap.domain.TapError -import com.tangem.tap.domain.TapWorkarounds.isStart2Coin import com.tangem.tap.domain.configurable.warningMessage.WarningMessage import com.tangem.tap.domain.extensions.minimalAmount import com.tangem.tap.domain.tokens.BlockchainNetwork diff --git a/app/src/main/java/com/tangem/tap/features/tokens/addCustomToken/AddCustomTokenFragment.kt b/app/src/main/java/com/tangem/tap/features/tokens/addCustomToken/AddCustomTokenFragment.kt new file mode 100644 index 0000000000..0c6e8bc2a1 --- /dev/null +++ b/app/src/main/java/com/tangem/tap/features/tokens/addCustomToken/AddCustomTokenFragment.kt @@ -0,0 +1,62 @@ +package com.tangem.tap.features.tokens.addCustomToken + +import android.os.Bundle +import android.view.View +import android.view.WindowManager +import androidx.appcompat.widget.Toolbar +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.runtime.MutableState +import androidx.compose.runtime.mutableStateOf +import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.ComposeView +import com.google.accompanist.appcompattheme.AppCompatTheme +import com.tangem.domain.features.addCustomToken.redux.AddCustomTokenState +import com.tangem.domain.redux.domainStore +import com.tangem.tap.features.BaseStoreFragment +import com.tangem.tap.features.tokens.addCustomToken.compose.AddCustomTokenScreen +import com.tangem.wallet.R +import org.rekotlin.StoreSubscriber + +/** +[REDACTED_AUTHOR] + */ +class AddCustomTokenFragment : BaseStoreFragment(R.layout.view_compose_fragment), StoreSubscriber { + + private var state: MutableState = mutableStateOf(domainStore.state.addCustomTokensState) + + override fun subscribeToStore() { + domainStore.subscribe(this) { state -> + state.skipRepeats { oldState, newState -> + oldState.addCustomTokensState == newState.addCustomTokensState + }.select { it.addCustomTokensState } + } + } + + override fun newState(state: AddCustomTokenState) { + if (activity == null || view == null) return + + this.state.value = state + } + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + + requireActivity().window.setSoftInputMode(WindowManager.LayoutParams.SOFT_INPUT_ADJUST_RESIZE); + view.findViewById(R.id.toolbar)?.let { + it.setTitle(R.string.add_custom_token_title) + } + + view.findViewById(R.id.view_compose)?.setContent { + AppCompatTheme(requireContext()) { + Box(modifier = Modifier + .fillMaxSize() + ) { + AddCustomTokenScreen(state) + } + + } + } +// addBackPressHandler(this) + } +} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/features/tokens/addCustomToken/DomainErrorConverter.kt b/app/src/main/java/com/tangem/tap/features/tokens/addCustomToken/DomainErrorConverter.kt new file mode 100644 index 0000000000..89c205afee --- /dev/null +++ b/app/src/main/java/com/tangem/tap/features/tokens/addCustomToken/DomainErrorConverter.kt @@ -0,0 +1,49 @@ +package com.tangem.tap.features.tokens.addCustomToken + +import android.content.Context +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 + +/** +[REDACTED_AUTHOR] + */ +class DomainErrorConverter( + private val context: Context +) : ErrorConverter { + + override fun convertError(error: DomainError): String { + val errorMessage = when (error) { + is AddCustomTokenError -> AddCustomTokenConverter(context).convertError(error) + else -> null + } + return errorMessage?.let { it } ?: "Unknown error: ${error::class.java.simpleName}" + } +} + +private class AddCustomTokenConverter( + private val context: Context +) : ErrorConverter { + + override fun convertError(error: DomainError): String { + val customTokenError = (error as? AddCustomTokenError) ?: throw UnsupportedOperationException() + + val rawMessage = when (customTokenError) { + AddCustomTokenWarning.PotentialScamToken -> R.string.custom_token_validation_error_not_found + AddCustomTokenWarning.TokenAlreadyAdded -> R.string.custom_token_validation_error_already_added + AddCustomTokenError.InvalidContractAddress -> R.string.custom_token_creation_error_invalid_contract_address + AddCustomTokenError.NetworkIsNotSelected -> R.string.custom_token_creation_error_network_not_selected + AddCustomTokenError.InvalidDerivationPath -> R.string.custom_token_creation_error_invalid_derivation_path + AddCustomTokenError.InvalidDecimalsCount -> R.string.custom_token_creation_error_wrong_decimals + AddCustomTokenError.FieldIsEmpty -> R.string.custom_token_creation_error_empty_fields + else -> null + } + return when (rawMessage) { + is Int -> context.getString(rawMessage) +// is String -> rawMessage + else -> "Unknown error: ${customTokenError::class.java.simpleName}" + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/features/tokens/addCustomToken/compose/AddCustomTokenScreen.kt b/app/src/main/java/com/tangem/tap/features/tokens/addCustomToken/compose/AddCustomTokenScreen.kt new file mode 100644 index 0000000000..ad66229ed4 --- /dev/null +++ b/app/src/main/java/com/tangem/tap/features/tokens/addCustomToken/compose/AddCustomTokenScreen.kt @@ -0,0 +1,205 @@ +package com.tangem.tap.features.tokens.addCustomToken.compose + +import androidx.compose.foundation.layout.* +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.material.* +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.Add +import androidx.compose.runtime.* +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.res.colorResource +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import com.tangem.domain.ErrorConverter +import com.tangem.domain.common.form.DataField +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.redux.AddCustomTokenAction +import com.tangem.domain.features.addCustomToken.redux.AddCustomTokenState +import com.tangem.domain.features.addCustomToken.redux.ScreenState +import com.tangem.domain.features.addCustomToken.redux.ViewStates +import com.tangem.domain.redux.domainStore +import com.tangem.tap.common.compose.ComposeDialogManager +import com.tangem.tap.common.compose.keyboardAsState +import com.tangem.tap.features.tokens.addCustomToken.DomainErrorConverter +import com.tangem.wallet.R + +/** +[REDACTED_AUTHOR] + */ +private class AddCustomTokenScreen {} // for simple search + +@Composable +fun AddCustomTokenScreen(state: MutableState) { + val scaffoldState = rememberScaffoldState() + + Scaffold( + scaffoldState = scaffoldState, + backgroundColor = colorResource(id = R.color.backgroundLightGray), + floatingActionButton = { + HangingOverKeyboardView(keyboardState = keyboardAsState()) { + AddButton(state) + } + }, + floatingActionButtonPosition = FabPosition.Center, + ) { + Box(Modifier.fillMaxSize()) { + LazyColumn( + contentPadding = PaddingValues(bottom = 90.dp) + ) { +// item { AddCustomTokenDebugActions() } + item { + Surface( + modifier = Modifier.padding(16.dp), + shape = MaterialTheme.shapes.small, + elevation = 4.dp, + ) { + Column( + modifier = Modifier + .fillMaxWidth() + .padding(16.dp) + ) { + FormFields(state) + } + } + } + item { Warnings(state.value.warnings.toList()) } + } + } + ComposeDialogManager() + } + + LaunchedEffect(key1 = Unit, block = { domainStore.dispatch(AddCustomTokenAction.OnCreate) }) + DisposableEffect(key1 = Unit, effect = { onDispose { domainStore.dispatch(AddCustomTokenAction.OnDestroy) } }) +} + +@Composable +private fun FormFields(state: MutableState) { + val context = LocalContext.current + val errorConverter = remember { DomainErrorConverter(context) } + + val stateValue = state.value + stateValue.form.fieldList.forEach { field -> + val data = ScreenFieldData.fromState(field, stateValue, errorConverter) + when (field.id) { + ContractAddress -> TokenContractAddressView(data) + Network -> TokenNetworkView(data, stateValue) + Name -> TokenNameView(data) + Symbol -> TokenSymbolView(data) + Decimals -> TokenDecimalsView(data) + DerivationPath -> TokenDerivationPathView(data, stateValue) + } + } +} + +@Composable +fun Warnings(warnings: List) { + if (warnings.isEmpty()) return + + val context = LocalContext.current + val warningConverter = remember { DomainErrorConverter(context) } + + Column { + warnings.forEachIndexed { index, item -> + val modifier = when (index) { + 0 -> Modifier.padding(16.dp, 16.dp, 16.dp, 0.dp) + warnings.lastIndex -> Modifier.padding(16.dp, 8.dp, 16.dp, 16.dp) + else -> Modifier.padding(16.dp, 8.dp, 16.dp, 0.dp) + } + Surface( + modifier = modifier.fillMaxWidth(), + shape = MaterialTheme.shapes.small, + color = colorResource(id = R.color.warning_warning), + elevation = 4.dp, + ) { + Text( + modifier = Modifier.padding(16.dp), + text = warningConverter.convertError(item), + color = colorResource(id = R.color.lightGray0), + fontSize = 14.sp + ) + } + } + } +} + +@Composable +private fun AddButton(state: MutableState) { + AddCustomTokenFab( + modifier = Modifier + .widthIn(210.dp, 280.dp), + isEnabled = state.value.screenState.addButton.isEnabled + ) { domainStore.dispatch(AddCustomTokenAction.OnAddCustomTokenClicked) } +} + +@Composable +fun AddCustomTokenFab( + modifier: Modifier = Modifier, + isEnabled: Boolean = true, + onClick: () -> Unit +) { + val contentColor = if (isEnabled) { + Color.White + } else { + colorResource(id = R.color.darkGray1) + } + val backgroundColor = Color(0xFF1ACE80) + + ExtendedFloatingActionButton( + modifier = modifier, + icon = { + Icon( + imageVector = Icons.Filled.Add, + tint = contentColor, + contentDescription = "Add", + ) + }, + text = { + Text( + text = stringResource(id = R.string.common_add), + ) + }, + onClick = onClick, + backgroundColor = backgroundColor, + contentColor = contentColor, + ) +} + +data class ScreenFieldData( + val field: DataField<*>, + val error: AddCustomTokenError?, + val errorConverter: ErrorConverter, + val viewState: ViewStates.TokenField +) { + companion object { + fun fromState( + field: DataField<*>, + state: AddCustomTokenState, + errorConverter: DomainErrorConverter + ): ScreenFieldData { + return ScreenFieldData( + field = field, + error = state.getError(field.id), + errorConverter = errorConverter, + viewState = selectField(field.id, state.screenState) + ) + } + + private fun selectField(id: FieldId, screenState: ScreenState): ViewStates.TokenField { + return when (id) { + ContractAddress -> screenState.contractAddressField + Network -> screenState.network + Name -> screenState.name + Symbol -> screenState.symbol + Decimals -> screenState.decimals + DerivationPath -> screenState.derivationPath + else -> throw UnsupportedOperationException() + } + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/features/tokens/addCustomToken/compose/DebugActions.kt b/app/src/main/java/com/tangem/tap/features/tokens/addCustomToken/compose/DebugActions.kt new file mode 100644 index 0000000000..18c1a5fc2e --- /dev/null +++ b/app/src/main/java/com/tangem/tap/features/tokens/addCustomToken/compose/DebugActions.kt @@ -0,0 +1,245 @@ +package com.tangem.tap.features.tokens.addCustomToken.compose + +import androidx.compose.foundation.layout.* +import androidx.compose.material.Button +import androidx.compose.material.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.ui.Modifier +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import com.tangem.blockchain.common.Blockchain +import com.tangem.common.extensions.VoidCallback +import com.tangem.common.services.Result +import com.tangem.domain.common.form.Field +import com.tangem.domain.features.addCustomToken.TangemTechServiceManager +import com.tangem.domain.features.addCustomToken.redux.AddCustomTokenAction +import com.tangem.domain.redux.domainStore +import com.tangem.network.api.tangemTech.TangemTechService +import com.tangem.wallet.BuildConfig +import timber.log.Timber + +/** +[REDACTED_AUTHOR] + */ +@Composable +fun AddCustomTokenDebugActions() { + if (!BuildConfig.DEBUG) return + + Column() { + // deep test +// ActionRow("Invalid field value") { InvalidFieldValue() } +// ActionRow("Active(true)") { ActiveTrue() } +// ActionRow("Active(false) && decimalCount != null") { ActiveFalseDecimals() } +// ActionRow("In several networks") { InSeveralNetworks() } +// ActionRow("Address not found") { UnknownContracts() } + + // test + ActionRow("All in one") { AllInOne() } + + // Any action +// ActionRow("CustomActions - find tokens active=false, decimals != null") { CustomActions() } + } +} + +@Composable +private fun AllInOne() { + // validation error + ContractAddressButton( + name = "invalid", + address = "unk" + ) + // active = true + ContractAddressButton( + name = "true", + address = "0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48" + ) + // active = false, decimalCount != null + ContractAddressButton( + name = "false", + address = "0x2147efff675e4a4ee1c2f918d181cdbd7a8e208f" + ) + // more than one network + ContractAddressButton( + name = ">1 network", + address = "0xa1faa113cbe53436df28ff0aee54275c13b40975" + ) + // unknown + ContractAddressButton( + name = "unk", + address = "0x1111111111111111112111111111111111111113" + ) +} + +@Composable +private fun InvalidFieldValue() { + ContractAddressButton( + name = "unk", + address = "unk" + ) + ContractAddressButton( + name = "someText", + address = "someText" + ) + ContractAddressButton( + name = "alskdml...fa s", + address = "alskdmlasd asdln alsdknflasd flasd fa s" + ) +} + +@Composable +private fun ActiveTrue() { + ContractAddressButton( + name = "USDC- Ethereum", + address = "0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48" + ) + ContractAddressButton( + name = "USDT- Avalanche", + address = "0xc7198437980c041c805a1edcba50c1ce5db95118" + ) + ContractAddressButton( + name = "VID- Fantom", + address = "0x922d641a426dcffaef11680e5358f34d97d112e1" + ) +} + +@Composable +private fun ActiveFalseDecimals() { + ContractAddressButton( + name = "ALPHA- avalanche", + address = "0x2147efff675e4a4ee1c2f918d181cdbd7a8e208f" + ) + ContractAddressButton( + name = "BETA- avalanche", + address = "0x511d35c52a3c244e7b8bd92c0c297755fbd89212" + ) +} + +@Composable +private fun InSeveralNetworks() { + ContractAddressButton( + name = "ALPHA- Eth,Bsc", + address = "0xa1faa113cbe53436df28ff0aee54275c13b40975" + ) + ContractAddressButton( + name = "BETA- Eth,Bsc", + address = "0xbe1a001fe942f96eea22ba08783140b9dcc09d28" + ) +} + +@Composable +private fun UnknownContracts() { + ContractAddressButton( + name = "0x11...113", + address = "0x1111111111111111112111111111111111111113" + ) + ContractAddressButton( + name = "0xc7...111", + address = "0xc7198437980c041c805a1edcba50c1ce5db95111" + ) +} + +@Composable +private fun CustomActions() { + + CustomActionButton( + name = "Find tokens in several networks", + action = { + val manager = TangemTechServiceManager(TangemTechService()) + val currencies = manager.tokens() + val asdfsd = mutableMapOf>() + val contractAddresses = currencies.mapNotNull { currency -> + currency.contracts?.map { it.address } + }.flatten() + contractAddresses.take(500).forEachIndexed() { index, address -> + when (val result = manager.checkAddress(address)) { + is Result.Success -> { + val contractList = mutableListOf() + result.data.forEach { token -> + token.contracts.forEach { contract -> + if (!contract.active && contract.decimalCount != null) { + contractList.add(contract) + } + } + } + if (contractList.isNotEmpty()) { + val list = asdfsd[address] ?: mutableListOf() + list.addAll(contractList) + asdfsd[address] = list + } + Timber.e("Success. handle $index item from size ${contractAddresses.size}. Result = ${asdfsd.size}") + } + is Result.Failure -> {} + } + } + val result = asdfsd.filter { it.value.size > 1 } + if (result.isEmpty()) return@CustomActionButton + } + ) +} + +@Composable +private fun ActionRow( + name: String, + content: @Composable () -> Unit +) { + Column() { + Text( + modifier = Modifier.padding(horizontal = 16.dp), + text = name, + fontSize = 14.sp + ) + Row( + Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.Start + ) { content() } + } + +} + +@Composable +private fun ActionButton( + name: String, + onClick: VoidCallback, +) { + Button( + modifier = Modifier.padding(horizontal = 4.dp), + onClick = onClick + ) { Text(name, fontSize = 8.sp) } +} + +@Composable +private fun ContractAddressButton( + name: String, + address: String, +) { + ActionButton(name = name) { + resetTokenValues() + domainStore.dispatch(AddCustomTokenAction.OnTokenContractAddressChanged(Field.Data(address, false))) + } +} + +@Composable +private fun CustomActionButton( + name: String, + action: suspend () -> Unit +) { + val startValue = 0 + val anyValue = remember { mutableStateOf(startValue) } + LaunchedEffect(key1 = anyValue.value, block = { + if (anyValue.value != startValue) { + action() + } + }) + + ActionButton(name) { anyValue.value = anyValue.value + 1 } +} + +private fun resetTokenValues() { + domainStore.dispatch(AddCustomTokenAction.OnTokenNetworkChanged(Field.Data(Blockchain.Unknown, false))) +// domainStore.dispatch(AddCustomTokenAction.OnTokenNameChanged(Field.Data("", false))) +// domainStore.dispatch(AddCustomTokenAction.OnTokenSymbolChanged(Field.Data("", false))) +// domainStore.dispatch(AddCustomTokenAction.OnTokenDecimalsChanged(Field.Data("", false))) +} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/features/tokens/addCustomToken/compose/FormFieldViews.kt b/app/src/main/java/com/tangem/tap/features/tokens/addCustomToken/compose/FormFieldViews.kt new file mode 100644 index 0000000000..e9207987e7 --- /dev/null +++ b/app/src/main/java/com/tangem/tap/features/tokens/addCustomToken/compose/FormFieldViews.kt @@ -0,0 +1,134 @@ +package com.tangem.tap.features.tokens.addCustomToken.compose + +import androidx.compose.foundation.text.KeyboardOptions +import androidx.compose.runtime.Composable +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.input.KeyboardType +import com.tangem.domain.common.form.Field +import com.tangem.domain.features.addCustomToken.TokenBlockchainField +import com.tangem.domain.features.addCustomToken.TokenDerivationPathField +import com.tangem.domain.features.addCustomToken.TokenField +import com.tangem.domain.features.addCustomToken.redux.AddCustomTokenAction +import com.tangem.domain.features.addCustomToken.redux.AddCustomTokenState +import com.tangem.domain.redux.domainStore +import com.tangem.tap.common.compose.BlockchainSpinner +import com.tangem.tap.common.compose.OutlinedTextFieldWidget +import com.tangem.tap.common.compose.SpacerH8 +import com.tangem.tap.common.compose.TitleSubtitle +import com.tangem.wallet.R + +/** +[REDACTED_AUTHOR] + */ +@Composable +fun TokenContractAddressView(screenFieldData: ScreenFieldData) { + if (!screenFieldData.viewState.isVisible) return + + val tokenField = screenFieldData.field as TokenField + + OutlinedTextFieldWidget( + textFieldData = tokenField.data, + labelId = R.string.custom_token_contract_address_input_title, + placeholder = "0x0000000000000000", + isEnabled = screenFieldData.viewState.isEnabled, + isLoading = screenFieldData.viewState.isLoading, + error = screenFieldData.error, + errorConverter = screenFieldData.errorConverter, + ) { + domainStore.dispatch(AddCustomTokenAction.OnTokenContractAddressChanged(Field.Data(it))) + } + SpacerH8() +} + +@Composable +fun TokenNameView(screenFieldData: ScreenFieldData) { + if (!screenFieldData.viewState.isVisible) return + + val tokenField = screenFieldData.field as TokenField + + OutlinedTextFieldWidget( + textFieldData = tokenField.data, + labelId = R.string.custom_token_name_input_title, + placeholderId = R.string.custom_token_name_input_placeholder, + isEnabled = screenFieldData.viewState.isEnabled, + error = screenFieldData.error, + errorConverter = screenFieldData.errorConverter, + ) { + domainStore.dispatch(AddCustomTokenAction.OnTokenNameChanged(Field.Data(it))) + } + SpacerH8() +} + +@Composable +fun TokenNetworkView(screenFieldData: ScreenFieldData, state: AddCustomTokenState) { + if (!screenFieldData.viewState.isVisible) return + + val notSelected = stringResource(id = R.string.custom_token_network_input_not_selected) + val networkField = screenFieldData.field as TokenBlockchainField + + BlockchainSpinner( + title = R.string.custom_token_network_input_title, + itemList = networkField.itemList, + selectedItem = networkField.data, + isEnabled = screenFieldData.viewState.isEnabled, + textFieldConverter = { state.convertBlockchainName(it, notSelected) }, + ) { domainStore.dispatch(AddCustomTokenAction.OnTokenNetworkChanged(Field.Data(it))) } + SpacerH8() +} + +@Composable +fun TokenSymbolView(screenFieldData: ScreenFieldData) { + if (!screenFieldData.viewState.isVisible) return + + val tokenField = screenFieldData.field as TokenField + + OutlinedTextFieldWidget( + textFieldData = tokenField.data, + labelId = R.string.custom_token_token_symbol_input_title, + placeholderId = R.string.custom_token_token_symbol_input_placeholder, + isEnabled = screenFieldData.viewState.isEnabled, + error = screenFieldData.error, + errorConverter = screenFieldData.errorConverter, + ) { domainStore.dispatch(AddCustomTokenAction.OnTokenSymbolChanged(Field.Data(it))) } + SpacerH8() +} + +@Composable +fun TokenDecimalsView(screenFieldData: ScreenFieldData) { + if (!screenFieldData.viewState.isVisible) return + + val tokenField = screenFieldData.field as TokenField + + OutlinedTextFieldWidget( + textFieldData = tokenField.data, + labelId = R.string.custom_token_decimals_input_title, + placeholder = "8", + isEnabled = screenFieldData.viewState.isEnabled, + error = screenFieldData.error, + errorConverter = screenFieldData.errorConverter, + keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Number), + ) { domainStore.dispatch(AddCustomTokenAction.OnTokenDecimalsChanged(Field.Data(it))) } + SpacerH8() +} + +@Composable +fun TokenDerivationPathView(screenFieldData: ScreenFieldData, state: AddCustomTokenState) { + if (!screenFieldData.viewState.isVisible) return + + val notSelected = stringResource(id = R.string.custom_token_derivation_path_default) + val networkField = screenFieldData.field as TokenDerivationPathField + + BlockchainSpinner( + title = R.string.custom_token_derivation_path_input_title, + itemList = networkField.itemList, + selectedItem = networkField.data, + isEnabled = screenFieldData.viewState.isEnabled, + textFieldConverter = { state.convertBlockchainName(it, notSelected) }, + dropdownItemView = { blockchain -> + val derivationPathLabel = state.convertDerivationPathLabel(blockchain, notSelected) + val blockchainName = state.convertBlockchainName(blockchain, notSelected) + TitleSubtitle(derivationPathLabel, blockchainName) + } + ) { domainStore.dispatch(AddCustomTokenAction.OnTokenDerivationPathChanged(Field.Data(it))) } + SpacerH8() +} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/features/tokens/addCustomToken/compose/HangingOverKeyboardView.kt b/app/src/main/java/com/tangem/tap/features/tokens/addCustomToken/compose/HangingOverKeyboardView.kt new file mode 100644 index 0000000000..46e31151a8 --- /dev/null +++ b/app/src/main/java/com/tangem/tap/features/tokens/addCustomToken/compose/HangingOverKeyboardView.kt @@ -0,0 +1,37 @@ +package com.tangem.tap.features.tokens.addCustomToken.compose + +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.BoxScope +import androidx.compose.foundation.layout.padding +import androidx.compose.runtime.Composable +import androidx.compose.runtime.State +import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.unit.Dp +import androidx.compose.ui.unit.dp +import com.tangem.tangem_sdk_new.extensions.pxToDp +import com.tangem.tap.common.compose.Keyboard + +/** +[REDACTED_AUTHOR] + */ +@Composable +fun HangingOverKeyboardView( + modifier: Modifier = Modifier, + keyboardState: State, + spaceBetweenKeyboard: Dp = 0.dp, + content: @Composable() (BoxScope.() -> Unit) +) { + + val context = LocalContext.current + val padding = when (keyboardState.value) { + Keyboard.Closed -> 0.dp + is Keyboard.Opened -> { + val keyboardHeight = (keyboardState.value as Keyboard.Opened).height + val keyboardPadding = context.pxToDp(keyboardHeight.toFloat()).dp + keyboardPadding + spaceBetweenKeyboard + } + } + + Box(modifier.padding(bottom = padding)) { content() } +} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/features/tokens/addCustomToken/compose/SelectTokenNetworkDialog.kt b/app/src/main/java/com/tangem/tap/features/tokens/addCustomToken/compose/SelectTokenNetworkDialog.kt new file mode 100644 index 0000000000..946690c107 --- /dev/null +++ b/app/src/main/java/com/tangem/tap/features/tokens/addCustomToken/compose/SelectTokenNetworkDialog.kt @@ -0,0 +1,21 @@ +package com.tangem.tap.features.tokens.addCustomToken.compose + +import androidx.compose.runtime.Composable +import androidx.compose.ui.res.stringResource +import com.tangem.domain.DomainDialog +import com.tangem.tap.common.compose.SimpleDialog +import com.tangem.tap.common.compose.TitleSubtitle +import com.tangem.wallet.R + +/** +[REDACTED_AUTHOR] + */ +@Composable +fun SelectTokenNetworkDialog(dialog: DomainDialog.SelectTokenDialog, onDismissRequest: () -> Unit) { + SimpleDialog( + title = stringResource(id = R.string.custom_token_type_network), + items = dialog.items, + onSelect = dialog.onSelect, + onDismissRequest = onDismissRequest + ) { contract -> TitleSubtitle(dialog.networkIdConverter(contract.networkId), contract.address) } +} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/features/tokens/redux/TokensAction.kt b/app/src/main/java/com/tangem/tap/features/tokens/redux/TokensAction.kt index 0dd7f91a36..e95dc24e86 100644 --- a/app/src/main/java/com/tangem/tap/features/tokens/redux/TokensAction.kt +++ b/app/src/main/java/com/tangem/tap/features/tokens/redux/TokensAction.kt @@ -2,7 +2,7 @@ package com.tangem.tap.features.tokens.redux import com.tangem.blockchain.common.Blockchain import com.tangem.blockchain.common.DerivationStyle -import com.tangem.tap.domain.tasks.product.ScanResponse +import com.tangem.domain.common.ScanResponse import com.tangem.tap.domain.tokens.Currency import com.tangem.tap.features.wallet.redux.WalletData import org.rekotlin.Action @@ -30,4 +30,6 @@ sealed class TokensAction : Action { val addedTokens: List, val addedBlockchains: List ) : TokensAction() + + object PrepareAndNavigateToAddCustomToken : TokensAction() } \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/features/tokens/redux/TokensMiddleware.kt b/app/src/main/java/com/tangem/tap/features/tokens/redux/TokensMiddleware.kt index c4a5e42d10..4ba47f905f 100644 --- a/app/src/main/java/com/tangem/tap/features/tokens/redux/TokensMiddleware.kt +++ b/app/src/main/java/com/tangem/tap/features/tokens/redux/TokensMiddleware.kt @@ -8,20 +8,25 @@ import com.tangem.common.card.EllipticCurve import com.tangem.common.extensions.ByteArrayKey import com.tangem.common.extensions.toMapKey import com.tangem.common.hdWallet.DerivationPath +import com.tangem.domain.DomainWrapped +import com.tangem.domain.common.KeyWalletPublicKey +import com.tangem.domain.common.ScanResponse +import com.tangem.domain.common.TapWorkarounds.derivationStyle +import com.tangem.domain.common.TapWorkarounds.isTestCard +import com.tangem.domain.features.addCustomToken.CompleteData +import com.tangem.domain.features.addCustomToken.redux.AddCustomTokenAction +import com.tangem.domain.features.addCustomToken.redux.AddedCurrencies +import com.tangem.domain.redux.domainStore import com.tangem.operations.derivation.ExtendedPublicKeysMap +import com.tangem.tap.* import com.tangem.tap.common.extensions.dispatchErrorNotification import com.tangem.tap.common.extensions.dispatchOnMain import com.tangem.tap.common.redux.AppState import com.tangem.tap.common.redux.global.GlobalAction +import com.tangem.tap.common.redux.navigation.AppScreen import com.tangem.tap.common.redux.navigation.NavigationAction -import com.tangem.tap.currenciesRepository -import com.tangem.tap.domain.DELAY_SDK_DIALOG_CLOSE import com.tangem.tap.domain.TapError -import com.tangem.tap.domain.TapWorkarounds.derivationStyle -import com.tangem.tap.domain.TapWorkarounds.isTestCard import com.tangem.tap.domain.extensions.makeWalletManagerForApp -import com.tangem.tap.domain.tasks.product.KeyWalletPublicKey -import com.tangem.tap.domain.tasks.product.ScanResponse import com.tangem.tap.domain.tokens.BlockchainNetwork import com.tangem.tap.features.wallet.redux.WalletAction import com.tangem.tap.scope @@ -31,6 +36,7 @@ import kotlinx.coroutines.async import kotlinx.coroutines.delay import kotlinx.coroutines.launch import org.rekotlin.Middleware +import timber.log.Timber class TokensMiddleware { @@ -40,6 +46,7 @@ class TokensMiddleware { when (action) { is TokensAction.LoadCurrencies -> handleLoadCurrencies(action) is TokensAction.SaveChanges -> handleSaveChanges(action) + is TokensAction.PrepareAndNavigateToAddCustomToken -> handleAddingCustomToken(action) } next(action) } @@ -88,6 +95,22 @@ class TokensMiddleware { } } + private fun handleAddingCustomToken(action: TokensAction.PrepareAndNavigateToAddCustomToken) { + val tokensState = store.state.tokensState + val addedTokensList = tokensState.addedTokens.map { + DomainWrapped.TokenWithBlockchain(it.token.copy(), it.blockchain) + } + val addedBlockchains = tokensState.addedBlockchains.map { it } + val addedCurrencies = AddedCurrencies(addedTokensList, addedBlockchains) + domainStore.dispatch(AddCustomTokenAction.Init.SetAddedCurrencies(addedCurrencies)) + + val callback = fun(data: CompleteData) { + Timber.e("Yoooohhhoooo") + } + domainStore.dispatch(AddCustomTokenAction.Init.SetOnAddTokenCallback(callback)) + store.dispatch(NavigationAction.NavigateTo(AppScreen.AddCustomToken)) + } + private fun deriveMissingBlockchains( scanResponse: ScanResponse, blockchains: List, diff --git a/app/src/main/java/com/tangem/tap/features/tokens/redux/TokensState.kt b/app/src/main/java/com/tangem/tap/features/tokens/redux/TokensState.kt index d328af9358..7fc19bf751 100644 --- a/app/src/main/java/com/tangem/tap/features/tokens/redux/TokensState.kt +++ b/app/src/main/java/com/tangem/tap/features/tokens/redux/TokensState.kt @@ -3,9 +3,9 @@ package com.tangem.tap.features.tokens.redux import com.tangem.blockchain.common.Blockchain import com.tangem.blockchain.common.DerivationStyle import com.tangem.blockchain.common.Token -import com.tangem.tap.domain.tasks.product.ScanResponse +import com.tangem.domain.common.ScanResponse +import com.tangem.domain.common.extensions.fromNetworkId import com.tangem.tap.domain.tokens.Currency -import com.tangem.tap.domain.tokens.fromNetworkId import com.tangem.tap.features.wallet.redux.WalletData import org.rekotlin.StateType diff --git a/app/src/main/java/com/tangem/tap/features/tokens/ui/AddTokensFragment.kt b/app/src/main/java/com/tangem/tap/features/tokens/ui/AddTokensFragment.kt index 2246cba6b4..37f541c7fe 100644 --- a/app/src/main/java/com/tangem/tap/features/tokens/ui/AddTokensFragment.kt +++ b/app/src/main/java/com/tangem/tap/features/tokens/ui/AddTokensFragment.kt @@ -104,6 +104,10 @@ class AddTokensFragment : Fragment(R.layout.fragment_add_tokens), override fun onOptionsItemSelected(item: MenuItem): Boolean { return when (item.itemId) { R.id.menu_search -> true + R.id.menu_navigate_add_custom_token -> { + store.dispatch(TokensAction.PrepareAndNavigateToAddCustomToken) + true + } else -> super.onOptionsItemSelected(item) } } diff --git a/app/src/main/java/com/tangem/tap/features/tokens/ui/compose/CollapsedCurrencyItem.kt b/app/src/main/java/com/tangem/tap/features/tokens/ui/compose/CollapsedCurrencyItem.kt index 5d5de44aa4..51db0013b3 100644 --- a/app/src/main/java/com/tangem/tap/features/tokens/ui/compose/CollapsedCurrencyItem.kt +++ b/app/src/main/java/com/tangem/tap/features/tokens/ui/compose/CollapsedCurrencyItem.kt @@ -16,10 +16,10 @@ import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp import coil.compose.SubcomposeAsyncImage import com.tangem.blockchain.common.Blockchain +import com.tangem.domain.common.extensions.fromNetworkId import com.tangem.tap.common.extensions.getGreyedOutIconRes import com.tangem.tap.common.extensions.getRoundIconRes import com.tangem.tap.domain.tokens.Currency -import com.tangem.tap.domain.tokens.fromNetworkId import com.tangem.tap.features.tokens.redux.TokenWithBlockchain import com.tangem.wallet.R diff --git a/app/src/main/java/com/tangem/tap/features/tokens/ui/compose/CurrenciesScreen.kt b/app/src/main/java/com/tangem/tap/features/tokens/ui/compose/CurrenciesScreen.kt index 5e78d665f3..983f0aaeed 100644 --- a/app/src/main/java/com/tangem/tap/features/tokens/ui/compose/CurrenciesScreen.kt +++ b/app/src/main/java/com/tangem/tap/features/tokens/ui/compose/CurrenciesScreen.kt @@ -17,12 +17,12 @@ import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.res.stringResource import androidx.compose.ui.unit.dp import com.tangem.blockchain.common.Blockchain +import com.tangem.domain.common.TapWorkarounds.useOldStyleDerivation +import com.tangem.domain.common.extensions.fromNetworkId import com.tangem.tap.common.compose.Keyboard import com.tangem.tap.common.compose.keyboardAsState import com.tangem.tap.common.extensions.pixelsToDp -import com.tangem.tap.domain.TapWorkarounds.useOldStyleDerivation import com.tangem.tap.domain.tokens.Currency -import com.tangem.tap.domain.tokens.fromNetworkId import com.tangem.tap.features.tokens.redux.ContractAddress import com.tangem.tap.features.tokens.redux.TokenWithBlockchain import com.tangem.tap.features.tokens.redux.TokensState diff --git a/app/src/main/java/com/tangem/tap/features/tokens/ui/compose/ExpandedCurrencyItem.kt b/app/src/main/java/com/tangem/tap/features/tokens/ui/compose/ExpandedCurrencyItem.kt index 1756881f8f..e5f57c2eaf 100644 --- a/app/src/main/java/com/tangem/tap/features/tokens/ui/compose/ExpandedCurrencyItem.kt +++ b/app/src/main/java/com/tangem/tap/features/tokens/ui/compose/ExpandedCurrencyItem.kt @@ -18,8 +18,8 @@ import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp import coil.compose.SubcomposeAsyncImage import com.tangem.blockchain.common.Blockchain +import com.tangem.domain.common.extensions.fromNetworkId import com.tangem.tap.domain.tokens.Currency -import com.tangem.tap.domain.tokens.fromNetworkId import com.tangem.tap.features.tokens.redux.ContractAddress import com.tangem.tap.features.tokens.redux.TokenWithBlockchain import com.tangem.wallet.R diff --git a/app/src/main/java/com/tangem/tap/features/wallet/redux/WalletState.kt b/app/src/main/java/com/tangem/tap/features/wallet/redux/WalletState.kt index 3f8d57e676..abecd0f54a 100644 --- a/app/src/main/java/com/tangem/tap/features/wallet/redux/WalletState.kt +++ b/app/src/main/java/com/tangem/tap/features/wallet/redux/WalletState.kt @@ -5,12 +5,12 @@ import com.tangem.blockchain.common.* import com.tangem.blockchain.common.address.AddressType import com.tangem.blockchain.extensions.isAboveZero import com.tangem.common.extensions.isZero +import com.tangem.domain.common.TapWorkarounds.derivationStyle import com.tangem.tap.common.entities.Button import com.tangem.tap.common.extensions.toQrCode import com.tangem.tap.common.redux.StateDialog import com.tangem.tap.common.redux.global.CryptoCurrencyName import com.tangem.tap.common.toggleWidget.WidgetState -import com.tangem.tap.domain.TapWorkarounds.derivationStyle import com.tangem.tap.domain.configurable.warningMessage.WarningMessage import com.tangem.tap.domain.extensions.buyIsAllowed import com.tangem.tap.domain.extensions.sellIsAllowed diff --git a/app/src/main/java/com/tangem/tap/features/wallet/redux/middlewares/WalletMiddleware.kt b/app/src/main/java/com/tangem/tap/features/wallet/redux/middlewares/WalletMiddleware.kt index 77e68e2383..fb5468a689 100644 --- a/app/src/main/java/com/tangem/tap/features/wallet/redux/middlewares/WalletMiddleware.kt +++ b/app/src/main/java/com/tangem/tap/features/wallet/redux/middlewares/WalletMiddleware.kt @@ -9,6 +9,7 @@ import com.tangem.common.CompletionResult import com.tangem.common.core.TangemSdkError import com.tangem.common.extensions.isZero import com.tangem.common.services.Result +import com.tangem.domain.common.extensions.withMainContext import com.tangem.operations.attestation.Attestation import com.tangem.operations.attestation.OnlineCardVerifier import com.tangem.tap.common.analytics.Analytics diff --git a/app/src/main/java/com/tangem/tap/features/wallet/redux/middlewares/WarningsMiddleware.kt b/app/src/main/java/com/tangem/tap/features/wallet/redux/middlewares/WarningsMiddleware.kt index e8ea067b6f..94ee1e747e 100644 --- a/app/src/main/java/com/tangem/tap/features/wallet/redux/middlewares/WarningsMiddleware.kt +++ b/app/src/main/java/com/tangem/tap/features/wallet/redux/middlewares/WarningsMiddleware.kt @@ -6,17 +6,17 @@ import com.tangem.blockchain.common.Wallet import com.tangem.blockchain.extensions.SimpleResult import com.tangem.common.card.Card import com.tangem.common.card.FirmwareVersion +import com.tangem.domain.common.ScanResponse +import com.tangem.domain.common.TapWorkarounds.isTestCard import com.tangem.tap.common.analytics.AnalyticsEvent import com.tangem.tap.common.extensions.isGreaterThan import com.tangem.tap.common.redux.global.GlobalState -import com.tangem.tap.domain.TapWorkarounds.isTestCard import com.tangem.tap.domain.configurable.warningMessage.WarningMessage import com.tangem.tap.domain.configurable.warningMessage.WarningMessagesManager import com.tangem.tap.domain.extensions.getSingleWallet import com.tangem.tap.domain.extensions.hasSignedHashes +import com.tangem.tap.domain.extensions.isMultiwalletAllowed import com.tangem.tap.domain.extensions.remainingSignatures -import com.tangem.tap.domain.isMultiwalletAllowed -import com.tangem.tap.domain.tasks.product.ScanResponse import com.tangem.tap.features.demo.isDemoCard import com.tangem.tap.features.wallet.redux.WalletAction import com.tangem.tap.network.NetworkConnectivity diff --git a/app/src/main/java/com/tangem/tap/features/wallet/redux/reducers/WalletReducer.kt b/app/src/main/java/com/tangem/tap/features/wallet/redux/reducers/WalletReducer.kt index 2a11d7740a..5af1e9c3be 100644 --- a/app/src/main/java/com/tangem/tap/features/wallet/redux/reducers/WalletReducer.kt +++ b/app/src/main/java/com/tangem/tap/features/wallet/redux/reducers/WalletReducer.kt @@ -3,6 +3,7 @@ package com.tangem.tap.features.wallet.redux.reducers import com.tangem.blockchain.common.AmountType import com.tangem.blockchain.common.Blockchain import com.tangem.blockchain.common.Wallet +import com.tangem.domain.common.TwinCardNumber import com.tangem.tap.common.extensions.toFiatString import com.tangem.tap.common.extensions.toFiatValue import com.tangem.tap.common.extensions.toFormattedCurrencyString @@ -13,7 +14,6 @@ import com.tangem.tap.domain.TapError import com.tangem.tap.domain.extensions.getArtworkUrl import com.tangem.tap.domain.getFirstToken import com.tangem.tap.domain.tokens.BlockchainNetwork -import com.tangem.tap.domain.twins.TwinCardNumber import com.tangem.tap.features.wallet.redux.* import com.tangem.tap.features.wallet.ui.BalanceStatus import com.tangem.tap.features.wallet.ui.BalanceWidgetData diff --git a/app/src/main/java/com/tangem/tap/features/wallet/ui/adapters/WalletAdapter.kt b/app/src/main/java/com/tangem/tap/features/wallet/ui/adapters/WalletAdapter.kt index 7218fd7758..fde2ce883d 100644 --- a/app/src/main/java/com/tangem/tap/features/wallet/ui/adapters/WalletAdapter.kt +++ b/app/src/main/java/com/tangem/tap/features/wallet/ui/adapters/WalletAdapter.kt @@ -8,10 +8,10 @@ import androidx.recyclerview.widget.RecyclerView import com.squareup.picasso.Picasso import com.tangem.blockchain.common.Blockchain import com.tangem.blockchain.common.Token +import com.tangem.domain.common.TapWorkarounds.derivationStyle import com.tangem.tap.common.extensions.getString import com.tangem.tap.common.extensions.loadCurrenciesIcon import com.tangem.tap.common.extensions.show -import com.tangem.tap.domain.TapWorkarounds.derivationStyle import com.tangem.tap.features.wallet.redux.Currency import com.tangem.tap.features.wallet.redux.WalletAction import com.tangem.tap.features.wallet.redux.WalletData diff --git a/app/src/main/java/com/tangem/tap/features/wallet/ui/wallet/MultiWalletView.kt b/app/src/main/java/com/tangem/tap/features/wallet/ui/wallet/MultiWalletView.kt index 3fff21289d..16c85a88fb 100644 --- a/app/src/main/java/com/tangem/tap/features/wallet/ui/wallet/MultiWalletView.kt +++ b/app/src/main/java/com/tangem/tap/features/wallet/ui/wallet/MultiWalletView.kt @@ -3,14 +3,14 @@ package com.tangem.tap.features.wallet.ui.wallet import android.app.Dialog import androidx.recyclerview.widget.LinearLayoutManager import com.tangem.common.card.Card +import com.tangem.domain.common.TapWorkarounds.derivationStyle +import com.tangem.domain.common.TapWorkarounds.isTestCard import com.tangem.tap.common.extensions.hide import com.tangem.tap.common.extensions.show import com.tangem.tap.common.redux.StateDialog import com.tangem.tap.common.redux.navigation.AppScreen import com.tangem.tap.common.redux.navigation.NavigationAction import com.tangem.tap.currenciesRepository -import com.tangem.tap.domain.TapWorkarounds.derivationStyle -import com.tangem.tap.domain.TapWorkarounds.isTestCard import com.tangem.tap.features.tokens.redux.TokensAction import com.tangem.tap.features.wallet.redux.WalletAction import com.tangem.tap.features.wallet.redux.WalletDialog diff --git a/app/src/main/java/com/tangem/tap/features/wallet/ui/wallet/SingleWalletView.kt b/app/src/main/java/com/tangem/tap/features/wallet/ui/wallet/SingleWalletView.kt index 224f717a2a..a26f2582fb 100644 --- a/app/src/main/java/com/tangem/tap/features/wallet/ui/wallet/SingleWalletView.kt +++ b/app/src/main/java/com/tangem/tap/features/wallet/ui/wallet/SingleWalletView.kt @@ -5,12 +5,12 @@ import android.view.View import android.view.ViewGroup import android.widget.Button import androidx.recyclerview.widget.LinearLayoutManager +import com.tangem.domain.common.TwinCardNumber import com.tangem.tap.common.extensions.beginDelayedTransition import com.tangem.tap.common.extensions.fitChipsByGroupWidth import com.tangem.tap.common.extensions.hide import com.tangem.tap.common.extensions.show import com.tangem.tap.common.redux.StateDialog -import com.tangem.tap.domain.twins.TwinCardNumber import com.tangem.tap.features.onboarding.products.twins.redux.TwinCardsState import com.tangem.tap.features.wallet.models.PendingTransaction import com.tangem.tap.features.wallet.redux.* diff --git a/app/src/main/java/com/tangem/tap/network/Retrofit.kt b/app/src/main/java/com/tangem/tap/network/Retrofit.kt deleted file mode 100644 index a2188e6d64..0000000000 --- a/app/src/main/java/com/tangem/tap/network/Retrofit.kt +++ /dev/null @@ -1,62 +0,0 @@ -package com.tangem.tap.network - -import com.squareup.moshi.FromJson -import com.squareup.moshi.Moshi -import com.squareup.moshi.ToJson -import com.squareup.moshi.kotlin.reflect.KotlinJsonAdapterFactory -import com.tangem.common.json.TangemSdkAdapter -import com.tangem.wallet.BuildConfig -import okhttp3.Interceptor -import okhttp3.OkHttpClient -import okhttp3.logging.HttpLoggingInterceptor -import retrofit2.Converter -import retrofit2.Retrofit -import retrofit2.converter.moshi.MoshiConverterFactory -import java.math.BigDecimal -import java.util.concurrent.TimeUnit - -fun createRetrofitInstance( - baseUrl: String, - interceptors: List = emptyList(), -): Retrofit { - val okHttpBuilder = OkHttpClient.Builder() - interceptors.forEach { okHttpBuilder.addInterceptor(it) } - addTimeOuts(okHttpBuilder) - - if (BuildConfig.DEBUG) okHttpBuilder.addInterceptor(createHttpLoggingInterceptor()) - return Retrofit.Builder() - .baseUrl(baseUrl) - .addConverterFactory(createMoshiConverterFactory()) - .client(okHttpBuilder.build()) - .build() -} - -private fun addTimeOuts(okHttpBuilder: OkHttpClient.Builder) { - okHttpBuilder.callTimeout(1, TimeUnit.SECONDS) - okHttpBuilder.connectTimeout(20, TimeUnit.SECONDS) - okHttpBuilder.readTimeout(20, TimeUnit.SECONDS) - okHttpBuilder.writeTimeout(20, TimeUnit.SECONDS) -} - -fun createMoshiConverterFactory(): Converter.Factory = MoshiConverterFactory.create(createMoshi()) - -fun createMoshi(): Moshi = Moshi.Builder() - .add(BigDecimalAdapter) - .add(KotlinJsonAdapterFactory()) - .add(TangemSdkAdapter.DerivationPathAdapter()) - .add(TangemSdkAdapter.DerivationNodeAdapter()) - .build() - -private fun createHttpLoggingInterceptor(): HttpLoggingInterceptor { - val logging = HttpLoggingInterceptor() - logging.level = HttpLoggingInterceptor.Level.BODY - return logging -} - -private object BigDecimalAdapter { - @FromJson - fun fromJson(string: String) = BigDecimal(string) - - @ToJson - fun toJson(value: BigDecimal) = value.toString() -} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/network/coinmarketcap/CoinMarketCapApi.kt b/app/src/main/java/com/tangem/tap/network/coinmarketcap/CoinMarketCapApi.kt index 5d78846280..6836861c8c 100644 --- a/app/src/main/java/com/tangem/tap/network/coinmarketcap/CoinMarketCapApi.kt +++ b/app/src/main/java/com/tangem/tap/network/coinmarketcap/CoinMarketCapApi.kt @@ -1,8 +1,7 @@ package com.tangem.tap.network.coinmarketcap -import com.tangem.tap.network.createRetrofitInstance -import okhttp3.Interceptor -import okhttp3.Response +import com.tangem.network.common.AddHeaderInterceptor +import com.tangem.network.common.createRetrofitInstance import retrofit2.http.GET import retrofit2.http.Query @@ -10,9 +9,9 @@ interface CoinMarketCapApi { @GET("v1/tools/price-conversion") suspend fun getRateInfo( - @Query("amount") amount: Int, - @Query("symbol") cryptoCurrencyName: String, - @Query("convert") fiatCurrencyName: String? = null + @Query("amount") amount: Int, + @Query("symbol") cryptoCurrencyName: String, + @Query("convert") fiatCurrencyName: String? = null ): RateInfoResponse @GET("v1/fiat/map") @@ -24,19 +23,11 @@ interface CoinMarketCapApi { fun create(apiKey: String): CoinMarketCapApi { return createRetrofitInstance( - baseUrl, - listOf(createCoinMarketRequestInterceptor(apiKey)), + baseUrl = baseUrl, + interceptors = listOf( + AddHeaderInterceptor(mapOf("X-CMC_PRO_API_KEY" to apiKey)), + ), ).create(CoinMarketCapApi::class.java) } } -} - -private fun createCoinMarketRequestInterceptor(apiKey: String): Interceptor { - return object : Interceptor { - override fun intercept(chain: Interceptor.Chain): Response { - val requestBuilder = chain.request().newBuilder() - requestBuilder.addHeader("X-CMC_PRO_API_KEY", apiKey) - return chain.proceed(requestBuilder.build()) - } - } } \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/network/exchangeServices/moonpay/MoonPayService.kt b/app/src/main/java/com/tangem/tap/network/exchangeServices/moonpay/MoonPayService.kt index f3ca64c4db..54fd9c558e 100644 --- a/app/src/main/java/com/tangem/tap/network/exchangeServices/moonpay/MoonPayService.kt +++ b/app/src/main/java/com/tangem/tap/network/exchangeServices/moonpay/MoonPayService.kt @@ -5,9 +5,9 @@ import android.util.Base64 import com.tangem.blockchain.common.Blockchain import com.tangem.common.services.Result import com.tangem.common.services.performRequest +import com.tangem.network.common.createRetrofitInstance import com.tangem.tap.common.extensions.urlEncode import com.tangem.tap.common.redux.global.CryptoCurrencyName -import com.tangem.tap.network.createRetrofitInstance import com.tangem.tap.network.exchangeServices.CurrencyExchangeManager import com.tangem.tap.network.exchangeServices.ExchangeService import com.tangem.tap.network.exchangeServices.ExchangeUrlBuilder diff --git a/app/src/main/java/com/tangem/tap/network/exchangeServices/onramper/OnnramperApi.kt b/app/src/main/java/com/tangem/tap/network/exchangeServices/onramper/OnnramperApi.kt index 01fec5d3d9..30a65ed706 100644 --- a/app/src/main/java/com/tangem/tap/network/exchangeServices/onramper/OnnramperApi.kt +++ b/app/src/main/java/com/tangem/tap/network/exchangeServices/onramper/OnnramperApi.kt @@ -1,8 +1,6 @@ package com.tangem.tap.network.exchangeServices.onramper import com.squareup.moshi.JsonClass -import okhttp3.Interceptor -import okhttp3.Response import retrofit2.http.GET import retrofit2.http.Path @@ -23,15 +21,6 @@ interface OnramperApi { } } -class AddKeyToHeaderInterceptor( - private val key: String -) : Interceptor { - override fun intercept(chain: Interceptor.Chain): Response { - val request = chain.request().newBuilder().addHeader("Authorization", "Basic $key").build() - return chain.proceed(request) - } -} - @JsonClass(generateAdapter = true) data class GatewaysResponse( val gateways: List diff --git a/app/src/main/java/com/tangem/tap/network/exchangeServices/onramper/OnramperService.kt b/app/src/main/java/com/tangem/tap/network/exchangeServices/onramper/OnramperService.kt index 0c5aebabec..94bdae395e 100644 --- a/app/src/main/java/com/tangem/tap/network/exchangeServices/onramper/OnramperService.kt +++ b/app/src/main/java/com/tangem/tap/network/exchangeServices/onramper/OnramperService.kt @@ -4,9 +4,10 @@ import android.net.Uri import com.tangem.blockchain.common.Blockchain import com.tangem.common.services.Result import com.tangem.common.services.performRequest +import com.tangem.network.common.AddHeaderInterceptor +import com.tangem.network.common.createRetrofitInstance import com.tangem.tap.common.extensions.urlEncode import com.tangem.tap.common.redux.global.CryptoCurrencyName -import com.tangem.tap.network.createRetrofitInstance import com.tangem.tap.network.exchangeServices.CurrencyExchangeManager import com.tangem.tap.network.exchangeServices.ExchangeService import com.tangem.tap.network.exchangeServices.ExchangeUrlBuilder @@ -23,10 +24,12 @@ class OnramperService( ) : ExchangeService, ExchangeUrlBuilder { private val api: OnramperApi by lazy { - createRetrofitInstance(OnramperApi.BASE_URL, - listOf(AddKeyToHeaderInterceptor(apiKey)) - ) - .create(OnramperApi::class.java) + createRetrofitInstance( + baseUrl = OnramperApi.BASE_URL, + interceptors = listOf( + AddHeaderInterceptor(mapOf("Authorization" to "Basic $apiKey")), + ) + ).create(OnramperApi::class.java) } private var status: OnramperStatus? = null diff --git a/app/src/main/java/com/tangem/tap/network/payid/PayIdService.kt b/app/src/main/java/com/tangem/tap/network/payid/PayIdService.kt index 6eab298c6f..b82c9d60df 100644 --- a/app/src/main/java/com/tangem/tap/network/payid/PayIdService.kt +++ b/app/src/main/java/com/tangem/tap/network/payid/PayIdService.kt @@ -3,7 +3,7 @@ package com.tangem.tap.network.payid import com.squareup.moshi.JsonClass import com.tangem.common.services.Result import com.tangem.common.services.performRequest -import com.tangem.tap.network.createRetrofitInstance +import com.tangem.network.common.createRetrofitInstance import retrofit2.Retrofit class PayIdService { diff --git a/app/src/main/java/com/tangem/tap/network/payid/PayIdVerifyService.kt b/app/src/main/java/com/tangem/tap/network/payid/PayIdVerifyService.kt index 13d79d4591..f3b327591b 100644 --- a/app/src/main/java/com/tangem/tap/network/payid/PayIdVerifyService.kt +++ b/app/src/main/java/com/tangem/tap/network/payid/PayIdVerifyService.kt @@ -2,7 +2,7 @@ package com.tangem.tap.network.payid import com.tangem.common.services.Result import com.tangem.common.services.performRequest -import com.tangem.tap.network.createRetrofitInstance +import com.tangem.network.common.createRetrofitInstance /** [REDACTED_AUTHOR] diff --git a/app/src/main/res/layout/view_compose.xml b/app/src/main/res/layout/view_compose.xml new file mode 100644 index 0000000000..0d5a7a4452 --- /dev/null +++ b/app/src/main/res/layout/view_compose.xml @@ -0,0 +1,5 @@ + + \ No newline at end of file diff --git a/app/src/main/res/layout/view_compose_fragment.xml b/app/src/main/res/layout/view_compose_fragment.xml new file mode 100644 index 0000000000..af9273a84d --- /dev/null +++ b/app/src/main/res/layout/view_compose_fragment.xml @@ -0,0 +1,33 @@ + + + + + + + + + + + + diff --git a/app/src/main/res/menu/popular_tokens.xml b/app/src/main/res/menu/popular_tokens.xml index 80d52565c9..5cc1ac82f8 100644 --- a/app/src/main/res/menu/popular_tokens.xml +++ b/app/src/main/res/menu/popular_tokens.xml @@ -9,4 +9,11 @@ app:actionViewClass="androidx.appcompat.widget.SearchView" app:showAsAction="always" /> + + \ No newline at end of file diff --git a/app/src/main/res/values-ru/strings.xml b/app/src/main/res/values-ru/strings.xml index c54c69a74d..02169fe239 100644 --- a/app/src/main/res/values-ru/strings.xml +++ b/app/src/main/res/values-ru/strings.xml @@ -187,7 +187,6 @@ Может быть лучше Один вопрос Вам нравится приложение Tangem? - Все поля обязательны к заполнению Токенов пока нет Добавленные токены Удалить токен %s @@ -197,20 +196,31 @@ Начните поиск Управление токенами Добавить пользовательский - Пользовательские токены Популярные токены - Добавить пользовательский токен - Пожалуйста заполните все поля - Введенное число не является допустимым десятичным числом - Имя - Символ токена - Адрес контракта - Десятичные - бывший USD Coin - бывший USDC Добавлен Удалить токен Значок токена + Добавить токен + Адрес контракта + Пожалуйста, выберите сеть + Пожалуйста, заполните все поля + Количество знаков после запятой должно быть корректным числом не больше %d + Адрес контракта некорректен + Путь деривации некорректен + Токены могут быть созданы кем угодно. Остерегайтесь мошеннических токенов, они могут ничего не стоить + Этот токен/сеть уже находится в вашем списке + Сеть + Токен + Знаков после запятой + Сеть + Не выбрано + Например, USD Coin + Название токена + Например, USDC + Символ токена + Деривация по BIP44 + По-умолчанию + Добавить токен Блокчейн Управление токенами Блокчейны diff --git a/app/src/main/res/values/colors.xml b/app/src/main/res/values/colors.xml index 3318e7609d..ecb9562aad 100644 --- a/app/src/main/res/values/colors.xml +++ b/app/src/main/res/values/colors.xml @@ -17,8 +17,17 @@ #FFB71B #FFFFFF - #8E8E93 - #636366 + + #F3F3F3 + #F8F8FB + #DADADF + #D0D0D5 + #C7C7CC + #D1D1D6 + #CACACC + #B6B6B8 + #8E8E90 + #666668 #48484A #3A3A3C #2C2C2E @@ -37,14 +46,6 @@ #14181D - #F3F3F3 - #F8F8FB - #DADADF - #D0D0D5 - #C7C7CC - #D1D1D6 - #C9C9CD - #1F000000 #14212121 diff --git a/app/src/main/res/values/strings_untranslated.xml b/app/src/main/res/values/strings_untranslated.xml index 40ba641e70..be969d2aae 100644 --- a/app/src/main/res/values/strings_untranslated.xml +++ b/app/src/main/res/values/strings_untranslated.xml @@ -67,8 +67,6 @@ One question How do you like Tangem? - All fields are required - //Manage tokens No tokens yet Added tokens @@ -80,19 +78,32 @@ Add Custom Custom tokens Popular tokens - Add Custom Token - Please, fill all fields - Entered number is not a valid decimals number - Name - Token symbol - Contract address - Decimals - ex. USD Coin - ex. USDC Added Remove token Token icon + Add Token + Contract address + Please select the network + Please fill in all the fields + Decimal number must be a valid integer, no higher than %d + Contract address is invalid + Derivation path is invalid + Note that tokens can be created by anyone. Be aware of adding scam tokens, they can cost nothing. + This token/network has already been added to your list + Network + Token + Decimals + Network + Not selected + E.g. USD Coin + Name + E.g. USDC + Token symbol + BIP44 coin type + Default + Add token + //Details Blockchain Manage tokens diff --git a/domain/.gitignore b/domain/.gitignore new file mode 100644 index 0000000000..42afabfd2a --- /dev/null +++ b/domain/.gitignore @@ -0,0 +1 @@ +/build \ No newline at end of file diff --git a/domain/build.gradle b/domain/build.gradle new file mode 100644 index 0000000000..6b60a72e31 --- /dev/null +++ b/domain/build.gradle @@ -0,0 +1,75 @@ +plugins { + id 'com.android.library' + id 'org.jetbrains.kotlin.android' +} + +android { + compileSdk 31 + + defaultConfig { + minSdk 21 + targetSdk 31 + + testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" + consumerProguardFiles "consumer-rules.pro" + } + + buildTypes { + debug { + debuggable true + minifyEnabled false + } + release { + debuggable false + minifyEnabled false + proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro' + } + } + kotlinOptions { + jvmTarget = JavaVersion.VERSION_1_8.toString() + } + compileOptions { + sourceCompatibility JavaVersion.VERSION_1_8 + targetCompatibility JavaVersion.VERSION_1_8 + } +// composeOptions { +// kotlinCompilerExtensionVersion '1.1.0' +// } + packagingOptions { + exclude 'lib/x86_64/darwin/libscrypt.dylib' + exclude 'lib/x86_64/freebsd/libscrypt.so' + exclude 'lib/x86_64/linux/libscrypt.so' + } +} + +dependencies { + implementation implementation(project(path: ':network')) + + // Tangem sdk's + implementation 'com.tangem:blockchain:develop-70' + implementation 'com.tangem.tangem-sdk-kotlin:core:develop-142' + implementation 'com.tangem.tangem-sdk-kotlin:android:develop-142' + + // Kotlin + implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-core:1.5.2' + + // State management + implementation "org.rekotlin:rekotlin:1.0.4" + + // Network + //TODO: it must depends from network module + implementation 'com.squareup.retrofit2:retrofit:2.8.1' + implementation 'com.squareup.retrofit2:converter-moshi:2.6.0' + implementation 'com.squareup.moshi:moshi:1.12.0' + implementation "com.squareup.moshi:moshi-kotlin:1.12.0" + implementation 'com.squareup.okhttp3:logging-interceptor:4.2.2' + + // Logs + implementation 'com.jakewharton.timber:timber:4.7.1' + + // Tests + testImplementation 'junit:junit:4.13.2' + testImplementation "com.google.truth:truth:1.1.3" + androidTestImplementation 'androidx.test.ext:junit:1.1.3' + androidTestImplementation 'androidx.test.espresso:espresso-core:3.4.0' +} \ No newline at end of file diff --git a/domain/proguard-rules.pro b/domain/proguard-rules.pro new file mode 100644 index 0000000000..481bb43481 --- /dev/null +++ b/domain/proguard-rules.pro @@ -0,0 +1,21 @@ +# Add project specific ProGuard rules here. +# You can control the set of applied configuration files using the +# proguardFiles setting in build.gradle. +# +# For more details, see +# http://developer.android.com/guide/developing/tools/proguard.html + +# If your project uses WebView with JS, uncomment the following +# and specify the fully qualified class name to the JavaScript interface +# class: +#-keepclassmembers class fqcn.of.javascript.interface.for.webview { +# public *; +#} + +# Uncomment this to preserve the line number information for +# debugging stack traces. +#-keepattributes SourceFile,LineNumberTable + +# If you keep the line number information, uncomment this to +# hide the original source file name. +#-renamesourcefileattribute SourceFile \ No newline at end of file diff --git a/domain/src/androidTest/java/com/tangem/domain/features/ExampleInstrumentedTest.kt b/domain/src/androidTest/java/com/tangem/domain/features/ExampleInstrumentedTest.kt new file mode 100644 index 0000000000..70e3d0ada2 --- /dev/null +++ b/domain/src/androidTest/java/com/tangem/domain/features/ExampleInstrumentedTest.kt @@ -0,0 +1,22 @@ +package com.tangem.domain.features + +import androidx.test.ext.junit.runners.AndroidJUnit4 +import androidx.test.platform.app.InstrumentationRegistry +import org.junit.Assert.assertEquals +import org.junit.Test +import org.junit.runner.RunWith + +/** + * Instrumented test, which will execute on an Android device. + * + * See [testing documentation](http://d.android.com/tools/testing). + */ +@RunWith(AndroidJUnit4::class) +class ExampleInstrumentedTest { + @Test + fun useAppContext() { + // Context of the app under test. + val appContext = InstrumentationRegistry.getInstrumentation().targetContext + assertEquals("com.tangem.feature2.test", appContext.packageName) + } +} \ No newline at end of file diff --git a/domain/src/main/AndroidManifest.xml b/domain/src/main/AndroidManifest.xml new file mode 100644 index 0000000000..ea7f515204 --- /dev/null +++ b/domain/src/main/AndroidManifest.xml @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/domain/src/main/java/com/tangem/domain/DomainError.kt b/domain/src/main/java/com/tangem/domain/DomainError.kt new file mode 100644 index 0000000000..7f6b14c1ed --- /dev/null +++ b/domain/src/main/java/com/tangem/domain/DomainError.kt @@ -0,0 +1,29 @@ +package com.tangem.domain + +/** +[REDACTED_AUTHOR] + * @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 DomainError : DomainMessage { + val code: Int + val message: String + val data: Any? +} + +open class AnError( + override val code: Int, + override val message: String, + override val data: Any? = null, +) : DomainError + +interface ErrorConverter { + fun convertError(error: DomainError): T +} + +interface Validator { + fun validate(data: Data? = null): Error? +} + +const val ERROR_CODE_ADD_CUSTOM_TOKEN = 100 \ No newline at end of file diff --git a/domain/src/main/java/com/tangem/domain/DomainException.kt b/domain/src/main/java/com/tangem/domain/DomainException.kt new file mode 100644 index 0000000000..f137262717 --- /dev/null +++ b/domain/src/main/java/com/tangem/domain/DomainException.kt @@ -0,0 +1,17 @@ +package com.tangem.domain + +/** +[REDACTED_AUTHOR] + * Must be handled by the module or sent to Crashlytics + */ +interface DomainInternalException + +sealed class DomainException(message: String?) : Throwable(message), DomainInternalException { + data class SelectTokeNetworkException(val networkId: String) : DomainException( + "Unknown network [$networkId] should not be included in the network selection dialog." + ) + + data class UnAppropriateInitializationException(val of: String, val info: String? = null) : DomainException( + "The [$of], must be properly initialized. Info []" + ) +} \ No newline at end of file diff --git a/domain/src/main/java/com/tangem/domain/DomainMessage.kt b/domain/src/main/java/com/tangem/domain/DomainMessage.kt new file mode 100644 index 0000000000..5ff7266ab9 --- /dev/null +++ b/domain/src/main/java/com/tangem/domain/DomainMessage.kt @@ -0,0 +1,15 @@ +package com.tangem.domain + +/** +[REDACTED_AUTHOR] + */ +sealed interface DomainMessage + +sealed interface DomainNotification : DomainMessage { + interface Toast : DomainNotification {} + + interface Snackbar : DomainNotification {} + + interface Dialog : DomainNotification {} + +} \ No newline at end of file diff --git a/domain/src/main/java/com/tangem/domain/DomainStateDialog.kt b/domain/src/main/java/com/tangem/domain/DomainStateDialog.kt new file mode 100644 index 0000000000..a3c7aa9da2 --- /dev/null +++ b/domain/src/main/java/com/tangem/domain/DomainStateDialog.kt @@ -0,0 +1,21 @@ +package com.tangem.domain + +import com.tangem.common.extensions.VoidCallback +import com.tangem.network.api.tangemTech.Coins + +/** +[REDACTED_AUTHOR] + */ +interface DomainStateDialog + +sealed class DomainDialog : DomainStateDialog { + + data class DialogError(val error: DomainError) : DomainDialog() + + data class SelectTokenDialog( + val items: List, + val networkIdConverter: (String) -> String, + val onSelect: (Coins.CheckAddressResponse.Token.Contract) -> Unit, + val onClose: VoidCallback = {} + ) : DomainDialog() +} \ No newline at end of file diff --git a/domain/src/main/java/com/tangem/domain/DomainWrapped.kt b/domain/src/main/java/com/tangem/domain/DomainWrapped.kt new file mode 100644 index 0000000000..41ab1c8569 --- /dev/null +++ b/domain/src/main/java/com/tangem/domain/DomainWrapped.kt @@ -0,0 +1,18 @@ +package com.tangem.domain + +import com.tangem.blockchain.common.Blockchain +import com.tangem.blockchain.common.Token + +/** +[REDACTED_AUTHOR] + * Provides a temporary copies of the app module classes, data structures, etc. + */ +//TODO: refactoring: : after refactoring they should be unwrapped and moved +// to appropriate parts of module +sealed interface DomainWrapped { + + data class TokenWithBlockchain( + val token: Token, + val blockchain: Blockchain + ) +} \ No newline at end of file diff --git a/domain/src/main/java/com/tangem/domain/common/FeatureCoroutineExceptionHandler.kt b/domain/src/main/java/com/tangem/domain/common/FeatureCoroutineExceptionHandler.kt new file mode 100644 index 0000000000..e6dde98009 --- /dev/null +++ b/domain/src/main/java/com/tangem/domain/common/FeatureCoroutineExceptionHandler.kt @@ -0,0 +1,23 @@ +package com.tangem.domain.common + +import kotlinx.coroutines.CoroutineExceptionHandler +import timber.log.Timber +import java.io.PrintWriter +import java.io.StringWriter + +/** +[REDACTED_AUTHOR] + */ +class FeatureCoroutineExceptionHandler { + + // add an external logger (FbAnalytics) for handling errors + companion object { + fun create(from: String): CoroutineExceptionHandler = CoroutineExceptionHandler { _, throwable -> + val sw = StringWriter() + throwable.printStackTrace(PrintWriter(sw)) + val exceptionAsString: String = sw.toString() + Timber.e("CoroutineException: from: %s, exception: %s", from, exceptionAsString) + throw throwable + } + } +} \ No newline at end of file diff --git a/domain/src/main/java/com/tangem/domain/common/ScanResponse.kt b/domain/src/main/java/com/tangem/domain/common/ScanResponse.kt new file mode 100644 index 0000000000..b41eb4f116 --- /dev/null +++ b/domain/src/main/java/com/tangem/domain/common/ScanResponse.kt @@ -0,0 +1,59 @@ +package com.tangem.domain.common + +import com.tangem.blockchain.common.Blockchain +import com.tangem.blockchain.common.Token +import com.tangem.common.card.Card +import com.tangem.common.card.WalletData +import com.tangem.common.extensions.ByteArrayKey +import com.tangem.domain.common.TapWorkarounds.getTangemNoteBlockchain +import com.tangem.operations.CommandResponse +import com.tangem.operations.backup.PrimaryCard +import com.tangem.operations.derivation.ExtendedPublicKeysMap + +/** +[REDACTED_AUTHOR] + */ +data class ScanResponse( + val card: Card, + val productType: ProductType, + val walletData: WalletData?, + val secondTwinPublicKey: String? = null, + val derivedKeys: Map = mapOf(), + val primaryCard: PrimaryCard? = null +) : CommandResponse { + + fun getBlockchain(): Blockchain { + if (productType == ProductType.Note) return card.getTangemNoteBlockchain() + ?: return Blockchain.Unknown + val blockchainName: String = walletData?.blockchain ?: return Blockchain.Unknown + return Blockchain.fromId(blockchainName) + } + + fun getPrimaryToken(): Token? { + val cardToken = walletData?.token ?: return null + return Token( + cardToken.name, + cardToken.symbol, + cardToken.contractAddress, + cardToken.decimals, + ) + } + + fun isTangemNote(): Boolean = productType == ProductType.Note + fun isTangemWallet(): Boolean = productType == ProductType.Wallet + fun isTangemTwins(): Boolean = productType == ProductType.Twins + + fun supportsHdWallet(): Boolean = card.settings.isHDWalletAllowed + fun supportsBackup(): Boolean = card.settings.isBackupAllowed + + fun twinsIsTwinned(): Boolean = + card.isTangemTwins() && walletData != null && secondTwinPublicKey != null +} + +enum class ProductType { + Note, Twins, Wallet +} + +typealias KeyWalletPublicKey = ByteArrayKey + +fun Card.isTangemTwins(): Boolean = TwinsHelper.getTwinCardNumber(cardId) != null \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/domain/TapWorkarounds.kt b/domain/src/main/java/com/tangem/domain/common/TapWorkarounds.kt similarity index 72% rename from app/src/main/java/com/tangem/tap/domain/TapWorkarounds.kt rename to domain/src/main/java/com/tangem/domain/common/TapWorkarounds.kt index 02a62b4215..a5ce7922c8 100644 --- a/app/src/main/java/com/tangem/tap/domain/TapWorkarounds.kt +++ b/domain/src/main/java/com/tangem/domain/common/TapWorkarounds.kt @@ -1,16 +1,13 @@ -package com.tangem.tap.domain +package com.tangem.domain.common import com.tangem.blockchain.common.Blockchain import com.tangem.blockchain.common.DerivationStyle import com.tangem.common.card.Card -import com.tangem.common.card.EllipticCurve -import com.tangem.common.card.FirmwareVersion -import com.tangem.tap.domain.TapWorkarounds.isStart2Coin -import com.tangem.tap.domain.TapWorkarounds.isTangemNote -import com.tangem.tap.domain.extensions.getSingleWallet -import com.tangem.tap.domain.twins.isTangemTwin import java.util.* +/** +[REDACTED_AUTHOR] + */ object TapWorkarounds { fun isStart2CoinIssuer(cardIssuer: String?): Boolean { @@ -24,7 +21,7 @@ object TapWorkarounds { get() = batchId == TEST_CARD_BATCH && cardId.startsWith(TEST_CARD_ID_STARTS_WITH) val Card.useOldStyleDerivation: Boolean - get() = batchId == "AC01" || batchId == "AC02" || batchId == "CB95" + get() = batchId == "AC01" || batchId == "AC02" || batchId == "CB95" val Card.derivationStyle: DerivationStyle? get() = if (!settings.isHDWalletAllowed) { @@ -50,7 +47,7 @@ object TapWorkarounds { fun isTangemWalletBatch(card: Card): Boolean = tangemWalletBatches.contains(card.batchId) - fun getTangemNoteBlockchain(card: Card): Blockchain? = tangemNoteBatches[card.batchId] + fun Card.getTangemNoteBlockchain(): Blockchain? = tangemNoteBatches[batchId] private const val START_2_COIN_ISSUER = "start2coin" private const val TEST_CARD_BATCH = "99FF" @@ -79,13 +76,8 @@ object TapWorkarounds { "AB07" to Blockchain.Bitcoin, "AB08" to Blockchain.Ethereum, ) -} -val DELAY_SDK_DIALOG_CLOSE = 1400L - -val Card.isMultiwalletAllowed: Boolean - get() { - return !isTangemTwin() && !isStart2Coin && !isTangemNote(this) - && (firmwareVersion >= FirmwareVersion.MultiWalletAvailable || - getSingleWallet()?.curve == EllipticCurve.Secp256k1) - } \ No newline at end of file + private val tangemWalletBatchesWithStandardDerivationType = listOf( + "AC01", "AC02", "CB95" + ) +} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/domain/twins/TwinsHelper.kt b/domain/src/main/java/com/tangem/domain/common/TwinsHelper.kt similarity index 98% rename from app/src/main/java/com/tangem/tap/domain/twins/TwinsHelper.kt rename to domain/src/main/java/com/tangem/domain/common/TwinsHelper.kt index 8196f39b22..e6f9c23475 100644 --- a/app/src/main/java/com/tangem/tap/domain/twins/TwinsHelper.kt +++ b/domain/src/main/java/com/tangem/domain/common/TwinsHelper.kt @@ -1,4 +1,4 @@ -package com.tangem.tap.domain.twins +package com.tangem.domain.common import com.tangem.common.card.Card import com.tangem.crypto.CryptoUtils diff --git a/domain/src/main/java/com/tangem/domain/common/extensions/Blockchain.kt b/domain/src/main/java/com/tangem/domain/common/extensions/Blockchain.kt new file mode 100644 index 0000000000..94e5daec42 --- /dev/null +++ b/domain/src/main/java/com/tangem/domain/common/extensions/Blockchain.kt @@ -0,0 +1,60 @@ +package com.tangem.domain.common.extensions + +import com.tangem.blockchain.common.Blockchain + +fun Blockchain.Companion.fromNetworkId(networkId: String): Blockchain? { + return when (networkId) { + "avalanche" -> Blockchain.Avalanche + "binancecoin" -> Blockchain.Binance + "binance-smart-chain" -> Blockchain.BSC + "ethereum" -> Blockchain.Ethereum + "polygon-pos" -> Blockchain.Polygon + "solana" -> Blockchain.Solana + "fantom" -> Blockchain.Fantom + "bitcoin" -> Blockchain.Bitcoin + "bitcoin-cash" -> Blockchain.BitcoinCash + "cardano" -> Blockchain.CardanoShelley + "dogecoin" -> Blockchain.Dogecoin + "ducatus" -> Blockchain.Ducatus + "litecoin" -> Blockchain.Litecoin + "rsk" -> Blockchain.RSK + "stellar" -> Blockchain.Stellar + "tezos" -> Blockchain.Tezos + "ripple" -> Blockchain.XRP + else -> null + } +} + +fun Blockchain.toNetworkId(): String { + return when (this) { + Blockchain.Unknown -> "unknown" + Blockchain.Avalanche -> "avalanche" + Blockchain.AvalancheTestnet -> "avalaunche" + Blockchain.Binance -> "binancecoin" + Blockchain.BinanceTestnet -> "binancecoin" + Blockchain.BSC -> "binance-smart-chain" + Blockchain.BSCTestnet -> "binance-smart-chain" + Blockchain.Bitcoin -> "bitcoin" + Blockchain.BitcoinTestnet -> "bitcoin" + Blockchain.BitcoinCash -> "bitcoin-cash" + Blockchain.BitcoinCashTestnet -> "bitcoin-cash" + Blockchain.Cardano -> "cardano" + Blockchain.CardanoShelley -> "cardano" + Blockchain.Dogecoin -> "dogecoin" + Blockchain.Ducatus -> "ducatus" + Blockchain.Ethereum -> "ethereum" + Blockchain.EthereumTestnet -> "ethereum" + Blockchain.Fantom -> "fantom" + Blockchain.FantomTestnet -> "fantom" + Blockchain.Litecoin -> "litecoin" + Blockchain.Polygon -> "matic-network" + Blockchain.PolygonTestnet -> "matic-networks" + Blockchain.RSK -> "rootstock" + Blockchain.Stellar -> "stellar" + Blockchain.StellarTestnet -> "stellar" + Blockchain.Solana -> "solana" + Blockchain.SolanaTestnet -> "solana" + Blockchain.Tezos -> "tezos" + Blockchain.XRP -> "ripple" + } +} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/common/extensions/Coroutine.kt b/domain/src/main/java/com/tangem/domain/common/extensions/Coroutine.kt similarity index 89% rename from app/src/main/java/com/tangem/tap/common/extensions/Coroutine.kt rename to domain/src/main/java/com/tangem/domain/common/extensions/Coroutine.kt index 57e88e4be2..0f7f23dd82 100644 --- a/app/src/main/java/com/tangem/tap/common/extensions/Coroutine.kt +++ b/domain/src/main/java/com/tangem/domain/common/extensions/Coroutine.kt @@ -1,4 +1,4 @@ -package com.tangem.tap.common.extensions +package com.tangem.domain.common.extensions import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers diff --git a/domain/src/main/java/com/tangem/domain/common/form/FieldDataConverters.kt b/domain/src/main/java/com/tangem/domain/common/form/FieldDataConverters.kt new file mode 100644 index 0000000000..a0cd94a04f --- /dev/null +++ b/domain/src/main/java/com/tangem/domain/common/form/FieldDataConverters.kt @@ -0,0 +1,40 @@ +package com.tangem.domain.common.form + +import com.tangem.common.json.MoshiJsonConverter + +/** +[REDACTED_AUTHOR] + */ +interface DataConverterVisitor { + fun visit(data: Data?) + fun getConvertedData(): Result +} + +interface FieldDataConverter : DataConverterVisitor + +abstract class BaseFieldDataConverter() : FieldDataConverter { + protected val collectIds: List + get() = getIdToCollect() + + protected val collectedData: MutableMap = mutableMapOf() + + override fun visit(data: Pair>?) { + val id = data?.first ?: return + + if (collectIds.contains(id)) { + collectedData[id] = data.second.value + } + } + + abstract fun getIdToCollect(): List +} + +class FieldToJsonConverter( + private val fieldsToConvert: List = listOf(), + protected val jsonConverter: MoshiJsonConverter +) : BaseFieldDataConverter() { + + override fun getConvertedData(): String = jsonConverter.toJson(collectedData, " ") + + override fun getIdToCollect(): List = fieldsToConvert +} \ No newline at end of file diff --git a/domain/src/main/java/com/tangem/domain/common/form/FieldsValidators.kt b/domain/src/main/java/com/tangem/domain/common/form/FieldsValidators.kt new file mode 100644 index 0000000000..83ba44c309 --- /dev/null +++ b/domain/src/main/java/com/tangem/domain/common/form/FieldsValidators.kt @@ -0,0 +1,63 @@ +package com.tangem.domain.common.form + +import com.tangem.blockchain.blockchains.ethereum.EthereumAddressService +import com.tangem.blockchain.common.Blockchain +import com.tangem.domain.Validator +import com.tangem.domain.features.addCustomToken.AddCustomTokenError + +/** +[REDACTED_AUTHOR] + */ +abstract class CustomTokenValidator : Validator + +class StringIsEmptyValidator : CustomTokenValidator() { + override fun validate(data: String?): AddCustomTokenError? = when { + data == null || data.isEmpty() -> null + else -> AddCustomTokenError.FieldIsNotEmpty + } +} + +class StringIsNotEmptyValidator : CustomTokenValidator() { + override fun validate(data: String?): AddCustomTokenError? = when { + data == null || data.isEmpty() -> AddCustomTokenError.FieldIsEmpty + else -> null + } +} + +class TokenContractAddressValidator : CustomTokenValidator() { + override fun validate(data: String?): AddCustomTokenError? { + if (data == null || data.isEmpty()) return null + + return if (EthereumAddressService().validate(data)) { + null + } else { + AddCustomTokenError.InvalidContractAddress + } + } +} + +class TokenNetworkValidator : CustomTokenValidator() { + override fun validate(data: Blockchain?): AddCustomTokenError? = when (data) { + null, Blockchain.Unknown -> AddCustomTokenError.NetworkIsNotSelected + else -> null + } +} + +class TokenNameValidator : CustomTokenValidator() { + override fun validate(data: String?): AddCustomTokenError? = StringIsNotEmptyValidator().validate(data) +} + +class TokenSymbolValidator : CustomTokenValidator() { + override fun validate(data: String?): AddCustomTokenError? = StringIsNotEmptyValidator().validate(data) +} + +class TokenDecimalsValidator : CustomTokenValidator() { + override fun validate(data: String?): AddCustomTokenError? { + val decimal = data?.toIntOrNull() ?: return AddCustomTokenError.FieldIsEmpty + + return when { + decimal > 30 -> AddCustomTokenError.InvalidDecimalsCount + else -> null + } + } +} \ No newline at end of file diff --git a/domain/src/main/java/com/tangem/domain/common/form/Form.kt b/domain/src/main/java/com/tangem/domain/common/form/Form.kt new file mode 100644 index 0000000000..5788f13b1e --- /dev/null +++ b/domain/src/main/java/com/tangem/domain/common/form/Form.kt @@ -0,0 +1,48 @@ +package com.tangem.domain.common.form + +/** +[REDACTED_AUTHOR] + */ +class Form( + val fieldList: List>, +) { + fun getField(id: FieldId): DataField<*>? = fieldList.firstOrNull { it.id == id } + + fun getData(id: FieldId): Pair? = getField(id)?.getData() + + // convert this form data whatever you want + fun visitDataConverter(converter: FieldDataConverter<*>) { + fieldList.forEach { it.visitDataConverter(converter) } + } +} + +interface FieldId + +interface Field { + val id: FieldId + var data: Data + + data class Data( + val value: Data, + val isUserInput: Boolean = true + ) +} + +typealias FieldData = Pair> + +interface DataField : Field { + fun getData(): Pair> + fun visitDataConverter(dataConverter: FieldDataConverter<*>) +} + +abstract class BaseDataField( + override val id: FieldId, + override var data: Field.Data, +) : DataField { + + override fun getData(): Pair> = id to data + + override fun visitDataConverter(dataConverter: FieldDataConverter<*>) { + dataConverter.visit(getData()) + } +} diff --git a/domain/src/main/java/com/tangem/domain/common/util/ValueDebouncer.kt b/domain/src/main/java/com/tangem/domain/common/util/ValueDebouncer.kt new file mode 100644 index 0000000000..36dd4441f4 --- /dev/null +++ b/domain/src/main/java/com/tangem/domain/common/util/ValueDebouncer.kt @@ -0,0 +1,40 @@ +package com.tangem.domain.common.util + +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.Job +import kotlinx.coroutines.flow.* +import kotlinx.coroutines.launch + +/** +[REDACTED_AUTHOR] + */ +class ValueDebouncer( + private val debounce: Long = 400, + private val onValueChanged: (T?) -> Unit +) { + + private var value: T? = null + private val debounceScope = CoroutineScope(Job() + Dispatchers.Main) + private val flow = MutableStateFlow(value) + + init { + initFlow() + } + + private fun initFlow() { + debounceScope.launch { + flow.filter { if (value == null) true else value != it } + .debounce(debounce) + .onEach { + value = it + onValueChanged(it) + } + .collect() + } + } + + fun emmit(emmitValue: T) { + debounceScope.launch { flow.emit(emmitValue) } + } +} \ No newline at end of file diff --git a/domain/src/main/java/com/tangem/domain/features/addCustomToken/AddCustomTokenStatePrinter.kt b/domain/src/main/java/com/tangem/domain/features/addCustomToken/AddCustomTokenStatePrinter.kt new file mode 100644 index 0000000000..04365d6c82 --- /dev/null +++ b/domain/src/main/java/com/tangem/domain/features/addCustomToken/AddCustomTokenStatePrinter.kt @@ -0,0 +1,63 @@ +package com.tangem.domain.features.addCustomToken + +import com.tangem.common.json.MoshiJsonConverter +import com.tangem.domain.common.form.FieldToJsonConverter +import com.tangem.domain.features.addCustomToken.redux.AddCustomTokenAction +import com.tangem.domain.features.addCustomToken.redux.AddCustomTokenState +import com.tangem.domain.redux.DomainState +import com.tangem.domain.redux.state.StatePrinter +import org.rekotlin.Action + +class AddCustomTokenStatePrinter : StatePrinter { + private val jsonConverter: MoshiJsonConverter = MoshiJsonConverter.INSTANCE + private var builder: StringBuilder = StringBuilder() + + override fun print(action: Action, domainState: DomainState): String? { + val action = (action as? AddCustomTokenAction) ?: return null + val state = domainState.addCustomTokensState + + val fieldConverter = FieldToJsonConverter(listOf( + CustomTokenFieldId.ContractAddress, + CustomTokenFieldId.Network, + CustomTokenFieldId.Name, + CustomTokenFieldId.Symbol, + CustomTokenFieldId.Decimals, + CustomTokenFieldId.DerivationPath, + ), jsonConverter) + state.visitDataConverter(fieldConverter) + val errors = state.formErrors.map { + "${it.key}: ${it.value::class.java.simpleName}" + } + val warnings = state.warnings.map { it::class.java.simpleName } + + printAction(action, state) + printStateValue("fields", fieldConverter.getConvertedData()) + printStateValue("fieldErrors", toJson(errors)) + printStateValue("warnings", toJson(warnings)) + printStateValue("screenState", toJson(state.screenState)) + printMessage("------------------------------------------------------") + + val printed = builder.toString() + builder = StringBuilder() + + return printed + } + + override fun getStateObject(domainState: DomainState): AddCustomTokenState = domainState.addCustomTokensState + + private fun printStateValue(name: String, value: String) { + printMessage("$name: $value") + } + + private fun printAction(action: AddCustomTokenAction, state: AddCustomTokenState) { + printMessage("action: $action, state: ${state::class.java.simpleName}") + } + + private fun toJson(value: Any): String { + return jsonConverter.prettyPrint(value) + } + + private fun printMessage(message: String) { + builder.append("$message\n") + } +} \ No newline at end of file diff --git a/domain/src/main/java/com/tangem/domain/features/addCustomToken/CompleteData.kt b/domain/src/main/java/com/tangem/domain/features/addCustomToken/CompleteData.kt new file mode 100644 index 0000000000..90b11472d2 --- /dev/null +++ b/domain/src/main/java/com/tangem/domain/features/addCustomToken/CompleteData.kt @@ -0,0 +1,59 @@ +package com.tangem.domain.features.addCustomToken + +import com.tangem.blockchain.common.Blockchain +import com.tangem.blockchain.common.Token +import com.tangem.domain.common.form.BaseFieldDataConverter +import com.tangem.domain.common.form.FieldId + +/** +[REDACTED_AUTHOR] + */ +enum class CompleteDataType { + Blockchain, Token +} + +sealed class CompleteData() { + + class CustomBlockchain( + val network: Blockchain, + val derivationPath: String? + ) : CompleteData() { + + class Converter : BaseFieldDataConverter() { + override fun getConvertedData(): CustomBlockchain { + val network = collectedData[CustomTokenFieldId.Network] as Blockchain + val derivationPath = collectedData[CustomTokenFieldId.DerivationPath] as? String + return CustomBlockchain(network, derivationPath) + } + + override fun getIdToCollect(): List = listOf(CustomTokenFieldId.Network, CustomTokenFieldId.DerivationPath) + } + } + + class CustomToken( + val token: Token, + val network: Blockchain, + val derivationPath: String?, + ) : CompleteData() { + + class Converter(val tokenId: String?) : BaseFieldDataConverter() { + + override fun getConvertedData(): CustomToken { + val token = Token( + name = collectedData[CustomTokenFieldId.Name] as String, + symbol = collectedData[CustomTokenFieldId.Symbol] as String, + contractAddress = collectedData[CustomTokenFieldId.ContractAddress] as String, + decimals = (collectedData[CustomTokenFieldId.Decimals] as String).toInt(), + id = tokenId, + ) + return CustomToken( + token, + collectedData[CustomTokenFieldId.Network] as Blockchain, + collectedData[CustomTokenFieldId.DerivationPath] as? String, + ) + } + + override fun getIdToCollect(): List = CustomTokenFieldId.values().toList() + } + } +} \ No newline at end of file diff --git a/domain/src/main/java/com/tangem/domain/features/addCustomToken/Errors.kt b/domain/src/main/java/com/tangem/domain/features/addCustomToken/Errors.kt new file mode 100644 index 0000000000..ebf097d964 --- /dev/null +++ b/domain/src/main/java/com/tangem/domain/features/addCustomToken/Errors.kt @@ -0,0 +1,25 @@ +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() +} \ No newline at end of file diff --git a/domain/src/main/java/com/tangem/domain/features/addCustomToken/FormFields.kt b/domain/src/main/java/com/tangem/domain/features/addCustomToken/FormFields.kt new file mode 100644 index 0000000000..33336b560e --- /dev/null +++ b/domain/src/main/java/com/tangem/domain/features/addCustomToken/FormFields.kt @@ -0,0 +1,32 @@ +package com.tangem.domain.features.addCustomToken + +import com.tangem.blockchain.common.Blockchain +import com.tangem.domain.common.form.BaseDataField +import com.tangem.domain.common.form.Field +import com.tangem.domain.common.form.FieldId + +/** +[REDACTED_AUTHOR] + */ +enum class CustomTokenFieldId : FieldId { + ContractAddress, + Network, + Name, + Symbol, + Decimals, + DerivationPath, +} + +data class TokenField( + override val id: FieldId, +) : BaseDataField(id, Field.Data("")) + +data class TokenBlockchainField( + override val id: FieldId, + val itemList: List, +) : BaseDataField(id, Field.Data(Blockchain.Unknown)) + +data class TokenDerivationPathField( + override val id: FieldId, + val itemList: List, +) : BaseDataField(id, Field.Data(Blockchain.Unknown)) \ No newline at end of file diff --git a/domain/src/main/java/com/tangem/domain/features/addCustomToken/TangemTechServiceManager.kt b/domain/src/main/java/com/tangem/domain/features/addCustomToken/TangemTechServiceManager.kt new file mode 100644 index 0000000000..438e75e47a --- /dev/null +++ b/domain/src/main/java/com/tangem/domain/features/addCustomToken/TangemTechServiceManager.kt @@ -0,0 +1,64 @@ +package com.tangem.domain.features.addCustomToken + +import com.tangem.common.services.Result +import com.tangem.network.api.tangemTech.Coins +import com.tangem.network.api.tangemTech.TangemTechService +import com.tangem.network.common.AddHeaderInterceptor + +/** +[REDACTED_AUTHOR] + */ +class TangemTechServiceManager( + private val tangemTechService: TangemTechService +) { + + suspend fun checkAddress( + contractAddress: String, + networkId: String? = null + ): Result> { + val result = tangemTechService.coins.checkAddress(contractAddress, networkId) + return when (result) { + is Result.Success -> { + val resultTokens = result.data.tokens + var tokensList = mutableListOf() + resultTokens.forEach { token -> + val contractsWithTheSameAddress = token.contracts + .filter { it.address == contractAddress } + .filter { it.decimalCount != null } + if (contractsWithTheSameAddress.isNotEmpty()) { + val newToken = token.copy(contracts = contractsWithTheSameAddress) + tokensList.add(newToken) + } + } + if (tokensList.size > 1) { + // https://tangem.slack.com/archives/GMXC6PP71/p1649672562078679 + tokensList = mutableListOf(tokensList[0]) + } + Result.Success(tokensList) + } + is Result.Failure -> result + } + } + + suspend fun tokens(): List { + return when (val result = tangemTechService.coins.tokens()) { + is Result.Success -> { + val tokens = result.data.tokens + tokens.filter { + it.contracts.isNullOrEmpty() + } + } + is Result.Failure -> emptyList() + } + } + + fun attachAuthKey(authKey: String) { + tangemTechService.addHeaderInterceptors(listOf( + CardPublicKeyHttpInterceptor(authKey), + )) + } +} + +private class CardPublicKeyHttpInterceptor(cardPublicKeyHex: String) : AddHeaderInterceptor(mapOf( + "card_public_key" to cardPublicKeyHex, +)) \ No newline at end of file diff --git a/domain/src/main/java/com/tangem/domain/features/addCustomToken/redux/AddCustomTokenAction.kt b/domain/src/main/java/com/tangem/domain/features/addCustomToken/redux/AddCustomTokenAction.kt new file mode 100644 index 0000000000..0b541fb309 --- /dev/null +++ b/domain/src/main/java/com/tangem/domain/features/addCustomToken/redux/AddCustomTokenAction.kt @@ -0,0 +1,67 @@ +package com.tangem.domain.features.addCustomToken.redux + +import com.tangem.blockchain.common.Blockchain +import com.tangem.blockchain.common.DerivationStyle +import com.tangem.domain.common.form.Field +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.CompleteData +import com.tangem.domain.features.addCustomToken.CustomTokenFieldId +import com.tangem.network.api.tangemTech.Coins +import org.rekotlin.Action + +/** +[REDACTED_AUTHOR] + */ +sealed class AddCustomTokenAction : Action { + sealed class Init : AddCustomTokenAction() { + data class SetAddedCurrencies(val addedCurrencies: AddedCurrencies) : AddCustomTokenAction() + + data class SetOnAddTokenCallback(val callback: (CompleteData) -> Unit) : AddCustomTokenAction() + } + + object OnCreate : AddCustomTokenAction() { + data class SetDerivationStyle(val derivationStyle: DerivationStyle?) : AddCustomTokenAction() + } + + object OnDestroy : AddCustomTokenAction() + + // from user, ui + data class OnTokenContractAddressChanged(val contractAddress: Field.Data) : AddCustomTokenAction() + data class OnTokenNetworkChanged(val blockchainNetwork: Field.Data) : AddCustomTokenAction() + data class OnTokenNameChanged(val tokenName: Field.Data) : AddCustomTokenAction() + data class OnTokenSymbolChanged(val tokenSymbol: Field.Data) : AddCustomTokenAction() + data class OnTokenDerivationPathChanged(val blockchainDerivationPath: Field.Data) : AddCustomTokenAction() + data class OnTokenDecimalsChanged(val tokenDecimals: Field.Data) : AddCustomTokenAction() + object OnAddCustomTokenClicked : AddCustomTokenAction() + + // form fields + data class UpdateForm(val state: AddCustomTokenState) : AddCustomTokenAction() + object ClearTokenFields : AddCustomTokenAction() + + data class FillTokenFields( + val token: Coins.CheckAddressResponse.Token, + val contract: Coins.CheckAddressResponse.Token.Contract, + ) : AddCustomTokenAction() + + sealed class FieldError : AddCustomTokenAction() { + data class Add(val id: CustomTokenFieldId, val error: AddCustomTokenError) : FieldError() + data class Remove(val id: CustomTokenFieldId) : FieldError() + } + + data class SetTokenId(val id: String) : AddCustomTokenAction() + + // warnings + sealed class Warning : AddCustomTokenAction() { + data class Add(val warnings: Set) : Warning() + data class Remove(val warnings: Set) : Warning() + data class Replace(val remove: Set, val add: Set) : Warning() + } + + // To change the screenState + sealed class Screen : AddCustomTokenAction() { + data class UpdateTokenFields(val pairs: List>) : Screen() + data class UpdateAddButton(val addButton: ViewStates.AddButton) : Screen() + } +} \ No newline at end of file diff --git a/domain/src/main/java/com/tangem/domain/features/addCustomToken/redux/AddCustomTokenHub.kt b/domain/src/main/java/com/tangem/domain/features/addCustomToken/redux/AddCustomTokenHub.kt new file mode 100644 index 0000000000..3e0ed80316 --- /dev/null +++ b/domain/src/main/java/com/tangem/domain/features/addCustomToken/redux/AddCustomTokenHub.kt @@ -0,0 +1,438 @@ +package com.tangem.domain.features.addCustomToken.redux + +import android.webkit.ValueCallback +import com.tangem.blockchain.common.Blockchain +import com.tangem.common.extensions.guard +import com.tangem.common.extensions.toHexString +import com.tangem.common.services.Result +import com.tangem.domain.DomainDialog +import com.tangem.domain.DomainException +import com.tangem.domain.common.TapWorkarounds.derivationStyle +import com.tangem.domain.common.extensions.fromNetworkId +import com.tangem.domain.common.extensions.toNetworkId +import com.tangem.domain.common.form.* +import com.tangem.domain.features.addCustomToken.* +import com.tangem.domain.features.addCustomToken.CustomTokenFieldId.* +import com.tangem.domain.features.addCustomToken.redux.AddCustomTokenAction.* +import com.tangem.domain.redux.BaseStoreHub +import com.tangem.domain.redux.DomainState +import com.tangem.domain.redux.dispatchOnMain +import com.tangem.domain.redux.domainStore +import com.tangem.domain.redux.global.DomainGlobalAction +import com.tangem.network.api.tangemTech.Coins +import com.tangem.network.api.tangemTech.TangemTechService +import kotlinx.coroutines.cancel +import kotlinx.coroutines.delay +import kotlinx.coroutines.launch +import org.rekotlin.Action +import timber.log.Timber + +/** +[REDACTED_AUTHOR] + */ +internal class AddCustomTokenHub : BaseStoreHub("AddCustomTokenHub") { + + private val hubState: AddCustomTokenState + get() = domainStore.state.addCustomTokensState + + override fun getHubState(storeState: DomainState): AddCustomTokenState = hubState + + override fun updateStoreState(storeState: DomainState, newHubState: AddCustomTokenState): DomainState { + return storeState.copy(addCustomTokensState = newHubState) + } + + override suspend fun handleAction( + action: Action, + storeState: DomainState, + cancel: ValueCallback + ) { + if (action !is AddCustomTokenAction) return + + when (action) { + is Init.SetAddedCurrencies -> {} + is Init.SetOnAddTokenCallback -> {} + is OnCreate -> { + hubState.addedCurrencies.guard { + return throwUnAppropriateInitialization("addedTokens") + } + } + is OnDestroy -> hubScope.cancel() + is OnTokenContractAddressChanged -> { + dispatchOnMain( + Screen.UpdateAddButton( + ViewStates.AddButton(!hubState.allFieldsIsEmpty()) + ) + ) + val contractAddress = action.contractAddress.value + val validator: TokenContractAddressValidator = hubState.getValidator(ContractAddress) + val error = validator.validate(contractAddress) + addOrRemoveError(ContractAddress, error) + + if (error != null || contractAddress.isEmpty()) { + dispatchOnMain(unlockTokenFields()) + return + } + if (!action.contractAddress.isUserInput) return + + manageTokenChanges(requestInfoAboutContractAddress(contractAddress)) + } + is OnTokenNetworkChanged -> { + if (!action.blockchainNetwork.isUserInput) return + + val contractAddress = hubState.getField(ContractAddress).data.value + manageTokenChanges(requestInfoAboutContractAddress(contractAddress)) + } + is OnTokenNameChanged -> { + val validator: TokenNameValidator = hubState.getValidator(Name) + addOrRemoveError(Name, validator.validate(action.tokenName.value)) + } + is OnTokenSymbolChanged -> { + val validator: TokenSymbolValidator = hubState.getValidator(Symbol) + addOrRemoveError(Symbol, validator.validate(action.tokenSymbol.value)) + } + is OnTokenDecimalsChanged -> { + val validator: TokenDecimalsValidator = hubState.getValidator(Decimals) + addOrRemoveError(Decimals, validator.validate(action.tokenDecimals.value)) + } +// is OnTokenDerivationPathChanged -> { +// val validator: TokenDerivationPathValidator = getValidator(DerivationPath, hubState) +// addOrRemoveError(DerivationPath, validator.validate(action.value.value)) +// } + is ClearTokenFields -> { + val nameField = hubState.getField(Name) + val symbolField = hubState.getField(Symbol) + val decimalsField = hubState.getField(Decimals) + + nameField.data = Field.Data("", false) + symbolField.data = Field.Data("", false) + decimalsField.data = Field.Data("", false) + + dispatchOnMain(UpdateForm(hubState)) + } + is FillTokenFields -> { + val networkField = hubState.getField(Network) + val nameField = hubState.getField(Name) + val symbolField = hubState.getField(Symbol) + val decimalsField = hubState.getField(Decimals) + + val token = action.token + val contract = action.contract + val blockchain = Blockchain.fromNetworkId(contract.networkId) ?: Blockchain.Unknown + networkField.data = Field.Data(blockchain, false) + nameField.data = Field.Data(token.name, false) + symbolField.data = Field.Data(token.symbol, false) + decimalsField.data = Field.Data(contract.decimalCount.toString(), false) + + dispatchOnMain(UpdateForm(hubState)) + } + is OnAddCustomTokenClicked -> { +// if (hubState.allFieldsIsEmpty()) { + dispatchOnMain( + DomainGlobalAction.ShowDialog(DomainDialog.DialogError( + AddCustomTokenError.FieldIsEmpty + ))) + return +// } + when { + !hubState.customTokensFieldsIsEmpty() && !hubState.networkIsEmpty() -> { + hubState.getCompleteData(CompleteDataType.Token) + } +// !hubState.customTokensFieldsIsEmpty() && -> { +// } + } +// if (true) { +// dispatchOnMain(NavigationAction.PopBackTo()) +// hubState.onTokenAddCallback?.invoke() +// } + } + else -> {} + } + } + + private suspend fun requestInfoAboutContractAddress( + contractAddress: String, + ): List { + val tangemTechServiceManager = requireNotNull(hubState.tangemTechServiceManager) + dispatchOnMain(Screen.UpdateTokenFields(listOf(ContractAddress to ViewStates.TokenField(isLoading = true)))) + val field = hubState.getField(Network) + val selectedNetworkId: String? = field.data.value.let { + if (it == Blockchain.Unknown) null else it + }?.toNetworkId() + + // simulate loading effect. It would be better if the delay would only run if tokenManager.checkAddress() + // got the result faster than 500ms and the delay would only be the difference between them. + delay(500) + + val foundTokensResult = tangemTechServiceManager.checkAddress(contractAddress, selectedNetworkId) + val result = when (foundTokensResult) { + is Result.Success -> foundTokensResult.data + is Result.Failure -> { +// val warning = AddCustomTokenWarning.Network.CheckAddressRequestError +// dispatchOnMain(Warning.Add(setOf(warning))) + emptyList() + } + } + dispatchOnMain(Screen.UpdateTokenFields(listOf(ContractAddress to ViewStates.TokenField(isLoading = false)))) + return result + } + + private suspend fun manageTokenChanges(foundTokens: List) { + val toAddWarnings = mutableSetOf() + val toRemoveWarnings = mutableSetOf() + + when { + foundTokens.isEmpty() -> { + toAddWarnings.add(AddCustomTokenWarning.PotentialScamToken) + toRemoveWarnings.add(AddCustomTokenWarning.TokenAlreadyAdded) + dispatchOnMain(ClearTokenFields) + dispatchOnMain(unlockTokenFields()) + } + else -> { + val token = foundTokens[0] + val contracts = token.contracts + when { + contracts.isEmpty() -> { + // TODO: refactoring: + Timber.e("Unexpected state -> throw to FB") + } + contracts.size == 1 -> { + val contract = contracts[0] + val isPersistIntoTheAppAddedTokenList = isPersistIntoTheAppAddedTokenList(token, contract) + + if (isPersistIntoTheAppAddedTokenList) { + toAddWarnings.add(AddCustomTokenWarning.TokenAlreadyAdded) + toRemoveWarnings.add(AddCustomTokenWarning.PotentialScamToken) + + dispatchOnMain(Screen.UpdateAddButton(ViewStates.AddButton(false))) + dispatchOnMain(lockTokenFields()) + } else { + toRemoveWarnings.add(AddCustomTokenWarning.TokenAlreadyAdded) + dispatchOnMain(Screen.UpdateAddButton(ViewStates.AddButton(true))) + + val isStandardDerivation = true + val tokenContract = token.contracts[0] + if (tokenContract.active && isStandardDerivation) { + toRemoveWarnings.add(AddCustomTokenWarning.PotentialScamToken) + dispatchOnMain(FillTokenFields(token, contract)) + dispatchOnMain(lockTokenFields()) + } else { + toAddWarnings.add(AddCustomTokenWarning.PotentialScamToken) + dispatchOnMain(ClearTokenFields) + dispatchOnMain(unlockTokenFields()) + } + } + } + else -> { + val dialog = DomainDialog.SelectTokenDialog( + items = contracts, + networkIdConverter = { networkId -> + val blockchain = Blockchain.fromNetworkId(networkId) + if (blockchain == null || blockchain == Blockchain.Unknown) { + throw DomainException.SelectTokeNetworkException(networkId) + } + hubState.convertBlockchainName(blockchain, "") + }, + onSelect = { selectedContract -> + hubScope.launch { + // find how to connect to the upper coroutineContext and dispatch through them + dispatchOnMain(FillTokenFields(token, selectedContract)) + dispatchOnMain(lockTokenFields()) + } + }, + ) + dispatchOnMain(DomainGlobalAction.ShowDialog(dialog)) + } + } + } + } + + if (toAddWarnings.isNotEmpty() || toRemoveWarnings.isNotEmpty()) { + dispatchOnMain(Warning.Replace(toRemoveWarnings.toSet(), toAddWarnings.toSet())) + } + } + + private fun isPersistIntoTheAppAddedTokenList( + token: Coins.CheckAddressResponse.Token, + contract: Coins.CheckAddressResponse.Token.Contract + ): Boolean = false + + private suspend fun addOrRemoveError(id: CustomTokenFieldId, error: AddCustomTokenError?) { + if (error == null) { + dispatchOnMain(FieldError.Remove(id)) + } else { + dispatchOnMain(FieldError.Add(id, error)) + } + } + + private fun lockTokenFields(): Action { + val state = hubState + return Screen.UpdateTokenFields(listOf( + Network to state.screenState.network.copy(isEnabled = false), + Name to state.screenState.name.copy(isEnabled = false), + Symbol to state.screenState.symbol.copy(isEnabled = false), + Decimals to state.screenState.decimals.copy(isEnabled = false), + )) + } + + private fun unlockTokenFields(): Action { + val state = hubState + return Screen.UpdateTokenFields(listOf( + Network to state.screenState.network.copy(isEnabled = true), + Name to state.screenState.name.copy(isEnabled = true), + Symbol to state.screenState.symbol.copy(isEnabled = true), + Decimals to state.screenState.decimals.copy(isEnabled = true), + )) + } + + override fun reduceAction(action: Action, state: AddCustomTokenState): AddCustomTokenState { + return when (action) { + is Init.SetAddedCurrencies -> { + state.copy(addedCurrencies = action.addedCurrencies) + } + is Init.SetOnAddTokenCallback -> { + state.copy(onTokenAddCallback = action.callback) + } + is OnCreate -> { + val card = requireNotNull(globalState.scanResponse?.card) + val tangemTechServiceManager = TangemTechServiceManager(TangemTechService()) + tangemTechServiceManager.attachAuthKey(card.cardPublicKey.toHexString()) + state.copy( + derivationStyle = card.derivationStyle, + tangemTechServiceManager = tangemTechServiceManager + ) + } + is OnDestroy -> state.reset() + is UpdateForm -> { + updateFormState(action.state) + } + is OnTokenContractAddressChanged -> { + val field: TokenField = state.getField(ContractAddress) + field.data = action.contractAddress + updateFormState(state) + } + is OnTokenNetworkChanged -> { + val field: TokenBlockchainField = state.getField(Network) + field.data = action.blockchainNetwork + updateFormState(state) + } + is OnTokenNameChanged -> { + val field: TokenField = state.getField(Name) + field.data = action.tokenName + updateFormState(state) + } + is OnTokenSymbolChanged -> { + val field: TokenField = state.getField(Symbol) + field.data = action.tokenSymbol + updateFormState(state) + } + is OnTokenDecimalsChanged -> { + val field: TokenField = state.getField(Decimals) + field.data = action.tokenDecimals + updateFormState(state) + } + is OnTokenDerivationPathChanged -> { + val field: TokenDerivationPathField = state.getField(DerivationPath) + field.data = action.blockchainDerivationPath + updateFormState(state) + } + is FieldError.Add -> { + val newMap = state.formErrors.toMutableMap().apply { this[action.id] = action.error } + state.copy(formErrors = newMap) + } + is FieldError.Remove -> { + val newMap = state.formErrors.toMutableMap().apply { remove(action.id) } + state.copy(formErrors = newMap) + } + is SetTokenId -> { + state.copy(tokenId = action.id) + } + is Warning.Add -> { + val newList = state.warnings.toMutableSet().apply { addAll(action.warnings) } + state.copy(warnings = newList.toSet()) + } + is Warning.Remove -> { + val newList = state.warnings.toMutableSet().apply { removeAll(action.warnings) } + state.copy(warnings = newList.toSet()) + } + is Warning.Replace -> { + val newList = state.warnings.toMutableSet().apply { + removeAll(action.remove) + addAll(action.add) + } + state.copy(warnings = newList.toSet()) + } + is Screen.UpdateTokenFields -> { + var newScreenState = state.screenState + action.pairs.forEach { + newScreenState = when (it.first) { + ContractAddress -> { + if (state.screenState.contractAddressField == it.second) { + newScreenState + } else { + newScreenState.copy(contractAddressField = it.second) + } + } + Network -> { + if (state.screenState.network == it.second) { + newScreenState + } else { + newScreenState.copy(network = it.second) + } + } + Name -> { + if (state.screenState.name == it.second) { + newScreenState + } else { + newScreenState.copy(name = it.second) + } + } + Symbol -> { + if (state.screenState.symbol == it.second) { + newScreenState + } else { + newScreenState.copy(symbol = it.second) + } + } + Decimals -> { + if (state.screenState.decimals == it.second) { + newScreenState + } else { + newScreenState.copy(decimals = it.second) + } + } + else -> newScreenState + } + } + if (state.screenState == newScreenState) { + state + } else { + state.copy(screenState = newScreenState) + } + } + is Screen.UpdateAddButton -> { + val newScreenState = if (state.screenState.addButton == action.addButton) { + state.screenState + } else { + state.screenState.copy(addButton = action.addButton) + } + if (newScreenState == state.screenState) { + state + } else { + state.copy(screenState = newScreenState) + } + } + else -> state + } + } + + private fun updateFormState(state: AddCustomTokenState): AddCustomTokenState { + return state.copy(form = Form(state.form.fieldList)) + } + + @Throws + private fun throwUnAppropriateInitialization(objName: String) { + throw DomainException.UnAppropriateInitializationException( + "AddCustomTokenHub", "$objName must be not NULL" + ) + } +} \ No newline at end of file diff --git a/domain/src/main/java/com/tangem/domain/features/addCustomToken/redux/AddCustomTokenState.kt b/domain/src/main/java/com/tangem/domain/features/addCustomToken/redux/AddCustomTokenState.kt new file mode 100644 index 0000000000..13acabb71e --- /dev/null +++ b/domain/src/main/java/com/tangem/domain/features/addCustomToken/redux/AddCustomTokenState.kt @@ -0,0 +1,149 @@ +package com.tangem.domain.features.addCustomToken.redux + +import com.tangem.blockchain.common.Blockchain +import com.tangem.blockchain.common.DerivationStyle +import com.tangem.domain.common.form.* +import com.tangem.domain.features.addCustomToken.* +import com.tangem.domain.features.addCustomToken.CustomTokenFieldId.* +import org.rekotlin.StateType + +data class AddCustomTokenState( + val addedCurrencies: AddedCurrencies? = null, + val onTokenAddCallback: ((CompleteData) -> Unit)? = null, + val derivationStyle: DerivationStyle? = null, + val form: Form = Form(createFormFields()), + val formValidators: Map> = createFormValidators(), + val formErrors: Map = emptyMap(), + val tokenId: String? = null, + val warnings: Set = emptySet(), + val screenState: ScreenState = createInitialScreenState(), + val tangemTechServiceManager: TangemTechServiceManager? = null +) : StateType { + + inline fun getField(id: FieldId): T = form.getField(id) as T + + inline fun getValidator(id: FieldId): T = formValidators[id] as T + + fun getError(id: FieldId): AddCustomTokenError? = formErrors[id] + + fun hasError(id: FieldId): Boolean = formErrors[id] != null + + fun getCompleteData(type: CompleteDataType): CompleteData = when (type) { + CompleteDataType.Token -> getToken() + CompleteDataType.Blockchain -> getBlockchain() + } + + inline fun visitDataConverter(converter: FieldDataConverter): T { + form.visitDataConverter(converter) + return converter.getConvertedData() + } + + fun convertBlockchainName(blockchain: Blockchain, unknown: String): String = when (blockchain) { + Blockchain.Unknown -> unknown + else -> blockchain.fullName + } + + fun convertDerivationPathLabel(blockchain: Blockchain, unknown: String): String { + return blockchain.derivationPath(derivationStyle)?.rawPath ?: unknown + } + + fun reset(): AddCustomTokenState { + return this.copy( + addedCurrencies = null, + onTokenAddCallback = null, + derivationStyle = null, + form = Form(createFormFields()), + formErrors = emptyMap(), + tokenId = null, + warnings = emptySet(), + screenState = createInitialScreenState(), + tangemTechServiceManager = null, + ) + } + + fun networkIsEmpty(): Boolean { + val network = getField(Network) + return network.data.value != Blockchain.Unknown + } + + fun customTokensFieldsIsEmpty(): Boolean { + val idsToCheck = listOf(ContractAddress, Name, Symbol, Decimals) + val fieldsToCheck = form.fieldList.filter { idsToCheck.contains(it.id) } + val validator = StringIsEmptyValidator() +// val errors = mutableMapOf<>() + fieldsToCheck.forEach { field -> + val error = validator.validate(field.data.value?.toString()) + if (error != null) return true + } + return false + } + + fun allFieldsIsEmpty(): Boolean = networkIsEmpty() && customTokensFieldsIsEmpty() + + private fun getToken(): CompleteData.CustomToken { + return CompleteData.CustomToken.Converter(tokenId) + .apply { visitDataConverter(this) } + .getConvertedData() + } + + private fun getBlockchain(): CompleteData.CustomBlockchain { + return CompleteData.CustomBlockchain.Converter() + .apply { visitDataConverter(this) } + .getConvertedData() + } + + companion object { + private fun createFormFields(): List> { + return listOf( + TokenField(ContractAddress), + TokenBlockchainField(Network, getSupportedNetworks()), + TokenField(Name), + TokenField(Symbol), + TokenField(Decimals), + TokenDerivationPathField(DerivationPath, getSupportedDerivations()), + ) + } + + private fun createFormValidators(): Map> { + return mapOf( + ContractAddress to TokenContractAddressValidator(), + Network to TokenNetworkValidator(), + Name to TokenNameValidator(), + Symbol to TokenSymbolValidator(), + Decimals to TokenDecimalsValidator(), + ) + } + + private fun getSupportedNetworks(): List { + return listOf( + Blockchain.Unknown, + Blockchain.Ethereum, + Blockchain.BSC, + Blockchain.Binance, + Blockchain.Polygon, + Blockchain.Avalanche, +// Blockchain.Solana, // not supported until tokens added to the Blockchain SDK + Blockchain.Fantom, + ) + } + + private fun getSupportedDerivations(): List { + val evmBlockchains = Blockchain.values().filter { + !it.isTestnet() && it.getChainId() != null + } + return listOf(Blockchain.Unknown) + evmBlockchains + } + + private fun createInitialScreenState(): ScreenState { + return ScreenState( + contractAddressField = ViewStates.TokenField(), + network = ViewStates.TokenField(), + name = ViewStates.TokenField(), + symbol = ViewStates.TokenField(), + decimals = ViewStates.TokenField(), + derivationPath = ViewStates.TokenField(), + addButton = ViewStates.AddButton() + ) + } + } +} \ No newline at end of file diff --git a/domain/src/main/java/com/tangem/domain/features/addCustomToken/redux/Models.kt b/domain/src/main/java/com/tangem/domain/features/addCustomToken/redux/Models.kt new file mode 100644 index 0000000000..1d9bc38a7a --- /dev/null +++ b/domain/src/main/java/com/tangem/domain/features/addCustomToken/redux/Models.kt @@ -0,0 +1,35 @@ +package com.tangem.domain.features.addCustomToken.redux + +import com.tangem.blockchain.common.Blockchain +import com.tangem.domain.DomainWrapped + +/** +[REDACTED_AUTHOR] + */ +// describes state the screen, except the form fields +data class ScreenState( + val contractAddressField: ViewStates.TokenField, + val network: ViewStates.TokenField, + val name: ViewStates.TokenField, + val symbol: ViewStates.TokenField, + val decimals: ViewStates.TokenField, + val derivationPath: ViewStates.TokenField, + val addButton: ViewStates.AddButton +) + +sealed class ViewStates { + data class TokenField( + val isLoading: Boolean = false, + val isEnabled: Boolean = true, + val isVisible: Boolean = true, + ) : ViewStates() + + data class AddButton( + val isEnabled: Boolean = true + ) : ViewStates() +} + +data class AddedCurrencies( + val addedTokens: List, + val addedBlockchains: List +) \ No newline at end of file diff --git a/domain/src/main/java/com/tangem/domain/redux/DomainState.kt b/domain/src/main/java/com/tangem/domain/redux/DomainState.kt new file mode 100644 index 0000000000..49cc235412 --- /dev/null +++ b/domain/src/main/java/com/tangem/domain/redux/DomainState.kt @@ -0,0 +1,13 @@ +package com.tangem.domain.redux + +import com.tangem.domain.features.addCustomToken.redux.AddCustomTokenState +import com.tangem.domain.redux.global.DomainGlobalState +import org.rekotlin.StateType + +/** +[REDACTED_AUTHOR] + */ +data class DomainState( + val globalState: DomainGlobalState = DomainGlobalState(), + val addCustomTokensState: AddCustomTokenState = AddCustomTokenState(), +) : StateType \ No newline at end of file diff --git a/domain/src/main/java/com/tangem/domain/redux/DomainStore.kt b/domain/src/main/java/com/tangem/domain/redux/DomainStore.kt new file mode 100644 index 0000000000..78bc7cf624 --- /dev/null +++ b/domain/src/main/java/com/tangem/domain/redux/DomainStore.kt @@ -0,0 +1,33 @@ +package com.tangem.domain.redux + +import com.tangem.domain.features.addCustomToken.redux.AddCustomTokenHub +import com.tangem.domain.redux.global.DomainGlobalHub +import com.tangem.domain.redux.state.observeReducedStates +import org.rekotlin.Store + +/** +[REDACTED_AUTHOR] + */ +private class DomainStore // for simple search + +private val RE_STORE_HUBS: List> = listOf( + DomainGlobalHub(), + AddCustomTokenHub(), +) + +val domainStore = Store( + state = DomainState(), + middleware = RE_STORE_HUBS.map { it.getMiddleware() }, + reducer = { action, state -> + requireNotNull(state) + + // we can examine the store state after each change by reducer + val reducedSates = RE_STORE_HUBS.mapNotNull { + val reducedState = it.reduce(action, state) + if (reducedState == state) null else Pair(action, reducedState) + } + observeReducedStates(reducedSates) + + if (reducedSates.isEmpty()) state else reducedSates.last().second + } +) diff --git a/domain/src/main/java/com/tangem/domain/redux/ReStoreHub.kt b/domain/src/main/java/com/tangem/domain/redux/ReStoreHub.kt new file mode 100644 index 0000000000..9de3cedcf3 --- /dev/null +++ b/domain/src/main/java/com/tangem/domain/redux/ReStoreHub.kt @@ -0,0 +1,114 @@ +package com.tangem.domain.redux + +import android.webkit.ValueCallback +import com.tangem.domain.common.FeatureCoroutineExceptionHandler +import com.tangem.domain.common.extensions.withIOContext +import com.tangem.domain.common.extensions.withMainContext +import com.tangem.domain.redux.global.DomainGlobalState +import kotlinx.coroutines.* +import org.rekotlin.Action +import org.rekotlin.DispatchFunction +import org.rekotlin.Middleware +import java.util.concurrent.Executors + +/** +[REDACTED_AUTHOR] + * ReStoreHub's should not store the or the , because this can lead to destabilization of + * a state behavior. + * All ReStoreHub's must be marked as internal + */ +internal interface ReStoreHub : HubMiddleware, HubReducer + +internal interface HubMiddleware { + fun getMiddleware(): Middleware +} + +internal interface HubReducer { + fun reduce(action: Action, storeState: StoreState): StoreState +} + +/** + * ReStoreHub is the entry point for actions. It processes it through middleware and reducer. + * Actions handled by ReStoreHub go into coroutine scope, which can be canceled while the action is being processed. + * All action went from the middleware must be dispatched through ReStoreHub.dispatchOnMain(Actions) to prevent + * concurrent modification in the Store + * Only the changed hub State will change its state in the DomainState + * Do not implement other states like as DomainGlobalState. Because it can dilute the responsibility of + * states. + * @param name - name of the Hub + * @param dispatcher - main coroutine dispatcher for actions + * @property globalState - state witch produce accessibility to global variables + */ +internal abstract class BaseStoreHub( + private val name: String, + private val dispatcher: CoroutineDispatcher = Executors.newFixedThreadPool(1).asCoroutineDispatcher() +) : ReStoreHub { + + val globalState: DomainGlobalState + get() = domainStore.state.globalState + + val hubScope = CoroutineScope( + Job() + dispatcher + CoroutineName(name) + FeatureCoroutineExceptionHandler.create(name) + ) + + private val actionsAndJobs = mutableMapOf() + + override fun getMiddleware(): Middleware { + return { dispatch, state -> + { next -> + { action -> + handle(state, action, dispatch) + next(action) + } + } + } + } + + /** + * Launches new coroutine and stores the action with it's coroutine job. (Coroutine can be cancelled + * through invoking the cancelActionJob() function inside a middleware). + * Removes the action when job is completed. + */ + protected open fun handle(storeStateHolder: () -> DomainState?, action: Action, dispatch: DispatchFunction) { + val storeState = storeStateHolder() + ?: throw UnsupportedOperationException("StoreState for the $name can't be NULL") + + hubScope.launch { + actionsAndJobs[action] = this.coroutineContext.job + actionsAndJobs[action]?.invokeOnCompletion { actionsAndJobs.remove(action) } + + handleAction(action, storeState) { + actionsAndJobs.remove(it)?.cancel() + } + } + } + + /** + * Reduce the action and check it. If the action hasn't updated the hubState, then it doesn't need to update + * storeState + */ + override fun reduce(action: Action, storeState: DomainState): DomainState { + val oldState = getHubState(storeState) + val newState = reduceAction(action, oldState) + return if (oldState === newState) { + storeState + } else { + updateStoreState(storeState, newState) + } + } + + protected abstract suspend fun handleAction(action: Action, storeState: DomainState, cancel: ValueCallback) + protected abstract fun reduceAction(action: Action, state: State): State + + protected abstract fun getHubState(storeState: DomainState): State + protected abstract fun updateStoreState(storeState: DomainState, newHubState: State): DomainState + +} + +internal suspend inline fun ReStoreHub<*, *>.dispatchOnMain(vararg actions: Action) { + withMainContext { actions.forEach { domainStore.dispatch(it) } } +} + +internal suspend inline fun ReStoreHub<*, *>.dispatchOnIO(vararg actions: Action) { + withIOContext { actions.forEach { domainStore.dispatch(it) } } +} diff --git a/domain/src/main/java/com/tangem/domain/redux/global/DomainGlobalAction.kt b/domain/src/main/java/com/tangem/domain/redux/global/DomainGlobalAction.kt new file mode 100644 index 0000000000..1ce8b77035 --- /dev/null +++ b/domain/src/main/java/com/tangem/domain/redux/global/DomainGlobalAction.kt @@ -0,0 +1,14 @@ +package com.tangem.domain.redux.global + +import com.tangem.domain.DomainStateDialog +import com.tangem.domain.common.ScanResponse +import org.rekotlin.Action + +/** +[REDACTED_AUTHOR] + */ +//TODO: refactoring: is alias for the GlobalAction +sealed class DomainGlobalAction : Action { + data class SetScanResponse(val scanResponse: ScanResponse?) : DomainGlobalAction() + data class ShowDialog(val stateDialog: DomainStateDialog?) : DomainGlobalAction() +} \ No newline at end of file diff --git a/domain/src/main/java/com/tangem/domain/redux/global/DomainGlobalHub.kt b/domain/src/main/java/com/tangem/domain/redux/global/DomainGlobalHub.kt new file mode 100644 index 0000000000..494dc41e7e --- /dev/null +++ b/domain/src/main/java/com/tangem/domain/redux/global/DomainGlobalHub.kt @@ -0,0 +1,44 @@ +package com.tangem.domain.redux.global + +import android.webkit.ValueCallback +import com.tangem.domain.redux.BaseStoreHub +import com.tangem.domain.redux.DomainState +import org.rekotlin.Action + +/** +[REDACTED_AUTHOR] + */ +//TODO: refactoring: is alias for the GlobalMiddleware and the GlobalReducer +internal class DomainGlobalHub : BaseStoreHub("DomainGlobalHub") { + + override fun getHubState(storeState: DomainState): DomainGlobalState { + return storeState.globalState + } + + override fun updateStoreState(storeState: DomainState, newHubState: DomainGlobalState): DomainState { + return storeState.copy(globalState = newHubState) + } + + override suspend fun handleAction( + action: Action, + storeState: DomainState, + cancel: ValueCallback + ) { + if (action !is DomainGlobalAction) return + + val state = storeState.globalState + + when (action) { + } + } + + override fun reduceAction(action: Action, state: DomainGlobalState): DomainGlobalState = when (action) { + is DomainGlobalAction.SetScanResponse -> { + state.copy(scanResponse = action.scanResponse) + } + is DomainGlobalAction.ShowDialog -> { + state.copy(dialog = action.stateDialog) + } + else -> state + } +} \ No newline at end of file diff --git a/domain/src/main/java/com/tangem/domain/redux/global/DomainGlobalState.kt b/domain/src/main/java/com/tangem/domain/redux/global/DomainGlobalState.kt new file mode 100644 index 0000000000..b629778ccc --- /dev/null +++ b/domain/src/main/java/com/tangem/domain/redux/global/DomainGlobalState.kt @@ -0,0 +1,13 @@ +package com.tangem.domain.redux.global + +import com.tangem.domain.DomainStateDialog +import com.tangem.domain.common.ScanResponse + +/** +[REDACTED_AUTHOR] + */ +//TODO: refactoring: is alias for the GlobalState +data class DomainGlobalState( + val scanResponse: ScanResponse? = null, + val dialog: DomainStateDialog? = null, +) diff --git a/domain/src/main/java/com/tangem/domain/redux/state/StateObservables.kt b/domain/src/main/java/com/tangem/domain/redux/state/StateObservables.kt new file mode 100644 index 0000000000..233bbaa618 --- /dev/null +++ b/domain/src/main/java/com/tangem/domain/redux/state/StateObservables.kt @@ -0,0 +1,36 @@ +package com.tangem.domain.redux.state + +import com.tangem.domain.features.addCustomToken.AddCustomTokenStatePrinter +import com.tangem.domain.features.addCustomToken.redux.AddCustomTokenAction +import com.tangem.domain.redux.DomainState +import org.rekotlin.Action +import timber.log.Timber + +/** +[REDACTED_AUTHOR] + * Use it only in debug mode! + */ +internal fun observeReducedStates(reducedSates: List>) { + // we can add any logic to watch for changes of actions, states, etc. + val isDebugMode = true + if (!isDebugMode) return + + logStates(reducedSates) +} + +private fun logStates(reducedSates: List>) { + reducedSates.forEach { + val printer = statePrinters.firstNotNullOfOrNull { entry -> + if (entry.key.isAssignableFrom(it.first::class.java)) entry.value else null + } ?: return@forEach + + val messageToPrint = printer.print(it.first, it.second) ?: return@forEach + + Timber.d(messageToPrint) + } +} + +// TODO: refactoring: mutate to factory +private val statePrinters = mutableMapOf( + AddCustomTokenAction::class.java to AddCustomTokenStatePrinter() +) \ No newline at end of file diff --git a/domain/src/main/java/com/tangem/domain/redux/state/StatePrinter.kt b/domain/src/main/java/com/tangem/domain/redux/state/StatePrinter.kt new file mode 100644 index 0000000000..8f617eea4d --- /dev/null +++ b/domain/src/main/java/com/tangem/domain/redux/state/StatePrinter.kt @@ -0,0 +1,12 @@ +package com.tangem.domain.redux.state + +import com.tangem.domain.redux.DomainState +import org.rekotlin.Action + +/** +[REDACTED_AUTHOR] + */ +interface StatePrinter { + fun print(action: Action, domainState: DomainState): String? + fun getStateObject(domainState: DomainState): S +} \ No newline at end of file diff --git a/domain/src/test/java/com/tangem/domain/features/ExampleUnitTest.kt b/domain/src/test/java/com/tangem/domain/features/ExampleUnitTest.kt new file mode 100644 index 0000000000..99368f65e4 --- /dev/null +++ b/domain/src/test/java/com/tangem/domain/features/ExampleUnitTest.kt @@ -0,0 +1,16 @@ +package com.tangem.domain.features + +import org.junit.Assert.assertEquals +import org.junit.Test + +/** + * Example local unit test, which will execute on the development machine (host). + * + * See [testing documentation](http://d.android.com/tools/testing). + */ +class ExampleUnitTest { + @Test + fun addition_isCorrect() { + assertEquals(4, 2 + 2) + } +} \ No newline at end of file diff --git a/network/.gitignore b/network/.gitignore new file mode 100644 index 0000000000..42afabfd2a --- /dev/null +++ b/network/.gitignore @@ -0,0 +1 @@ +/build \ No newline at end of file diff --git a/network/build.gradle b/network/build.gradle new file mode 100644 index 0000000000..f89fb54c38 --- /dev/null +++ b/network/build.gradle @@ -0,0 +1,23 @@ +plugins { + id 'java-library' + id 'org.jetbrains.kotlin.jvm' +} + +java { + sourceCompatibility = JavaVersion.VERSION_1_8 + targetCompatibility = JavaVersion.VERSION_1_8 +} + +dependencies { + // Tangem sdk's + implementation 'com.tangem.tangem-sdk-kotlin:core:develop-142' + + // Network + implementation(platform("com.squareup.okhttp3:okhttp-bom:4.9.3")) + implementation("com.squareup.okhttp3:okhttp") + implementation("com.squareup.okhttp3:logging-interceptor") + implementation 'com.squareup.retrofit2:retrofit:2.8.1' + implementation 'com.squareup.retrofit2:converter-moshi:2.6.0' + implementation 'com.squareup.moshi:moshi:1.13.0' + implementation "com.squareup.moshi:moshi-kotlin:1.13.0" +} \ No newline at end of file diff --git a/network/src/main/java/com/tangem/network/api/tangemTech/Responses.kt b/network/src/main/java/com/tangem/network/api/tangemTech/Responses.kt new file mode 100644 index 0000000000..534074aa0c --- /dev/null +++ b/network/src/main/java/com/tangem/network/api/tangemTech/Responses.kt @@ -0,0 +1,61 @@ +package com.tangem.network.api.tangemTech + +import java.math.BigDecimal + +/** +[REDACTED_AUTHOR] + */ +interface HttpResponse +sealed interface TangemTechResponse : HttpResponse + +sealed class Coins : TangemTechResponse { + data class PricesResponse(val prices: List) : Coins() { + data class Price( + val name: String, + val price: BigDecimal, + ) + } + + data class CheckAddressResponse(val imageHost: String?, val tokens: List, val total: Int) : Coins() { + data class Token( + val id: String, + val name: String, + val symbol: String, + val active: Boolean, + val contracts: List + ) { + data class Contract( + val networkId: String, + val address: String, + val decimalCount: BigDecimal?, + val active: Boolean + ) + } + } + + data class TokensResponse(val imageHost: String, val tokens: List, val total: Int) : Coins() { + data class Token( + val id: String, + val name: String, + val symbol: String, + val contracts: List? + ) { + data class Contract( + val networkId: String, + val address: String, + val decimalCount: BigDecimal?, + ) + } + } + + data class CurrenciesResponse(val currencies: List) { + data class Currency( + val id: String, + val code: String, + val name: String, + val rateBTC: String, + val unit: String, + val type: String, + ) + } +} \ No newline at end of file diff --git a/network/src/main/java/com/tangem/network/api/tangemTech/TangemTechApi.kt b/network/src/main/java/com/tangem/network/api/tangemTech/TangemTechApi.kt new file mode 100644 index 0000000000..2917063e30 --- /dev/null +++ b/network/src/main/java/com/tangem/network/api/tangemTech/TangemTechApi.kt @@ -0,0 +1,29 @@ +package com.tangem.network.api.tangemTech + +import retrofit2.http.GET +import retrofit2.http.Query + +/** +[REDACTED_AUTHOR] + */ +interface TangemTechApi { + + @GET("coins/prices") + suspend fun coinsPrices( + @Query("currency") currency: String, + @Query("ids") ids: List, + ): Coins.PricesResponse + + @GET("coins/check-address") + suspend fun coinsCheckAddress( + @Query("contractAddress") contractAddress: String, + @Query("networkId") networkId: String? = null, + ): Coins.CheckAddressResponse + + @GET("coins/currencies") + suspend fun coinsCurrencies(): Coins.CurrenciesResponse + + @GET("coins/tokens") + suspend fun coinsTokens(): Coins.TokensResponse + +} \ No newline at end of file diff --git a/network/src/main/java/com/tangem/network/api/tangemTech/TangemTechService.kt b/network/src/main/java/com/tangem/network/api/tangemTech/TangemTechService.kt new file mode 100644 index 0000000000..10c0ceb3cc --- /dev/null +++ b/network/src/main/java/com/tangem/network/api/tangemTech/TangemTechService.kt @@ -0,0 +1,80 @@ +package com.tangem.network.api.tangemTech + +import com.tangem.common.services.Result +import com.tangem.common.services.performRequest +import com.tangem.network.common.AddHeaderInterceptor +import com.tangem.network.common.CacheControlHttpInterceptor +import com.tangem.network.common.createRetrofitInstance + +/** +[REDACTED_AUTHOR] + */ +class TangemTechService { + + val coins: CoinsRoute = CoinsRoute() + + private val techRoutes: List = listOf( + coins + ) + + private val headerInterceptors = mutableListOf( + CacheControlHttpInterceptor(cacheMaxAge) + ) + + private var api: TangemTechApi = createApi() + + fun addHeaderInterceptors(interceptors: List) { + headerInterceptors.removeAll(interceptors) + headerInterceptors.addAll(interceptors) + api = createApi() + } + + private fun createApi(): TangemTechApi { + val retrofit = createRetrofitInstance( + baseUrl = baseUrl, + interceptors = headerInterceptors.toList() + ) + return retrofit.create(TangemTechApi::class.java).apply { + techRoutes.forEach { it.setApi(this) } + } + } + + companion object { + const val baseUrl = "https://api.tangem-tech.com/" + const val cacheMaxAge = 600 + } +} + +private interface TangemTechRoute { + fun setApi(api: TangemTechApi) +} + +class CoinsRoute : TangemTechRoute { + private lateinit var api: TangemTechApi + + override fun setApi(api: TangemTechApi) { + this.api = api + } + + suspend fun prices( + currency: String, + ids: List + ): Result { + return performRequest { api.coinsPrices(currency, ids) } + } + + suspend fun checkAddress( + contractAddress: String, + networkId: String? = null + ): Result { + return performRequest { api.coinsCheckAddress(contractAddress, networkId) } + } + + suspend fun currencies(): Result { + return performRequest { api.coinsCurrencies() } + } + + suspend fun tokens(): Result { + return performRequest { api.coinsTokens() } + } +} \ No newline at end of file diff --git a/network/src/main/java/com/tangem/network/common/Interceptors.kt b/network/src/main/java/com/tangem/network/common/Interceptors.kt new file mode 100644 index 0000000000..dff259332e --- /dev/null +++ b/network/src/main/java/com/tangem/network/common/Interceptors.kt @@ -0,0 +1,26 @@ +package com.tangem.network.common + +import okhttp3.Interceptor +import okhttp3.Response + +/** +[REDACTED_AUTHOR] + */ +open class AddHeaderInterceptor( + private val headers: Map +) : Interceptor { + + override fun intercept(chain: Interceptor.Chain): Response { + val request = chain.request().newBuilder().apply { + headers.forEach { + addHeader(it.key, it.value) + } + }.build() + + return chain.proceed(request) + } +} + +class CacheControlHttpInterceptor(maxAgeSeconds: Int) : AddHeaderInterceptor(mapOf( + "Cache-Control" to "max-age=$maxAgeSeconds", +)) \ No newline at end of file diff --git a/network/src/main/java/com/tangem/network/common/MoshiConverter.kt b/network/src/main/java/com/tangem/network/common/MoshiConverter.kt new file mode 100644 index 0000000000..1f967032e3 --- /dev/null +++ b/network/src/main/java/com/tangem/network/common/MoshiConverter.kt @@ -0,0 +1,38 @@ +package com.tangem.network.common + +import com.squareup.moshi.FromJson +import com.squareup.moshi.Moshi +import com.squareup.moshi.ToJson +import com.squareup.moshi.kotlin.reflect.KotlinJsonAdapterFactory +import com.tangem.common.json.MoshiJsonConverter +import com.tangem.common.json.TangemSdkAdapter +import retrofit2.Converter +import retrofit2.converter.moshi.MoshiConverterFactory +import java.math.BigDecimal + +/** +[REDACTED_AUTHOR] + */ +class MoshiConverter { + + companion object { + fun createFactory(moshi: Moshi = defaultMoshi()): Converter.Factory = MoshiConverterFactory.create(moshi) + + fun defaultMoshi(): Moshi = Moshi.Builder() + .add(BigDecimalAdapter) + .add(KotlinJsonAdapterFactory()) + .add(TangemSdkAdapter.DerivationPathAdapter()) + .add(TangemSdkAdapter.DerivationNodeAdapter()) + .build() + + fun sdkMoshi(): Moshi = MoshiJsonConverter.INSTANCE.moshi + } +} + +object BigDecimalAdapter { + @FromJson + fun fromJson(string: String) = BigDecimal(string) + + @ToJson + fun toJson(value: BigDecimal) = value.toString() +} \ No newline at end of file diff --git a/network/src/main/java/com/tangem/network/common/Retrofit.kt b/network/src/main/java/com/tangem/network/common/Retrofit.kt new file mode 100644 index 0000000000..86ab156c51 --- /dev/null +++ b/network/src/main/java/com/tangem/network/common/Retrofit.kt @@ -0,0 +1,37 @@ +package com.tangem.network.common + +import okhttp3.Interceptor +import okhttp3.OkHttpClient +import okhttp3.logging.HttpLoggingInterceptor +import retrofit2.Converter +import retrofit2.Retrofit +import java.util.concurrent.TimeUnit + +// TODO: refactoring: make it better through factory +fun createRetrofitInstance( + baseUrl: String, + okHttpBuilder: OkHttpClient.Builder = OkHttpClient.Builder(), + interceptors: List = emptyList(), + converterFactory: Converter.Factory = MoshiConverter.createFactory(), + logEnabled: Boolean = false +): Retrofit { + okHttpBuilder.apply { + callTimeout(10, TimeUnit.SECONDS) + connectTimeout(20, TimeUnit.SECONDS) + readTimeout(20, TimeUnit.SECONDS) + writeTimeout(20, TimeUnit.SECONDS) + } + interceptors.forEach { okHttpBuilder.addInterceptor(it) } + + if (logEnabled) okHttpBuilder.addInterceptor(createHttpLoggingInterceptor()) + + return Retrofit.Builder() + .baseUrl(baseUrl) + .addConverterFactory(converterFactory) + .client(okHttpBuilder.build()) + .build() +} + +private fun createHttpLoggingInterceptor(): HttpLoggingInterceptor = HttpLoggingInterceptor().apply { + level = HttpLoggingInterceptor.Level.BODY +} \ No newline at end of file diff --git a/settings.gradle b/settings.gradle index 9d495b34f8..a6c2d48bed 100644 --- a/settings.gradle +++ b/settings.gradle @@ -1 +1,3 @@ -include ':app' \ No newline at end of file +include ':app' +include ':domain' +include ':network'