Updated on 2026-08-14

This commit is contained in:
Tangem 2025-08-06 17:09:04 +07:00
parent fd3d95097f
commit f6fad0e6d1
24 changed files with 870 additions and 25 deletions

View file

@ -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)

View file

@ -120,6 +120,7 @@
<string name="common_add_token">Token hinzufügen</string>
<string name="common_all">Alle</string>
<string name="common_allow">Erlauben</string>
<string name="common_amount">Betrag</string>
<string name="common_analytics">Analysen</string>
<string name="common_apply">Anwenden</string>
<string name="common_approval">Genehmigung</string>
@ -165,6 +166,7 @@
<string name="common_disabled">Deaktiviert</string>
<string name="common_disconnect">Trennen</string>
<string name="common_done">Erledigt</string>
<string name="common_edit">Bearbeiten</string>
<string name="common_enable">Aktivieren</string>
<string name="common_enabled">Aktiviert</string>
<string name="common_error">Fehler</string>

View file

@ -109,6 +109,7 @@
<string name="common_add_token">Agregar token</string>
<string name="common_all">Todos</string>
<string name="common_allow">Autorizar</string>
<string name="common_amount">Montante</string>
<string name="common_analytics">Analítica</string>
<string name="common_apply">Aplicar</string>
<string name="common_approval">Aprobación</string>
@ -153,6 +154,7 @@
<string name="common_disabled">Desactivado</string>
<string name="common_disconnect">Desconectar</string>
<string name="common_done">Listo</string>
<string name="common_edit">Editar</string>
<string name="common_enable">Activar</string>
<string name="common_enabled">Activado</string>
<string name="common_error">Error</string>

View file

@ -99,6 +99,7 @@
<string name="common_add_token">Ajouter un jeton</string>
<string name="common_all">Tous</string>
<string name="common_allow">Permettre</string>
<string name="common_amount">Montant</string>
<string name="common_analytics">Analytique</string>
<string name="common_apply">Appliquer</string>
<string name="common_approval">Approbation</string>
@ -140,6 +141,7 @@
<string name="common_disabled">Désactivé</string>
<string name="common_disconnect">Se déconnecter</string>
<string name="common_done">Exécuté</string>
<string name="common_edit">Modifier</string>
<string name="common_enable">Activer</string>
<string name="common_enabled">Activé</string>
<string name="common_error">Erreur</string>

View file

@ -5,6 +5,7 @@
<string name="alert_unsupported_card">Questa carta non è progettata per funzionare con Tangem</string>
<string name="cardano_low_ada">L\'importo inviato e il cambio non può essere inferiore a 1 ADA</string>
<string name="common_accept">Accetta</string>
<string name="common_amount">Importo</string>
<string name="common_balance">Saldo: %s</string>
<string name="common_build_tx_error">Impossibile creare la transazione</string>
<string name="common_camera_denied_alert_message">Non hai fornito l\'accesso alla tua videocamera, modifica le tue impostazioni sulla privacy</string>

View file

@ -12,6 +12,18 @@
<string name="access_code_create_description">ウォレットのロックを解除するには、 %s桁のアクセスコードを設定します。</string>
<string name="access_code_create_title">アクセスコードの作成</string>
<string name="access_code_navtitle">アクセスコード</string>
<string name="account_form_create_button">アカウントを追加</string>
<string name="account_form_edit_button">保存</string>
<string name="account_form_name">アカウント名</string>
<string name="account_form_placeholder_edit_account">アカウント</string>
<string name="account_form_placeholder_new_account">新しいアカウント</string>
<string name="account_form_title_create">アカウントを追加</string>
<string name="account_form_title_edit">アカウントを編集</string>
<string name="account_unsaved_dialog_action_first">編集を続ける</string>
<string name="account_unsaved_dialog_action_second">破棄</string>
<string name="account_unsaved_dialog_message_create">新しいアカウントを破棄してもよろしいですか?</string>
<string name="account_unsaved_dialog_message_edit">編集内容を破棄してもよろしいですか?</string>
<string name="account_unsaved_dialog_title">保存されていない変更</string>
<string name="action_buttons_buy_empty_search_message">トークンが見つかりませんか?メインページのマーケットセクションに移動し、ポートフォリオに追加して買付できるようにします。</string>
<string name="action_buttons_sell_empty_search_message">トークンが見つかりませんか?メインページのマーケットセクションに移動し、ポートフォリオに追加して売却できるようにします。</string>
<string name="action_buttons_sell_navigation_bar_title">売却</string>
@ -127,6 +139,7 @@
<string name="common_add_token">トークンを追加</string>
<string name="common_all">すべて</string>
<string name="common_allow">許可する</string>
<string name="common_amount">金額</string>
<string name="common_analytics">アナリティクス</string>
<string name="common_apply">適用する</string>
<string name="common_approval">承認</string>
@ -170,6 +183,7 @@
<string name="common_disabled">無効</string>
<string name="common_disconnect">切断</string>
<string name="common_done">完了</string>
<string name="common_edit">編集</string>
<string name="common_enable">有効にする</string>
<string name="common_enabled">有効</string>
<string name="common_error">エラー</string>
@ -1110,6 +1124,7 @@
<string name="swapping_fee_estimation_error_text">手数料見積りエラーです。サポートにフィードバックをお送りください。</string>
<string name="swapping_from_title">スワップする</string>
<string name="swapping_high_price_impact_description">選択したトークンをこの量を交換すると、価格に大きな影響が生じ、結果が減少します。</string>
<string name="swapping_high_price_impact_title">価格への影響が甚大です</string>
<string name="swapping_insufficient_funds">残高不足</string>
<string name="swapping_permission_header">許可を与える</string>
<string name="swapping_swap_action">スワップ</string>
@ -1285,6 +1300,7 @@
<string name="wallet_settings_access_code_description">このウォレットを保護するためのシークレットコードです。ログインと署名に使用されます。</string>
<string name="wallet_settings_access_code_title">アクセスコードの設定 / 変更</string>
<string name="wallet_settings_push_notifications_description">ウォレットの受信取引とTangemの更新について通知を受け取る。</string>
<string name="wallet_settings_push_notifications_huawei_warning">現在、Huaweiデバイスではプッシュ通知が機能しない可能性があります。現在、解決策の検討に取り組んでおり、今後のアップデートで修正をリリースする予定です。ご理解のほどよろしくお願いいたします。</string>
<string name="wallet_settings_push_notifications_title">取引通知</string>
<string name="wallet_settings_title">ウォレット設定</string>
<string name="wallet_title">Tangem</string>
@ -1426,15 +1442,19 @@
<string name="wc_disconnect_all_alert_title">すべてのdAppを接続解除する</string>
<string name="wc_estimated_wallet_changes">ウォレットの変更の予測</string>
<string name="wc_estimated_wallet_changes_not_simulated">取引をシミュレーションできませんでした。注意して続行してください。</string>
<string name="wc_estimation_is_not_supported">%sでは見積もりはサポートされていません</string>
<string name="wc_fee_suggested">%sによる提案</string>
<string name="wc_malicious_transaction">悪意のある取引</string>
<string name="wc_missing_required_network_description">このウォレットのポートフォリオに%sネットワークを追加します</string>
<string name="wc_missing_required_network_title">ウォレットに必要なネットワークはありません</string>
<string name="wc_new_connection">新しい接続</string>
<string name="wc_no_sessions_desc">ウォレットを別のdAppに接続する</string>
<string name="wc_no_sessions_title">セッションなし</string>
<string name="wc_no_wallet_changes_detected">ウォレットの変更は検出されませんでした</string>
<string name="wc_notification_security_risk_subtitle">このドメインは複数のセキュリティプロバイダーから安全でないとの警告を受けています。あなたの資産を守るため、直ちにアクセスを中止してください。</string>
<string name="wc_notification_security_risk_title">既知のセキュリティリスク</string>
<string name="wc_request_from">リクエスト元</string>
<string name="wc_send_anyway">とにかく送金</string>
<string name="wc_signature_type">署名タイプ</string>
<string name="wc_transaction_info_to_title">宛先</string>
<string name="wc_transaction_request">取引リクエスト</string>
@ -1443,6 +1463,7 @@
<string name="wc_uri_already_used_description">各ペアリング試行で、新しくユニークなURIが使用されていることを確認します</string>
<string name="wc_uri_already_used_title">URIはすでに使用されています</string>
<string name="wc_wallet_connect">ウォレットコネクト</string>
<string name="wc_warning_transaction">不審な取引</string>
<string name="welcome_interrupted_backup_alert_discard">破棄</string>
<string name="welcome_interrupted_backup_alert_message">バックアップが中断されました。再開しますか?</string>
<string name="welcome_interrupted_backup_alert_resume">はい、再開します</string>

View file

@ -96,6 +96,7 @@
<string name="common_add_token">Добавить токен</string>
<string name="common_all">Все</string>
<string name="common_allow">Разрешить</string>
<string name="common_amount">Сумма</string>
<string name="common_analytics">Аналитика</string>
<string name="common_apply">Применить</string>
<string name="common_approval">Одобрение</string>
@ -140,6 +141,7 @@
<string name="common_disabled">Отключено</string>
<string name="common_disconnect">Отключить</string>
<string name="common_done">Готово</string>
<string name="common_edit">Изменить</string>
<string name="common_enable">Включить</string>
<string name="common_enabled">Включено</string>
<string name="common_error">Ошибка</string>

View file

@ -96,6 +96,7 @@
<string name="common_add_token">Додати токен</string>
<string name="common_all">Усе</string>
<string name="common_allow">Дозволити</string>
<string name="common_amount">Сума</string>
<string name="common_analytics">Аналітика</string>
<string name="common_apply">Застосовувати</string>
<string name="common_approval">Затвердження</string>
@ -139,6 +140,7 @@
<string name="common_disabled">Вимкнуто</string>
<string name="common_disconnect">Від\'єднати</string>
<string name="common_done">Готово</string>
<string name="common_edit">Редагувати</string>
<string name="common_enable">Увімкнути</string>
<string name="common_enabled">Увімкнено</string>
<string name="common_error">Помилка</string>

View file

@ -40,6 +40,7 @@
<string name="card_settings_title">卡片設置</string>
<string name="cardano_low_ada">發送金額和找零不能少於1個ADA</string>
<string name="common_accept">接受</string>
<string name="common_amount">數量</string>
<string name="common_approve">允許</string>
<string name="common_attention">注意</string>
<string name="common_balance">餘額: %s</string>

View file

@ -12,7 +12,18 @@
<string name="access_code_create_description">Set a %s-digit Access Code to unlock your wallet.</string>
<string name="access_code_create_title">Create Access Code</string>
<string name="access_code_navtitle">Access code</string>
<string name="account_form_create_button">Add account</string>
<string name="account_form_edit_button">Save</string>
<string name="account_form_name">Account name</string>
<string name="account_form_placeholder_edit_account">Account</string>
<string name="account_form_placeholder_new_account">New account</string>
<string name="account_form_title_create">Add account</string>
<string name="account_form_title_edit">Edit account</string>
<string name="account_unsaved_dialog_action_first">Keep Editing</string>
<string name="account_unsaved_dialog_action_second">Discard</string>
<string name="account_unsaved_dialog_message_create">Are you sure you want to discard new account?</string>
<string name="account_unsaved_dialog_message_edit">Are you sure you want to discard edits?</string>
<string name="account_unsaved_dialog_title">Unsaved Changes</string>
<string name="action_buttons_buy_empty_search_message">Cant find your token? Go to the Market section on the main page and add it to your portfolio for purchase</string>
<string name="action_buttons_sell_empty_search_message">Cant find your token? Go to the Market section on the main page and add it to your portfolio for selling.</string>
<string name="action_buttons_sell_navigation_bar_title">Sell</string>
@ -132,6 +143,7 @@
<string name="common_add_token">Add token</string>
<string name="common_all">All</string>
<string name="common_allow">Allow</string>
<string name="common_amount">Amount</string>
<string name="common_analytics">Analytics</string>
<string name="common_apply">Apply</string>
<string name="common_approval">Approval</string>
@ -177,6 +189,7 @@
<string name="common_disabled">Disabled</string>
<string name="common_disconnect">Disconnect</string>
<string name="common_done">Done</string>
<string name="common_edit">Edit</string>
<string name="common_enable">Enable</string>
<string name="common_enabled">Enabled</string>
<string name="common_error">Error</string>
@ -432,6 +445,8 @@
<string name="hw_activation_need_finish">Finish Now</string>
<string name="hw_activation_need_title">Finish Wallet Activation</string>
<string name="hw_activation_need_warning_description">To complete setup, secure app access with Access Code.</string>
<string name="hw_backup_alert_description">If you do, you\'ll need to start over.</string>
<string name="hw_backup_alert_title">Are you sure you want to exit the activation process?</string>
<string name="hw_backup_google_drive_description">Recover an existing wallet stored in your Google Drive backup</string>
<string name="hw_backup_google_drive_title">Google Drive Backup</string>
<string name="hw_backup_need_action">Go to backup</string>
@ -1129,6 +1144,7 @@
<string name="swapping_fee_estimation_error_text">Fee estimation error. Please send feedback to support.</string>
<string name="swapping_from_title">You swap</string>
<string name="swapping_high_price_impact_description">Swapping this amount of selected tokens will cause a significant price impact and reduce your outcome.</string>
<string name="swapping_high_price_impact_title">High price impact</string>
<string name="swapping_insufficient_funds">Insufficient funds</string>
<string name="swapping_permission_header">Give Permission</string>
<string name="swapping_swap_action">Swap</string>
@ -1495,6 +1511,8 @@
<string name="wc_disconnect_all_alert_title">Disconect All dApps</string>
<string name="wc_estimated_wallet_changes">Estimated wallet changes</string>
<string name="wc_estimated_wallet_changes_not_simulated">The transaction couldn\'t be simulated. Please proceed with caution.</string>
<string name="wc_estimation_is_not_supported">Estimation is not supported for %s</string>
<string name="wc_fee_suggested">Suggested by %s</string>
<string name="wc_malicious_transaction">Malicious transaction</string>
<string name="wc_missing_required_network_description">Add the %s network to your portfolio for this wallet</string>
<string name="wc_missing_required_network_title">The wallet has no required networks</string>
@ -1505,6 +1523,7 @@
<string name="wc_notification_security_risk_subtitle">This domain is flagged as unsafe by multiple security providers. Leave immediately to protect your assets</string>
<string name="wc_notification_security_risk_title">Known security risk</string>
<string name="wc_request_from">Request from</string>
<string name="wc_send_anyway">Send anyway</string>
<string name="wc_signature_type">Signature Type</string>
<string name="wc_transaction_info_to_title">To</string>
<string name="wc_transaction_request">Transaction request</string>

View file

@ -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(

View file

@ -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<AutoSizeTextFieldPreviewData> {
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

View file

@ -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,

1
features/account/api/.gitignore vendored Normal file
View file

@ -0,0 +1 @@
/build

View file

@ -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)
}

View file

@ -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<Params, AccountCreateEditComponent>
sealed interface Params {
data object Create : Params
data object Edit : Params
}
}

1
features/account/impl/.gitignore vendored Normal file
View file

@ -0,0 +1 @@
/build

View file

@ -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)
}

View file

@ -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<AccountCreateEditComponent.Params>()
val uiState: StateFlow<AccountCreateEditUM>
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 },
),
)
}
}

View file

@ -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
}
}

View file

@ -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
}

View file

@ -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<CryptoPortfolioIcon.Color> = persistentListOf(),
val onColorSelect: (CryptoPortfolioIcon.Color) -> Unit = {},
)
data class Icons(
val selected: CryptoPortfolioIcon.Icon,
val list: ImmutableList<CryptoPortfolioIcon.Icon> = persistentListOf(),
val onIconSelect: (CryptoPortfolioIcon.Icon) -> Unit = {},
)
data class Button(
val isButtonEnabled: Boolean = false,
val onConfirmClick: () -> Unit = {},
val text: TextReference = TextReference.EMPTY,
)
}

View file

@ -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<AccountCreateEditUM>(
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)
},
)

View file

@ -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