From f6fad0e6d18011e7f5b573f8978381b29094cbd5 Mon Sep 17 00:00:00 2001 From: Tangem Date: Wed, 6 Aug 2025 17:09:04 +0700 Subject: [PATCH] Updated on 2026-08-14 --- app/build.gradle.kts | 2 + core/res/src/main/res/values-de/strings.xml | 2 + core/res/src/main/res/values-es/strings.xml | 2 + core/res/src/main/res/values-fr/strings.xml | 2 + core/res/src/main/res/values-it/strings.xml | 1 + core/res/src/main/res/values-ja/strings.xml | 21 ++ core/res/src/main/res/values-ru/strings.xml | 2 + .../src/main/res/values-uk-rUA/strings.xml | 2 + .../src/main/res/values-zh-rTW/strings.xml | 1 + core/res/src/main/res/values/strings.xml | 19 + .../ui/components/fields/AmountTextField.kt | 27 +- .../ui/components/fields/AutoSizeTextField.kt | 204 +++++++++++ .../ui/components/fields/SimpleTextField.kt | 11 +- features/account/api/.gitignore | 1 + features/account/api/build.gradle.kts | 16 + .../account/AccountCreateEditComponent.kt | 14 + features/account/impl/.gitignore | 1 + features/account/impl/build.gradle.kts | 59 +++ .../createedit/AccountCreateEditModel.kt | 54 +++ .../DefaultAccountCreateEditComponent.kt | 38 ++ .../createedit/di/AccountCreateEditModule.kt | 27 ++ .../createedit/entity/AccountCreateEditUM.kt | 42 +++ .../createedit/ui/AccountCreateEditContent.kt | 344 ++++++++++++++++++ settings.gradle.kts | 3 + 24 files changed, 870 insertions(+), 25 deletions(-) create mode 100644 core/ui/src/main/java/com/tangem/core/ui/components/fields/AutoSizeTextField.kt create mode 100644 features/account/api/.gitignore create mode 100644 features/account/api/build.gradle.kts create mode 100644 features/account/api/src/main/java/com/tangem/features/account/AccountCreateEditComponent.kt create mode 100644 features/account/impl/.gitignore create mode 100644 features/account/impl/build.gradle.kts create mode 100644 features/account/impl/src/main/java/com/tangem/features/account/createedit/AccountCreateEditModel.kt create mode 100644 features/account/impl/src/main/java/com/tangem/features/account/createedit/DefaultAccountCreateEditComponent.kt create mode 100644 features/account/impl/src/main/java/com/tangem/features/account/createedit/di/AccountCreateEditModule.kt create mode 100644 features/account/impl/src/main/java/com/tangem/features/account/createedit/entity/AccountCreateEditUM.kt create mode 100644 features/account/impl/src/main/java/com/tangem/features/account/createedit/ui/AccountCreateEditContent.kt diff --git a/app/build.gradle.kts b/app/build.gradle.kts index e90dbf8a85..0f40005c3c 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -235,6 +235,8 @@ dependencies { implementation(projects.features.createWalletSelection.impl) implementation(projects.features.home.api) implementation(projects.features.home.impl) + implementation(projects.features.account.api) + implementation(projects.features.account.impl) /** AndroidX libraries */ implementation(deps.androidx.core.ktx) diff --git a/core/res/src/main/res/values-de/strings.xml b/core/res/src/main/res/values-de/strings.xml index ead5525ab6..47e0920719 100644 --- a/core/res/src/main/res/values-de/strings.xml +++ b/core/res/src/main/res/values-de/strings.xml @@ -120,6 +120,7 @@ Token hinzufügen Alle Erlauben + Betrag Analysen Anwenden Genehmigung @@ -165,6 +166,7 @@ Deaktiviert Trennen Erledigt + Bearbeiten Aktivieren Aktiviert Fehler diff --git a/core/res/src/main/res/values-es/strings.xml b/core/res/src/main/res/values-es/strings.xml index 1ce6e28079..39ee2f7881 100644 --- a/core/res/src/main/res/values-es/strings.xml +++ b/core/res/src/main/res/values-es/strings.xml @@ -109,6 +109,7 @@ Agregar token Todos Autorizar + Montante Analítica Aplicar Aprobación @@ -153,6 +154,7 @@ Desactivado Desconectar Listo + Editar Activar Activado Error diff --git a/core/res/src/main/res/values-fr/strings.xml b/core/res/src/main/res/values-fr/strings.xml index b28e3174c2..3973a3df94 100644 --- a/core/res/src/main/res/values-fr/strings.xml +++ b/core/res/src/main/res/values-fr/strings.xml @@ -99,6 +99,7 @@ Ajouter un jeton Tous Permettre + Montant Analytique Appliquer Approbation @@ -140,6 +141,7 @@ Désactivé Se déconnecter Exécuté + Modifier Activer Activé Erreur diff --git a/core/res/src/main/res/values-it/strings.xml b/core/res/src/main/res/values-it/strings.xml index 7132029cfe..5caec91961 100644 --- a/core/res/src/main/res/values-it/strings.xml +++ b/core/res/src/main/res/values-it/strings.xml @@ -5,6 +5,7 @@ Questa carta non è progettata per funzionare con Tangem L\'importo inviato e il cambio non può essere inferiore a 1 ADA Accetta + Importo Saldo: %s Impossibile creare la transazione Non hai fornito l\'accesso alla tua videocamera, modifica le tue impostazioni sulla privacy diff --git a/core/res/src/main/res/values-ja/strings.xml b/core/res/src/main/res/values-ja/strings.xml index af6731c66e..93c2b842f3 100644 --- a/core/res/src/main/res/values-ja/strings.xml +++ b/core/res/src/main/res/values-ja/strings.xml @@ -12,6 +12,18 @@ ウォレットのロックを解除するには、 %s桁のアクセスコードを設定します。 アクセスコードの作成 アクセスコード + アカウントを追加 + 保存 + アカウント名 + アカウント + 新しいアカウント + アカウントを追加 + アカウントを編集 + 編集を続ける + 破棄 + 新しいアカウントを破棄してもよろしいですか? + 編集内容を破棄してもよろしいですか? + 保存されていない変更 トークンが見つかりませんか?メインページのマーケットセクションに移動し、ポートフォリオに追加して買付できるようにします。 トークンが見つかりませんか?メインページのマーケットセクションに移動し、ポートフォリオに追加して売却できるようにします。 売却 @@ -127,6 +139,7 @@ トークンを追加 すべて 許可する + 金額 アナリティクス 適用する 承認 @@ -170,6 +183,7 @@ 無効 切断 完了 + 編集 有効にする 有効 エラー @@ -1110,6 +1124,7 @@ 手数料見積りエラーです。サポートにフィードバックをお送りください。 スワップする 選択したトークンをこの量を交換すると、価格に大きな影響が生じ、結果が減少します。 + 価格への影響が甚大です 残高不足 許可を与える スワップ @@ -1285,6 +1300,7 @@ このウォレットを保護するためのシークレットコードです。ログインと署名に使用されます。 アクセスコードの設定 / 変更 ウォレットの受信取引とTangemの更新について通知を受け取る。 + 現在、Huaweiデバイスではプッシュ通知が機能しない可能性があります。現在、解決策の検討に取り組んでおり、今後のアップデートで修正をリリースする予定です。ご理解のほどよろしくお願いいたします。 取引通知 ウォレット設定 Tangem @@ -1426,15 +1442,19 @@ すべてのdAppを接続解除する ウォレットの変更の予測 取引をシミュレーションできませんでした。注意して続行してください。 + %sでは見積もりはサポートされていません + %sによる提案 悪意のある取引 このウォレットのポートフォリオに%sネットワークを追加します ウォレットに必要なネットワークはありません 新しい接続 ウォレットを別のdAppに接続する セッションなし + ウォレットの変更は検出されませんでした このドメインは複数のセキュリティプロバイダーから安全でないとの警告を受けています。あなたの資産を守るため、直ちにアクセスを中止してください。 既知のセキュリティリスク リクエスト元 + とにかく送金 署名タイプ 宛先 取引リクエスト @@ -1443,6 +1463,7 @@ 各ペアリング試行で、新しくユニークなURIが使用されていることを確認します URIはすでに使用されています ウォレットコネクト + 不審な取引 破棄 バックアップが中断されました。再開しますか? はい、再開します diff --git a/core/res/src/main/res/values-ru/strings.xml b/core/res/src/main/res/values-ru/strings.xml index 5a430645a7..474f997c12 100644 --- a/core/res/src/main/res/values-ru/strings.xml +++ b/core/res/src/main/res/values-ru/strings.xml @@ -96,6 +96,7 @@ Добавить токен Все Разрешить + Сумма Аналитика Применить Одобрение @@ -140,6 +141,7 @@ Отключено Отключить Готово + Изменить Включить Включено Ошибка diff --git a/core/res/src/main/res/values-uk-rUA/strings.xml b/core/res/src/main/res/values-uk-rUA/strings.xml index 9b3e3dfe52..5a09461ddc 100644 --- a/core/res/src/main/res/values-uk-rUA/strings.xml +++ b/core/res/src/main/res/values-uk-rUA/strings.xml @@ -96,6 +96,7 @@ Додати токен Усе Дозволити + Сума Аналітика Застосовувати Затвердження @@ -139,6 +140,7 @@ Вимкнуто Від\'єднати Готово + Редагувати Увімкнути Увімкнено Помилка diff --git a/core/res/src/main/res/values-zh-rTW/strings.xml b/core/res/src/main/res/values-zh-rTW/strings.xml index d5ccd296e0..b33588b325 100644 --- a/core/res/src/main/res/values-zh-rTW/strings.xml +++ b/core/res/src/main/res/values-zh-rTW/strings.xml @@ -40,6 +40,7 @@ 卡片設置 發送金額和找零不能少於1個ADA 接受 + 數量 允許 注意 餘額: %s diff --git a/core/res/src/main/res/values/strings.xml b/core/res/src/main/res/values/strings.xml index 704c48f319..70f17192aa 100644 --- a/core/res/src/main/res/values/strings.xml +++ b/core/res/src/main/res/values/strings.xml @@ -12,7 +12,18 @@ Set a %s-digit Access Code to unlock your wallet. Create Access Code Access code + Add account + Save Account name + Account + New account + Add account + Edit account + Keep Editing + Discard + Are you sure you want to discard new account? + Are you sure you want to discard edits? + Unsaved Changes Can’t find your token? Go to the Market section on the main page and add it to your portfolio for purchase Can’t find your token? Go to the Market section on the main page and add it to your portfolio for selling. Sell @@ -132,6 +143,7 @@ Add token All Allow + Amount Analytics Apply Approval @@ -177,6 +189,7 @@ Disabled Disconnect Done + Edit Enable Enabled Error @@ -432,6 +445,8 @@ Finish Now Finish Wallet Activation To complete setup, secure app access with Access Code. + If you do, you\'ll need to start over. + Are you sure you want to exit the activation process? Recover an existing wallet stored in your Google Drive backup Google Drive Backup Go to backup @@ -1129,6 +1144,7 @@ Fee estimation error. Please send feedback to support. You swap Swapping this amount of selected tokens will cause a significant price impact and reduce your outcome. + High price impact Insufficient funds Give Permission Swap @@ -1495,6 +1511,8 @@ Disconect All dApps Estimated wallet changes The transaction couldn\'t be simulated. Please proceed with caution. + Estimation is not supported for %s + Suggested by %s Malicious transaction Add the %s network to your portfolio for this wallet The wallet has no required networks @@ -1505,6 +1523,7 @@ This domain is flagged as unsafe by multiple security providers. Leave immediately to protect your assets Known security risk Request from + Send anyway Signature Type To Transaction request diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/fields/AmountTextField.kt b/core/ui/src/main/java/com/tangem/core/ui/components/fields/AmountTextField.kt index be5c0a9f2c..e4d65b95dd 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/components/fields/AmountTextField.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/components/fields/AmountTextField.kt @@ -12,12 +12,7 @@ import androidx.compose.ui.Alignment.Companion.TopCenter import androidx.compose.ui.Alignment.Companion.TopStart import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color -import androidx.compose.ui.platform.LocalContext -import androidx.compose.ui.platform.LocalDensity -import androidx.compose.ui.text.AnnotatedString -import androidx.compose.ui.text.ParagraphIntrinsics import androidx.compose.ui.text.TextStyle -import androidx.compose.ui.text.font.createFontFamilyResolver import androidx.compose.ui.text.input.KeyboardType import androidx.compose.ui.text.input.VisualTransformation import androidx.compose.ui.text.style.TextAlign @@ -77,24 +72,10 @@ fun AmountTextField( ) { val decimalFormat = rememberDecimalFormat() BoxWithConstraints(modifier = modifier) { - var fontSize = textStyle.fontSize - if (isAutoResize) { - val calculateIntrinsics = @Composable { - val transformedText = visualTransformation.filter(AnnotatedString(value)).text.text - ParagraphIntrinsics( - text = transformedText, - style = textStyle.copy(fontSize = fontSize), - density = LocalDensity.current, - fontFamilyResolver = createFontFamilyResolver(LocalContext.current), - ) - } - var intrinsics = calculateIntrinsics() - with(LocalDensity.current) { - while (intrinsics.maxIntrinsicWidth > maxWidth.toPx()) { - fontSize *= reduceFactor - intrinsics = calculateIntrinsics() - } - } + val fontSize = if (isAutoResize) { + resizeFont(visualTransformation, value, textStyle, reduceFactor) + } else { + textStyle.fontSize } val textColor = if (value.isBlank()) TangemTheme.colors.text.disabled else color SimpleTextField( diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/fields/AutoSizeTextField.kt b/core/ui/src/main/java/com/tangem/core/ui/components/fields/AutoSizeTextField.kt new file mode 100644 index 0000000000..23a4cf2348 --- /dev/null +++ b/core/ui/src/main/java/com/tangem/core/ui/components/fields/AutoSizeTextField.kt @@ -0,0 +1,204 @@ +package com.tangem.core.ui.components.fields + +import android.annotation.SuppressLint +import androidx.annotation.FloatRange +import androidx.compose.foundation.interaction.MutableInteractionSource +import androidx.compose.foundation.layout.BoxWithConstraints +import androidx.compose.foundation.layout.BoxWithConstraintsScope +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.text.KeyboardActions +import androidx.compose.foundation.text.KeyboardOptions +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.platform.LocalDensity +import androidx.compose.ui.text.AnnotatedString +import androidx.compose.ui.text.ParagraphIntrinsics +import androidx.compose.ui.text.TextStyle +import androidx.compose.ui.text.font.createFontFamilyResolver +import androidx.compose.ui.text.input.VisualTransformation +import androidx.compose.ui.text.style.TextDirection +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.tooling.preview.PreviewParameter +import androidx.compose.ui.tooling.preview.PreviewParameterProvider +import androidx.compose.ui.unit.TextUnit +import com.tangem.core.ui.extensions.TextReference +import com.tangem.core.ui.extensions.stringReference +import com.tangem.core.ui.res.TangemTheme +import com.tangem.core.ui.res.TangemThemePreview + +/** + * Simple text field for auto size input. + * Can display aligned placeholder. + * + * @param value initial text + * @param onValueChange callback + * @param isAutoResize is text font auto resize + * @param reduceFactor font resize factor + * @param textStyle text and placeholder styles + * @param textFieldModifier modifier for [SimpleTextField] + * @param boxModifier modifier for [BoxWithConstraints] + * @see [SimpleTextField] for other text field params + */ +@SuppressLint("UnusedBoxWithConstraintsScope") +@Composable +fun AutoSizeTextField( + value: String, + onValueChange: (String) -> Unit, + + // region AutoSize + isAutoResize: Boolean = true, + @FloatRange(from = 0.0, to = 1.0, fromInclusive = false, toInclusive = false) + reduceFactor: Double = 0.9, + + // region TextField + textFieldModifier: Modifier = Modifier, + boxModifier: Modifier = Modifier, + placeholder: TextReference? = null, + singleLine: Boolean = isAutoResize, + centered: Boolean = false, + visualTransformation: VisualTransformation = VisualTransformation.None, + keyboardOptions: KeyboardOptions = KeyboardOptions.Default, + keyboardActions: KeyboardActions = KeyboardActions.Default, + color: Color = TangemTheme.colors.text.primary1, + textStyle: TextStyle = TangemTheme.typography.body2.copy(color = color), + placeholderColor: Color = TangemTheme.colors.text.disabled, + readOnly: Boolean = false, + interactionSource: MutableInteractionSource = remember { MutableInteractionSource() }, + isValuePasted: Boolean = false, + onValuePastedTriggerDismiss: () -> Unit = {}, + decorationBox: (@Composable (innerTextField: @Composable () -> Unit) -> Unit)? = null, +) { + BoxWithConstraints(modifier = boxModifier) { + val fontSize = if (isAutoResize) { + resizeFont(visualTransformation, value, textStyle, reduceFactor) + } else { + textStyle.fontSize + } + val textColor = if (value.isBlank()) TangemTheme.colors.text.disabled else color + SimpleTextField( + value = value, + onValueChange = onValueChange, + textStyle = textStyle.copy( + fontSize = fontSize, + textDirection = TextDirection.ContentOrLtr, + ), + isValuePasted = isValuePasted, + onValuePastedTriggerDismiss = onValuePastedTriggerDismiss, + color = textColor, + keyboardOptions = keyboardOptions, + keyboardActions = keyboardActions, + placeholder = placeholder, + placeholderColor = placeholderColor, + singleLine = singleLine, + interactionSource = interactionSource, + readOnly = readOnly, + centered = centered, + visualTransformation = visualTransformation, + decorationBox = decorationBox, + modifier = textFieldModifier, + ) + } +} + +@Composable +internal fun BoxWithConstraintsScope.resizeFont( + visualTransformation: VisualTransformation, + value: String, + textStyle: TextStyle, + reduceFactor: Double, +): TextUnit { + var result = textStyle.fontSize + val calculateIntrinsics = @Composable { + val transformedText = visualTransformation.filter(AnnotatedString(value)).text.text + ParagraphIntrinsics( + text = transformedText, + style = textStyle.copy(fontSize = result), + density = LocalDensity.current, + fontFamilyResolver = createFontFamilyResolver(LocalContext.current), + ) + } + var intrinsics = calculateIntrinsics() + with(LocalDensity.current) { + while (intrinsics.maxIntrinsicWidth > maxWidth.toPx()) { + result *= reduceFactor + intrinsics = calculateIntrinsics() + } + } + return result +} + +// region preview +@Preview(widthDp = 360, showBackground = true) +@Composable +private fun AmountTextFieldPreview( + @PreviewParameter(AutoSizeTextFieldPreviewProvider::class) data: AutoSizeTextFieldPreviewData, +) { + var text by remember { mutableStateOf(data.value) } + TangemThemePreview { + AutoSizeTextField( + textFieldModifier = Modifier.fillMaxWidth(), + value = text, + onValueChange = { text = it }, + centered = data.centered, + isAutoResize = data.isAutoResize, + placeholder = data.placeholder, + ) + } +} + +private class AutoSizeTextFieldPreviewProvider : PreviewParameterProvider { + override val values = sequenceOf( + AutoSizeTextFieldPreviewData( + value = "AutoSizeTextField", + placeholder = stringReference("placeholder"), + isAutoResize = true, + centered = false, + ), + AutoSizeTextFieldPreviewData( + value = "AutoSizeTextFieldAutoSizeTextFieldAutoSizeTextFieldAutoSizeTextField", + placeholder = stringReference("placeholder"), + isAutoResize = true, + centered = false, + ), + AutoSizeTextFieldPreviewData( + value = "AutoSizeTextFieldAutoSizeTextFieldAutoSizeTextFieldAutoSizeTextField", + placeholder = stringReference("Placeholder"), + isAutoResize = true, + centered = false, + ), + AutoSizeTextFieldPreviewData( + value = "", + placeholder = stringReference("Placeholder"), + isAutoResize = true, + centered = false, + ), + AutoSizeTextFieldPreviewData( + value = "AutoSizeTextField", + placeholder = stringReference("Placeholder"), + isAutoResize = false, + centered = true, + ), + AutoSizeTextFieldPreviewData( + value = "AutoSizeTextFieldAutoSizeTextFieldAutoSizeTextFieldAutoSizeTextField", + placeholder = stringReference("Placeholder"), + isAutoResize = false, + centered = true, + ), + AutoSizeTextFieldPreviewData( + value = "", + placeholder = stringReference("Placeholder"), + isAutoResize = false, + centered = true, + ), + ) +} + +private data class AutoSizeTextFieldPreviewData( + val value: String, + val placeholder: TextReference, + val isAutoResize: Boolean, + val centered: Boolean, +) +// endregion \ No newline at end of file diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/fields/SimpleTextField.kt b/core/ui/src/main/java/com/tangem/core/ui/components/fields/SimpleTextField.kt index c37307f136..1616968df4 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/components/fields/SimpleTextField.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/components/fields/SimpleTextField.kt @@ -8,6 +8,7 @@ import androidx.compose.foundation.text.KeyboardActions import androidx.compose.foundation.text.KeyboardOptions import androidx.compose.material3.Text import androidx.compose.runtime.* +import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.focus.FocusRequester import androidx.compose.ui.focus.focusRequester @@ -17,6 +18,7 @@ import androidx.compose.ui.text.TextRange import androidx.compose.ui.text.TextStyle import androidx.compose.ui.text.input.TextFieldValue import androidx.compose.ui.text.input.VisualTransformation +import androidx.compose.ui.text.style.TextAlign import com.tangem.core.ui.extensions.TextReference import com.tangem.core.ui.extensions.resolveReference import com.tangem.core.ui.res.TangemTheme @@ -39,6 +41,7 @@ fun SimpleTextField( textStyle: TextStyle = TangemTheme.typography.body2.copy(color = color), placeholderColor: Color = TangemTheme.colors.text.disabled, readOnly: Boolean = false, + centered: Boolean = false, interactionSource: MutableInteractionSource = remember { MutableInteractionSource() }, isValuePasted: Boolean = false, onValuePastedTriggerDismiss: () -> Unit = {}, @@ -80,6 +83,8 @@ fun SimpleTextField( onValuePastedTriggerDismiss() } } + var textStyle = textStyle.copy(color = color) + if (centered) textStyle = textStyle.copy(textAlign = TextAlign.Center) BasicTextField( value = textFieldValue, @@ -91,7 +96,7 @@ fun SimpleTextField( if (stringChangedSinceLastInvocation) onValueChange(newTextFieldValueState.text) }, - textStyle = textStyle.copy(color = color), + textStyle = textStyle, cursorBrush = SolidColor(TangemTheme.colors.text.primary1), singleLine = singleLine, readOnly = readOnly, @@ -105,6 +110,7 @@ fun SimpleTextField( value = value, textStyle = textStyle, textValue = textValue, + centered = centered, color = placeholderColor, ) }, @@ -118,10 +124,11 @@ private fun SimpleTextPlaceholder( placeholder: TextReference?, value: String, textStyle: TextStyle, + centered: Boolean, textValue: @Composable () -> Unit, color: Color = TangemTheme.colors.text.disabled, ) { - Box { + Box(contentAlignment = if (centered) Alignment.Center else Alignment.TopStart) { if (value.isBlank() && placeholder != null) { AnimatedContent( targetState = placeholder, diff --git a/features/account/api/.gitignore b/features/account/api/.gitignore new file mode 100644 index 0000000000..42afabfd2a --- /dev/null +++ b/features/account/api/.gitignore @@ -0,0 +1 @@ +/build \ No newline at end of file diff --git a/features/account/api/build.gradle.kts b/features/account/api/build.gradle.kts new file mode 100644 index 0000000000..349aee77c0 --- /dev/null +++ b/features/account/api/build.gradle.kts @@ -0,0 +1,16 @@ +plugins { + alias(deps.plugins.android.library) + alias(deps.plugins.kotlin.android) + id("configuration") +} + +android { + namespace = "com.tangem.features.account.api" +} + +dependencies { + + /* Project - Core */ + implementation(projects.core.decompose) + implementation(projects.core.ui) +} \ No newline at end of file diff --git a/features/account/api/src/main/java/com/tangem/features/account/AccountCreateEditComponent.kt b/features/account/api/src/main/java/com/tangem/features/account/AccountCreateEditComponent.kt new file mode 100644 index 0000000000..1f6de5f0b2 --- /dev/null +++ b/features/account/api/src/main/java/com/tangem/features/account/AccountCreateEditComponent.kt @@ -0,0 +1,14 @@ +package com.tangem.features.account + +import com.tangem.core.decompose.factory.ComponentFactory +import com.tangem.core.ui.decompose.ComposableContentComponent + +interface AccountCreateEditComponent : ComposableContentComponent { + interface Factory : ComponentFactory + + sealed interface Params { + + data object Create : Params + data object Edit : Params + } +} \ No newline at end of file diff --git a/features/account/impl/.gitignore b/features/account/impl/.gitignore new file mode 100644 index 0000000000..42afabfd2a --- /dev/null +++ b/features/account/impl/.gitignore @@ -0,0 +1 @@ +/build \ No newline at end of file diff --git a/features/account/impl/build.gradle.kts b/features/account/impl/build.gradle.kts new file mode 100644 index 0000000000..55714f3021 --- /dev/null +++ b/features/account/impl/build.gradle.kts @@ -0,0 +1,59 @@ +plugins { + alias(deps.plugins.android.library) + alias(deps.plugins.kotlin.android) + alias(deps.plugins.kotlin.kapt) + alias(deps.plugins.kotlin.serialization) + alias(deps.plugins.hilt.android) + id("configuration") +} + +android { + namespace = "com.tangem.features.account.impl" +} + +dependencies { + /** Api */ + implementation(projects.features.account.api) + + /** Core modules */ + implementation(projects.core.analytics) + implementation(projects.core.analytics.models) + implementation(projects.core.utils) + implementation(projects.core.ui) + implementation(projects.core.res) + implementation(projects.core.decompose) + implementation(projects.core.navigation) + implementation(projects.core.datasource) + + /** Domain */ + implementation(projects.domain.models) + + /** Common */ + implementation(projects.common.ui) + implementation(projects.common.routing) + + /** AndroidX libraries */ + implementation(deps.androidx.core.ktx) + implementation(deps.lifecycle.runtime.ktx) + + /** Compose libraries */ + implementation(deps.compose.material3) + implementation(deps.compose.animation) + implementation(deps.compose.foundation) + implementation(deps.compose.ui) + implementation(deps.compose.ui.tooling) + implementation(deps.compose.coil) + implementation(deps.decompose.ext.compose) + implementation(deps.androidx.activity.compose) + + /** Other libraries */ + implementation(deps.arrow.core) + implementation(deps.kotlin.immutable.collections) + implementation(deps.kotlin.serialization) + implementation(deps.timber) + implementation(deps.firebase.crashlytics) + + /** DI */ + implementation(deps.hilt.android) + kapt(deps.hilt.kapt) +} \ No newline at end of file diff --git a/features/account/impl/src/main/java/com/tangem/features/account/createedit/AccountCreateEditModel.kt b/features/account/impl/src/main/java/com/tangem/features/account/createedit/AccountCreateEditModel.kt new file mode 100644 index 0000000000..872b7af23f --- /dev/null +++ b/features/account/impl/src/main/java/com/tangem/features/account/createedit/AccountCreateEditModel.kt @@ -0,0 +1,54 @@ +package com.tangem.features.account.createedit + +import com.tangem.core.decompose.di.ModelScoped +import com.tangem.core.decompose.model.Model +import com.tangem.core.decompose.model.ParamsContainer +import com.tangem.core.decompose.navigation.Router +import com.tangem.core.decompose.ui.UiMessageSender +import com.tangem.core.res.R +import com.tangem.core.ui.extensions.resourceReference +import com.tangem.core.ui.message.DialogMessage +import com.tangem.core.ui.message.EventMessageAction +import com.tangem.features.account.AccountCreateEditComponent +import com.tangem.features.account.createedit.entity.AccountCreateEditUM +import com.tangem.utils.coroutines.CoroutineDispatcherProvider +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow +import javax.inject.Inject + +@ModelScoped +internal class AccountCreateEditModel @Inject constructor( + paramsContainer: ParamsContainer, + private val messageSender: UiMessageSender, + private val router: Router, + override val dispatchers: CoroutineDispatcherProvider, +) : Model() { + + private val params = paramsContainer.require() + + val uiState: StateFlow + field = MutableStateFlow(TODO()) + + init { + params + } + + fun unsaveChangeDialog() { + val firstAction = EventMessageAction( + title = resourceReference(R.string.account_unsaved_dialog_action_first), + onClick = {}, + ) + val secondAction = EventMessageAction( + title = resourceReference(R.string.account_unsaved_dialog_action_second), + onClick = { router.pop() }, + ) + messageSender.send( + DialogMessage( + title = resourceReference(R.string.account_unsaved_dialog_title), + message = resourceReference(R.string.account_unsaved_dialog_message_create), + firstActionBuilder = { firstAction }, + secondActionBuilder = { secondAction }, + ), + ) + } +} \ No newline at end of file diff --git a/features/account/impl/src/main/java/com/tangem/features/account/createedit/DefaultAccountCreateEditComponent.kt b/features/account/impl/src/main/java/com/tangem/features/account/createedit/DefaultAccountCreateEditComponent.kt new file mode 100644 index 0000000000..06725717ad --- /dev/null +++ b/features/account/impl/src/main/java/com/tangem/features/account/createedit/DefaultAccountCreateEditComponent.kt @@ -0,0 +1,38 @@ +package com.tangem.features.account.createedit + +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.ui.Modifier +import androidx.lifecycle.compose.collectAsStateWithLifecycle +import com.tangem.core.decompose.context.AppComponentContext +import com.tangem.core.decompose.model.getOrCreateModel +import com.tangem.features.account.AccountCreateEditComponent +import com.tangem.features.account.createedit.ui.AccountCreateEditContent +import dagger.assisted.Assisted +import dagger.assisted.AssistedFactory +import dagger.assisted.AssistedInject + +internal class DefaultAccountCreateEditComponent @AssistedInject constructor( + @Assisted appComponentContext: AppComponentContext, + @Assisted params: AccountCreateEditComponent.Params, +) : AppComponentContext by appComponentContext, AccountCreateEditComponent { + + private val model: AccountCreateEditModel = getOrCreateModel(params) + + @Composable + override fun Content(modifier: Modifier) { + val state by model.uiState.collectAsStateWithLifecycle() + AccountCreateEditContent( + modifier = modifier, + state = state, + ) + } + + @AssistedFactory + interface Factory : AccountCreateEditComponent.Factory { + override fun create( + context: AppComponentContext, + params: AccountCreateEditComponent.Params, + ): DefaultAccountCreateEditComponent + } +} \ No newline at end of file diff --git a/features/account/impl/src/main/java/com/tangem/features/account/createedit/di/AccountCreateEditModule.kt b/features/account/impl/src/main/java/com/tangem/features/account/createedit/di/AccountCreateEditModule.kt new file mode 100644 index 0000000000..ab899b3f5c --- /dev/null +++ b/features/account/impl/src/main/java/com/tangem/features/account/createedit/di/AccountCreateEditModule.kt @@ -0,0 +1,27 @@ +package com.tangem.features.account.createedit.di + +import com.tangem.core.decompose.model.Model +import com.tangem.features.account.AccountCreateEditComponent +import com.tangem.features.account.createedit.AccountCreateEditModel +import com.tangem.features.account.createedit.DefaultAccountCreateEditComponent +import dagger.Binds +import dagger.Module +import dagger.hilt.InstallIn +import dagger.hilt.components.SingletonComponent +import dagger.multibindings.ClassKey +import dagger.multibindings.IntoMap + +@Module +@InstallIn(SingletonComponent::class) +internal interface AccountCreateEditModule { + + @Binds + fun bindAccountCreateEditComponentFactory( + impl: DefaultAccountCreateEditComponent.Factory, + ): AccountCreateEditComponent.Factory + + @Binds + @IntoMap + @ClassKey(AccountCreateEditModel::class) + fun bindAccountCreateEditModel(model: AccountCreateEditModel): Model +} \ No newline at end of file diff --git a/features/account/impl/src/main/java/com/tangem/features/account/createedit/entity/AccountCreateEditUM.kt b/features/account/impl/src/main/java/com/tangem/features/account/createedit/entity/AccountCreateEditUM.kt new file mode 100644 index 0000000000..4ef4877b97 --- /dev/null +++ b/features/account/impl/src/main/java/com/tangem/features/account/createedit/entity/AccountCreateEditUM.kt @@ -0,0 +1,42 @@ +package com.tangem.features.account.createedit.entity + +import com.tangem.core.ui.extensions.TextReference +import com.tangem.domain.models.account.CryptoPortfolioIcon +import kotlinx.collections.immutable.ImmutableList +import kotlinx.collections.immutable.persistentListOf + +data class AccountCreateEditUM( + val title: TextReference = TextReference.EMPTY, + val account: Account = Account(), + val colorsState: Colors, + val iconsState: Icons, + val buttonState: Button = Button(), + val onCloseClick: () -> Unit = {}, +) { + + data class Account( + val name: TextReference = TextReference.EMPTY, + val portfolioIcon: CryptoPortfolioIcon = CryptoPortfolioIcon.ofDefaultCustomAccount(), + val derivationInfo: TextReference = TextReference.EMPTY, + val inputPlaceholder: TextReference = TextReference.EMPTY, + val onNameChange: (String) -> Unit = {}, + ) + + data class Colors( + val selected: CryptoPortfolioIcon.Color, + val list: ImmutableList = persistentListOf(), + val onColorSelect: (CryptoPortfolioIcon.Color) -> Unit = {}, + ) + + data class Icons( + val selected: CryptoPortfolioIcon.Icon, + val list: ImmutableList = persistentListOf(), + val onIconSelect: (CryptoPortfolioIcon.Icon) -> Unit = {}, + ) + + data class Button( + val isButtonEnabled: Boolean = false, + val onConfirmClick: () -> Unit = {}, + val text: TextReference = TextReference.EMPTY, + ) +} \ No newline at end of file diff --git a/features/account/impl/src/main/java/com/tangem/features/account/createedit/ui/AccountCreateEditContent.kt b/features/account/impl/src/main/java/com/tangem/features/account/createedit/ui/AccountCreateEditContent.kt new file mode 100644 index 0000000000..dd927df0f0 --- /dev/null +++ b/features/account/impl/src/main/java/com/tangem/features/account/createedit/ui/AccountCreateEditContent.kt @@ -0,0 +1,344 @@ +package com.tangem.features.account.createedit.ui + +import android.content.res.Configuration +import androidx.compose.foundation.background +import androidx.compose.foundation.border +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.* +import androidx.compose.foundation.lazy.grid.GridCells +import androidx.compose.foundation.lazy.grid.LazyVerticalGrid +import androidx.compose.foundation.lazy.grid.itemsIndexed +import androidx.compose.foundation.shape.CircleShape +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.Icon +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.vector.ImageVector +import androidx.compose.ui.res.vectorResource +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.tooling.preview.PreviewParameter +import androidx.compose.ui.tooling.preview.datasource.CollectionPreviewParameterProvider +import androidx.compose.ui.unit.dp +import com.tangem.common.ui.R +import com.tangem.common.ui.account.getResId +import com.tangem.common.ui.account.getUiColor +import com.tangem.core.ui.components.PrimaryButton +import com.tangem.core.ui.components.SpacerH +import com.tangem.core.ui.components.SpacerH24 +import com.tangem.core.ui.components.SpacerH8 +import com.tangem.core.ui.components.appbar.AppBarWithBackButton +import com.tangem.core.ui.components.fields.AutoSizeTextField +import com.tangem.core.ui.extensions.resolveReference +import com.tangem.core.ui.extensions.stringReference +import com.tangem.core.ui.extensions.stringResourceSafe +import com.tangem.core.ui.res.TangemTheme +import com.tangem.core.ui.res.TangemThemePreview +import com.tangem.domain.models.account.CryptoPortfolioIcon +import com.tangem.features.account.createedit.entity.AccountCreateEditUM +import com.tangem.features.account.createedit.entity.AccountCreateEditUM.Account +import kotlinx.collections.immutable.toImmutableList + +@Suppress("LongMethod", "MagicNumber") +@Composable +internal fun AccountCreateEditContent(state: AccountCreateEditUM, modifier: Modifier = Modifier) { + Column( + modifier = modifier + .background(color = TangemTheme.colors.background.tertiary) + .fillMaxSize() + .imePadding() + .systemBarsPadding(), + horizontalAlignment = Alignment.CenterHorizontally, + ) { + AppBarWithBackButton( + text = state.title.resolveReference(), + onBackClick = state.onCloseClick, + iconRes = R.drawable.ic_close_24, + modifier = Modifier.height(TangemTheme.dimens.size56), + ) + + Column( + modifier = Modifier + .padding(horizontal = 16.dp) + .weight(1f), + + ) { + AccountSummary(state.account, state.account.onNameChange) + SpacerH24() + AccountColor(state.colorsState) + SpacerH24() + AccountIcon(state.iconsState) + SpacerH8() + Text( + modifier = Modifier.padding(horizontal = 8.dp), + text = state.account.derivationInfo.resolveReference(), + style = TangemTheme.typography.caption2, + color = TangemTheme.colors.text.tertiary, + ) + } + PrimaryButton( + modifier = Modifier + .fillMaxWidth() + .padding(16.dp), + enabled = state.buttonState.isButtonEnabled, + text = state.buttonState.text.resolveReference(), + onClick = state.buttonState.onConfirmClick, + ) + } +} + +@Composable +private fun AccountSummary(account: AccountCreateEditUM.Account, onNameChange: (String) -> Unit) { + Column( + modifier = Modifier + .clip(RoundedCornerShape(16.dp)) + .fillMaxWidth() + .background(TangemTheme.colors.background.action), + horizontalAlignment = Alignment.CenterHorizontally, + ) { + Spacer(modifier = Modifier.height(24.dp)) + + AccountIcon(account) + Spacer(modifier = Modifier.height(24.dp)) + + Text( + text = stringResourceSafe(R.string.account_form_name), + style = TangemTheme.typography.caption2, + color = TangemTheme.colors.text.tertiary, + ) + Spacer(modifier = Modifier.height(2.dp)) + + AutoSizeTextField( + centered = true, + textStyle = TangemTheme.typography.head, + placeholder = account.inputPlaceholder, + value = account.name.resolveReference(), + singleLine = true, + onValueChange = onNameChange, + ) + SpacerH(20.dp) + } +} + +@Composable +private fun AccountIcon(account: AccountCreateEditUM.Account) { + Box( + contentAlignment = Alignment.Center, + modifier = Modifier + .size(88.dp) + .clip(RoundedCornerShape(TangemTheme.dimens.radius24)) + .background(account.portfolioIcon.color.getUiColor()), + ) { + val icon = account.portfolioIcon.value + when { + icon == CryptoPortfolioIcon.Icon.Letter -> Text( + text = account.name.resolveReference().first().uppercase(), + style = TangemTheme.typography.head, + color = TangemTheme.colors.text.constantWhite, + ) + else -> Icon( + modifier = Modifier.size(44.dp), + tint = TangemTheme.colors.text.constantWhite, + imageVector = ImageVector.vectorResource(id = icon.getResId()), + contentDescription = null, + ) + } + } +} + +@Suppress("LongMethod", "MagicNumber") +@Composable +private fun AccountColor(colorsState: AccountCreateEditUM.Colors) { + Box( + Modifier + .clip(RoundedCornerShape(16.dp)) + .fillMaxWidth() + .background(TangemTheme.colors.background.action), + ) { + val columns = GridCells.Fixed(6) + val contentPadding = PaddingValues(horizontal = 8.dp, vertical = 12.dp) + LazyVerticalGrid( + columns = columns, + contentPadding = contentPadding, + horizontalArrangement = Arrangement.spacedBy(4.dp), + ) { + itemsIndexed(colorsState.list) { index, color -> + val isSelected = color == colorsState.selected + Box( + contentAlignment = Alignment.Center, + modifier = Modifier + .wrapContentSize() + .clickable(onClick = { colorsState.onColorSelect(color) }) + .size(48.dp), + ) { + if (isSelected) { + Box( + modifier = Modifier + .size(47.dp) + .border(2.dp, color.getUiColor(), shape = CircleShape), + ) + Box( + modifier = Modifier + .size(36.dp) + .background(color = color.getUiColor(), shape = CircleShape), + ) + } else { + Box( + modifier = Modifier + .size(40.dp) + .background(color = color.getUiColor(), shape = CircleShape), + ) + } + } + } + } + } +} + +@Suppress("LongMethod", "MagicNumber") +@Composable +private fun AccountIcon(iconsState: AccountCreateEditUM.Icons) { + Box( + Modifier + .clip(RoundedCornerShape(16.dp)) + .fillMaxWidth() + .background(TangemTheme.colors.background.action) + .padding(8.dp), + ) { + val columns = GridCells.Fixed(6) + LazyVerticalGrid( + columns = columns, + ) { + itemsIndexed(iconsState.list) { index, icon -> + val isSelected = icon == iconsState.selected + Box( + contentAlignment = Alignment.Center, + modifier = Modifier + .wrapContentSize() + .clickable(onClick = { iconsState.onIconSelect(icon) }) + .size(52.dp), + ) { + if (isSelected) { + val borderColor: Color + val iconTint: Color + val backgroundTint: Color + if (index == 0) { + borderColor = TangemTheme.colors.icon.accent + iconTint = TangemTheme.colors.icon.accent + backgroundTint = TangemTheme.colors.icon.accent.copy(alpha = 0.1f) + } else { + borderColor = TangemTheme.colors.icon.informative + iconTint = TangemTheme.colors.icon.secondary + backgroundTint = TangemTheme.colors.field.focused + } + Box( + contentAlignment = Alignment.Center, + modifier = Modifier + .size(44.dp) + .border(2.dp, borderColor, shape = CircleShape), + ) { + Box( + contentAlignment = Alignment.Center, + modifier = Modifier + .size(36.dp) + .background(color = backgroundTint, shape = CircleShape), + ) { + Icon( + imageVector = ImageVector.vectorResource(id = icon.getResId()), + contentDescription = null, + tint = iconTint, + ) + } + } + } else { + val iconTint: Color + val backgroundTint: Color + if (index == 0) { + iconTint = TangemTheme.colors.icon.accent + backgroundTint = TangemTheme.colors.icon.accent.copy(alpha = 0.1f) + } else { + iconTint = TangemTheme.colors.text.tertiary + backgroundTint = TangemTheme.colors.field.focused + } + Box( + contentAlignment = Alignment.Center, + modifier = Modifier + .size(40.dp) + .background(color = backgroundTint, shape = CircleShape), + ) { + Icon( + imageVector = ImageVector.vectorResource(id = icon.getResId()), + contentDescription = null, + tint = iconTint, + ) + } + } + } + } + } + } +} + +@Preview(showBackground = true, widthDp = 360) +@Preview(showBackground = true, widthDp = 360, uiMode = Configuration.UI_MODE_NIGHT_YES) +@Composable +private fun WcConnectionsContentPreview(@PreviewParameter(PreviewStateProvider::class) params: AccountCreateEditUM) { + TangemThemePreview { + AccountCreateEditContent(state = params) + } +} + +private class PreviewStateProvider : CollectionPreviewParameterProvider( + buildList { + val colors = CryptoPortfolioIcon.Color.entries.toImmutableList() + val icons = CryptoPortfolioIcon.Icon.entries.toImmutableList() + var portfolioIcon = CryptoPortfolioIcon.ofDefaultCustomAccount() + val first = AccountCreateEditUM( + title = stringReference("Add account"), + account = Account( + portfolioIcon = portfolioIcon, + derivationInfo = stringReference("Account #03 — used for address derivation."), + ), + colorsState = AccountCreateEditUM.Colors( + selected = portfolioIcon.color, + list = colors.toImmutableList(), + ), + iconsState = AccountCreateEditUM.Icons( + selected = portfolioIcon.value, + list = icons.toImmutableList(), + ), + buttonState = AccountCreateEditUM.Button( + text = stringReference("Add account"), + ), + ) + add(first) + + portfolioIcon = CryptoPortfolioIcon.ofCustomAccount( + value = CryptoPortfolioIcon.Icon.Letter, + color = CryptoPortfolioIcon.Color.entries.random(), + ) + val second = AccountCreateEditUM( + title = stringReference("Edit account"), + account = Account( + portfolioIcon = portfolioIcon, + name = stringReference("Main account"), + derivationInfo = stringReference("Account #03 — used for address derivation."), + ), + colorsState = AccountCreateEditUM.Colors( + selected = portfolioIcon.color, + list = colors.toImmutableList(), + ), + iconsState = AccountCreateEditUM.Icons( + selected = portfolioIcon.value, + list = icons.toImmutableList(), + ), + buttonState = AccountCreateEditUM.Button( + text = stringReference("Save"), + ), + ) + add(second) + }, +) \ No newline at end of file diff --git a/settings.gradle.kts b/settings.gradle.kts index bab69533de..2e4a4b6f20 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -269,6 +269,9 @@ include(":features:create-wallet-selection:impl") include(":features:welcome:api") include(":features:welcome:impl") + +include(":features:account:api") +include(":features:account:impl") // endregion Feature modules // region Domain modules