diff --git a/core/res/src/main/res/values-de/strings.xml b/core/res/src/main/res/values-de/strings.xml index 07bf709771..6582af4d32 100644 --- a/core/res/src/main/res/values-de/strings.xml +++ b/core/res/src/main/res/values-de/strings.xml @@ -306,6 +306,7 @@ Stunde Importieren In Arbeit + Unzureichende Mittel Später Mehr erfahren %1$s übrig @@ -421,6 +422,7 @@ Token existiert bereits Dezimalzahl muss eine gültige Ganzzahl sein, bis zu %d Benutzerdefinierte Ableitung(derivation) + Die Funktion \"Dynamische Adressen\" ist aktiviert. Benutzerdefinierte \\"change\\" und \\"index\\" sind nicht verfügbar. E. g. m/00\'/0000\'/0\'/0/0 Benutzerdefinierte Ableitung (Derivation) eingeben Dezimalstellen @@ -473,6 +475,19 @@ Das Senden von Vermögenswerten in anderen Netzwerken führt zu dauerhaftem Verlust. %s Netzwerk Sende Geld nur mit + Dynamische Adressen + Dynamische Adressen ist aktiviert. Die benutzerdefinierten Funktionen \"Ändern\" und \"Index\" sind nicht verfügbar. + Einige Adressen fehlen + Verwenden Sie für jede Transaktion eine neue Adresse, um die Rückverfolgbarkeit zu verringern und den Datenschutz in der Kette zu verbessern. + Verbesserter Datenschutz + Einfacher Geldempfang in UTXO-basierten Netzwerken mit automatischer Adressgenerierung - keine manuelle Adressverwaltung erforderlich. + Nahtloser Empfang + Dynamische Adressen aktivieren + Bei dynamischen Adressen wird jedes Mal eine neue Adresse erstellt, um den Datenschutz zu gewährleisten - Ihr Gesamtguthaben bleibt gleich. + Dynamische Adressen können nicht aktiviert werden, da einige benutzerdefinierte Adressen/Tokens einen geänderten Ableitungspfad verwenden, der die erforderlichen Kriterien nicht erfüllt. + Nicht verfügbar + Wir können im Moment keine Verbindung zum Provider herstellen. Bitte versuchen Sie es später noch einmal. + Der Dienst ist nicht verfügbar. Bitte versuchen Sie es erneut. Beste Gelegenheiten Filter löschen Die Liste ist vorübergehend leer, da sie gerade aktualisiert wird. Schauen Sie in Kürze wieder rein. @@ -749,12 +764,18 @@ Die Daten dieses Abschnitts stammen aus den folgenden Netzwerken: %s Die Daten konnten nicht geladen werden... Keine Daten + **Hinzufügen zu Ihrem Portfolio**, um mit dem Kauf, Tausch oder Erhalt dieses Vermögenswerts zu beginnen + Zum Portfolio hinzufügen Marktimpuls Schnelle Aktionen + Alles löschen Markt durchsuchen + Neueste + In Ihrem Portfolio Ergebnis Token unter 100k USD Marktkapitalisierung anzeigen Token anzeigen + Krypto, Nachrichten und mehr Kein Ergebnis Netzwerk auswählen Wallet auswählen @@ -827,9 +848,11 @@ Position im Krypto-Rating zwischen allen Coins basierend auf der Marktkapitalisierung Marktposition Maximale Versorgung + Zirkulation und maximale Versorgung Die maximale Anzahl von Coins oder Tokens, die jemals für eine bestimmte Kryptowährung existieren können Maximale Versorgung Metriken + Nicht begrenzt Offizielle Links Preisleistung Aufbewahrungsort @@ -1104,10 +1127,14 @@ Einstellungen Du hast keinen Zugriff auf deine Kamera gewährt Kamerazugriff verweigert + Der angeforderte Token wurde Ihrer Wallet nicht hinzugefügt. Bitte fügen Sie ihn hinzu und versuchen Sie es erneut. + Token nicht hinzugefügt Dieser QR-Code konnte leider nicht erkannt werden. Unerkannter QR-Code Dieses Netzwerk wird von keinem der von Ihnen hinzugefügten Token unterstützt. Fügen Sie einen unterstützten Token hinzu, um Krypto zu senden. Keine unterstützten Token gefunden + Dieser QR-Code enthält Parameter, die nicht erkannt werden: %s. Einige Zahlungsdetails können verloren gehen, wenn Sie fortfahren. + Unbekannte Parameter Kein Memo erforderlich %1$s ( %2$s ) im %3$s Netzwerk %1$s im %2$s Netzwerk @@ -1222,6 +1249,10 @@ Memo: %s Ungültiges Memo Abdeckung der Netzgebühren + + %d Token ist mit dieser Adresse nicht kompatibel + %d -Tokens sind mit dieser Adresse nicht kompatibel + Nonce Eindeutige Nummer für jede Transaktion. Verwende diese Nummer, um eine ausstehende Transaktion erneut zu senden oder abzubrechen. Nonce eingeben… @@ -1489,14 +1520,18 @@ Fehler bei der Gebührenschätzung. Bitte sende dein Feedback an den Support. Du wechselst Der Tausch dieser Menge ausgewählter Token hat erhebliche Auswirkungen auf den Preis und verringert dein Ergebnis. + Aufgrund geringer Liquidität erhalten Sie möglicherweise deutlich weniger. Versuchen Sie es mit einem kleineren Betrag oder einem anderen Anbieter. Hoher Einfluss auf den Preis Unzureichende Mittel + Nicht genügend Geldmittel, um diese Transaktion abzuschließen. Verringern Sie den zu erhaltenden Betrag oder fügen Sie weitere Mittel hinzu. Erlaubnis erteilen Tauschen Tauschen... Du erhältst Token auswählen Nicht verfügbar + Nicht genug Liquidität für diesen Handel. Reduzieren Sie den Betrag oder wählen Sie einen anderen Anbieter. + Handel zu groß Wir freuen uns über Ihr Feedback Tangem Pay jetzt in der Beta Karte eingefroren @@ -1567,7 +1602,7 @@ Empfangen ist jetzt nicht verfügbar Aufdecken Details anzeigen - Tausche beliebige Vermögenswerte in Deinem Portfolio gegen eine Karte. + Tausche beliebige Vermögenswerte in Deinem Portfolio für deine Karte. Kartendetails Karte entsperren Komm zurück zur App, falls du es vergisst. @@ -1657,6 +1692,7 @@ Token in %%image%% %1$s Netzwerk Der %1$s (%2$s) Token ist die Hauptwährung im %3$s Netzwerk und kann nicht versteckt werden, solange du andere Token dieses Netzwerks in der Liste aktiv hast. %s kann nicht ausgeblendet werden + N / A QR-Code anzeigen Tausche diesen Token gegen einen anderen zu %1$s Servicegebühren von Februar %2$s-%3$s. Tausche mit Changelly, %s Gebühren @@ -1699,6 +1735,9 @@ Es ist ein Fehler aufgetreten. Fehlercode: %s. Bitte kontaktiere unseren Support. Verwende %s oder scanne eine Karte oder Ring, um Zugriff auf deine Wallet zu erhalten. Verbindung fehlgeschlagen: Diese dApp verwendet Wallet Connect Version 1.0, die nicht unterstützt wird. Bitte stelle sicher, dass die dApp Wallet Connect Version 2.0 unterstützt, um eine erfolgreiche Verbindung herzustellen. + Die vorherige Genehmigung wird widerrufen und eine neue erteilt. Das Netzwerk erhebt eine Token-Genehmigungsgebühr für jede dieser Aktionen. Als Beweis für den Widerruf wird in der Historie eine Transaktion mit einem Betrag von Null angezeigt. + Die Transaktion übersteigt den zuvor genehmigten Betrag.\nErlaubnis zum Fortfahren aktualisieren + Erlaubnis zur Aktualisierung Upgrade auf Hardware-Wallet Bleib auf dem Laufenden mit den neuesten Funktionen und Neuigkeiten Echtzeit-Warnungen für Transaktionen, Umtausch und wichtige Aktualisierungen. diff --git a/core/res/src/main/res/values-es/strings.xml b/core/res/src/main/res/values-es/strings.xml index b257cbe438..970e3f83af 100644 --- a/core/res/src/main/res/values-es/strings.xml +++ b/core/res/src/main/res/values-es/strings.xml @@ -161,7 +161,7 @@ Se requiere atención Se produjo un error al procesar tu código promocional. Inténtalo de nuevo más tarde. Error de activación - Tu código promocional se activó correctamente. Una recompensa se acreditará en tu cuenta dentro de 14 días. + Su código promocional se activó correctamente. Se acreditará la recompensa en su cuenta dentro de 14 días. Código promocional activado Este código promocional ya ha sido utilizado y no puede activarse de nuevo. Código no disponible @@ -306,6 +306,7 @@ hora Importe En progreso + Fondos insuficientes Más tarde Más información %1$s quedan @@ -326,6 +327,7 @@ NFT No Ninguna dirección + Sin resultados No agregadas No disponible Ahora no @@ -384,6 +386,7 @@ A A %s Hoy + Token para enviar %d token %d tokens @@ -396,6 +399,7 @@ Entiendo Entiendo, continuar Hubo un error. Por favor inténtelo de nuevo. + Desbloquear Inaccesible Termine el staking Debido a limitaciones sobre %1$s, solo %2$d UTXO pueden caber en una sola transacción. Esto significa que solo puedes enviar %3$s o menos. Debe reducir la cantidad. @@ -405,7 +409,7 @@ semana con - Modo de rendimiento + Modo de Rendimiento Dirección de contrato copiada Redes disponibles La derivación de su token coincide con la derivación de %1$s. Su token se añadirá a esta cuenta. @@ -418,6 +422,7 @@ Este token ya existe Los decimales deben ser un número entero válido, hasta %d Derivación personalizada + La opción Direcciones dinámicas está habilitada. Las opciones personalizadas \\"cambiar\\" e \\"índice\\" no están disponibles. Por ejemplo m/00\'/0000\'/0\'/0/0 Introduzca una derivación personalizada Decimales @@ -460,7 +465,7 @@ Firmado Enviar comentarios Detalles - Puedes tener solo una billetera móvil a la vez. Puede actualizarse a una billetera fría Tangem o usarse junto con una nueva billetera fría. + Puede tener solo una billetera móvil. Puede actualizarse a una billetera fría Tangem o añadir una billetera de hardware nueva. Compruebe su conexión a internet o cambia a una red diferente Condiciones de uso Dirección por defecto @@ -470,6 +475,19 @@ Enviar activos a otras redes resultará en una pérdida permanente. Red de %s Envíe fondos utilizando solo + Direcciones dinámicas + La opción Direcciones dinámicas está habilitada. Las opciones personalizadas \"cambiar\" e \"índice\" no están disponibles. + Direcciones dinámicas habilitadas + Utilice una nueva dirección para cada transacción para reducir la trazabilidad y mejorar la privacidad on-chain. + Privacidad mejorada + Reciba fondos fácilmente en redes basadas en UTXO con generación automática de direcciones, sin necesidad de gestión manual de direcciones. + Recepción sin problemas + Habilitar direcciones dinámicas + Las direcciones dinámicas crean una nueva cada vez para mayor privacidad - su saldo total no varía. + Las direcciones dinámicas no pueden activarse porque algunas direcciones/tokens personalizadas utilizan una ruta de derivación modificada, que no cumple los criterios requeridos. + Direcciones dinámicas no disponibles + No podemos conectar con el proveedor en este momento. Vuelva a intentarlo más tarde. + Servicio no disponible. Por favor, inténtelo de nuevo. Las mejores oportunidades Limpiar filtro La lista está temporalmente vacía porque se está actualizando. Vuelva a consultarla en un momento. @@ -478,9 +496,9 @@ Filtrar por Mis redes Redes - Mayormente usado + Popular Sin resultados - Ganar + Modo Staking & Rendimiento Hola equipo de soporte, he encontrado un error con el código: %s Error de WalletConnect Ha usado una tarjeta de otra billetera. Toque la tarjeta asociada con esta billetera @@ -653,7 +671,7 @@ ¿Está seguro que desea hacer esto? Olvidar billetera Ir a copia de seguridad - Ver copia de seguridad + Ver frase de recuperación Olvidar la billetera Olvidar de todos modos Esta billetera tiene una copia de seguridad. Asegúrese de poder recuperarla antes de olvidarla. @@ -689,6 +707,9 @@ Acerque para escanear Toque para firmar Coloque la tarjeta o el anillo + Su billetera está sincronizada y lista. \n Faltan algunos tokens? + Billetera importada correctamente + Restaurando %d%% Ha actualizado la biometría, escanee su tarjeta o anillo para entrar Su saldo debe ser mayor que el valor de la tarifa para hacer una transferencia Saldo insuficiente @@ -700,6 +721,7 @@ Nivel de Mana Para hacer un seguimiento de sus criptomonedas y transacciones, agregue tokens Gestionar tokens + Escanee el código QR para enviar fondos o conectarse a una aplicación Para acceder a todas las redes necesita escanear la tarjeta Escanee su tarjeta o anillo Disfrute de %1$s comisiones de servicio en los swaps a través de Changelly a partir de febrero %2$s-%3$s @@ -734,15 +756,22 @@ APY %s Mi portafolio Mercado - Gane con Tangem + Modo Staking & Rendimiento Para generar direcciones para las redes seleccionadas, debe escanear su tarjeta Tangem Para agregar tokens, abra esta página o pulse sobre la barra de búsqueda + Deslice el dedo hacia arriba para explorar el mercado + Descubre nuevas gemas ocultas Los datos de este apartado proceden de las siguientes redes: %s No se pueden cargar los datos… Sin datos + **Añadir a su portafolio** para empezar a comprar, intercambiar o recibir este activo + En su portafolio Análisis del Mercado Acciones rápidas + Borrar todo Buscar tokens + Recientes + En su portafolio Resultado Ver tokens con marketcap inferior a 100.000$ Mostrar tokens @@ -799,6 +828,10 @@ Titulares El cambio en el número de poseedores de tokens dentro de un período de tiempo específico Titulares + D + M + S + A Ideas Enlaces Liquidez @@ -814,9 +847,11 @@ Posición en la clasificación de criptomonedas entre todas las monedas según la capitalización de mercado Evaluación de mercado Suministro máximo + Suministro circulante y máximo La cantidad máxima de monedas o tokens que pueden existir para una criptomoneda en particular Suministro máximo Métrica + Sin limitación Enlaces oficiales Rendimiento de precios Repositorio @@ -826,9 +861,11 @@ Suministro total La cantidad máxima de monedas o tokens que pueden existir para una criptomoneda en particular Suministro total + 24h Volumen de operaciones (24 horas) La cantidad total de una criptomoneda que se ha negociado en las últimas 24 horas, lo que indica el nivel de actividad y liquidez en el mercado. Volumen de operaciones (24 horas) + %s en total Volumen Tire hacia arriba o toque la barra de búsqueda para agregar tokens directamente desde el mercado Agregar tokens @@ -852,6 +889,7 @@ Tokens relacionados Noticias relacionadas Manténgase informado + Puntuación de tendencia NFC no está disponible en su dispositivo Acerca de NFT Activo NFT @@ -909,8 +947,8 @@ Obtén $10 en BTC con cada billetera \n ¡Date prisa! Black Friday: hasta 30% DESCUENTO Vamos - Crea el par perfecto de packs de Tangem. Por tiempo limitado. - 1+1: Compra una billetera y obtén un 50% dto. en la segunda + ¡Tiempo limitado! + 1+1: Compra una billetera y obtenga un 50% dto. en la segunda Únase ahora Comparta su código y gane 5 USDT por venta. Su amigo obtiene un 10% de descuento. ¡Obtenga RECOMPENSAS por cada amigo! @@ -1088,6 +1126,14 @@ Ajustes No ha dado acceso a su cámara Acceso a la cámara denegado + El token solicitado no se ha añadido a su billetera. Por favor, añádalo e inténtelo de nuevo. + Token no añadido + Lo sentimos, no se ha podido reconocer este código QR. + Código QR no reconocido + Esta red no es compatible con ninguno de sus tokens añadidos. Añada un token compatible para enviar cripto. + No se encontraron tokens compatibles + Este código QR contiene parámetros que no son reconocidos: %s. Si continúa, es posible que se pierdan algunos detalles de pago. + Parámetros desconocidos No se requiere nota %1$s (%2$s) en la red %3$s %1$s en la red %2$s @@ -1164,7 +1210,7 @@ Ha especificado una comisión inferior a la cantidad recomendada, lo que podría provocar un retraso en su transacción. ¿Continuar? Razón: %1$s\nCódigo: %2$s La transacción está incompleta - Convertir a otro token + Cambiar a otro token o red Montante Se enviará al destinatario Puede establecer su tarifa de transacción ajustando el valor en el campo Satoshi por vByte. @@ -1202,6 +1248,10 @@ Memo: %s Memo no válido Cobertura de tarifa de red + + El token %d no es compatible con esta dirección + Los tokens %d no son compatibles con esta dirección + Nonce Número único para cada transacción. Utilícelo para reenviar o cancelar una transacción pendiente. Introduzca nonce... @@ -1229,6 +1279,11 @@ Limitación de transacciones Opcional Por favor, alinee su código QR con el cuadrado para escanearlo. Asegúrese de escanear la dirección de la red %s. + Al utilizar un tipo de cambio fijo, el importe que recibe queda garantizado en el momento de la operación. Esto le protege de las fluctuaciones de precio durante la transacción. + Tasa fija + Un tipo variable significa que la cantidad final que recibe puede variar ligeramente en función de las condiciones del mercado entre el momento en que inicia y finaliza el swap. + Tasa flotante + La tasa es fija Reciente Destinatario No es una dirección válida @@ -1272,6 +1327,7 @@ El destinatario recibe %s ¿Seguro que desea cancelar la conversión? Se borrarán sus datos anteriores. Eliminar conversión + Algo salió mal. Inténtelo de nuevo. Enviar con swap Transacción enviada Escanee la tarjeta/anillo que quiere configurar @@ -1463,14 +1519,18 @@ Error en la estimación de la tarifa. Envíe sus comentarios al servicio de asistencia. Usted intercambia Hacer un intercambio de esta cantidad del token seleccionado causará un impacto significativo en el precio y reducirá su resultado. + Es posible que reciba una cantidad significativamente menor debido a la baja liquidez. Pruebe con una cantidad menor o con otro proveedor. Alto impacto en los precios Fondos insuficientes + No hay fondos suficientes para completar esta transacción. Reduzca el importe a recibir o añada más fondos. Dar autorización Intercambiar Intercambiando... Usted recibe Elige token no disponible + No hay liquidez suficiente para esta operación. Reduzca el importe o elija otro proveedor. + Operación demasiado grande Nos encantaría recibir tus comentarios Tangem Pay ya está en beta Tarjeta congelada @@ -1509,6 +1569,7 @@ Ocultar verificación de la pantalla Agregar fondos Opciones de recarga + Añadir a Google Wallet Número de tarjeta Modificar PIN La tarjeta está completamente lista para pagos. @@ -1596,7 +1657,7 @@ Sesión expirada Restablecer acceso Usa USDC para pagos cotidianos - Tangem Pay temporalmente no disponible + Tangem Pay no está disponible temporalmente. Tangem Pay Haga clic en el botón de abajo para restaurar el acceso Tu saldo USDC Polygon on-chain difiere de tu saldo de tarjeta y se actualiza dentro de 2 días hábiles después de una compra. Los fondos de compras reembolsadas no se devolverán a tu saldo on-chain ni estarán disponibles para retiro, pero permanecerán en tu saldo de tarjeta para compras. @@ -1630,6 +1691,7 @@ Token en la red %%image%% %1$s El token %1$s (%2$s) es la moneda principal en la red %3$s y no se puede ocultar mientras tengas otros tokens de esta red en la lista No se puede ocultar %s + N/A Mostrar código QR Cambie este token por otro por una tarifa de servicio de %1$s del %2$s al %3$s de febrero. Intercambie con Changelly, %s comisiones @@ -1672,6 +1734,9 @@ Hemos encontrado un error. Código de error: %s. Póngase en contacto con nuestro servicio de soporte. Use %s o escanee una tarjeta/anillo para tener acceso a su billetera Error de conexión: Esta dApp utiliza la versión 1.0 de Wallet Connect, que no es compatible. Asegúrese de que la dApp sea compatible con la versión 2.0 de Wallet Connect para conectarse correctamente. + Se revocará el permiso anterior y se emitirá uno nuevo. La red cobrará una tarifa de aprobación de tokens por cada una de estas acciones. Verá una transacción de importe cero en el historial como comprobante de la revocación. + La transacción excede el límite de permisos otorgados previamente.\nActualice los permisos para continuar. + Actualizar permisos Actualización a billetera de hardware Manténgase actualizado con las últimas funciones y noticias Alertas en tiempo real de transacciones, intercambios y actualizaciones críticas. @@ -1842,7 +1907,7 @@ Este código de acceso protege su billetera y se utiliza para iniciar sesión y firmar transacciones. Establecer/Cambiar código de acceso Cambiar código de acceso - Manténgase informado sobre las transacciones entrantes de la billetera y las actualizaciones de Tangem. + Reciba notificaciones push para las transacciones entrantes en su billetera. Es posible que las notificaciones push no funcionen actualmente en dispositivos Huawei. Estamos trabajando activamente en una solución y la publicaremos en una próxima actualización. ¡Gracias por su comprensión! Notificaciones de transacciones Establecer código de acceso @@ -1870,7 +1935,7 @@ Según la documentación oficial de Clore, todas las monedas recibidas antes del 21 de diciembre serán migradas a Clore (token ERC-20); las monedas recibidas después de esa fecha no lo serán. Se está desarrollando una solución de transferencia — mantente atento. Mensaje Abrir el portal de reclamaciones - Para mantener el acceso a sus fondos, comience la migración de acuerdo con las pautas oficiales de Clore. + Para continuar utilizando sus tokens de Clore, debe completar la migración de tokens de acuerdo con la información del Portal de Reclamación. Migración de la red Clore Firmar Firma @@ -1925,7 +1990,7 @@ La red no está disponible actualmente. Por favor, inténtalo de nuevo más tarde. La red no está disponible Recargue su billetera - Su billetera no ha sido respaldado. Realice este procedimiento para proteger sus activos ahora. + Su billetera no tiene aún una copia de seguridad. Hágala ahora para proteger sus activos. Falta backup Esta tarjeta se ha utilizado previamente para transacciones. Si la recibió de una fuente no confiable, considere retirar todos los fondos. Si es su tarjeta, no se requiere ninguna acción. La tarjeta ya ha firmado transacciones @@ -2099,7 +2164,7 @@ La tasa de red es actualmente demasiado alta para ejecutar préstamos. Los fondos se suministrarán una vez que baje a %1$s o menos. Mis fondos Su %1$s está ahora depositado en Aave y devenga intereses. Tiene un token%2$s, que representa su saldo y crece con el tiempo. Cuando deposite más fondos, estos se suministrarán a Aave para ganar intereses, menos una comisión por transacción. - Modo de rendimiento + Modo Rendimiento Ganancias totales Transferencias a Aave Explore Aave @@ -2111,7 +2176,7 @@ Tarifa máxima El monto mínimo se calcula en función de la comisión de la red actual para que no supere el 4%% del importe de la recarga, lo que hace que el mínimo sea %1$s (%2$s). Recarga mínima - Política de tarifas + Política de tarifas por recarga Tangem también cobra una comisión de servicio del 15% sobre el rendimiento obtenido. Sus fondos se suministrarán automáticamente a Aave una vez que las comisiones de red sean más bajas o su saldo alcance el importe mínimo requerido. Las comisiones son más altas de lo habitual debido a la alta actividad del mercado. Puede continuar ahora o volver a consultar más tarde cuando las comisiones sean más bajas. @@ -2131,7 +2196,7 @@ Aave es un protocolo on-chain para crear mercados de liquidez no custodiales y ganar intereses a tasa variable. Descentralizado y autocustodiado Al utilizar este servicio, usted acepta que el proveedor\n%1$s y %2$s - Conectar Aave + Conectar con Aave Consiga un %1$s%% APY\nen su saldo Aave %1$s%% - Tipo de interés variable Tipo de interés variable @@ -2144,7 +2209,7 @@ Cuando recargue, sus fondos se enviarán automáticamente a Aave para comenzar a ganar intereses. %s se deducirá para cubrir la tarifa de transacción. Suministro de activos Su %s se suministrará a Aave, pero seguirá siendo gestionable. - Ver política de tarifas + Ver política de tarifas por recarga Sus próximas recargas se suministrarán automáticamente a Aave. Todos tus futuros depósitos %1$s se suministrarán automáticamente a Aave. Activo @@ -2158,8 +2223,8 @@ APY Los intereses se devengan automáticamente Los intereses se devengan automáticamente - Modo de rendimiento - Procesando su depósito + Modo Rendimiento + Activando Modo Rendimiento Modo Rendimiento Implementación del contrato del Modo Rendimiento Modo Rendimiento activado diff --git a/core/res/src/main/res/values-fr/strings.xml b/core/res/src/main/res/values-fr/strings.xml index 9075ee073a..5cfb59bdbc 100644 --- a/core/res/src/main/res/values-fr/strings.xml +++ b/core/res/src/main/res/values-fr/strings.xml @@ -1086,6 +1086,14 @@ Paramètres Vous n\'avez pas donné accès à votre caméra Accès à la caméra refusé + Le jeton demandé n\'est pas ajouté à votre portefeuille. Veuillez l\'ajouter, puis réessayez. + Le jeton n\'a pas été ajouté + Désolé, ce code QR n\'a pas pu être reconnu. + Code QR non reconnu + Ce réseau n\'est pris en charge par aucun des jetons que vous avez ajoutés. Ajoutez un jeton pris en charge pour envoyer des cryptomonnaies. + Aucun jeton pris en charge n\'a été trouvé + Ce code QR contient des paramètres non reconnus : %s. Si vous continuez, certaines informations de paiement risquent d\'être perdues. + Paramètres inconnus Aucun mémo requis %1$s (%2$s) sur le réseau %3$s %1$s sur le réseau %2$s diff --git a/core/res/src/main/res/values-ja/strings.xml b/core/res/src/main/res/values-ja/strings.xml index 404b67969c..c2dfddaf6f 100644 --- a/core/res/src/main/res/values-ja/strings.xml +++ b/core/res/src/main/res/values-ja/strings.xml @@ -415,6 +415,7 @@ トークンはすでに存在します 小数は%dまでの有効な整数である必要があります カスタム派生パス + 動的アドレスが有効です。カスタムの「change」と「index」は利用できません。 例:m/00\'/0000\'/0\'/0/0 カスタム派生パスを入力 小数 @@ -756,11 +757,13 @@ このセクションのデータは、次のネットワークから取得されています: %s データを読み込めません… データなし + **ポートフォリオに追加して**、この資産の買付・交換・受け取りを始めましょう + ポートフォリオ内 マーケット動向 クイックアクション すべてクリア トークンを探す - 最近 + 最近の検索 ポートフォリオ内 結果 時価総額10万ドル以下のトークンを見る @@ -1712,6 +1715,9 @@ エラーが発生しました。エラー コード:%s。サポートにお問い合わせください。 %sを使用するか、カード / リングをスキャンしてウォレットにアクセスしてください 接続に失敗しました:このdAppは、サポートされていないWallet Connectバージョン1.0を使用しています。正常に接続するには、dAppがWallet Connectバージョン2.0をサポートしていることを確認してください。 + 以前の承認は取り消され、新しい承認が発行されます。これらの各操作には、ネットワークによるトークン承認手数料がかかります。履歴には、取り消しの証拠として金額0の取引が表示されます。 + 取引金額が、以前に許可された承認額を超えています。\n続行するには、承認内容を更新してください。 + 承認を更新 ハードウェアウォレットにアップグレード 最新の機能とニュースをお届けします 取引・スワップ・重要な更新に関するリアルタイム通知。 diff --git a/core/res/src/main/res/values-pt-rBR/strings.xml b/core/res/src/main/res/values-pt-rBR/strings.xml index 94c47b4783..e931a23bdc 100644 --- a/core/res/src/main/res/values-pt-rBR/strings.xml +++ b/core/res/src/main/res/values-pt-rBR/strings.xml @@ -422,6 +422,7 @@ O token já existe. O valor decimal deve ser um número inteiro válido, até um limite de %d Derivação personalizada + O recurso de Endereços Dinâmicos está ativado. As opções personalizadas \"alterar\" e \"indexar\" não estão disponíveis. Exemplo: m/00\'/0000\'/0\'/0/0 Insira uma derivação personalizada Decimais @@ -474,6 +475,19 @@ O envio de ativos por outras redes resultará em perda permanente. %s rede Envie fundos usando apenas + Endereços dinâmicos + O recurso de Endereços Dinâmicos está ativado. As opções personalizadas \"alterar\" e \"indexar\" não estão disponíveis. + Endereços dinâmicos ativados + Use um novo endereço para cada transação para reduzir a rastreabilidade e melhorar a privacidade na blockchain. + Privacidade aprimorada + Receba fundos facilmente em redes baseadas em UTXO com geração automática de endereços — sem necessidade de gerenciamento manual de endereços. + Recebimento perfeito + Habilitar endereços dinâmicos + Endereços dinâmicos criam um novo a cada vez para maior privacidade — seu saldo total permanece o mesmo. + Não é possível ativar os endereços dinâmicos porque alguns endereços/tokens personalizados usam um caminho de derivação modificado, que não atende aos critérios necessários. + Endereços dinâmicos indisponíveis + Não foi possível conectar-se ao provedor neste momento. Tente novamente mais tarde. + Serviço indisponível. Por favor, tente novamente. Melhores oportunidades Limpar filtro A lista está temporariamente vazia, pois está sendo atualizada. Volte daqui a pouco. @@ -750,9 +764,14 @@ Os dados desta seção são provenientes das seguintes redes: %s Não foi possível carregar os dados… Sem dados + **Adicione ao seu portfólio** para começar a comprar, trocar ou receber este ativo. + Em seu portfólio Pulso do mercado Ações rápidas + Limpar tudo Pesquisar tokens + Recentes + Em seu portfólio Resultado Veja tokens com capitalização de mercado inferior a 100 mil dólares. Mostrar tokens @@ -1114,6 +1133,8 @@ Código QR não reconhecido Esta rede não é compatível com nenhum dos tokens adicionados. Adicione um token compatível para enviar criptomoedas. Nenhum token compatível encontrado + Este código QR contém parâmetros que não são reconhecidos: %sAlgumas informações de pagamento podem ser perdidas se você continuar. + Parâmetros desconhecidos Não é necessário memorando %1$s (%2$s) sobre %3$s rede %1$s sobre %2$s rede @@ -1499,6 +1520,7 @@ Erro na estimativa de custos. Por favor, envie seu feedback para o suporte. Você troca Trocar essa quantidade de tokens selecionados causará um impacto significativo no preço e reduzirá seu resultado. + Você pode receber um valor significativamente menor devido à baixa liquidez. Tente um valor menor ou outro provedor. Alto impacto nos preços Fundos insuficientes Não há fundos suficientes para concluir esta transação. Reduza o valor a receber ou adicione mais fundos. @@ -1508,6 +1530,8 @@ Você recebe Escolha o token não disponível + Liquidez insuficiente para esta transação. \n o valor ou escolha outro provedor. + Negociação em grande escala Teremos todo o prazer em receber seu feedback. O Tangem Pay agora está em versão beta. Cartão bloqueado @@ -1711,6 +1735,9 @@ Ocorreu um erro. Código do erro: %sPor favor, entre em contato com nosso suporte. Usar %s ou escaneie um cartão/anel para acessar sua carteira. Falha na conexão: Este aplicativo descentralizado (dApp) usa a versão 1.0 do Wallet Connect, que não é compatível. Certifique-se de que o dApp seja compatível com a versão 2.0 do Wallet Connect para que a conexão seja estabelecida com sucesso. + A permissão anterior será revogada e uma nova será emitida. A rede cobrará uma taxa de aprovação de tokens para cada uma dessas ações. Você verá uma transação de valor zero no histórico como comprovante de revogação. + A transação excede o limite de permissão concedido anteriormente.\nAtualize a permissão para prosseguir. + Atualizar permissão Faça upgrade para uma carteira de hardware. Fique por dentro das últimas novidades e recursos. Alertas em tempo real para transações, câmbio e atualizações críticas. 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 8df7a978d6..ffcd7afc54 100644 --- a/core/res/src/main/res/values-uk-rUA/strings.xml +++ b/core/res/src/main/res/values-uk-rUA/strings.xml @@ -1124,6 +1124,14 @@ Налаштування Ви не надали доступ до своєї камери Доступ до камери заборонено + Запитуваний токен не додано до вашого гаманця. Будь ласка, додайте його та спробуйте ще раз. + Токен не додано + На жаль, цей QR-код не вдалося розпізнати. + Нерозпізнаний QR-код + Ця мережа не підтримується жодним із ваших доданих токенів. Додайте підтримуваний токен, щоб надіслати криптовалюту. + Підтримуваних токенів не знайдено + Цей QR-код містить нерозпізнані параметри: %s. Деякі деталі платежу можуть бути втрачені, якщо ви продовжите. + Невідомі параметри Memo не вимагається %1$s (%2$s) у мережі %3$s %1$s у мережі %2$s diff --git a/core/res/src/main/res/values-zh-rCN/strings.xml b/core/res/src/main/res/values-zh-rCN/strings.xml index 9a07214403..13a1972ba4 100644 --- a/core/res/src/main/res/values-zh-rCN/strings.xml +++ b/core/res/src/main/res/values-zh-rCN/strings.xml @@ -1,19 +1,650 @@ - 添加代幣 - 沒有結果 - 要發送的代幣 - 解鎖 - 您的錢包已同步並準備就緒。\n缺少某些代幣? - 錢包導入成功 - 恢復%d%% + 更改代码 + 您的访问代码可解锁和保护您的钱包访问权限 + 无论如何都要用 + 这个访问码很容易猜到。 + 您不能创建超过 %1$s 账户。存档一个账户以添加新账户。 + 无法添加新帐户 + 账户已存档 + 已存档账户 + 您即将恢复 \"%1$s\"。 + 恢复账户 + 您已达到20个活跃账户的上限。请归档一个账户以恢复此账户。 + 无法恢复帐户 + 我们无法存档账户。请稍后再试。 + 此账户正在参与推荐计划。 + 此帐户无法存档 + 我们无法创建账户。请稍后再试。 + 账户已创建 + 存档... + 无法编辑帐户 + 账户已保存 + 奖励账户 + 已存在使用此名称的帐户。请选择其他名称。 + 帐户名已被使用 + 请稍后再试。如果问题仍然存在,请联系客服,我们将协助您解决问题。 + %1$s 在 %2$s + 主账户 + 您已超过 %1$s 活动账户的限制。存档一个以恢复 + 无法恢复帐户 + 账户已恢复 + 长按账户可重新排序 + 一些自定义代币将自动从 \"%1$s\" 移至 \"%2$s\",因为它们的派生词属于该账户。 + 部分自定义代币将自动移动 + 生物识别出现问题。请尝试重置设备上的生物识别技术或联系技术支持。 + 验证错误 + 设置访问码以启用生物识别功能 + 稍后会要求您提供访问代码以进行安全存储。 + 这将删除所有已保存的钱包访问码。您需要重新输入访问码才能使用钱包。 + 您的设备已关闭生物识别功能,因此无法使用此功能解锁钱包。请在设备设置中启用生物识别功能,即可再次使用此方法。 + 生物识别认证已禁用 + 您的生物识别尝试次数已达上限。请使用卡片/指环解锁钱包,或输入您的访问码。 + 生物识别认证已锁定 + 生物识别登录暂时锁定。请30秒后重试,或轻触设备或输入访问码解锁您的钱包。 + 您设备上的生物识别信息已更新。请选择您的钱包并输入其访问码以重新启用生物识别登录。 + 需要注意 + 处理您的优惠码时出错,请稍后再试。 + 激活错误 + 您的优惠码已成功激活。奖励将在14天内计入您的比特币账户。 + 促销代码已激活 + 此优惠码已被使用,无法再次激活。 + 代码不可用 + 此优惠码无效,无法激活。 + 无效代码 + 您需要提供比特币地址才能领取奖励。请将比特币地址添加到您的钱包,然后重试激活。 + 需要比特币地址 + 请重置下一个设备以继续。 + 钱包重置 + 所有 Tangem 设备均已重置。您现在可以继续升级钱包。 + 再次升级 + 重置完成 + 我们建议在此钱包中完成所有 Tangem 设备的重置过程。 + 部分 Tangem 设备仍需重置。 + 账户 + 账户 + 激活 + 添加 + 添加代币 + 已添加 + + 选择账户 + 联系客服 + 充值网络费 + 忘记 + + 从 %s + 开始 + 获取代币 + 保持到 %s + 余额不足 + 了解更多 + 传统比特币 + 锁定的钱包 + + %d网络 + + 新地址 + 新闻 + 无结果 + 不可用 + 或者 + 推荐 + 重置 + 出问题了 + Tangem + 点击并按住 + + 到 %s + 要发送的代币 + 无法加载数据…… + 我明白,请继续 + 解锁 + 钱包 + 收益模式 + 您的代币派生信息与 %1$s的派生信息一致。 您的代币将被添加到该账户。 + 派生属于另一个账户。 + 代币已添加到 %1$s 帐户 + 动态地址已启用。自定义\“更改”\和\“索引”\不可用。 + 升级到硬件钱包 + 您只能拥有一个手机钱包。将其升级为 Tangem 硬件钱包,或添加一个新的硬件钱包。 + 默认地址 + 传统 %s 地址 + %s 地址 + 动态地址 + 动态地址已启用。自定义“更改”和“索引”功能不可用。 + 已启用动态地址 + 每次交易都使用一个新地址,以减少可追溯性并提高链上隐私性。 + 增强隐私 + 通过自动生成地址,轻松在基于UTXO的网络中接收资金——无需手动管理地址。 + 无缝接收 + 启用动态地址 + 动态地址每次都会创建一个新地址,以增强隐私保护——您的总余额保持不变。 + 无法启用动态地址,因为某些自定义地址/代币使用的是修改后的派生路径,不符合所需的标准。 + 动态地址不可用 + 我们目前无法连接到服务提供商,请稍后再试。 + 服务不可用,请稍后再试。 + 最佳机会 + 清除筛选 + 列表正在刷新,暂时为空。请稍后再查看。 + 所有网络 + 所有类型 + 筛选方式 + 我的网络 + 网络 + 热门 + 无结果 + 质押与收益模式 + 将您投资组合中的任何资产交换为该代币 + 有竞争力的费率 + 速度越快,确认速度越快,但网络费用也越高。 %s + 选择速度 + 选择用于支付网络费用的代币。 %s + 选择代币 + 市场与新闻 + Tangem AI + 当下热门 + 备份问题 + 资金不足 + 转账费 + 地址直接在您的 Tangem 硬件钱包上生成,随时可用,并受到全面保护。 + 新地址 + 添加 Tangem 钱包 + 您的私钥将直接在 Tangem 卡内生成,并且永远不会离开该卡。 + 密钥生成 + 所有加密操作都在安全芯片内部进行,该芯片经过认证,可防止克隆和物理篡改。 + 硬件级安全 + 您是否允许“Tangem”使用生物识别认证来确认您的身份并打开应用程序? + 让您的加密货币安全离线存储。轻薄如信用卡,安全胜过银行金库。 + 创建一个安全的钱包并转移资金,以加强保护。 + 创建新钱包 + 使用卓越的 Tangem 硬件钱包,提升您的安全保障。 + 硬件钱包 + 将您当前的手机钱包转换为 Tangem 冷钱包。 + 升级当前钱包 + 先完成备份 + 其他方法 + 将您的恢复短语保存在安全的地方,保持其私密性以保护您的资金,并设置访问密码以提高安全性。 + 要使用访问码保护您的钱包,请完成备份过程。 + 要升级到硬件钱包,请完成备份过程。 + 将您的手机钱包升级到 Tangem 硬件钱包,享受最高级别的安全性。导入您的钱包或将资金转移到新钱包。 + 升级到冷钱包 + 随时安全地将手机钱包转移到 Tangem 卡片或指环上。 + 升级到硬件钱包 + 导入现有钱包 + 此恢复短语已导入 + 忘记钱包 + 此钱包将从您的设备中永久移除。 + 你确定要这么做吗? + 忘记钱包 + 转到备份 + 查看恢复短语 + 忘记钱包 + 确定忘记 + 这个钱包有备份。在忘记钱包之前,请确保你能找回备份。 + 如果您忘记了这个钱包而又没有备份,您将永远无法使用您的资金。 + 你确定要忘记这个钱包吗? + 我知道,如果我在移除钱包之前没有备份钱包,我将无法访问钱包。 + 我知道移除钱包并不会将其删除,只会将其从我的设备中移除。 + 升级 + 当您将手机钱包升级为 Tangem 卡或指环时,您的所有地址和余额都可以完全访问。 + 相同地址 + 无法升级。此设备上已有一个钱包。 + 请换一台设备。这台设备不能用于升级。 + 操作过程中发生错误。 + 在此过程中,您的资金将保持安全且完全可用。 + 获得资金 + 升级后,您的移动钱包将从应用程序中移除并存储在您的 Tangem 硬件钱包中。您的助记词将保留在您身边。 + 一般安全 + 私钥将从应用程序转移到您的 Tangem 硬件钱包。 + 私钥迁移 + 扫描设备 + 开始升级 + 您即将升级到我们的硬件钱包。它会将您的资产安全地存储在冷存储中。 + Tangem钱包 + 升级到我们的硬件钱包 + 使用 Tangem 的顶级硬件钱包,确保您的加密货币安全。 + 将您的钱包升级到硬件安全级别 + 您的钱包已同步并准备就绪。\n缺少某些代币? + 钱包已成功导入 + 恢复 %d%% + 扫描二维码即可发送资金或连接到应用程序 + 关于硬币 + 质押与收益模式 + 向上轻扫,探索市场 + 寻找新的隐藏宝石 + **添加到您的投资组合**,即可开始购买、交换或接收此资产 + 在您的投资组合中 + 市场脉搏 + 全部清除 + 最近的 + 在您的投资组合中 + 加密货币、新闻及更多 + 收益模式 + 代币已添加 - + - 使用固定利率時,您在置換交易時收到的金額將被鎖定,這可以保護您免受交易過程中價格波動的影響。 + 循环和最大供应 + 无限制 + 24小时 + %s 总共 + 添加更多代币 + 增强资产性能,并使其能够即时访问。 %s + 激活收益模式 + 创建手机钱包前必须更新至 %1$s + 手机钱包需要 %1$s 或更高版本 + 所有新闻 + 喜欢 + + 小时之前 + + + 分钟之前 + + 快速回顾 + 新闻 + 相关代币 + 相关新闻 + 随时了解最新动态 + 趋势得分 + 每个钱包均可获得价值 10 美元的 BTC \n快来领取! + 黑色星期五:最多可省 30 美元% + 我们出发吧 + 限时优惠! + 1+1:买一个钱包,优惠 50% + 购买加密货币 + 通过 SEPA 转账购买加密货币时,可享受 ** 0% 费用**。 + 使用 SEPA 购买加密货币 + 加入候补名单,即可获得一张与众不同的支付卡。 + Tangem Visa卡 + 条款和条件 + 存款 100 美元以上,持有 30 天,即可获得 10 美元 + 加入 \"收益模式 \"活动 + 生物识别 + 您只能拥有一个手机钱包。将其升级为 Tangem 硬件钱包,或添加一个新的硬件钱包。 + 所有优惠 + 此交易已处理完毕,无需进一步操作。 + 获得最佳利率... + 即时 + 服务由外部供应商提供。\nTangem对此不承担任何责任。 + 最快处理 + 付款方式 + + 提供者 + + 提供商 + 最近使用过 + 推荐 + + 至多%d天 + + %s 分钟 + 可从 + 最高可提供 + 你得到 + 服务由外部供应商提供。\nTangem 不承担任何责任。 + 至多 + 通过 %s + 您将支付 + %s 支持 + 从图库中选择 + 设置 + 您尚未授予摄像头访问权限 + 摄像头访问权限被拒绝 + 您请求的代币未添加到您的钱包。请添加后重试。 + 代币未添加 + 抱歉,无法识别此二维码。 + 无法识别的二维码 + 您添加的所有代币均不支持此网络。请添加受支持的代币以发送加密货币。 + 未找到支持的代币 + 此二维码包含无法识别的参数: %s如果您继续操作,部分支付信息可能会丢失。 + 未知参数 + 奖励地址 + 我明白,我将完全失去对 Tangem Pay 卡及其上所有资金的访问权,且无法挽回 + 所有 Tangem 设备均已重置。 + 激活过程中出了问题。请逐一重置卡片。 + 卡片验证失败 + 请重置下一个设备以继续 + 拥有 root 权限的设备安全性较低。您的数据可能面临额外的风险。 + 检测到 root 访问 + 您发送 + + 此地址与代币不兼容 + + 使用固定利率时,您在互换交易时收到的金额将被锁定。这可以保护您免受交易过程中价格波动的影响。 固定利率 - 浮動利率意味著您最終收到的金額可能會根據您開始和完成置換之間的市場情況略有變化。 - 浮動利率 + 浮动利率意味着您最终收到的金额可能会根据您开始和完成互换之间的市场情况略有变化。 + 浮动利率 利率固定 + 出错了。请再试一次。 + 简单易用 + 让您的加密货币安全离线存储。轻薄如信用卡,安全胜过银行金库。 + 无助记词 + 一流的硬件钱包 + Tangem 冷钱包 + 网络费用是处理和确认您在区块链上的交易时所需的小额费用。 + 要开始质押,您必须通过交易充值 1 TON 来激活您的 TON 账户。资金会保留在您的账户中,因为此步骤仅用于激活账户以进行质押。 + 激活账户 + 网络费用已更改。请在继续操作前查看新金额。 + 网络费用已更新 + 年利率 %1$s%% + 您的质押奖励将在 5 个周期(约 25 天)后开始发放,在此期间您的委托将由网络注册和统计。 + 年收益率 + 年利率 (APY):显示您一年内通过复利计算可获得的总利息。复利是指您获得的利息会添加到您的本金中,因此您还可以获得该利息的利息。 + APY + 奖励将累积到您的质押余额中。已赚取资金: %s + 最高金额: %s + 要开始质押,您需要先激活您的 TON 账户。 + 激活账户 + Solana 的奖励会自动添加到您的质押余额中,无法单独显示。 + 批准后,您即允许智能合约在未来的交易中使用您的代币。 + 还在寻找其他代币?\n尝试搜索或探索其他加密货币! + 搜索任何代币,即使它还不在你的列表中。 + 使用搜索查找所需内容 + 您的资产 + 由于流动性低,您收到的资金可能会大大减少。请尝试较小的金额或另一个提供商。 + 账户余额不足,无法完成此交易。请减少收款金额或增加余额。 + 互换... + 此交易流动性不足,请减少金额或选择其他供应商。 + 交易额过大 + 我们非常乐意收到您的反馈。 + Tangem Pay 现已进入测试阶段 + 卡片已冻结 + 卡片支付 + 存款 + 争议 + 探索交易 + 服务费 + 费用 + 确保您的资金安全。您可以随时解冻。 + 冻结您的卡片? + 冻结卡片失败,请稍后再试。 + 冻结 + 您的卡片已被冻结。 + 获取帮助 + 原因: %s + %s · %s + MCC %s + 其他 + 无法在已 root 的设备上使用 + 已完成 + 拒绝 + 待定 + 已反转 + 条款、费用和限制 + 条款和限制 + 银行拒绝了这项交易请求。 + 这笔费用用于支付您办理转账时的费用。 + 商家部分或全部撤销了交易 + 继续使用您的资金。您可以随时冻结资金。 + 要解冻您的卡片? + 卡片解冻失败,请稍后再试。 + 您的卡片已解冻。 + 提款 + 无法在已root的设备上使用 + 从主屏幕隐藏 KYC 页面 + 增加资金 + 充值选项 + 添加到 Google 钱包 + 卡号 + 更改PIN码 + 该卡已完全准备好用于支付。 + 已创建 PIN 码 + CVC + 数据加载失败,请稍后再试。 + 到期 + 冻结卡片 + 隐藏详情 + 隐藏 + 打开 Google 钱包 + 只需轻点几下即可设置 Tangem Pay,然后即可开始使用 Google Pay 付款。 + 只需轻点几下即可设置 Tangem Pay,然后即可开始使用 Apple Pay 付款。 + 将您的卡片添加到 Google Pay + 将您的卡片添加到 Apple Pay + 打开 Google 钱包 + 点击右上角的“+”按钮 + 打开 Apple 钱包 + 点击“添加卡片” + 点击“借记卡或信用卡” + 手动输入卡片信息 + 使用发送到您设备上的 OTP 来验证卡片。 + 一切就绪!您的卡已可以使用。 + 将卡片添加到 Google Pay + 将卡片添加到 Apple Pay + PIN码 + 分享您的地址或出示二维码 + 检测到技术问题。请稍后再试或联系技术支持。 + 目前无法接收 + 显示 + 显示详情 + 将您投资组合中的任何资产互换到卡片 + 卡片详情 + 解冻卡片 + 如果忘记了,请返回应用查看。 + 您的PIN码 + 提款 + 目前无法提款 + 当前提款交易完成之前,您无法发起新的提款交易或互换交易。 + 提款进行中 + 卡片设置 + 更改PIN码 + 如果忘记了,请返回应用程序。 + 我明白,我将完全失去对 Tangem Pay 卡及其上所有资金的访问权,且无法挽回 + 发卡失败 + 发生技术故障,请点击下方按钮重试。 + 发生技术故障,请联系技术支持。 + 免费领取您的 Tangem Visa 虚拟卡 + 获取 Tangem Pay + 前往支持页面 + 通常需要最多15分钟 + 设置您的 Tangem 卡 + 正在发放您的卡片 + 该卡通常会在 5 分钟内自动发放。极少数情况下,如果需要人工审核,则可能需要长达 48 小时。 + Tangem Pay + 确认取消 + 您确定要停止 KYC 流程吗?您可以随时返回继续。 + 我们无法验证您的个人资料。如有任何疑问,请联系客服。 + 很抱歉,我们无法验证您的身份。 + KYC 被拒 + KYC正在进行中 + 查看状态 + Tangem Pay 的 KYC 流程正在进行中 + 文件通常会在 5 分钟内自动验证。极少数情况下,如果需要人工审核,则可能需要长达 48 小时。 + 已拒绝 + 隐藏 KYC 块 + 抱歉,我们无法验证 + 您的个人资料。 + 免费领取您的 Tangem Visa 虚拟卡 + 使用 USDC 进行日常支付 + 获取卡片 + 数字卡可与 Apple Pay 和 Google Pay 一起使用 + 随意支配您的资产 + 购买无需支付任何额外费用 + 实际支付金额与所示金额一致 + 将在不透露您的地址和资产的情况下创建一个单独的付款账户 + 无与伦比的隐私保护 + 几分钟内即可获得免费的 Tangem Pay 卡 + 支付账户 + 支付账户未同步 + 无效PIN码:请避免使用连续或重复的密码。 + 我们正在修复技术问题,请稍后再试。 + 服务暂时不可用 + 无法显示详细信息。但刷卡支付功能仍然可用。 + 设置 PIN 码 + 会话已过期 + 恢复访问权限 + 使用 USDC 进行日常支付 + Tangem Pay暂时无法使用。 + Tangem Pay + 点击下方按钮恢复访问权限 + 您的Polygon链上 USDC 余额与您的卡片余额不同,并在购买后 2 个工作日内更新。购物退款的资金不会退还至您的链上余额,也不能提现,但会保留在您的卡片余额中用于购物。 + 请注意 + 您的PIN码 + 授权已被撤销。您的资金仍处于收益模式。如需执行操作,请进入收益模式并重新授予权限。 + 不适用 + 显示二维码 + 为 %s + 之前的授权将被撤销,并颁发新的授权。网络将对每次此类操作收取代币批准费。您将在交易记录中看到一笔金额为零的交易,作为撤销授权的证据。 + 交易金额超过先前授权的金额。\n请更新授权以继续 + 更新权限 + 升级到硬件钱包 + 实时提醒交易、兑换和重要更新。 + 交易提醒 + 抢先体验最新功能和专属优惠。 + 专题报道和新闻更新 + 启用推送通知,即可在资金到账时收到提醒。 + 不要错过任何一笔交易 + 如果您在没有备份的情况下删除此钱包,您将永久失去对您的资金的访问权限。 + 选择您的钱包类型 + 扫描您的 Tangem 卡或指环以恢复卡号或从其他钱包导入卡号。 + 创建硬件钱包 + 想购买 Tangem 钱包吗? + 导入助记词 + 在手机上恢复您的钱包或从其他应用程序导入—方便,但安全性不如 Tangem 卡。 + 选择哪个? + 多部分交易 + 为了顺利完成交易,您的交易将被拆分成多个部分。您需要多次刷卡才能完成交易。 + 交易正在处理中。请多次用卡轻触以完成交易。 + 交易进行中 + 创建 Tangem 钱包 + 开始迁移 + 复制 + 要继续使用您的资金,请根据 Clore 官方指南开始迁移。 + 该网络不支持信息签名 + 无法签署信息。请重试。 + 根据 Clore 的官方文档,所有在 12 月 21 日之前收到的 Clore 代币(ERC-20 代币)都将迁移到 Clore;此日期之后收到的代币则不会迁移。迁移方案即将推出,敬请期待。 + 消息 + 打开索赔通道 + 要继续使用您的 Clore 代币,您必须根据索赔通道上的信息完成代币迁移。 + Clore 网络迁移 + 签署 + 签名 + Clore 网络迁移 + 此交易对不支持互换。请选择其他代币重试。 + 不支持的互换对 + 所需区块链 %s 尚未添加到您的投资组合中。请先添加,然后再进行连接。 + 将区块链添加到投资组合 + 要继续,请将您的 dApp 会话重新连接到所需的区块链网络 %s。 + 区块链网络未连接 + 检查您的区块链连接 + 请求超时 + 该区块链 %s Tangem Wallet 不支持,无法连接。 + 不支持的区块链 + 通过批准,您允许 dApp 或智能合约在未来的交易中使用代币。 + 已经拥有 Tangem 钱包? + 数千种资产 + 顶级硬件钱包 + 快速发货 + 一触即开始 + 无缝且安全 + 无助记词 + 简单易用 + 用 Tangem 创建硬件钱包。像银行卡一样纤薄,像银行保险库一样安全。 + 创建或导入软件钱包 + 在您的手机上创建或导入软件钱包。 + 从手机钱包开始 + 其他方法 + 使用 Tangem 硬件钱包 + 了解更多并购买 + 启用收益模式后,所有未来充值到此地址的资金都将转入 Aave。您仍然可以自由管理您的资金。 + 你的 %s 提供给 Aave + 供应 %1$s %2$s 到 Aave + 批准 + 您的代币授权已被撤销。请重新授权代币以恢复服务功能。 + 需要批准 + 费用将被扣除,您的资产将被重新提供。 + 要继续产生收益,需要获得批准。 + 确认批准 + 您的资金目前已提供给 Aave 协议,但您可以随时对其进行管理。 + 你的 %s 提供给 Aave + 无法加载图表... + 供应 %1$s %2$s 到 Aave。 + 禁用收益模式 + APY %1$s%% + 可用 + 当前年收益率 + 充值贷款时,将从余额中扣除不超过 %1$s 的网络费。 + 目前网络费用过高,无法执行借贷。一旦降至 %1$s 或以下,将立即提供资金。 + 我的资金 + 您的 %1$s 现已部署到 Aave 并产生收益。您持有 %2$s 这些代币代表您的余额,并会自动累积收益。当您充值时,扣除手续费后,资金将提供给 Aave 以产生更多收益。 + 收益模式 + 总收益 + 转至Aave + 探索 Aave + 这是 %s上的当前供应费。实际费用将显示在激活选项卡上。 + 当前费用 + 在扣除交易费后,今后所有 %s 充值都将自动提供给 Aave。 + 以后每次充值都会扣除大约 %1$s (%2$s) 的网络费,但不会超过 %3$s (%4$s) 的限额。 + 如果网络费用超过最高限额,交易将无法完成,直到网络费用降低为止。您可以稍后更改此限额。 + 最高费用 + 最低金额根据当前网络费用计算,确保不超过充值金额的 4%% ,相当于最低 %1$s (%2$s)。 + 最低充值金额 + 充值费用政策 + Tangem 还从产生的收益中提取 15% 服务费。 + 一旦网络费用降低或您的余额达到最低金额要求,您的资金将自动转入 Aave。 + 由于市场活跃,手续费高于平时。您可以立即进行交易,也可以稍后手续费降低时再来查看。 + 高昂的网络费用 + 历史回报 + 启动 %1$s%% 您余额的APY + 您的代币在收益模式下的授权已被撤销。请打开代币以重新授予权限。 + 需要代币批准 + 检查您的网络连接 + 网络费用信息无法获取 + 每次充值都将自动提供给 Aave。 + 您账户上的所有 %1$s 都将自动供应给 Aave。 + 自动供应至Aave + 您可以随时随地发送、兑换或出售您的资金。 + 无锁仓 + 它是如何运作的? + Aave 是一个链上协议,提供非托管流动性市场,使用户能够以可变利率累积收益。 + 去中心化和自托管 + 使用此服务即表示您同意服务提供商的条款\n%1$s 和 %2$s + 连接到 Aave + 启动 %1$s%% APY\非您的余额 + Aave %1$s%% - 可变利率 + 浮动利率 + Aave + 平均值 %s + 去年的收益 + 当前利率始终是浮动的,由 Aave 的链上智能合约根据实时供求关系自动计算。 + 技术支持 + 利率为浮动利率 + 充值后,您的资金将自动转入 Aave 账户,开始产生收益。 %s 将从中扣除交易手续费。 + 供应资产 + 您的 %s 将提供给 Aave ,没有锁仓,并且可以完全访问。 + 请参见充值费用政策 + 您的下一笔充值将自动发送至 Aave。 + 您未来所有的收入 %1$s 存款将自动存入 Aave。 + 活跃 + 暂停 + 禁用收益模式 + 关闭此功能会将您的资产从 Aave 提取,并将其转换回 %s 放入钱包,同时收益累积停止。 + 退出收益模式时,区块链会收取网络费用。 + 禁用收益模式 + 供应 + 供应 APY + APY + 利息自动累积。 + 利息自动累积 + 收益模式 + 启用收益模式 + 收益模式 + 收益模式合约部署 + 已启用收益模式 + %1$s 供应给 Aave + 已禁用收益模式 + %1$s 已从 Aave 提款 + 收益模式已初始化 + 收益模式重新激活 + 供应给 Aave + %1$s 供应给 Aave + 从 Aave提款 + 自动 + 添加 %1$s %2$s 以支付交易网络费。 + 无法覆盖 %s 费用 + 收益模式暂时不可用。请稍后再试。 + 收益模式不可用 + 无法加载图表... diff --git a/core/res/src/main/res/values/strings.xml b/core/res/src/main/res/values/strings.xml index 75603cdf2e..dd706dc203 100644 --- a/core/res/src/main/res/values/strings.xml +++ b/core/res/src/main/res/values/strings.xml @@ -1535,6 +1535,7 @@ Trade too large We would be happy to receive your feedback Tangem Pay is now in beta + Unable to rename card Card frozen Card payment Deposit @@ -1601,10 +1602,13 @@ Share your address or show QR-code Technical issues detected. Please try again later or contact support. Receive unavailable now + Only letters and numbers are allowed. + Invalid characters Reveal Show details Swap any asset in your portfolio for card Card details + Please try again later. Unfreeze Card Come back to the app if you forget it. Your PIN code @@ -1692,6 +1696,10 @@ Staking Service %1$s token in %%image%% %2$s network Token in %%image%% %1$s network + %s network + %1$s in %2$s network + %1$s in %%image%% %2$s + %1$s in %2$s %%image%% The %1$s (%2$s) token is the main currency on the %3$s network and cannot be hidden as long as you have other tokens on this network in the list Unable to hide %s N/A diff --git a/features/tokendetails/impl/build.gradle.kts b/features/tokendetails/impl/build.gradle.kts index 36bed031e2..fb3369069e 100644 --- a/features/tokendetails/impl/build.gradle.kts +++ b/features/tokendetails/impl/build.gradle.kts @@ -11,6 +11,9 @@ android { namespace = "com.tangem.features.tokendetails.impl" } +tasks.withType().configureEach { + useJUnitPlatform() +} dependencies { /** AndroidX */ @@ -113,4 +116,9 @@ dependencies { implementation(deps.decompose.ext.compose) + /** Tests */ + testImplementation(deps.test.junit5) + testRuntimeOnly(deps.test.junit5.engine) + testImplementation(deps.test.mockk) + testImplementation(deps.test.truth) } \ No newline at end of file diff --git a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/DefaultTokenDetailsComponent.kt b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/DefaultTokenDetailsComponent.kt index 035e4511bf..639595b1f4 100644 --- a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/DefaultTokenDetailsComponent.kt +++ b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/DefaultTokenDetailsComponent.kt @@ -97,7 +97,6 @@ internal class DefaultTokenDetailsComponent @AssistedInject constructor( TokenDetailsScreen( tokenDetailsUM = tokenDetailsUM, - tokenMarketBlockComponent = tokenMarketBlockComponent, modifier = modifier, ) } else { diff --git a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/model/TokenDetailsDialogFactory.kt b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/model/TokenDetailsDialogFactory.kt new file mode 100644 index 0000000000..df70f292b1 --- /dev/null +++ b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/model/TokenDetailsDialogFactory.kt @@ -0,0 +1,73 @@ +package com.tangem.feature.tokendetails.presentation.tokendetails.model + +import com.tangem.core.decompose.di.GlobalUiMessageSender +import com.tangem.core.decompose.di.ModelScoped +import com.tangem.core.decompose.ui.UiMessageSender +import com.tangem.core.ui.extensions.TextReference +import com.tangem.core.ui.extensions.resourceReference +import com.tangem.core.ui.extensions.wrappedList +import com.tangem.core.ui.message.DialogMessage +import com.tangem.core.ui.message.EventMessageAction +import com.tangem.domain.models.currency.CryptoCurrency +import com.tangem.features.tokendetails.impl.R +import javax.inject.Inject + +@ModelScoped +internal class TokenDetailsDialogFactory @Inject constructor( + @GlobalUiMessageSender private val uiMessageSender: UiMessageSender, +) { + + fun showConfirmHideToken(currency: CryptoCurrency, onConfirm: () -> Unit) { + uiMessageSender.send( + DialogMessage( + title = resourceReference( + id = R.string.token_details_hide_alert_title, + formatArgs = wrappedList(currency.name), + ), + message = resourceReference(R.string.token_details_hide_alert_message), + firstActionBuilder = { + EventMessageAction( + title = resourceReference(R.string.token_details_hide_alert_hide), + isWarning = true, + onClick = onConfirm, + ) + }, + secondActionBuilder = { cancelAction() }, + ), + ) + } + + fun showLinkedTokens(currency: CryptoCurrency) { + uiMessageSender.send( + DialogMessage( + title = resourceReference( + id = R.string.token_details_unable_hide_alert_title, + formatArgs = wrappedList(currency.symbol), + ), + message = resourceReference( + id = R.string.token_details_unable_hide_alert_message, + formatArgs = wrappedList(currency.name, currency.symbol, currency.network.name), + ), + ), + ) + } + + fun showDismissIncompleteTransactionConfirm(onConfirm: () -> Unit) { + uiMessageSender.send( + DialogMessage( + message = resourceReference(R.string.warning_kaspa_unfinished_token_transaction_discard_message), + firstActionBuilder = { + EventMessageAction( + title = resourceReference(R.string.common_yes), + onClick = onConfirm, + ) + }, + secondActionBuilder = { cancelAction() }, + ), + ) + } + + fun showError(text: TextReference) { + uiMessageSender.send(DialogMessage(message = text)) + } +} \ No newline at end of file diff --git a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/model/TokenDetailsModel.kt b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/model/TokenDetailsModel.kt index 46d9a4eb87..f5def6e3a1 100644 --- a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/model/TokenDetailsModel.kt +++ b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/model/TokenDetailsModel.kt @@ -25,6 +25,10 @@ import com.tangem.core.analytics.api.AnalyticsEventHandler import com.tangem.core.analytics.models.AnalyticsParam import com.tangem.core.analytics.models.event.OfframpAnalyticsEvent import com.tangem.core.decompose.di.GlobalUiMessageSender +import com.tangem.common.ui.userwallet.converter.WalletIconUMConverter +import com.tangem.domain.account.supplier.SingleAccountListSupplier +import com.tangem.domain.account.usecase.IsAccountsModeEnabledUseCase +import com.tangem.domain.common.wallets.UserWalletsListRepository import com.tangem.core.decompose.di.ModelScoped import com.tangem.core.decompose.model.Model import com.tangem.core.decompose.model.ParamsContainer @@ -32,9 +36,7 @@ import com.tangem.core.decompose.ui.UiMessageSender import com.tangem.core.navigation.url.UrlOpener import com.tangem.common.ui.tokens.getUnavailabilityReasonText import com.tangem.core.ui.clipboard.ClipboardManager -import com.tangem.core.ui.components.containers.pullToRefresh.PullToRefreshConfig -import com.tangem.core.ui.components.currency.icon.CurrencyIconState -import com.tangem.core.ui.components.marketprice.MarketPriceBlockState +import com.tangem.core.ui.ds.image.DeviceIconUM import com.tangem.core.ui.extensions.TextReference import com.tangem.core.ui.extensions.resourceReference import com.tangem.core.ui.extensions.stringReference @@ -86,6 +88,7 @@ import com.tangem.domain.transaction.error.SendTransactionError import com.tangem.domain.transaction.usecase.* import com.tangem.domain.txhistory.usecase.GetExplorerTransactionUrlUseCase import com.tangem.domain.wallets.usecase.GetExploreUrlUseCase +import com.tangem.domain.wallets.usecase.GetWalletIconUseCase import com.tangem.domain.wallets.usecase.GetExtendedPublicKeyForCurrencyUseCase import com.tangem.domain.wallets.usecase.GetUserWalletUseCase import com.tangem.domain.wallets.usecase.NetworkHasDerivationUseCase @@ -98,13 +101,14 @@ import com.tangem.feature.tokendetails.presentation.tokendetails.analytics.Token import com.tangem.feature.tokendetails.presentation.tokendetails.analytics.TokenDetailsNotificationsAnalyticsSender import com.tangem.feature.tokendetails.presentation.tokendetails.route.TokenDetailsBottomSheetConfig import com.tangem.feature.tokendetails.presentation.tokendetails.state.TokenBalanceSegmentedButtonConfig -import com.tangem.feature.tokendetails.presentation.tokendetails.state.TokenBalanceTypeUM -import com.tangem.feature.tokendetails.presentation.tokendetails.state.TokenDetailsBalanceBlockUM import com.tangem.feature.tokendetails.presentation.tokendetails.state.TokenDetailsState -import com.tangem.feature.tokendetails.presentation.tokendetails.state.TokenDetailsTopAppBarUM +import com.tangem.feature.tokendetails.presentation.tokendetails.state.TokenDetailsStateController import com.tangem.feature.tokendetails.presentation.tokendetails.state.TokenDetailsUM import com.tangem.feature.tokendetails.presentation.tokendetails.state.factory.TokenDetailsStateFactory import com.tangem.feature.tokendetails.presentation.tokendetails.state.factory.express.TokenDetailsExpressStatusFactory +import com.tangem.feature.tokendetails.presentation.tokendetails.state.transformer.InitializeWithCryptoCurrencyTransformer +import com.tangem.feature.tokendetails.presentation.tokendetails.state.transformer.SetTopBarTitleTransformer +import com.tangem.feature.tokendetails.presentation.tokendetails.state.transformer.UpdateTopBarMenuTransformer import com.tangem.features.tokendetails.TokenDetailsComponent import com.tangem.features.tokendetails.impl.R import com.tangem.features.txhistory.entity.TxHistoryContentUpdateEmitter @@ -114,7 +118,6 @@ import com.tangem.utils.Provider import com.tangem.utils.coroutines.* import com.tangem.utils.extensions.isZero import kotlinx.collections.immutable.PersistentList -import kotlinx.collections.immutable.persistentListOf import kotlinx.collections.immutable.toImmutableList import kotlinx.coroutines.async import kotlinx.coroutines.awaitAll @@ -172,6 +175,13 @@ internal class TokenDetailsModel @Inject constructor( private val isXpubDerivedUseCase: IsXpubDerivedUseCase, private val dynamicAddressesRepository: DynamicAddressesRepository, private val dynamicAddressesFeatureToggles: DynamicAddressesFeatureToggles, + private val dialogFactory: TokenDetailsDialogFactory, + private val userWalletsListRepository: UserWalletsListRepository, + private val singleAccountListSupplier: SingleAccountListSupplier, + private val getWalletIconUseCase: GetWalletIconUseCase, + private val walletIconUMConverter: WalletIconUMConverter, + private val isAccountsModeEnabledUseCase: IsAccountsModeEnabledUseCase, + private val redesignStateController: TokenDetailsStateController, ) : Model(), TokenDetailsClickIntents, ExpressTransactionsClickIntents, @@ -213,11 +223,10 @@ internal class TokenDetailsModel @Inject constructor( userWalletId = userWalletId, ) - private val internalUiState = MutableStateFlow(stateFactory.getInitialState(cryptoCurrency)) - val uiState: StateFlow = internalUiState + val uiState: StateFlow + field = MutableStateFlow(stateFactory.getInitialState(cryptoCurrency)) - private val internalRedesignUiState = MutableStateFlow(createInitialRedesignState()) - val redesignUiState: StateFlow = internalRedesignUiState + val redesignUiState: StateFlow get() = redesignStateController.uiState // region Clore migration // TODO: Remove after Clore migration ends ([REDACTED_TASK_KEY]) @@ -279,7 +288,10 @@ internal class TokenDetailsModel @Inject constructor( } init { + initRedesignState() updateTopBarMenu() + updateRedesignTopBarMenu() + observeRedesignTopBarTitle() initButtons() updateContent() handleBalanceHiding() @@ -329,7 +341,7 @@ internal class TokenDetailsModel @Inject constructor( private fun handleBalanceHiding() { getBalanceHidingSettingsUseCase() .onEach { settings -> - internalUiState.value = stateFactory.getStateWithUpdatedHidden( + uiState.value = stateFactory.getStateWithUpdatedHidden( isBalanceHidden = settings.isBalanceHidden, ) } @@ -345,7 +357,7 @@ internal class TokenDetailsModel @Inject constructor( .distinctUntilChanged() .onEach { state -> sendButtonsEvents(state.states) - internalUiState.value = stateFactory.getManageButtonsState(actions = state.states) + uiState.value = stateFactory.getManageButtonsState(actions = state.states) } .flowOn(dispatchers.main) .launchIn(modelScope) @@ -377,8 +389,8 @@ internal class TokenDetailsModel @Inject constructor( .distinctUntilChanged() .onEach { warnings -> val updatedState = stateFactory.getStateWithNotifications(warnings) - notificationsAnalyticsSender.send(internalUiState.value, updatedState.notifications) - internalUiState.value = updatedState + notificationsAnalyticsSender.send(uiState.value, updatedState.notifications) + uiState.value = updatedState } .launchIn(modelScope) .saveIn(warningsJobHolder) @@ -391,7 +403,7 @@ internal class TokenDetailsModel @Inject constructor( .map { it.status.right() } .distinctUntilChanged() .onEach { maybeCurrencyStatus -> - internalUiState.value = stateFactory.getCurrencyLoadedBalanceState(maybeCurrencyStatus) + uiState.value = stateFactory.getCurrencyLoadedBalanceState(maybeCurrencyStatus) maybeCurrencyStatus.onRight { status -> sendOneTimeBalanceLoadedAnalyticsEvent(status) cryptoCurrencyStatus = status @@ -413,7 +425,7 @@ internal class TokenDetailsModel @Inject constructor( .distinctUntilChanged() .onEach { waitForFirstExpressStatusEmmit.value = true } .onEach { expressTxs -> - internalUiState.value = expressStatusFactory.getStateWithUpdatedExpressTxs( + uiState.value = expressStatusFactory.getStateWithUpdatedExpressTxs( expressTxs = expressTxs, updateBalance = ::updateNetworkToSwapBalance, ) @@ -424,11 +436,11 @@ internal class TokenDetailsModel @Inject constructor( delay = EXPRESS_STATUS_UPDATE_DELAY, task = { runSuspendCatching { - expressStatusFactory.getUpdatedExpressStatuses(internalUiState.value.expressTxs) + expressStatusFactory.getUpdatedExpressStatuses(uiState.value.expressTxs) } }, onSuccess = { updatedTxs -> - internalUiState.value = expressStatusFactory.getStateWithUpdatedExpressTxs( + uiState.value = expressStatusFactory.getStateWithUpdatedExpressTxs( updatedTxs, ::updateNetworkToSwapBalance, ) @@ -449,14 +461,14 @@ internal class TokenDetailsModel @Inject constructor( } yieldSupplyGetRewardsBalanceUseCase(status = status, appCurrency = selectedAppCurrencyFlow.value) .onEach { formatted -> - internalUiState.value = stateFactory.getStateWithUpdatedYieldSupplyDisplayBalance(formatted) + uiState.value = stateFactory.getStateWithUpdatedYieldSupplyDisplayBalance(formatted) } .flowOn(dispatchers.main) .launchIn(modelScope) .saveIn(yieldSupplyBalanceJobHolder) } else { yieldSupplyBalanceJobHolder.cancel() - internalUiState.value = stateFactory.getStateWithUpdatedYieldSupplyDisplayBalance( + uiState.value = stateFactory.getStateWithUpdatedYieldSupplyDisplayBalance( YieldSupplyRewardBalance.empty(), ) } @@ -493,7 +505,7 @@ internal class TokenDetailsModel @Inject constructor( null } - internalUiState.update { state -> + uiState.update { state -> stateFactory.getStakingInfoState( state = state, stakingEntryInfo = stakingEntryInfo, @@ -517,7 +529,7 @@ internal class TokenDetailsModel @Inject constructor( val isSupported = isXPUBSupported() val isDynamicAddressesAvailable = isSupported && isDynamicAddressesAvailable() - internalUiState.value = stateFactory.getStateWithUpdatedMenu( + uiState.value = stateFactory.getStateWithUpdatedMenu( userWallet = userWallet, hasDerivations = hasDerivations, isSupported = isSupported, @@ -890,7 +902,7 @@ internal class TokenDetailsModel @Inject constructor( } override fun onRefreshSwipe(isRefreshing: Boolean) { - internalUiState.value = stateFactory.getRefreshingState() + uiState.value = stateFactory.getRefreshingState() modelScope.launch(dispatchers.main) { listOf( @@ -902,29 +914,29 @@ internal class TokenDetailsModel @Inject constructor( subscribeOnExpressTransactionsUpdates() }, ).awaitAll() - internalUiState.value = stateFactory.getRefreshedState() + uiState.value = stateFactory.getRefreshedState() }.saveIn(refreshStateJobHolder) } override fun onDismissBottomSheet() { - when (val bsContent = internalUiState.value.bottomSheetConfig?.content) { + when (val bsContent = uiState.value.bottomSheetConfig?.content) { is ExpressStatusBottomSheetConfig -> { modelScope.launch(dispatchers.main) { expressStatusFactory.removeTransactionOnBottomSheetClosed(bsContent.value) } } } - internalUiState.value = stateFactory.getStateWithClosedBottomSheet() + uiState.value = stateFactory.getStateWithClosedBottomSheet() } override fun onCloseRentInfoNotification() { - internalUiState.value = stateFactory.getStateWithRemovedRentNotification() + uiState.value = stateFactory.getStateWithRemovedRentNotification() } override fun onExpressTransactionClick(txId: String) { - val expressTxState = internalUiState.value.expressTxsToDisplay.firstOrNull { it.info.txId == txId } + val expressTxState = uiState.value.expressTxsToDisplay.firstOrNull { it.info.txId == txId } ?: return - internalUiState.value = expressStatusFactory.getStateWithExpressStatusBottomSheet(expressTxState) + uiState.value = expressStatusFactory.getStateWithExpressStatusBottomSheet(expressTxState) } override fun onGoToProviderClick(url: String) { @@ -1018,7 +1030,7 @@ internal class TokenDetailsModel @Inject constructor( } }, ifRight = { - internalUiState.value = stateFactory.getStateWithRemovedKaspaIncompleteTransactionNotification() + uiState.value = stateFactory.getStateWithRemovedKaspaIncompleteTransactionNotification() }, ) } @@ -1060,7 +1072,7 @@ internal class TokenDetailsModel @Inject constructor( showErrorDialog(message) } }, - ifRight = { internalUiState.value = stateFactory.getStateWithRemovedRequiredTrustlineNotification() }, + ifRight = { uiState.value = stateFactory.getStateWithRemovedRequiredTrustlineNotification() }, ) } } @@ -1086,7 +1098,7 @@ internal class TokenDetailsModel @Inject constructor( TangemLogger.e("Error: $e") }, ifRight = { - internalUiState.value = stateFactory.getStateWithRemovedKaspaIncompleteTransactionNotification() + uiState.value = stateFactory.getStateWithRemovedKaspaIncompleteTransactionNotification() }, ) } @@ -1120,13 +1132,13 @@ internal class TokenDetailsModel @Inject constructor( } } }, - ifRight = { internalUiState.value = stateFactory.getStateWithRemovedHederaAssociateNotification() }, + ifRight = { uiState.value = stateFactory.getStateWithRemovedHederaAssociateNotification() }, ) } } override fun onBalanceSelect(config: TokenBalanceSegmentedButtonConfig) { - internalUiState.value = stateFactory.getStateWithUpdatedBalanceSegmentedButtonConfig(config) + uiState.value = stateFactory.getStateWithUpdatedBalanceSegmentedButtonConfig(config) } override fun onConfirmDisposeExpressStatus() { @@ -1134,7 +1146,7 @@ internal class TokenDetailsModel @Inject constructor( } override fun onDisposeExpressStatus() { - val bottomSheetState = internalUiState.value.bottomSheetConfig?.content + val bottomSheetState = uiState.value.bottomSheetConfig?.content if (bottomSheetState is ExpressStatusBottomSheetConfig) { modelScope.launch { expressStatusFactory.removeTransactionOnBottomSheetClosed( @@ -1143,7 +1155,7 @@ internal class TokenDetailsModel @Inject constructor( ) } } - internalUiState.value = stateFactory.getStateWithClosedBottomSheet() + uiState.value = stateFactory.getStateWithClosedBottomSheet() } override fun onYieldInfoClick() { @@ -1194,52 +1206,16 @@ internal class TokenDetailsModel @Inject constructor( } private fun showConfirmHideTokenDialog(currency: CryptoCurrency) { - uiMessageSender.send( - DialogMessage( - title = resourceReference( - id = R.string.token_details_hide_alert_title, - formatArgs = wrappedList(currency.name), - ), - message = resourceReference(R.string.token_details_hide_alert_message), - firstActionBuilder = { - EventMessageAction( - title = resourceReference(R.string.token_details_hide_alert_hide), - isWarning = true, - onClick = ::onHideConfirmed, - ) - }, - secondActionBuilder = { cancelAction() }, - ), - ) + dialogFactory.showConfirmHideToken(currency = currency, onConfirm = ::onHideConfirmed) } private fun showLinkedTokensDialog(currency: CryptoCurrency) { - uiMessageSender.send( - DialogMessage( - title = resourceReference( - id = R.string.token_details_unable_hide_alert_title, - formatArgs = wrappedList(currency.symbol), - ), - message = resourceReference( - id = R.string.token_details_unable_hide_alert_message, - formatArgs = wrappedList(currency.name, currency.symbol, currency.network.name), - ), - ), - ) + dialogFactory.showLinkedTokens(currency = currency) } private fun showDismissIncompleteTransactionConfirmDialog() { - uiMessageSender.send( - DialogMessage( - message = resourceReference(R.string.warning_kaspa_unfinished_token_transaction_discard_message), - firstActionBuilder = { - EventMessageAction( - title = resourceReference(R.string.common_yes), - onClick = ::onConfirmDismissIncompleteTransactionClick, - ) - }, - secondActionBuilder = { cancelAction() }, - ), + dialogFactory.showDismissIncompleteTransactionConfirm( + onConfirm = ::onConfirmDismissIncompleteTransactionClick, ) } @@ -1262,7 +1238,7 @@ internal class TokenDetailsModel @Inject constructor( } private fun showErrorDialog(text: TextReference) { - uiMessageSender.send(DialogMessage(message = text)) + dialogFactory.showError(text = text) } private fun checkForActionUpdates() { @@ -1402,29 +1378,79 @@ internal class TokenDetailsModel @Inject constructor( // endregion Clore migration - private fun createInitialRedesignState(): TokenDetailsUM { - return TokenDetailsUM( - topAppBarUM = TokenDetailsTopAppBarUM( - title = stringReference(cryptoCurrency.name), - subtitle = stringReference(cryptoCurrency.symbol), - menuItems = persistentListOf(), + private fun updateRedesignTopBarMenu() { + modelScope.launch(dispatchers.main) { + val hasDerivations = networkHasDerivationUseCase( + userWallet = userWallet, + network = cryptoCurrency.network, + ).getOrElse { false } + + val isSupported = isXPUBSupported() + + redesignStateController.update( + UpdateTopBarMenuTransformer( + userWallet = userWallet, + hasDerivations = hasDerivations, + isXPubSupported = isSupported, + onGenerateExtendedKey = ::onGenerateExtendedKey, + onHideClick = ::onHideClick, + ), + ) + } + } + + private fun initRedesignState() { + redesignStateController.update( + InitializeWithCryptoCurrencyTransformer( + cryptoCurrency = cryptoCurrency, + onBackClick = ::onBackClick, ), - balanceBlockUM = TokenDetailsBalanceBlockUM.Loading( - actionButtons = persistentListOf(), - tokenBalanceTypeUM = TokenBalanceTypeUM.Single, - currencyIconState = CurrencyIconState.Loading, - ), - marketPriceBlockState = MarketPriceBlockState.Loading(currencySymbol = cryptoCurrency.symbol), - stakingBlocksState = null, - pullToRefreshConfig = PullToRefreshConfig( - isRefreshing = false, - onRefresh = {}, - ), - isBalanceHidden = false, - isMarketPriceAvailable = false, ) } + private fun observeRedesignTopBarTitle() { + combine( + flow = userWalletsListRepository.userWallets.filterNotNull(), + flow2 = isAccountsModeEnabledUseCase.invoke(), + flow3 = singleAccountListSupplier(userWalletId), + flow4 = getAccountCryptoCurrencyStatusUseCase(userWalletId, cryptoCurrency) + .map { status -> status.account } + .distinctUntilChanged(), + ) { wallets, accountsModeEnabled, accountList, currentAccount -> + val currentWallet = wallets.firstOrNull { it.walletId == userWalletId } ?: userWallet + TopBarTitleInputs( + hasMultipleWallets = wallets.size > 1, + hasMultipleAccounts = accountsModeEnabled && accountList.accounts.size > 1, + walletName = currentWallet.name, + deviceIconUM = walletIconUMConverter.convert(getWalletIconUseCase(currentWallet)), + account = currentAccount, + ) + } + .distinctUntilChanged() + .onEach { inputs -> + redesignStateController.update( + SetTopBarTitleTransformer( + cryptoCurrency = cryptoCurrency, + hasMultipleWallets = inputs.hasMultipleWallets, + hasMultipleAccounts = inputs.hasMultipleAccounts, + walletName = inputs.walletName, + deviceIconUM = inputs.deviceIconUM, + account = inputs.account, + ), + ) + } + .flowOn(dispatchers.default) + .launchIn(modelScope) + } + + private data class TopBarTitleInputs( + val hasMultipleWallets: Boolean, + val hasMultipleAccounts: Boolean, + val walletName: String, + val deviceIconUM: DeviceIconUM, + val account: Account.CryptoPortfolio?, + ) + private companion object { const val EXPRESS_STATUS_UPDATE_DELAY = 10_000L const val BASE_DERIVATION_NODE_COUNT = 5 diff --git a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/TokenDetailsStateController.kt b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/TokenDetailsStateController.kt new file mode 100644 index 0000000000..41e505d325 --- /dev/null +++ b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/TokenDetailsStateController.kt @@ -0,0 +1,54 @@ +package com.tangem.feature.tokendetails.presentation.tokendetails.state + +import com.tangem.core.decompose.di.ModelScoped +import com.tangem.core.ui.components.containers.pullToRefresh.PullToRefreshConfig +import com.tangem.core.ui.components.currency.icon.CurrencyIconState +import com.tangem.core.ui.components.marketprice.MarketPriceBlockState +import com.tangem.core.ui.extensions.TextReference +import com.tangem.utils.transformer.Transformer +import kotlinx.collections.immutable.persistentListOf +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.update +import javax.inject.Inject + +@ModelScoped +internal class TokenDetailsStateController @Inject constructor() { + + val uiState: StateFlow + field = MutableStateFlow(value = getInitialState()) + + val value: TokenDetailsUM get() = uiState.value + + fun update(function: (TokenDetailsUM) -> TokenDetailsUM) { + uiState.update(function = function) + } + + fun update(transformer: Transformer) { + uiState.update(function = transformer::transform) + } + + private fun getInitialState(): TokenDetailsUM { + return TokenDetailsUM( + topAppBarUM = TokenDetailsTopAppBarUM( + titleState = TokenDetailsTopAppBarUM.TitleState.Simple(tokenName = ""), + subtitle = TextReference.EMPTY, + onBackClick = {}, + menuItems = persistentListOf(), + ), + balanceBlockUM = TokenDetailsBalanceBlockUM.Loading( + actionButtons = persistentListOf(), + tokenBalanceTypeUM = TokenBalanceTypeUM.Single, + currencyIconState = CurrencyIconState.Loading, + ), + marketPriceBlockState = MarketPriceBlockState.Loading(currencySymbol = ""), + stakingBlocksState = null, + pullToRefreshConfig = PullToRefreshConfig( + isRefreshing = false, + onRefresh = {}, + ), + isBalanceHidden = false, + isMarketPriceAvailable = false, + ) + } +} \ No newline at end of file diff --git a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/TokenDetailsUM.kt b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/TokenDetailsUM.kt index 14b55185e8..91ee9dc832 100644 --- a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/TokenDetailsUM.kt +++ b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/TokenDetailsUM.kt @@ -1,12 +1,15 @@ package com.tangem.feature.tokendetails.presentation.tokendetails.state -import androidx.compose.runtime.Stable +import androidx.compose.runtime.Immutable +import com.tangem.common.ui.account.AccountIconUM import com.tangem.core.ui.components.containers.pullToRefresh.PullToRefreshConfig +import com.tangem.core.ui.components.dropdownmenu.TangemDropdownMenuItem import com.tangem.core.ui.components.marketprice.MarketPriceBlockState +import com.tangem.core.ui.ds.image.DeviceIconUM import com.tangem.core.ui.extensions.TextReference import kotlinx.collections.immutable.ImmutableList -@Stable +@Immutable internal data class TokenDetailsUM( val topAppBarUM: TokenDetailsTopAppBarUM, val balanceBlockUM: TokenDetailsBalanceBlockUM, @@ -17,8 +20,31 @@ internal data class TokenDetailsUM( val isMarketPriceAvailable: Boolean, ) +@Immutable internal data class TokenDetailsTopAppBarUM( - val title: TextReference, + val titleState: TitleState, val subtitle: TextReference, - val menuItems: ImmutableList, -) \ No newline at end of file + val onBackClick: () -> Unit, + val menuItems: ImmutableList, +) { + @Immutable + sealed interface TitleState { + val tokenName: String + + data class Simple( + override val tokenName: String, + ) : TitleState + + data class WithWallet( + override val tokenName: String, + val walletName: String, + val deviceIconUM: DeviceIconUM, + ) : TitleState + + data class WithAccount( + override val tokenName: String, + val accountName: TextReference, + val accountIconUM: AccountIconUM.CryptoPortfolio, + ) : TitleState + } +} \ No newline at end of file diff --git a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/transformer/InitializeWithCryptoCurrencyTransformer.kt b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/transformer/InitializeWithCryptoCurrencyTransformer.kt new file mode 100644 index 0000000000..64f054639c --- /dev/null +++ b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/transformer/InitializeWithCryptoCurrencyTransformer.kt @@ -0,0 +1,23 @@ +package com.tangem.feature.tokendetails.presentation.tokendetails.state.transformer + +import com.tangem.core.ui.components.marketprice.MarketPriceBlockState +import com.tangem.core.ui.extensions.stringReference +import com.tangem.domain.models.currency.CryptoCurrency +import com.tangem.feature.tokendetails.presentation.tokendetails.state.TokenDetailsTopAppBarUM +import com.tangem.feature.tokendetails.presentation.tokendetails.state.TokenDetailsUM +import com.tangem.utils.transformer.Transformer + +internal class InitializeWithCryptoCurrencyTransformer( + private val cryptoCurrency: CryptoCurrency, + private val onBackClick: () -> Unit, +) : Transformer { + + override fun transform(prevState: TokenDetailsUM): TokenDetailsUM = prevState.copy( + topAppBarUM = prevState.topAppBarUM.copy( + titleState = TokenDetailsTopAppBarUM.TitleState.Simple(tokenName = cryptoCurrency.name), + subtitle = stringReference(cryptoCurrency.symbol), + onBackClick = onBackClick, + ), + marketPriceBlockState = MarketPriceBlockState.Loading(currencySymbol = cryptoCurrency.symbol), + ) +} \ No newline at end of file diff --git a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/transformer/SetTopBarTitleTransformer.kt b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/transformer/SetTopBarTitleTransformer.kt new file mode 100644 index 0000000000..443e904a62 --- /dev/null +++ b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/transformer/SetTopBarTitleTransformer.kt @@ -0,0 +1,78 @@ +package com.tangem.feature.tokendetails.presentation.tokendetails.state.transformer + +import com.tangem.common.ui.account.AccountIconUM +import com.tangem.common.ui.account.toUM +import com.tangem.core.ui.ds.image.DeviceIconUM +import com.tangem.core.ui.extensions.TextReference +import com.tangem.core.ui.extensions.resourceReference +import com.tangem.core.ui.extensions.wrappedList +import com.tangem.domain.models.account.Account +import com.tangem.domain.models.currency.CryptoCurrency +import com.tangem.domain.models.network.Network +import com.tangem.feature.tokendetails.presentation.tokendetails.state.TokenDetailsTopAppBarUM.TitleState +import com.tangem.feature.tokendetails.presentation.tokendetails.state.TokenDetailsUM +import com.tangem.utils.transformer.Transformer +import com.tangem.core.res.R as CoreResR + +internal class SetTopBarTitleTransformer( + private val cryptoCurrency: CryptoCurrency, + private val hasMultipleWallets: Boolean, + private val hasMultipleAccounts: Boolean, + private val walletName: String, + private val deviceIconUM: DeviceIconUM, + private val account: Account.CryptoPortfolio?, +) : Transformer { + + override fun transform(prevState: TokenDetailsUM): TokenDetailsUM = prevState.copy( + topAppBarUM = prevState.topAppBarUM.copy( + titleState = createTitleState(), + subtitle = createSubtitle(), + ), + ) + + private fun createTitleState(): TitleState { + val tokenName = cryptoCurrency.name + + return when { + hasMultipleAccounts && account != null -> { + val accountNameUM = account.accountName.toUM() + TitleState.WithAccount( + tokenName = tokenName, + accountName = accountNameUM.value, + accountIconUM = AccountIconUM.CryptoPortfolio( + value = account.icon.value, + color = account.icon.color, + ), + ) + } + hasMultipleWallets -> TitleState.WithWallet( + tokenName = tokenName, + walletName = walletName, + deviceIconUM = deviceIconUM, + ) + else -> TitleState.Simple(tokenName = tokenName) + } + } + + private fun createSubtitle(): TextReference { + val networkName = cryptoCurrency.network.name + return when (cryptoCurrency) { + is CryptoCurrency.Token -> { + val standardName = cryptoCurrency.network.standardType + .takeIf { it !is Network.StandardType.Unspecified } + ?.name + if (standardName != null) { + resourceReference( + CoreResR.string.token_details_toolbar_subtitle_standard, + wrappedList(standardName, networkName), + ) + } else { + resourceReference(CoreResR.string.token_details_toolbar_subtitle_network, wrappedList(networkName)) + } + } + is CryptoCurrency.Coin -> { + resourceReference(CoreResR.string.token_details_toolbar_subtitle_network, wrappedList(networkName)) + } + } + } +} \ No newline at end of file diff --git a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/transformer/UpdateTopBarMenuTransformer.kt b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/transformer/UpdateTopBarMenuTransformer.kt new file mode 100644 index 0000000000..320e28bdce --- /dev/null +++ b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/transformer/UpdateTopBarMenuTransformer.kt @@ -0,0 +1,51 @@ +package com.tangem.feature.tokendetails.presentation.tokendetails.state.transformer + +import com.tangem.core.ui.components.dropdownmenu.TangemDropdownMenuItem +import com.tangem.core.ui.extensions.resourceReference +import com.tangem.core.ui.extensions.themedColor +import com.tangem.core.ui.res.TangemTheme +import com.tangem.domain.card.common.util.cardTypesResolver +import com.tangem.domain.models.wallet.UserWallet +import com.tangem.feature.tokendetails.presentation.tokendetails.state.TokenDetailsUM +import com.tangem.features.tokendetails.impl.R +import com.tangem.utils.transformer.Transformer +import kotlinx.collections.immutable.persistentListOf +import kotlinx.collections.immutable.toImmutableList + +internal class UpdateTopBarMenuTransformer( + private val userWallet: UserWallet, + private val hasDerivations: Boolean, + private val isXPubSupported: Boolean, + private val onGenerateExtendedKey: () -> Unit, + private val onHideClick: () -> Unit, +) : Transformer { + + override fun transform(prevState: TokenDetailsUM): TokenDetailsUM = prevState.copy( + topAppBarUM = prevState.topAppBarUM.copy(menuItems = createMenuItems()), + ) + + private fun createMenuItems() = if (userWallet is UserWallet.Cold && + userWallet.cardTypesResolver.isSingleWalletWithToken() + ) { + persistentListOf() + } else { + buildList { + if (isXPubSupported && hasDerivations) { + add( + TangemDropdownMenuItem( + title = resourceReference(R.string.token_details_generate_xpub), + textColor = themedColor { TangemTheme.colors.text.primary1 }, + onClick = onGenerateExtendedKey, + ), + ) + } + add( + TangemDropdownMenuItem( + title = resourceReference(R.string.token_details_hide_token), + textColor = themedColor { TangemTheme.colors.text.warning }, + onClick = onHideClick, + ), + ) + }.toImmutableList() + } +} \ No newline at end of file diff --git a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/ui/TokenDetailsScreen.kt b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/ui/TokenDetailsScreen.kt index 01902e1cab..6a1b96d329 100644 --- a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/ui/TokenDetailsScreen.kt +++ b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/ui/TokenDetailsScreen.kt @@ -1,30 +1,148 @@ package com.tangem.feature.tokendetails.presentation.tokendetails.ui +import android.content.res.Configuration +import androidx.compose.animation.core.animateFloatAsState import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.fillMaxSize -import androidx.compose.material3.Text +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.runtime.Composable -import androidx.compose.ui.Alignment +import androidx.compose.runtime.getValue import androidx.compose.ui.Modifier +import androidx.compose.ui.input.nestedscroll.nestedScroll +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import com.tangem.common.ui.account.AccountIconUM +import com.tangem.core.ui.components.containers.pullToRefresh.PullToRefreshConfig +import com.tangem.core.ui.components.currency.icon.CurrencyIconState +import com.tangem.core.ui.components.dropdownmenu.TangemDropdownMenuItem +import com.tangem.core.ui.components.haze.hazeEffectTangem +import com.tangem.core.ui.components.haze.hazeSourceTangem +import com.tangem.core.ui.components.marketprice.MarketPriceBlockState +import com.tangem.core.ui.ds.topbar.collapsing.TangemCollapsingTopBar +import com.tangem.core.ui.ds.topbar.collapsing.rememberTangemExitUntilCollapsedScrollBehavior +import com.tangem.core.ui.extensions.stringReference +import com.tangem.core.ui.extensions.themedColor +import com.tangem.core.ui.res.LocalRootBackgroundColor import com.tangem.core.ui.res.TangemTheme +import com.tangem.core.ui.res.TangemThemePreviewRedesign +import com.tangem.domain.models.account.CryptoPortfolioIcon +import com.tangem.feature.tokendetails.presentation.tokendetails.state.TokenBalanceTypeUM +import com.tangem.feature.tokendetails.presentation.tokendetails.state.TokenDetailsBalanceBlockUM +import com.tangem.feature.tokendetails.presentation.tokendetails.state.TokenDetailsTopAppBarUM +import com.tangem.feature.tokendetails.presentation.tokendetails.state.TokenDetailsTopAppBarUM.TitleState import com.tangem.feature.tokendetails.presentation.tokendetails.state.TokenDetailsUM -import com.tangem.features.markets.token.block.TokenMarketBlockComponent +import dev.chrisbanes.haze.HazeProgressive +import dev.chrisbanes.haze.HazeTint +import kotlinx.collections.immutable.persistentListOf -@Suppress("UnusedParameter") @Composable -internal fun TokenDetailsScreen( - tokenDetailsUM: TokenDetailsUM, - tokenMarketBlockComponent: TokenMarketBlockComponent?, - modifier: Modifier = Modifier, -) { +internal fun TokenDetailsScreen(tokenDetailsUM: TokenDetailsUM, modifier: Modifier = Modifier) { + val topAppBarUM = tokenDetailsUM.topAppBarUM + + // TODO [REDACTED_TASK_KEY] Token Details Make Balance with actions + val balanceBlockHeight = 200.dp + val partialCollapsedHeight = 0.dp + val expandedHeight = balanceBlockHeight + partialCollapsedHeight + + val behavior = rememberTangemExitUntilCollapsedScrollBehavior( + expandedHeight = expandedHeight, + partialCollapsedHeight = partialCollapsedHeight, + ) + Box( modifier = modifier.fillMaxSize(), - contentAlignment = Alignment.Center, ) { - Text( - text = "Token Details Redesign", - style = TangemTheme.typography.h2, - color = TangemTheme.colors.text.primary1, + Box( + modifier = Modifier + .fillMaxSize() + .hazeSourceTangem(zIndex = -2f), + ) { + TangemCollapsingTopBar( + state = behavior.state, + collapsingPart = { + // [REDACTED_TASK_KEY] Token Details Make Balance with actions + Box( + modifier = Modifier + .fillMaxWidth() + .height(balanceBlockHeight), + ) + }, + body = { + LazyColumn( + modifier = Modifier + .fillMaxSize() + .nestedScroll(behavior.nestedScrollConnection), + ) { + // TODO [REDACTED_TASK_KEY] Token Details Make Transaction History + } + }, + ) + } + + val rootBackground by LocalRootBackgroundColor.current + val hazeIntensity by animateFloatAsState( + targetValue = (behavior.state.collapsedFraction * 2f).coerceIn(0f, 1f), + label = "TopBarHazeIntensity", + ) + Box( + modifier = Modifier.hazeEffectTangem { + fallbackTint = HazeTint(rootBackground.copy(alpha = hazeIntensity / 2f)) + progressive = HazeProgressive.verticalGradient( + startIntensity = hazeIntensity, + endIntensity = 0f, + preferPerformance = true, + ) + }, + ) { + TokenDetailsTopBar(topAppBarUM = topAppBarUM) + } + } +} + +// region Preview +@Composable +@Preview(showBackground = true, widthDp = 360) +@Preview(showBackground = true, widthDp = 360, uiMode = Configuration.UI_MODE_NIGHT_YES) +private fun TokenDetailsScreen_Preview() { + TangemThemePreviewRedesign { + TokenDetailsScreen( + tokenDetailsUM = TokenDetailsUM( + topAppBarUM = TokenDetailsTopAppBarUM( + titleState = TitleState.WithAccount( + tokenName = "Tether", + accountName = stringReference("Portfolio"), + accountIconUM = AccountIconUM.CryptoPortfolio( + value = CryptoPortfolioIcon.Icon.Star, + color = CryptoPortfolioIcon.Color.Azure, + ), + ), + subtitle = stringReference("ERC-20 in Ethereum network"), + onBackClick = {}, + menuItems = persistentListOf( + TangemDropdownMenuItem( + title = stringReference("Hide Token"), + textColor = themedColor { TangemTheme.colors.text.warning }, + onClick = {}, + ), + ), + ), + balanceBlockUM = TokenDetailsBalanceBlockUM.Loading( + actionButtons = persistentListOf(), + tokenBalanceTypeUM = TokenBalanceTypeUM.Single, + currencyIconState = CurrencyIconState.Loading, + ), + marketPriceBlockState = MarketPriceBlockState.Loading(currencySymbol = "USDT"), + stakingBlocksState = null, + pullToRefreshConfig = PullToRefreshConfig( + isRefreshing = false, + onRefresh = {}, + ), + isBalanceHidden = false, + isMarketPriceAvailable = true, + ), ) } -} \ No newline at end of file +} +// endregion Preview \ No newline at end of file diff --git a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/ui/TokenDetailsTopBar.kt b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/ui/TokenDetailsTopBar.kt new file mode 100644 index 0000000000..aaf3a4838f --- /dev/null +++ b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/ui/TokenDetailsTopBar.kt @@ -0,0 +1,478 @@ +package com.tangem.feature.tokendetails.presentation.tokendetails.ui + +import android.content.res.Configuration +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.BoxWithConstraints +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.statusBarsPadding +import androidx.compose.foundation.text.InlineTextContent +import androidx.compose.foundation.text.TextAutoSize +import androidx.compose.foundation.text.appendInlineContent +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.Immutable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.rememberUpdatedState +import androidx.compose.runtime.saveable.rememberSaveable +import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.platform.LocalDensity +import androidx.compose.ui.text.Placeholder +import androidx.compose.ui.text.PlaceholderVerticalAlign +import androidx.compose.ui.text.TextStyle +import androidx.compose.ui.text.buildAnnotatedString +import androidx.compose.ui.text.rememberTextMeasurer +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.text.style.TextOverflow +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.Dp +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import androidx.compose.ui.util.fastForEach +import com.tangem.common.ui.account.AccountIcon +import com.tangem.common.ui.account.AccountIconUM +import com.tangem.core.ui.components.account.AccountIconSize +import com.tangem.core.ui.components.dropdownmenu.TangemDropdownItem +import com.tangem.core.ui.components.dropdownmenu.TangemDropdownMenu +import com.tangem.core.ui.components.dropdownmenu.TangemDropdownMenuItem +import com.tangem.core.ui.ds.image.DeviceIconUM +import com.tangem.core.ui.ds.image.TangemDeviceIcon +import com.tangem.core.ui.ds.topbar.TangemTopBar +import com.tangem.core.ui.ds.topbar.TangemTopBarActionContent +import com.tangem.core.ui.ds.topbar.TangemTopBarActionUM +import com.tangem.core.ui.extensions.resolveAnnotatedReference +import com.tangem.core.ui.extensions.stringReference +import com.tangem.core.ui.extensions.stringResourceSafe +import com.tangem.core.ui.extensions.themedColor +import com.tangem.core.ui.res.TangemTheme +import com.tangem.core.ui.res.TangemThemePreviewRedesign +import com.tangem.domain.models.account.CryptoPortfolioIcon +import com.tangem.feature.tokendetails.presentation.tokendetails.state.TokenDetailsTopAppBarUM +import com.tangem.feature.tokendetails.presentation.tokendetails.state.TokenDetailsTopAppBarUM.TitleState +import kotlinx.collections.immutable.persistentListOf +import com.tangem.core.ui.R as CoreUiR + +@Composable +internal fun TokenDetailsTopBar(topAppBarUM: TokenDetailsTopAppBarUM, modifier: Modifier = Modifier) { + TangemTopBar( + modifier = modifier.statusBarsPadding(), + startContent = { + TangemTopBarActionContent( + actionUM = TangemTopBarActionUM( + iconRes = CoreUiR.drawable.ic_back_24, + onClick = topAppBarUM.onBackClick, + ghostModeProgress = 1f, + ), + ) + }, + endContent = if (topAppBarUM.menuItems.isNotEmpty()) { + { + var isDropdownMenuShown by rememberSaveable { mutableStateOf(false) } + Box { + TangemTopBarActionContent( + actionUM = TangemTopBarActionUM( + iconRes = CoreUiR.drawable.ic_more_default_24, + onClick = { isDropdownMenuShown = true }, + ghostModeProgress = 1f, + ), + ) + TangemDropdownMenu( + expanded = isDropdownMenuShown, + modifier = Modifier.background(TangemTheme.colors.background.primary), + onDismissRequest = { isDropdownMenuShown = false }, + content = { + topAppBarUM.menuItems.fastForEach { menuItem -> + TangemDropdownItem( + item = menuItem, + dismissParent = { isDropdownMenuShown = false }, + ) + } + }, + ) + } + } + } else { + null + }, + content = { + Column( + modifier = Modifier + .weight(1f) + .padding(horizontal = TangemTheme.dimens2.x1), + horizontalAlignment = Alignment.CenterHorizontally, + verticalArrangement = Arrangement.spacedBy(TangemTheme.dimens2.x0_5), + ) { + TokenDetailsTitle(titleState = topAppBarUM.titleState) + Text( + text = topAppBarUM.subtitle.resolveAnnotatedReference(), + color = TangemTheme.colors2.text.neutral.secondary, + style = TangemTheme.typography2.captionMedium12, + textAlign = TextAlign.Center, + maxLines = 1, + overflow = TextOverflow.Ellipsis, + ) + } + }, + ) +} + +@Composable +private fun TokenDetailsTitle(titleState: TitleState) { + val appearance = TitleAppearance( + style = TangemTheme.typography2.bodySemibold16, + iconSize = TangemTheme.dimens2.x5, + spacing = TangemTheme.dimens2.x1, + ) + + when (titleState) { + is TitleState.Simple -> { + Text( + text = titleState.tokenName, + color = TangemTheme.colors2.text.neutral.primary, + style = appearance.style, + textAlign = TextAlign.Center, + maxLines = 1, + overflow = TextOverflow.Ellipsis, + autoSize = TextAutoSize.StepBased( + minFontSize = MIN_TITLE_FONT_SIZE, + maxFontSize = MAX_TITLE_FONT_SIZE, + ), + ) + } + is TitleState.WithWallet -> { + AdaptiveTokenWithSecondaryRow( + tokenName = titleState.tokenName, + secondaryName = titleState.walletName, + template = stringResourceSafe( + id = CoreUiR.string.token_details_toolbar_title_token_in_wallet, + titleState.tokenName, + titleState.walletName, + ), + appearance = appearance, + icon = { + TangemDeviceIcon( + state = titleState.deviceIconUM, + modifier = Modifier.size(appearance.iconSize), + ) + }, + ) + } + is TitleState.WithAccount -> { + val accountNameStr = titleState.accountName.resolveAnnotatedReference().toString() + AdaptiveTokenWithSecondaryRow( + tokenName = titleState.tokenName, + secondaryName = accountNameStr, + template = stringResourceSafe( + id = CoreUiR.string.token_details_toolbar_title_token_in_account, + titleState.tokenName, + accountNameStr, + ), + appearance = appearance, + icon = { + AccountIcon( + name = titleState.accountName, + icon = titleState.accountIconUM, + size = AccountIconSize.ExtraSmall, + ) + }, + ) + } + } +} + +/** + * Adaptive title for [TitleState.WithAccount] / [TitleState.WithWallet]. + * + * Phrase template carries the [IMAGE_PLACEHOLDER] marker — translator decides where + * the icon sits (e.g. "Tether in [⭐] Portfolio" or "Tether in My Wallet [⭐]"). + * RTL is handled by BiDi inside the single [Text]. + * + * Width-driven cascade: + * 1–2. Full phrase as single [Text] with inline icon; [TextOverflow.Ellipsis] + * trims the secondary name tail when needed. + * 3. No meaningful tail left → fall back to `[tokenName] [icon]` in a [Row]; + * icon is a sibling so ellipsis trims only tokenName, never the icon. + * 4–5. tokenName itself doesn't fit → [TextAutoSize] shrinks to + * [MIN_TITLE_FONT_SIZE], then [TextOverflow.Ellipsis] tails. + * + * #1/#2 vs #3 is decided here via [rememberTextMeasurer]; #4/#5 are delegated + * to [TextAutoSize] + [TextOverflow.Ellipsis] in the fallback branch. + */ +@Composable +private fun AdaptiveTokenWithSecondaryRow( + tokenName: String, + secondaryName: String, + template: String, + appearance: TitleAppearance, + icon: @Composable () -> Unit, +) { + val (beforeIcon, afterIcon) = remember(template) { splitTemplate(template) } + val inlineContent = rememberIconInlineContent(appearance.iconSize, icon) + + BoxWithConstraints( + modifier = Modifier.fillMaxWidth(), + contentAlignment = Alignment.Center, + ) { + val isFullTextShown = rememberShouldShowFullText( + beforeIcon = beforeIcon, + afterIcon = afterIcon, + secondaryName = secondaryName, + appearance = appearance, + maxWidthPx = constraints.maxWidth, + ) + if (isFullTextShown) { + FullPhraseTitle( + beforeIcon = beforeIcon, + afterIcon = afterIcon, + style = appearance.style, + inlineContent = inlineContent, + ) + } else { + FallbackTokenWithIconTitle( + tokenName = tokenName, + appearance = appearance, + icon = icon, + ) + } + } +} + +private fun splitTemplate(template: String): Pair { + val parts = template.split(IMAGE_PLACEHOLDER, limit = 2) + return if (parts.size == 2) parts[0] to parts[1] else template to "" +} + +@Composable +private fun rememberIconInlineContent(iconSize: Dp, icon: @Composable () -> Unit): Map { + val iconSizeSp = with(LocalDensity.current) { iconSize.toSp() } + val currentIcon by rememberUpdatedState(icon) + return remember(iconSizeSp) { + mapOf( + ICON_INLINE_ID to InlineTextContent( + placeholder = Placeholder( + width = iconSizeSp, + height = iconSizeSp, + placeholderVerticalAlign = PlaceholderVerticalAlign.Center, + ), + children = { + Box(modifier = Modifier.fillMaxSize(), contentAlignment = Alignment.Center) { + currentIcon() + } + }, + ), + ) + } +} + +@Composable +private fun rememberShouldShowFullText( + beforeIcon: String, + afterIcon: String, + secondaryName: String, + appearance: TitleAppearance, + maxWidthPx: Int, +): Boolean { + val measurer = rememberTextMeasurer() + val density = LocalDensity.current + return remember(beforeIcon, afterIcon, secondaryName, maxWidthPx, appearance, density) { + if (maxWidthPx <= 0) return@remember true + val fullTextWidthPx = measurer + .measure(text = beforeIcon + afterIcon, style = appearance.style, softWrap = false) + .size.width + val secondaryWidthPx = measurer + .measure(text = secondaryName, style = appearance.style, softWrap = false) + .size.width + val staticWidthPx = (fullTextWidthPx - secondaryWidthPx).coerceAtLeast(0) + val iconReservePx = with(density) { + (appearance.iconSize + appearance.spacing * 2).toPx() + }.toInt() + val minSecondaryPx = with(density) { MIN_SECONDARY_NAME_WIDTH.toPx() }.toInt() + staticWidthPx + iconReservePx + minSecondaryPx <= maxWidthPx + } +} + +@Composable +private fun FullPhraseTitle( + beforeIcon: String, + afterIcon: String, + style: TextStyle, + inlineContent: Map, +) { + val fullText = remember(beforeIcon, afterIcon) { + buildAnnotatedString { + append(beforeIcon) + appendInlineContent(ICON_INLINE_ID, IMAGE_PLACEHOLDER) + append(afterIcon) + } + } + Text( + text = fullText, + inlineContent = inlineContent, + color = TangemTheme.colors2.text.neutral.primary, + style = style, + textAlign = TextAlign.Center, + maxLines = 1, + overflow = TextOverflow.Ellipsis, + ) +} + +@Composable +private fun FallbackTokenWithIconTitle(tokenName: String, appearance: TitleAppearance, icon: @Composable () -> Unit) { + Row( + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(appearance.spacing, Alignment.CenterHorizontally), + ) { + Text( + text = tokenName, + color = TangemTheme.colors2.text.neutral.primary, + style = appearance.style, + maxLines = 1, + overflow = TextOverflow.Ellipsis, + autoSize = TextAutoSize.StepBased( + minFontSize = MIN_TITLE_FONT_SIZE, + maxFontSize = MAX_TITLE_FONT_SIZE, + ), + modifier = Modifier.weight(weight = 1f, fill = false), + ) + icon() + } +} + +@Immutable +private data class TitleAppearance( + val style: TextStyle, + val iconSize: Dp, + val spacing: Dp, +) + +private const val ICON_INLINE_ID = "account_icon" +private const val IMAGE_PLACEHOLDER = "%image%" + +private val MIN_TITLE_FONT_SIZE = 12.sp +private val MAX_TITLE_FONT_SIZE = 16.sp +private val MIN_SECONDARY_NAME_WIDTH = 48.dp + +// region Preview +@Composable +@Preview(showBackground = true, widthDp = 360) +@Preview(showBackground = true, widthDp = 360, uiMode = Configuration.UI_MODE_NIGHT_YES) +private fun TokenDetailsTopBar_Preview( + @PreviewParameter(TokenDetailsTopBarPreviewProvider::class) titleState: TitleState, +) { + TangemThemePreviewRedesign { + TokenDetailsTopBar( + topAppBarUM = TokenDetailsTopAppBarUM( + titleState = titleState, + subtitle = stringReference("ERC-20 in Ethereum network"), + onBackClick = {}, + menuItems = persistentListOf( + TangemDropdownMenuItem( + title = stringReference("Hide Token"), + textColor = themedColor { TangemTheme.colors.text.warning }, + onClick = {}, + ), + ), + ), + ) + } +} + +private class TokenDetailsTopBarPreviewProvider : PreviewParameterProvider { + override val values: Sequence + get() = sequenceOf( + // === Base title states === + // Simple — 1 wallet, 1 account + TitleState.Simple(tokenName = "Tether"), + // WithWallet — N wallets, 1 account + TitleState.WithWallet( + tokenName = "Tether", + walletName = "Tangem wallet", + deviceIconUM = DeviceIconUM.Card( + mainColor = Color.DarkGray, + secondColor = null, + ), + ), + // WithAccount — 1 wallet, N accounts + TitleState.WithAccount( + tokenName = "Tether", + accountName = stringReference("Portfolio"), + accountIconUM = AccountIconUM.CryptoPortfolio( + value = CryptoPortfolioIcon.Icon.Star, + color = CryptoPortfolioIcon.Color.Azure, + ), + ), + // === AdaptiveTokenTitleRow cascade cases === + // Cascade #1 — full text fits as is (short token + short wallet) + TitleState.WithWallet( + tokenName = "BTC", + walletName = "Main", + deviceIconUM = DeviceIconUM.Card( + mainColor = Color.DarkGray, + secondColor = null, + ), + ), + // Cascade #2 — full text doesn't fit, ellipsized tail still meaningful + TitleState.WithWallet( + tokenName = "Tether", + walletName = "My Long Tangem Hardware Wallet", + deviceIconUM = DeviceIconUM.Card( + mainColor = Color.DarkGray, + secondColor = null, + ), + ), + // Cascade #3 — secondary part too small to be meaningful, drop to token-only + icon + TitleState.WithWallet( + tokenName = "USDCoinWrapped", + walletName = "Super Extra Long Wallet Name That Definitely Wont Fit", + deviceIconUM = DeviceIconUM.Card( + mainColor = Color.DarkGray, + secondColor = null, + ), + ), + // Cascade #4 — even tokenName alone doesn't fit at full font size: TextAutoSize shrinks it + TitleState.WithAccount( + tokenName = "VeryLongTokenNameThatOverflowsVeryLongTokenNameThatOverflows", + accountName = stringReference("Portfolio"), + accountIconUM = AccountIconUM.CryptoPortfolio( + value = CryptoPortfolioIcon.Icon.Star, + color = CryptoPortfolioIcon.Color.Azure, + ), + ), + // Cascade #5 — even at MIN_TITLE_FONT_SIZE doesn't fit: TextOverflow.Ellipsis tails it + TitleState.Simple( + tokenName = "ExtremelyLongTokenNameThatCannotPossiblyFitEvenAtMinFontSize", + ), + // Account variant — long account name triggers ellipsized tail (#2) + TitleState.WithAccount( + tokenName = "Tether", + accountName = stringReference("My Personal Long Account Name"), + accountIconUM = AccountIconUM.CryptoPortfolio( + value = CryptoPortfolioIcon.Icon.Star, + color = CryptoPortfolioIcon.Color.Azure, + ), + ), + // Account variant — drop secondary, icon-only fallback (#3) + TitleState.WithAccount( + tokenName = "USDCoinWrapped", + accountName = stringReference("Super Extra Long Account Name That Wont Fit Anywhere"), + accountIconUM = AccountIconUM.CryptoPortfolio( + value = CryptoPortfolioIcon.Icon.Star, + color = CryptoPortfolioIcon.Color.Azure, + ), + ), + ) +} +// endregion Preview \ No newline at end of file diff --git a/features/tokendetails/impl/src/test/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/transformer/InitializeWithCryptoCurrencyTransformerTest.kt b/features/tokendetails/impl/src/test/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/transformer/InitializeWithCryptoCurrencyTransformerTest.kt new file mode 100644 index 0000000000..43672cf4c9 --- /dev/null +++ b/features/tokendetails/impl/src/test/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/transformer/InitializeWithCryptoCurrencyTransformerTest.kt @@ -0,0 +1,128 @@ +package com.tangem.feature.tokendetails.presentation.tokendetails.state.transformer + +import com.google.common.truth.Truth.assertThat +import com.tangem.core.ui.components.containers.pullToRefresh.PullToRefreshConfig +import com.tangem.core.ui.components.marketprice.MarketPriceBlockState +import com.tangem.core.ui.extensions.stringReference +import com.tangem.domain.models.currency.CryptoCurrency +import com.tangem.feature.tokendetails.presentation.tokendetails.state.TokenDetailsBalanceBlockUM +import com.tangem.feature.tokendetails.presentation.tokendetails.state.TokenDetailsTopAppBarUM +import com.tangem.feature.tokendetails.presentation.tokendetails.state.TokenDetailsTopAppBarUM.TitleState +import com.tangem.feature.tokendetails.presentation.tokendetails.state.TokenDetailsUM +import io.mockk.every +import io.mockk.mockk +import io.mockk.verify +import kotlinx.collections.immutable.persistentListOf +import org.junit.jupiter.api.Test + +class InitializeWithCryptoCurrencyTransformerTest { + + private val cryptoCurrency: CryptoCurrency = mockk(relaxed = true) { + every { name } returns TOKEN_NAME + every { symbol } returns TOKEN_SYMBOL + } + private val onBackClick: () -> Unit = mockk(relaxed = true) + + @Test + fun `GIVEN crypto currency WHEN transform THEN top bar title is Simple with token name`() { + // GIVEN + val transformer = InitializeWithCryptoCurrencyTransformer( + cryptoCurrency = cryptoCurrency, + onBackClick = onBackClick, + ) + + // WHEN + val result = transformer.transform(initialState()) + + // THEN + assertThat(result.topAppBarUM.titleState).isEqualTo(TitleState.Simple(tokenName = TOKEN_NAME)) + } + + @Test + fun `GIVEN crypto currency WHEN transform THEN subtitle is token symbol`() { + // GIVEN + val transformer = InitializeWithCryptoCurrencyTransformer( + cryptoCurrency = cryptoCurrency, + onBackClick = onBackClick, + ) + + // WHEN + val result = transformer.transform(initialState()) + + // THEN + assertThat(result.topAppBarUM.subtitle).isEqualTo(stringReference(TOKEN_SYMBOL)) + } + + @Test + fun `GIVEN onBackClick callback WHEN top bar onBackClick invoked THEN callback is dispatched`() { + // GIVEN + val transformer = InitializeWithCryptoCurrencyTransformer( + cryptoCurrency = cryptoCurrency, + onBackClick = onBackClick, + ) + + // WHEN + val result = transformer.transform(initialState()) + result.topAppBarUM.onBackClick() + + // THEN + verify(exactly = 1) { onBackClick.invoke() } + } + + @Test + fun `GIVEN crypto currency WHEN transform THEN market price loading carries currency symbol`() { + // GIVEN + val transformer = InitializeWithCryptoCurrencyTransformer( + cryptoCurrency = cryptoCurrency, + onBackClick = onBackClick, + ) + + // WHEN + val result = transformer.transform(initialState()) + + // THEN + assertThat(result.marketPriceBlockState) + .isEqualTo(MarketPriceBlockState.Loading(currencySymbol = TOKEN_SYMBOL)) + } + + @Test + fun `GIVEN any state WHEN transform THEN unrelated fields are preserved`() { + // GIVEN + val state = initialState() + val transformer = InitializeWithCryptoCurrencyTransformer( + cryptoCurrency = cryptoCurrency, + onBackClick = onBackClick, + ) + + // WHEN + val result = transformer.transform(state) + + // THEN — only top bar title/subtitle/onBackClick and marketPriceBlockState are touched + assertThat(result.topAppBarUM.menuItems).isEqualTo(state.topAppBarUM.menuItems) + assertThat(result.balanceBlockUM).isSameInstanceAs(state.balanceBlockUM) + assertThat(result.stakingBlocksState).isEqualTo(state.stakingBlocksState) + assertThat(result.pullToRefreshConfig).isSameInstanceAs(state.pullToRefreshConfig) + assertThat(result.isBalanceHidden).isEqualTo(state.isBalanceHidden) + assertThat(result.isMarketPriceAvailable).isEqualTo(state.isMarketPriceAvailable) + } + + private fun initialState(): TokenDetailsUM = TokenDetailsUM( + topAppBarUM = TokenDetailsTopAppBarUM( + titleState = TitleState.Simple(tokenName = ""), + subtitle = stringReference(""), + onBackClick = {}, + menuItems = persistentListOf(), + ), + balanceBlockUM = mockk(relaxed = true), + marketPriceBlockState = mockk(relaxed = true), + stakingBlocksState = null, + pullToRefreshConfig = mockk(relaxed = true), + isBalanceHidden = false, + isMarketPriceAvailable = false, + ) + + private companion object { + const val TOKEN_NAME = "Tether" + const val TOKEN_SYMBOL = "USDT" + } +} \ No newline at end of file diff --git a/features/tokendetails/impl/src/test/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/transformer/SetTopBarTitleTransformerTest.kt b/features/tokendetails/impl/src/test/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/transformer/SetTopBarTitleTransformerTest.kt new file mode 100644 index 0000000000..7f72295f3a --- /dev/null +++ b/features/tokendetails/impl/src/test/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/transformer/SetTopBarTitleTransformerTest.kt @@ -0,0 +1,204 @@ +package com.tangem.feature.tokendetails.presentation.tokendetails.state.transformer + +import androidx.compose.ui.graphics.Color +import com.google.common.truth.Truth.assertThat +import com.tangem.common.ui.account.AccountIconUM +import com.tangem.core.ui.components.containers.pullToRefresh.PullToRefreshConfig +import com.tangem.core.ui.components.marketprice.MarketPriceBlockState +import com.tangem.core.ui.ds.image.DeviceIconUM +import com.tangem.core.ui.extensions.stringReference +import com.tangem.domain.models.account.Account +import com.tangem.domain.models.account.AccountName +import com.tangem.domain.models.account.CryptoPortfolioIcon +import com.tangem.domain.models.currency.CryptoCurrency +import com.tangem.feature.tokendetails.presentation.tokendetails.state.TokenDetailsBalanceBlockUM +import com.tangem.feature.tokendetails.presentation.tokendetails.state.TokenDetailsTopAppBarUM +import com.tangem.feature.tokendetails.presentation.tokendetails.state.TokenDetailsTopAppBarUM.TitleState +import com.tangem.feature.tokendetails.presentation.tokendetails.state.TokenDetailsUM +import io.mockk.every +import io.mockk.mockk +import kotlinx.collections.immutable.persistentListOf +import org.junit.jupiter.api.Test + +class SetTopBarTitleTransformerTest { + + private val tokenName = "Tether" + private val walletName = "My Wallet" + private val deviceIconUM: DeviceIconUM = DeviceIconUM.Card( + mainColor = Color.DarkGray, + secondColor = null, + ) + private val cryptoCurrency: CryptoCurrency.Coin = mockk(relaxed = true) { + every { name } returns tokenName + } + + @Test + fun `GIVEN single wallet single account WHEN transform THEN Simple title`() { + // GIVEN + val transformer = createTransformer( + hasMultipleWallets = false, + hasMultipleAccounts = false, + account = null, + ) + + // WHEN + val result = transformer.transform(initialState()) + + // THEN + assertThat(result.topAppBarUM.titleState).isEqualTo(TitleState.Simple(tokenName = tokenName)) + } + + @Test + fun `GIVEN multiple wallets and single account WHEN transform THEN WithWallet title`() { + // GIVEN + val transformer = createTransformer( + hasMultipleWallets = true, + hasMultipleAccounts = false, + account = null, + ) + + // WHEN + val result = transformer.transform(initialState()) + + // THEN + val expected = TitleState.WithWallet( + tokenName = tokenName, + walletName = walletName, + deviceIconUM = deviceIconUM, + ) + assertThat(result.topAppBarUM.titleState).isEqualTo(expected) + } + + @Test + fun `GIVEN single wallet and multiple accounts WHEN transform THEN WithAccount title`() { + // GIVEN + val transformer = createTransformer( + hasMultipleWallets = false, + hasMultipleAccounts = true, + account = stubAccount(), + ) + + // WHEN + val result = transformer.transform(initialState()) + + // THEN + val title = result.topAppBarUM.titleState + assertThat(title).isInstanceOf(TitleState.WithAccount::class.java) + assertThat((title as TitleState.WithAccount).tokenName).isEqualTo(tokenName) + } + + @Test + fun `GIVEN multiple wallets AND multiple accounts WHEN transform THEN WithAccount wins`() { + // GIVEN — design priority: account branch wins over wallet branch + val transformer = createTransformer( + hasMultipleWallets = true, + hasMultipleAccounts = true, + account = stubAccount(), + ) + + // WHEN + val result = transformer.transform(initialState()) + + // THEN + assertThat(result.topAppBarUM.titleState).isInstanceOf(TitleState.WithAccount::class.java) + } + + @Test + fun `GIVEN multiple accounts but null account WHEN transform THEN falls back to wallet branch`() { + // GIVEN — race protection: hasMultipleAccounts=true but account not loaded yet + val transformer = createTransformer( + hasMultipleWallets = true, + hasMultipleAccounts = true, + account = null, + ) + + // WHEN + val result = transformer.transform(initialState()) + + // THEN + assertThat(result.topAppBarUM.titleState).isInstanceOf(TitleState.WithWallet::class.java) + } + + @Test + fun `GIVEN multiple accounts but null account AND single wallet WHEN transform THEN falls back to Simple`() { + // GIVEN + val transformer = createTransformer( + hasMultipleWallets = false, + hasMultipleAccounts = true, + account = null, + ) + + // WHEN + val result = transformer.transform(initialState()) + + // THEN + assertThat(result.topAppBarUM.titleState).isEqualTo(TitleState.Simple(tokenName = tokenName)) + } + + @Test + fun `GIVEN account with custom icon WHEN transform THEN icon is propagated`() { + // GIVEN + val account = stubAccount( + iconValue = CryptoPortfolioIcon.Icon.Star, + iconColor = CryptoPortfolioIcon.Color.Azure, + ) + val transformer = createTransformer( + hasMultipleWallets = false, + hasMultipleAccounts = true, + account = account, + ) + + // WHEN + val result = transformer.transform(initialState()) + + // THEN + val title = result.topAppBarUM.titleState as TitleState.WithAccount + val expectedIcon = AccountIconUM.CryptoPortfolio( + value = CryptoPortfolioIcon.Icon.Star, + color = CryptoPortfolioIcon.Color.Azure, + ) + assertThat(title.accountIconUM).isEqualTo(expectedIcon) + } + + private fun createTransformer( + hasMultipleWallets: Boolean, + hasMultipleAccounts: Boolean, + account: Account.CryptoPortfolio?, + ) = SetTopBarTitleTransformer( + cryptoCurrency = cryptoCurrency, + hasMultipleWallets = hasMultipleWallets, + hasMultipleAccounts = hasMultipleAccounts, + walletName = walletName, + deviceIconUM = deviceIconUM, + account = account, + ) + + private fun stubAccount( + iconValue: CryptoPortfolioIcon.Icon = CryptoPortfolioIcon.Icon.Star, + iconColor: CryptoPortfolioIcon.Color = CryptoPortfolioIcon.Color.Azure, + ): Account.CryptoPortfolio { + val icon: CryptoPortfolioIcon = mockk { + every { value } returns iconValue + every { color } returns iconColor + } + return mockk { + every { accountName } returns AccountName.DefaultMain + every { this@mockk.icon } returns icon + } + } + + private fun initialState(): TokenDetailsUM = TokenDetailsUM( + topAppBarUM = TokenDetailsTopAppBarUM( + titleState = TitleState.Simple(tokenName = ""), + subtitle = stringReference(""), + onBackClick = {}, + menuItems = persistentListOf(), + ), + balanceBlockUM = mockk(relaxed = true), + marketPriceBlockState = mockk(relaxed = true), + stakingBlocksState = null, + pullToRefreshConfig = mockk(relaxed = true), + isBalanceHidden = false, + isMarketPriceAvailable = false, + ) +} \ No newline at end of file diff --git a/features/tokendetails/impl/src/test/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/transformer/UpdateTopBarMenuTransformerTest.kt b/features/tokendetails/impl/src/test/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/transformer/UpdateTopBarMenuTransformerTest.kt new file mode 100644 index 0000000000..f5c2d80d1e --- /dev/null +++ b/features/tokendetails/impl/src/test/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/transformer/UpdateTopBarMenuTransformerTest.kt @@ -0,0 +1,214 @@ +package com.tangem.feature.tokendetails.presentation.tokendetails.state.transformer + +import com.google.common.truth.Truth.assertThat +import com.tangem.core.ui.components.containers.pullToRefresh.PullToRefreshConfig +import com.tangem.core.ui.components.marketprice.MarketPriceBlockState +import com.tangem.core.ui.extensions.stringReference +import com.tangem.domain.card.CardTypesResolver +import com.tangem.domain.card.common.util.cardTypesResolver +import com.tangem.domain.models.wallet.UserWallet +import com.tangem.feature.tokendetails.presentation.tokendetails.state.TokenDetailsBalanceBlockUM +import com.tangem.feature.tokendetails.presentation.tokendetails.state.TokenDetailsTopAppBarUM +import com.tangem.feature.tokendetails.presentation.tokendetails.state.TokenDetailsTopAppBarUM.TitleState +import com.tangem.feature.tokendetails.presentation.tokendetails.state.TokenDetailsUM +import io.mockk.every +import io.mockk.mockk +import io.mockk.mockkStatic +import io.mockk.unmockkStatic +import io.mockk.verify +import kotlinx.collections.immutable.persistentListOf +import org.junit.jupiter.api.AfterEach +import org.junit.jupiter.api.BeforeEach +import org.junit.jupiter.api.Test + +class UpdateTopBarMenuTransformerTest { + + private val coldWallet: UserWallet.Cold = mockk(relaxed = true) + private val hotWallet: UserWallet.Hot = mockk(relaxed = true) + private val cardTypesResolver: CardTypesResolver = mockk(relaxed = true) + private val onGenerateExtendedKey: () -> Unit = mockk(relaxed = true) + private val onHideClick: () -> Unit = mockk(relaxed = true) + + @BeforeEach + fun setUp() { + mockkStatic(UserWallet.Cold::cardTypesResolver) + every { coldWallet.cardTypesResolver } returns cardTypesResolver + } + + @AfterEach + fun tearDown() { + unmockkStatic(UserWallet.Cold::cardTypesResolver) + } + + @Test + fun `GIVEN cold wallet AND single wallet with token WHEN transform THEN menu is empty`() { + // GIVEN + every { cardTypesResolver.isSingleWalletWithToken() } returns true + val transformer = createTransformer( + userWallet = coldWallet, + hasDerivations = true, + isXPubSupported = true, + ) + + // WHEN + val result = transformer.transform(initialState()) + + // THEN + assertThat(result.topAppBarUM.menuItems).isEmpty() + } + + @Test + fun `GIVEN cold wallet AND multi-wallet WHEN transform THEN Hide item is the only one`() { + // GIVEN + every { cardTypesResolver.isSingleWalletWithToken() } returns false + val transformer = createTransformer( + userWallet = coldWallet, + hasDerivations = false, + isXPubSupported = false, + ) + + // WHEN + val result = transformer.transform(initialState()) + + // THEN + assertThat(result.topAppBarUM.menuItems).hasSize(1) + + result.topAppBarUM.menuItems.single().onClick() + verify(exactly = 1) { onHideClick.invoke() } + verify(exactly = 0) { onGenerateExtendedKey.invoke() } + } + + @Test + fun `GIVEN hot wallet WHEN transform THEN Hide item is shown regardless of single-wallet flag`() { + // GIVEN — flag is read only for cold wallets, so no stub needed for hotWallet + val transformer = createTransformer( + userWallet = hotWallet, + hasDerivations = false, + isXPubSupported = false, + ) + + // WHEN + val result = transformer.transform(initialState()) + + // THEN + assertThat(result.topAppBarUM.menuItems).hasSize(1) + } + + @Test + fun `GIVEN xPub supported AND derivations exist WHEN transform THEN both items are shown`() { + // GIVEN + every { cardTypesResolver.isSingleWalletWithToken() } returns false + val transformer = createTransformer( + userWallet = coldWallet, + hasDerivations = true, + isXPubSupported = true, + ) + + // WHEN + val result = transformer.transform(initialState()) + + // THEN — Generate xPub first, Hide token second + assertThat(result.topAppBarUM.menuItems).hasSize(2) + } + + @Test + fun `GIVEN xPub supported but no derivations WHEN transform THEN xPub item is hidden`() { + // GIVEN + every { cardTypesResolver.isSingleWalletWithToken() } returns false + val transformer = createTransformer( + userWallet = coldWallet, + hasDerivations = false, + isXPubSupported = true, + ) + + // WHEN + val result = transformer.transform(initialState()) + + // THEN + assertThat(result.topAppBarUM.menuItems).hasSize(1) + } + + @Test + fun `GIVEN derivations exist but xPub unsupported WHEN transform THEN xPub item is hidden`() { + // GIVEN + every { cardTypesResolver.isSingleWalletWithToken() } returns false + val transformer = createTransformer( + userWallet = coldWallet, + hasDerivations = true, + isXPubSupported = false, + ) + + // WHEN + val result = transformer.transform(initialState()) + + // THEN + assertThat(result.topAppBarUM.menuItems).hasSize(1) + } + + @Test + fun `GIVEN any state WHEN transform THEN unrelated state fields are preserved`() { + // GIVEN + every { cardTypesResolver.isSingleWalletWithToken() } returns false + val state = initialState() + val transformer = createTransformer( + userWallet = coldWallet, + hasDerivations = false, + isXPubSupported = false, + ) + + // WHEN + val result = transformer.transform(state) + + // THEN — only menuItems is touched + assertThat(result.topAppBarUM.titleState).isEqualTo(state.topAppBarUM.titleState) + assertThat(result.topAppBarUM.subtitle).isEqualTo(state.topAppBarUM.subtitle) + assertThat(result.balanceBlockUM).isSameInstanceAs(state.balanceBlockUM) + assertThat(result.marketPriceBlockState).isSameInstanceAs(state.marketPriceBlockState) + } + + @Test + fun `GIVEN callbacks WHEN menu items invoked THEN callbacks are dispatched`() { + // GIVEN + every { cardTypesResolver.isSingleWalletWithToken() } returns false + val transformer = createTransformer( + userWallet = coldWallet, + hasDerivations = true, + isXPubSupported = true, + ) + + // WHEN + val result = transformer.transform(initialState()) + result.topAppBarUM.menuItems.forEach { it.onClick() } + + // THEN + verify(exactly = 1) { onGenerateExtendedKey.invoke() } + verify(exactly = 1) { onHideClick.invoke() } + } + + private fun createTransformer( + userWallet: UserWallet, + hasDerivations: Boolean, + isXPubSupported: Boolean, + ) = UpdateTopBarMenuTransformer( + userWallet = userWallet, + hasDerivations = hasDerivations, + isXPubSupported = isXPubSupported, + onGenerateExtendedKey = onGenerateExtendedKey, + onHideClick = onHideClick, + ) + + private fun initialState(): TokenDetailsUM = TokenDetailsUM( + topAppBarUM = TokenDetailsTopAppBarUM( + titleState = TitleState.Simple(tokenName = "Tether"), + subtitle = stringReference("ERC-20 in Ethereum network"), + onBackClick = {}, + menuItems = persistentListOf(), + ), + balanceBlockUM = mockk(relaxed = true), + marketPriceBlockState = mockk(relaxed = true), + stakingBlocksState = null, + pullToRefreshConfig = mockk(relaxed = true), + isBalanceHidden = false, + isMarketPriceAvailable = false, + ) +} \ No newline at end of file