diff --git a/app/src/main/assets/tangem-app-config b/app/src/main/assets/tangem-app-config index 8285f8d289..098662beb5 160000 --- a/app/src/main/assets/tangem-app-config +++ b/app/src/main/assets/tangem-app-config @@ -1 +1 @@ -Subproject commit 8285f8d2896d2994337f55a7b3a54fd5acc24304 +Subproject commit 098662beb5b0123b11f5ee4873d4bd667ac93c73 diff --git a/app/src/main/java/com/tangem/tap/di/domain/TransactionDomainModule.kt b/app/src/main/java/com/tangem/tap/di/domain/TransactionDomainModule.kt index 8965f60090..5ce513772f 100644 --- a/app/src/main/java/com/tangem/tap/di/domain/TransactionDomainModule.kt +++ b/app/src/main/java/com/tangem/tap/di/domain/TransactionDomainModule.kt @@ -265,4 +265,18 @@ internal object TransactionDomainModule { ): SendLargeSolanaTransactionUseCase { return SendLargeSolanaTransactionUseCase(cardSdkConfigRepository, walletManagersFacade) } + + @Provides + @Singleton + fun provideSignCloreMessageUseCase( + walletManagersFacade: WalletManagersFacade, + cardSdkConfigRepository: CardSdkConfigRepository, + tangemHotWalletSignerFactory: TangemHotWalletSigner.Factory, + ): SignCloreMessageUseCase { + return SignCloreMessageUseCase( + walletManagersFacade = walletManagersFacade, + cardSdkConfigRepository = cardSdkConfigRepository, + getHotWalletSigner = tangemHotWalletSignerFactory::create, + ) + } } \ No newline at end of file diff --git a/core/res/src/main/res/values-es/strings.xml b/core/res/src/main/res/values-es/strings.xml index 901dbaefc1..124b0453e1 100644 --- a/core/res/src/main/res/values-es/strings.xml +++ b/core/res/src/main/res/values-es/strings.xml @@ -299,6 +299,7 @@ Ir al token Entendido Ocultar + Mantener para %s hora Importe En progreso @@ -323,6 +324,7 @@ No Ninguna dirección No agregadas + No disponible Ahora no Ahora OK @@ -373,6 +375,7 @@ Intercambiar Tangem Tangem Wallet + Toque y mantenga presionado términos y condiciones Condiciones de uso A @@ -462,6 +465,11 @@ Enviar activos a otras redes resultará en una pérdida permanente. Red de %s Envíe fondos utilizando solo + Las mejores oportunidades + Todas las redes + Todos los tipos + Mayormente usado + Ganar 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 @@ -561,6 +569,8 @@ Comentarios en Tangem No se puede completar una transacción Error de descripción de moneda + Fondos insuficientes + Tarifa de transacción Ocurrió un error Ocurrió un error. Código: %s Requiere memo @@ -610,6 +620,7 @@ Finalizar la copia de seguridad primero Incompleto Otros métodos + Guarde su frase de recuperación en un lugar seguro y manténgala privada para proteger sus fondos y configure un código de acceso para mayor seguridad. Guarde su frase de recuperación en un lugar seguro y manténgala en privado para proteger sus fondos. Frase de recuperación Para proteger su billetera con un código de acceso, complete el proceso de copia de seguridad. @@ -995,6 +1006,7 @@ Restaurar código de acceso Tarjetas idénticas Código de acceso + Solo puede tener una billetera móvil. Actualícela a la billetera de hardware Tangem o añada una nueva billetera de hardware. Todas las ofertas Disponible con %s La compra de criptomonedas en Tangem se lleva a cabo utilizando proveedores externos. @@ -1266,6 +1278,7 @@ La cantidad de staking se redondeará a %1$s TRX debido a las reglas de la red. La cantidad de unstaking se redondeará a %1$s TRX debido a las reglas de la red. APR %1$s%% + Sus recompensas de staking comenzarán después de 5 épocas (~25 días) mientras su delegación esté registrada y contabilizada por la red. Reclamar lo que no está en staking Tarifa de staking de la cuenta Una cuenta de staking es una cuenta especial donde se almacenan los tokens SOL de staking. Se crea cuando delegas sus tokens a un validador para participar en la validación de transacciones y ganar recompensas. Se cobra una pequeña tarifa por crear la cuenta de staking, que se devuelve una vez que se completa el staking. @@ -1276,6 +1289,7 @@ APR APY Las recompensas se acumulan automáticamente en su saldo de staking diariamente. + Las recompensas se acumulan en su saldo de staking. Fondos ganados: %s Disponible Tasa de recompensa promedio ¿Cómo funciona el staking? @@ -1420,6 +1434,7 @@ Sencillo e intuitivo, permite cambiar tokens con solo unos cuantos toques Simplemente cómodo Intercambio a través del proveedor + Sus activos La cantidad incluye:\n• Tarifas del proveedor de servicios\n• Tarifas de red por enviar %s desde el intercambio a la dirección del usuario. El importe incluye:\n- comisión del proveedor de servicios\n- comisión de red por el envío de %1$s desde el exchange de vuelta a la dirección del usuario. \n\nEl deslizamiento (slippage) del proveedor es de hasta %2$s El importe incluye los honorarios del proveedor de servicios. @@ -1434,6 +1449,7 @@ Fondos insuficientes Dar autorización Intercambiar + Intercambiando... Usted recibe Elige token no disponible @@ -1452,6 +1468,9 @@ Mantén tu dinero seguro. Puedes desbloquear en cualquier momento. Tu tarjeta está congelada. Obtener ayuda + Razón: %s + %s · %s + MCC %s Otro No se puede usar en dispositivos rooteados Completado @@ -1528,10 +1547,15 @@ ¿Seguro que desea detener el proceso KYC? Puede retomarlo en cualquier momento. No pudimos verificar tu perfil. Si tienes alguna pregunta, contacta con el soporte. Lamentablemente, no pudimos verificar tu identidad + El proceso KYC ha fallado KYC en curso Ver estado KYC en progreso para Tangem Pay Los documentos suelen verificarse automáticamente en menos de 5 minutos. En casos excepcionales que requieran revisión manual, el proceso puede tardar hasta 48 horas. + KYC rechazado + Ocultar el bloque KYC + Lo sentimos, no pudimos verificarle + su perfil. Obtén tu tarjeta virtual Tangem Visa gratuita Usa USDC para pagos cotidianos Obtener tarjeta @@ -1818,7 +1842,17 @@ Entendido ¡Realmente genial! Actualizar + Iniciar migración + Copiar + La firma de mensajes no es compatible con esta red + No se puede firmar el mensaje. Por favor, inténtelo de nuevo. 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. + Migración de la red Clore + Firmar + Firma Migración de la red Clore Actualmente estás en el modo Demo Modo demo activo diff --git a/core/res/src/main/res/values-fr/strings.xml b/core/res/src/main/res/values-fr/strings.xml index f0253cda22..9e7bbf5db8 100644 --- a/core/res/src/main/res/values-fr/strings.xml +++ b/core/res/src/main/res/values-fr/strings.xml @@ -43,6 +43,7 @@ Ajouter un compte Enregistrer Nom du compte + Un compte portant ce nom existe déjà. Veuillez choisir un autre nom. Nom de compte déjà utilisé Compte Nouveau compte @@ -298,6 +299,7 @@ Aller au jeton Compris Cacher + Maintenir contre %s heure Importez En cours @@ -322,6 +324,7 @@ Non Aucune adresse Non ajouté + Indisponible Pas maintenant Maintenant OK @@ -372,6 +375,7 @@ Échanger Tangem Tangem Wallet + Appuyez et maintenez enfoncé termes et conditions Conditions d\'utilisation À @@ -399,6 +403,8 @@ Mode Rendement Adresse du contrat copiée ! Réseaux disponibles + La dérivation de votre token correspond à la dérivation de %1$s. Votre token sera ajouté à ce compte. + La dérivation appartient à un autre compte. Jeton ajouté au compte %1$s Adresse du contrat L\'adresse du contrat n\'est pas valide @@ -459,6 +465,11 @@ L’envoi d’actifs sur d’autres réseaux entraînera une perte définitive. %s réseau Envoyez des fonds en utilisant uniquement + Meilleures opportunités + Tous les réseaux + Tous les types + Principalement utilisé + Gagner Bonjour équipe de support, j’ai rencontré une erreur avec le code : %s Erreur WalletConnect Vous avez utilisé une carte d\'un autre portefeuille. Appuyez sur la carte associée à ce portefeuille @@ -558,6 +569,8 @@ Commentaires sur Tangem Impossible d\'effectuer une transaction Erreur de description de la pièce + Fonds insuffisants + Frais de transaction Une erreur s\'est produite Une erreur s\'est produite. Code:%s Un mémo est requis @@ -607,6 +620,7 @@ Finalisez d\'abord la sauvegarde. Incomplet Autres méthodes + Conservez votre seed phrase dans un endroit sûr et gardez-la confidentielle afin de protéger vos fonds, puis configurez un code d\'accès pour plus de sécurité. Conservez votre seed phrase dans un endroit sûr et gardez-la confidentielle afin de protéger vos fonds. Seed phrase Pour sécuriser votre wallet avec un code d\'accès, effectuez la procédure de sauvegarde. @@ -644,7 +658,7 @@ Une erreur s\'est produite pendant l\'opération. Vos fonds restent en sécurité et entièrement accessibles pendant toute la durée du processus. Accès aux fonds - Après la mise à niveau, votre portefeuille mobile sera supprimé de l\'application et stocké sur votre hardware wallet; votre seedphrase restera en votre possession. + Après la mise à niveau, votre portefeuille mobile sera supprimé de l\'application et stocké sur votre hardware wallet; votre seed phrase restera en votre possession. Sécurité générale Les clés privées seront transférées de l\'application vers votre hardware wallet Tangem. Migration des clés @@ -992,6 +1006,7 @@ Restauration du code d\'accès Cartes identiques Code d\'accès + Vous ne pouvez avoir qu\'un seul portefeuille mobile. Passez au portefeuille matériel Tangem ou ajoutez un nouveau portefeuille matériel. Toutes les offres Disponible avec %s L\'achat de crypto-monnaies sur Tangem est assuré par des fournisseurs tiers selon leurs conditions. @@ -1036,6 +1051,7 @@ Disponible auprès de Disponible jusqu\'à Vous obtenez + Le service est fourni par un prestataire externe. Tangem n\'est pas responsable. Vous pouvez vérifier l\'état de la transaction à partir de la page du jeton Jusqu\'à Via @@ -1248,18 +1264,21 @@ Cela supprimera le portefeuille de l\'application. Le portefeuille lui-même peut être ajouté à nouveau. Facile à utiliser Gardez vos cryptomonnaies en sécurité et hors ligne. Aussi fin qu\'une carte de crédit, plus sûr qu\'un coffre-fort bancaire. - Pas de seedphrase + Pas de seed phrase Le meilleur hardware wallet Tangem Hardware Wallet Nom Faites travailler votre jeton pour vous + Les frais de réseau sont une petite somme à payer pour traiter et confirmer ta transaction sur la blockchain. Pour commencer à staker, votre compte TON doit être activé avec une transaction de 1 TON. Les fonds restent sur votre compte, car cette étape sert uniquement à l\'activer pour le staking. + Activation du compte Les frais de réseau ont changé. Veuillez vérifier le nouveau montant avant de continuer. Frais de réseau mis à jour Le montant à staker doit être au moins %s Le montant du staking sera arrondi à %1$s TRX en raison des règles du réseau. Le montant d\'annulation du staking sera arrondi à %1$s TRX en raison des règles du réseau. TAEG %1$s%% + Vos récompenses de staking commenceront après 5 époques (~25 jours), une fois que votre délégation aura été enregistrée et comptabilisée par le réseau. Réclamation déstakée Frais de staking du compte Un compte de staking est un compte spécial où sont stockés les jetons SOL stakés. Il est créé lorsque vous déléguez vos jetons à un validateur pour participer à la validation des transactions et gagner des récompenses. Des frais minimes sont facturés pour la création du compte de staking, qui sont restitués une fois le staking terminé. @@ -1270,6 +1289,7 @@ APR APY Les récompenses s\'accumulent automatiquement sur votre solde de staking quotidiennement. + Les récompenses sont ajoutées à votre solde de staking. Fonds gagnés : %s Disponible Taux de récompense moyen Qu\'est-ce que le Staking ? @@ -1325,6 +1345,7 @@ L\'option de restaker vous permet de déplacer vos fonds d\'un validateur à un autre sans avoir besoin de les déstaker. Vous êtes sur le point de staker l\'intégralité de votre solde. Nous vous recommandons de laisser un petit montant pour couvrir les frais de réseau pour unstaking ou la réclamation des récompenses. Pour commencer le staking, vous devez d’abord activer votre compte TON + Activation du compte Pour commencer à staker TON, effectuez d\'abord une transaction sortante de n\'importe quel montant — cela activera votre portefeuille. Jusqu\'à 0,2 TON peuvent être requis en plus des frais de réseau pour terminer la transaction. Tout montant non utilisé sera remboursé. 0,2 TON requis pour effectuer cette opération, en plus des frais de réseau. Veuillez recharger votre solde. @@ -1413,6 +1434,7 @@ Simple et intuitif, vous permettant d\'échanger des jetons en quelques clics Plus Simple Que Jamais Échange via le fournisseur + Vos actifs Le montant comprend :\n• les frais du fournisseur de services\n• les frais de réseau pour l\'envoi de %s depuis l\'échange vers l\'adresse de l\'utilisateur. Le montant comprend :\n• les frais du fournisseur de services\n• les frais de réseau pour l\'envoi de %1$s depuis l\'échange vers l\'adresse de l\'utilisateur. \n\nLe slippage du fournisseur peut atteindre %2$s Le montant comprend les frais du fournisseur de services. @@ -1427,6 +1449,7 @@ Fonds insuffisants Donner l\'autorisation Échanger + Échange... Vous recevez Choisir le jeton non disponible @@ -1445,6 +1468,9 @@ Geler Votre carte est gelée. Obtenir de l\'aide + Raison : %s + %s · %s + MCC %s Autre Impossible à utiliser sur les appareils rootés Terminé @@ -1521,10 +1547,15 @@ Êtes-vous sûr de vouloir interrompre le processus KYC ? Vous pouvez y revenir à tout moment. Nous n\'avons pas pu vérifier votre profil. Pour toute question, veuillez contacter le support. Malheureusement, nous n\'avons pas pu vérifier votre identité + KYC a échoué KYC en cours Voir le statut KYC en cours pour Tangem Pay Les documents sont généralement vérifiés automatiquement en moins de 5 minutes. Dans de rares cas nécessitant une vérification manuelle, le processus peut prendre jusqu’à 48 heures. + KYC rejeté + Masquer le bloc KYC + Désolé, nous n\'avons pas pu vérifier. + votre profil. Obtenez votre carte virtuelle Tangem Visa gratuite Utilisez USDC pour les paiements quotidiens Obtenir la carte @@ -1579,6 +1610,7 @@ Jeton dans le %%image%% %1$s réseau Le jeton %1$s (%2$s) est la principale devise du réseau %3$s et ne peut pas être masqué tant que vous avez d\'autres jetons de ce réseau dans la liste Impossible de masquer %s + Afficher le code QR Échangez ce jeton contre un autre moyennant des frais de service de %1$s du %2$s au %3$s février. Échangez avec Changelly, %s frais Échangez maintenant @@ -1710,6 +1742,12 @@ Scannez votre carte pour déverrouiller l\'accès Déverrouillage nécessaire Choisissez comment ajouter votre portefeuille + Scannez votre carte ou votre bague Tangem pour la restaurer ou l\'importer depuis un autre portefeuille. + Créer un Hardware Wallet + Vous souhaitez acheter un portefeuille Tangem ? + Importer la seed phrase + Restaurez votre portefeuille sur votre téléphone ou importez-le depuis une autre application — pratique, mais moins sécurisé qu\'une carte Tangem. + Que choisir ? La blockchain n\'est pas accessible. Réessayez plus tard Scanner la carte ou la bague Ce portefeuille a déjà été activé auparavant.\nSi cela n\'a pas été fait par vous, veuillez contacter le support.\nTangem ne vend jamais de portefeuilles avec le code d\'accès pré-généré. @@ -1766,9 +1804,9 @@ Ajouter un portefeuille existant Dispositifs physiques qui stockent votre clé privée hors ligne en toute sécurité. Scanner un Tangem Wallet - Importez un portefeuille existant à l\'aide de votre seedphrase. + Importez un portefeuille existant à l\'aide de votre seed phrase. Importer un wallet - Entrez la seedphrase + Entrez la seed phrase Vous avez importé votre portefeuille avec succès. Importer un wallet Importation terminée @@ -1984,7 +2022,7 @@ Livraison rapide Commencez en un seul clic Sans faille et sécurisé - Pas de seedphrase + Pas de seed phrase Facile à utiliser Créez un harware wallet avec Tangem. Aussi fin qu\'une carte bancaire, aussi sûr qu\'un coffre-fort. Créer ou importer un portefeuille logiciel @@ -2105,4 +2143,5 @@ Impossible de couvrir les frais %s Le mode rendement n\'est pas disponible pour le moment. Veuillez réessayer plus tard. Le mode Rendement est indisponible + Impossible de charger le graphique... diff --git a/core/res/src/main/res/values-ja/strings.xml b/core/res/src/main/res/values-ja/strings.xml index cae90804ea..bbdf1ef746 100644 --- a/core/res/src/main/res/values-ja/strings.xml +++ b/core/res/src/main/res/values-ja/strings.xml @@ -294,6 +294,7 @@ トークンへ移動 わかりました 非表示 + %sまで長押し 時間 インポート 進行中 @@ -368,6 +369,7 @@ スワップ Tangem Tangem Wallet + タップして長押し 利用規約 利用規約 宛先 @@ -456,6 +458,11 @@ 他のネットワークで資産を送金すると、永久に失われます。 %sネットワーク 下記のみを使用して資金を送金する + おすすめ + すべてのネットワーク + すべての種類 + よく使われています + 運用 こんにちは、サポートチームの皆さん、コード %s のエラーが発生しました。 WalletConnectエラー 別のウォレットのカードまたはリングを使用しました。このウォレットにリンクしているカードまたはリングをタップしてください。 @@ -935,11 +942,11 @@ 生体認証 シードフレーズについてもっと読む - これらの %d 単語を以下の順番通りに書き留め、安全かつ秘密の場所に保管してください。 + 以下に表示される%d個の単語を順番どおりに書き留め、安全で他人に知られない場所に保管してください。 あなたのシードフレーズ - %d 単語 + %d個の単語 ウォレットをインポートするには、下のフィールドにシードフレーズを入力してください。 シードフレーズを生成する @@ -1252,6 +1259,7 @@ ネットワークルールにより、ステーキング金額は%1$s TRX に切り上げられます。 ネットワークルールにより、ステーキング解除の量は%1$s TRX に切り上げられます。 APR %1$s%% + デリゲーションがネットワークに登録され、集計されるまでの間、ステーキング報酬は5エポック後(約25日後)から開始されます。 ステーキング解除分を請求する ステーキングアカウント手数料 ステーキングアカウントとは、ステーキングされたSOLが保管される特別なアカウントです。トークンをバリデーターに委任し、取引の検証に参加して報酬を受け取る際に作成されます。ステーキングアカウントの作成には少額の手数料がかかりますが、ステーキング完了後に返金されます。 @@ -1441,6 +1449,9 @@ 一時停止 カードが凍結されています サポートを受ける + 理由:%s + %s・%s + MCC %s その他 Root化された端末では使用できません 完了 @@ -1643,25 +1654,69 @@ ウォレット名の変更 すべてのロックを解除 %sですべてをロック解除 + AML認証済み + 利用可能 + ブロック中 + 負債 + 利用上限 + その他(OTPなし) + 単一の取引 + 合計 %d日間利用可能 + 利用可能残高は、保留中の取引、ブロック中の金額、デビット残高を考慮した、実際に利用できる資金を示します。 + %1$sまで利用可能 残高と限度額 + 支払いアカウントの情報が見つかりません。サポートにお問い合わせください。 + トークンの情報を読み込めません。 + 情報を読み込み中です。しばらくお待ちください。 + コストの管理、セキュリティの向上、リスク管理のために利用上限が設けられています。店舗でのカード決済では期間内に%1$s、サブスクリプションや支払いなどのその他の取引では%2$sまで利用できます。 アクセスコードは、支払いアカウントの管理および不正アクセスからの保護に使用されます。 アクセスコード + アカウント有効化 + 登録手続きを開始したウォレットを選択し、ブロックチェーン上でアカウントを作成するための取引に署名してください。 + 有効化をキャンセル 本当に終了してもよろしいですか?中断したところから後で続行できます。 長くはかかりません。アカウントを設定しています。 長くはかかりません。アクティベーションを完了しています。 準備完了です! + その他のウォレット 4桁のコードを設定します。 \nお支払いの際に使用されます。 + PINコード PINコードを作成する PINの認証に失敗しました。もう一度お試しいただくか、別のコードを使用してください。 無効な暗証番号:連続や繰り返しを避けてください + 準備完了です! + Tangemカードを用意し、タップして承認してください。 + Tangemウォレットを準備してください。 + \nサードパーティのWebサイト\nで接続手続きを完了し、その後Tangemアプリに戻ってください。 ウェブサイトに移動 + ウォレット接続 + ウォレットを選択 + 有効化を続行 アカウントの設定を続けましょう。 お帰りなさい! + 有効化を開始 手順に従ってアカウントを設定してください。 ようこそ! + ブロックチェーン上の金額 + 通貨コード + 日付 + エラーコード + 取引詳細 + 加盟店カテゴリーコード + 加盟店の所在地 + 加盟店の国コード + 加盟店名 + リクエストID + ステータス + 取引 + 取引金額 + トランザクションハッシュ + 取引リクエスト + 取引ステータス + 種類 この取引に異議を唱える ロック解除 カードをスキャンしてアクセスロックを解除する diff --git a/core/res/src/main/res/values-ru/strings.xml b/core/res/src/main/res/values-ru/strings.xml index 45f190c03a..2e53a41bee 100644 --- a/core/res/src/main/res/values-ru/strings.xml +++ b/core/res/src/main/res/values-ru/strings.xml @@ -1800,7 +1800,17 @@ Понятно! Очень круто! Обновить + Начать миграцию + Копировать + Подписание сообщений не поддерживается в этой сети + Невозможно подписать сообщение. Пожалуйста, попробуй позже. Согласно официальной документации Clore, все монеты, полученные до 21 декабря, будут мигрированы в токен Clore (ERC-20); монеты, полученные после этой даты, — нет. Решение для перевода находится в разработке — следите за обновлениями. + Сообщение + Открыть портал клейма + Чтобы сохранить доступ к своим средствам, начните миграцию в соответствии с официальной инструкцией Clore. + Миграция сети Clore + Подписать + Подпись Миграция сети Clore Вы находитесь в режиме демо Демо режим включен 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 61ee270e13..8ff96286b1 100644 --- a/core/res/src/main/res/values-uk-rUA/strings.xml +++ b/core/res/src/main/res/values-uk-rUA/strings.xml @@ -44,6 +44,7 @@ Зберегти Назва акаунту Акаунт з такою назвою вже існує. Будь ласка, виберіть іншу назву. + Обліковий запис з таким ім’ям уже існує Акаунт Новий акаунт Додати акаунт diff --git a/core/res/src/main/res/values/strings.xml b/core/res/src/main/res/values/strings.xml index 900691f421..222d92855d 100644 --- a/core/res/src/main/res/values/strings.xml +++ b/core/res/src/main/res/values/strings.xml @@ -1469,6 +1469,9 @@ Freeze Your card is frozen. Get Help + Reason: %s + %s · %s + MCC %s Other Unable to use on rooted devices Completed @@ -1840,7 +1843,17 @@ Ok, Got it! Really cool! Refresh + Start migration + Copy + Message signing is not supported for this network + Unable to sign message. Please try again. According to Clore’s official documentation, all coins received before December 21 will be migrated to Clore (ERC-20 token); coins received after that date will not. A transfer solution is coming — stay tuned. + Message + Open Claim portal + To keep access to your funds, start the migration according to the official Clore guidelines. + Clore Network Migration + Sign + Signature Clore Network Migration You are currently in the Demo mode Demo mode active diff --git a/domain/tokens/models/src/main/java/com/tangem/domain/tokens/model/details/NavigationAction.kt b/domain/tokens/models/src/main/java/com/tangem/domain/tokens/model/details/NavigationAction.kt index e3343ec90b..ca8dbfd9f9 100644 --- a/domain/tokens/models/src/main/java/com/tangem/domain/tokens/model/details/NavigationAction.kt +++ b/domain/tokens/models/src/main/java/com/tangem/domain/tokens/model/details/NavigationAction.kt @@ -6,4 +6,5 @@ import kotlinx.serialization.Serializable sealed class NavigationAction { data object Staking : NavigationAction() data class YieldSupply(val isActive: Boolean) : NavigationAction() + data object CloreMigration : NavigationAction() } \ No newline at end of file diff --git a/domain/transaction/src/main/java/com/tangem/domain/transaction/error/SignCloreMessageError.kt b/domain/transaction/src/main/java/com/tangem/domain/transaction/error/SignCloreMessageError.kt new file mode 100644 index 0000000000..a87418223c --- /dev/null +++ b/domain/transaction/src/main/java/com/tangem/domain/transaction/error/SignCloreMessageError.kt @@ -0,0 +1,10 @@ +package com.tangem.domain.transaction.error + +sealed class SignCloreMessageError { + + data object WalletManagerNotFound : SignCloreMessageError() + + data object MessageSigningNotSupported : SignCloreMessageError() + + data class SigningFailed(val message: String) : SignCloreMessageError() +} \ No newline at end of file diff --git a/domain/transaction/src/main/java/com/tangem/domain/transaction/usecase/SignCloreMessageUseCase.kt b/domain/transaction/src/main/java/com/tangem/domain/transaction/usecase/SignCloreMessageUseCase.kt new file mode 100644 index 0000000000..c4e4913822 --- /dev/null +++ b/domain/transaction/src/main/java/com/tangem/domain/transaction/usecase/SignCloreMessageUseCase.kt @@ -0,0 +1,58 @@ +package com.tangem.domain.transaction.usecase + +import arrow.core.Either +import arrow.core.left +import arrow.core.right +import com.tangem.blockchain.common.MessageSigner +import com.tangem.blockchain.common.TransactionSigner +import com.tangem.common.CompletionResult +import com.tangem.domain.card.repository.CardSdkConfigRepository +import com.tangem.domain.models.currency.CryptoCurrency +import com.tangem.domain.models.wallet.UserWallet +import com.tangem.domain.transaction.error.SignCloreMessageError +import com.tangem.domain.walletmanager.WalletManagersFacade + +class SignCloreMessageUseCase( + private val walletManagersFacade: WalletManagersFacade, + private val cardSdkConfigRepository: CardSdkConfigRepository, + private val getHotWalletSigner: (UserWallet.Hot) -> TransactionSigner, +) { + + suspend operator fun invoke( + userWallet: UserWallet, + currency: CryptoCurrency, + message: String, + ): Either { + val walletManager = walletManagersFacade.getOrCreateWalletManager( + userWalletId = userWallet.walletId, + network = currency.network, + ) + + if (walletManager == null) { + return SignCloreMessageError.WalletManagerNotFound.left() + } + + if (walletManager !is MessageSigner) { + return SignCloreMessageError.MessageSigningNotSupported.left() + } + + val signer = when (userWallet) { + is UserWallet.Cold -> { + val card = userWallet.scanResponse.card + val isCardNotBackedUp = card.backupStatus?.isActive != true + cardSdkConfigRepository.getCommonSigner( + cardId = card.cardId.takeIf { isCardNotBackedUp }, + twinKey = null, + ) + } + is UserWallet.Hot -> getHotWalletSigner(userWallet) + } + + return when (val result = walletManager.signMessage(message, signer)) { + is CompletionResult.Success -> result.data.right() + is CompletionResult.Failure -> SignCloreMessageError.SigningFailed( + message = result.error.message ?: "Unknown error", + ).left() + } + } +} \ No newline at end of file diff --git a/features/tokendetails/impl/build.gradle.kts b/features/tokendetails/impl/build.gradle.kts index 05ec40ad40..10cb869ad1 100644 --- a/features/tokendetails/impl/build.gradle.kts +++ b/features/tokendetails/impl/build.gradle.kts @@ -48,6 +48,7 @@ dependencies { /** Core modules */ implementation(projects.common.routing) implementation(projects.core.navigation) + implementation(projects.core.res) implementation(projects.core.ui) implementation(projects.core.utils) implementation(projects.core.analytics) diff --git a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/model/CloreMigrationModel.kt b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/model/CloreMigrationModel.kt new file mode 100644 index 0000000000..51e1125b6b --- /dev/null +++ b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/model/CloreMigrationModel.kt @@ -0,0 +1,113 @@ +package com.tangem.feature.tokendetails.presentation.tokendetails.model + +import com.tangem.core.decompose.ui.UiMessageSender +import com.tangem.core.ui.clipboard.ClipboardManager +import com.tangem.core.ui.extensions.resourceReference +import com.tangem.core.ui.extensions.stringReference +import com.tangem.core.ui.message.SnackbarMessage +import com.tangem.domain.models.currency.CryptoCurrency +import com.tangem.domain.models.wallet.UserWallet +import com.tangem.domain.transaction.error.SignCloreMessageError +import com.tangem.domain.transaction.usecase.SignCloreMessageUseCase +import com.tangem.feature.tokendetails.presentation.router.InnerTokenDetailsRouter +import com.tangem.feature.tokendetails.presentation.tokendetails.state.TokenDetailsState +import com.tangem.feature.tokendetails.presentation.tokendetails.state.factory.TokenDetailsStateFactory +import com.tangem.features.tokendetails.impl.R +import com.tangem.utils.coroutines.CoroutineDispatcherProvider +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.launch + +// TODO: Remove after Clore migration ends ([REDACTED_TASK_KEY]) +@Suppress("LongParameterList") +internal class CloreMigrationModel( + private val stateFactory: TokenDetailsStateFactory, + private val signCloreMessageUseCase: SignCloreMessageUseCase, + private val clipboardManager: ClipboardManager, + private val uiMessageSender: UiMessageSender, + private val router: InnerTokenDetailsRouter, + private val userWallet: UserWallet, + private val cryptoCurrency: CryptoCurrency, + private val coroutineScope: CoroutineScope, + private val dispatchers: CoroutineDispatcherProvider, + private val onStateUpdate: (TokenDetailsState) -> Unit, +) { + + private val state = MutableStateFlow(CloreMigrationState()) + + fun onCloreMigrationClick() { + state.value = CloreMigrationState() + updateBottomSheet() + } + + fun onCloreSignMessage(message: String) { + if (message.isBlank()) return + + state.value = state.value.copy(message = message) + updateBottomSheet(isSigningInProgress = true) + + coroutineScope.launch(dispatchers.main) { + signCloreMessageUseCase( + userWallet = userWallet, + currency = cryptoCurrency, + message = message, + ).fold( + ifLeft = { error -> + val errorMessage = when (error) { + is SignCloreMessageError.SigningFailed -> stringReference(error.message) + SignCloreMessageError.MessageSigningNotSupported -> + resourceReference(R.string.warning_clore_migration_error_signing_not_supported) + SignCloreMessageError.WalletManagerNotFound -> + resourceReference(R.string.warning_clore_migration_error_wallet_manager_not_found) + } + state.value = state.value.copy(signature = "") + updateBottomSheet() + uiMessageSender.send(SnackbarMessage(errorMessage)) + }, + ifRight = { signature -> + state.value = state.value.copy(signature = signature) + onStateUpdate(stateFactory.getStateWithUpdatedCloreMigrationSignature(signature)) + }, + ) + } + } + + fun onOpenCloreClaimPortal() { + router.openUrl(CLAIM_PORTAL_URL) + } + + private fun updateBottomSheet(isSigningInProgress: Boolean = false) { + val currentState = state.value + onStateUpdate( + stateFactory.getStateWithCloreMigrationBottomSheet( + message = currentState.message, + signature = currentState.signature, + isSigningInProgress = isSigningInProgress, + onMessageChange = { newMessage -> + state.value = state.value.copy(message = newMessage) + updateBottomSheet() + }, + onSignClick = { onCloreSignMessage(state.value.message) }, + onCopyClick = { + val signature = state.value.signature + if (signature.isNotBlank()) { + clipboardManager.setText(text = signature, isSensitive = false) + uiMessageSender.send( + SnackbarMessage(resourceReference(R.string.wallet_notification_address_copied)), + ) + } + }, + onOpenPortalClick = { onOpenCloreClaimPortal() }, + ), + ) + } + + private companion object { + const val CLAIM_PORTAL_URL = "https://claim-portal.clore.ai/" + } +} + +private data class CloreMigrationState( + val message: String = "", + val signature: String = "", +) \ No newline at end of file diff --git a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/model/TokenDetailsClickIntents.kt b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/model/TokenDetailsClickIntents.kt index 66d495e481..b98369cf0c 100644 --- a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/model/TokenDetailsClickIntents.kt +++ b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/model/TokenDetailsClickIntents.kt @@ -72,6 +72,19 @@ interface TokenDetailsClickIntents { fun onBalanceSelect(config: TokenBalanceSegmentedButtonConfig) + fun onYieldInfoClick() + + // region Clore migration + // TODO: Remove after 2025-04-01 when Clore migration ends ([REDACTED_TASK_KEY]) + + fun onCloreMigrationClick() + + fun onCloreSignMessage(message: String) + + fun onOpenCloreClaimPortal() + + // endregion Clore migration + fun onGoToRefundedTokenClick(cryptoCurrency: CryptoCurrency) fun onOpenUrlClick(url: String) @@ -79,6 +92,4 @@ interface TokenDetailsClickIntents { fun onConfirmDisposeExpressStatus() fun onDisposeExpressStatus() - - fun onYieldInfoClick() } \ 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 f1e1c2c815..91134bfd30 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 @@ -155,13 +155,17 @@ internal class TokenDetailsModel @Inject constructor( private val getAccountCryptoCurrencyStatusUseCase: GetAccountCurrencyStatusUseCase, private val manageCryptoCurrenciesUseCase: ManageCryptoCurrenciesUseCase, private val yieldSupplyGetRewardsBalanceUseCase: YieldSupplyGetRewardsBalanceUseCase, -) : Model(), TokenDetailsClickIntents, YieldSupplyDepositedWarningComponent.ModelCallback { + private val signCloreMessageUseCase: SignCloreMessageUseCase, +) : Model(), + TokenDetailsClickIntents, + YieldSupplyDepositedWarningComponent.ModelCallback { private val params = paramsContainer.require() private val userWalletId: UserWalletId = params.userWalletId private val cryptoCurrency: CryptoCurrency = params.currency - private val userWallet: UserWallet = getUserWalletUseCase(userWalletId).getOrNull() ?: error("UserWallet not found") + private val userWallet: UserWallet = getUserWalletUseCase(userWalletId).getOrNull() + ?: error("UserWallet not found") private val marketPriceJobHolder = JobHolder() private val refreshStateJobHolder = JobHolder() @@ -193,6 +197,27 @@ internal class TokenDetailsModel @Inject constructor( yieldSupplyFeatureToggles = yieldSupplyFeatureToggles, ) + private val internalUiState = MutableStateFlow(stateFactory.getInitialState(cryptoCurrency)) + val uiState: StateFlow = internalUiState + + // region Clore migration + // TODO: Remove after Clore migration ends ([REDACTED_TASK_KEY]) + private val cloreMigrationModel by lazy(mode = LazyThreadSafetyMode.NONE) { + CloreMigrationModel( + stateFactory = stateFactory, + signCloreMessageUseCase = signCloreMessageUseCase, + clipboardManager = clipboardManager, + uiMessageSender = uiMessageSender, + router = router, + userWallet = userWallet, + cryptoCurrency = cryptoCurrency, + coroutineScope = modelScope, + dispatchers = dispatchers, + onStateUpdate = { internalUiState.value = it }, + ) + } + // endregion + private val expressStatusFactory by lazy(mode = LazyThreadSafetyMode.NONE) { expressStatusFactory.create( clickIntents = this, @@ -215,9 +240,6 @@ internal class TokenDetailsModel @Inject constructor( TokenDetailsCurrencyStatusAnalyticsSender(analyticsEventsHandler) } - private val internalUiState = MutableStateFlow(stateFactory.getInitialState(cryptoCurrency)) - val uiState: StateFlow = internalUiState - init { updateTopBarMenu() initButtons() @@ -666,9 +688,8 @@ internal class TokenDetailsModel @Inject constructor( vibratorHapticManager.performOneTime(TangemHapticEffect.OneTime.Click) clipboardManager.setText(text = extendedKey, isSensitive = true) - uiMessageSender.send( - message = SnackbarMessage(message = resourceReference(R.string.wallet_notification_address_copied)), - ) + val message = resourceReference(R.string.wallet_notification_address_copied) + uiMessageSender.send(message = SnackbarMessage(message = message)) } } } @@ -785,7 +806,8 @@ internal class TokenDetailsModel @Inject constructor( modelScope.launch(dispatchers.main) { when (val addresses = currencyStatus.value.networkAddress) { is NetworkAddress.Selectable -> { - internalUiState.value = stateFactory.getStateWithChooseAddressBottomSheet(cryptoCurrency, addresses) + internalUiState.value = + stateFactory.getStateWithChooseAddressBottomSheet(cryptoCurrency, addresses) } is NetworkAddress.Single -> { router.openUrl( @@ -1265,11 +1287,26 @@ internal class TokenDetailsModel @Inject constructor( } private fun handleNavigationParam() { - if (params.navigationAction is NavigationAction.Staking) { - openStaking() + when (params.navigationAction) { + is NavigationAction.Staking -> openStaking() + is NavigationAction.CloreMigration -> onCloreMigrationClick() + is NavigationAction.YieldSupply, + null, + -> Unit } } + // region Clore migration + // TODO: Remove after Clore migration ends ([REDACTED_TASK_KEY]) + + override fun onCloreMigrationClick() = cloreMigrationModel.onCloreMigrationClick() + + override fun onCloreSignMessage(message: String) = cloreMigrationModel.onCloreSignMessage(message) + + override fun onOpenCloreClaimPortal() = cloreMigrationModel.onOpenCloreClaimPortal() + + // endregion Clore migration + private companion object { const val EXPRESS_STATUS_UPDATE_DELAY = 10_000L } diff --git a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/components/TokenDetailsNotification.kt b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/components/TokenDetailsNotification.kt index 5733db0e18..b8a992221f 100644 --- a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/components/TokenDetailsNotification.kt +++ b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/components/TokenDetailsNotification.kt @@ -258,9 +258,15 @@ internal sealed class TokenDetailsNotification(val config: NotificationConfig) { subtitle = resourceReference(id = R.string.warning_matic_migration_message), ) - data object MigrationClore : Warning( + data class MigrationClore( + private val onMigrationClick: () -> Unit, + ) : Warning( title = resourceReference(id = R.string.warning_clore_migration_title), - subtitle = resourceReference(id = R.string.warning_clore_migration_message), + subtitle = resourceReference(id = R.string.warning_clore_migration_sheet_description), + buttonsState = NotificationConfig.ButtonsState.SecondaryButtonConfig( + text = resourceReference(id = R.string.warning_clore_migration_button), + onClick = onMigrationClick, + ), ) data object UsedOutdatedData : TokenDetailsNotification( diff --git a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/TokenDetailsNotificationConverter.kt b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/TokenDetailsNotificationConverter.kt index eb54aca878..38588b2f69 100644 --- a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/TokenDetailsNotificationConverter.kt +++ b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/TokenDetailsNotificationConverter.kt @@ -155,7 +155,9 @@ internal class TokenDetailsNotificationConverter( }, ) is CryptoCurrencyWarning.MigrationMaticToPol -> MigrationMaticToPol - is CryptoCurrencyWarning.MigrationClore -> MigrationClore + is CryptoCurrencyWarning.MigrationClore -> MigrationClore( + onMigrationClick = clickIntents::onCloreMigrationClick, + ) is CryptoCurrencyWarning.UsedOutdatedDataWarning -> UsedOutdatedData } } diff --git a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/TokenDetailsStateFactory.kt b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/TokenDetailsStateFactory.kt index da13ce39ba..953f1376e8 100644 --- a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/TokenDetailsStateFactory.kt +++ b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/TokenDetailsStateFactory.kt @@ -4,6 +4,7 @@ import arrow.core.Either import com.tangem.common.ui.bottomsheet.chooseaddress.ChooseAddressBottomSheetConfig import com.tangem.common.ui.bottomsheet.receive.TokenReceiveBottomSheetConfig import com.tangem.common.ui.tokens.getUnavailabilityReasonText +import com.tangem.feature.tokendetails.presentation.tokendetails.ui.components.clore.CloreMigrationBottomSheetConfig import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig import com.tangem.core.ui.components.dropdownmenu.TangemDropdownMenuItem import com.tangem.core.ui.extensions.TextReference @@ -378,4 +379,62 @@ internal class TokenDetailsStateFactory( }.toImmutableList(), ) } + + // region Clore migration + // TODO: Remove after Clore migration ends ([REDACTED_TASK_KEY]) + + fun getStateWithCloreMigrationBottomSheet( + message: String, + signature: String, + isSigningInProgress: Boolean, + onMessageChange: (String) -> Unit, + onSignClick: () -> Unit, + onCopyClick: () -> Unit, + onOpenPortalClick: () -> Unit, + ): TokenDetailsState { + return currentStateProvider().copy( + bottomSheetConfig = TangemBottomSheetConfig( + isShown = true, + onDismissRequest = clickIntents::onDismissBottomSheet, + content = CloreMigrationBottomSheetConfig( + message = message, + signature = signature, + isSigningInProgress = isSigningInProgress, + onMessageChange = onMessageChange, + onSignClick = onSignClick, + onCopyClick = onCopyClick, + onOpenPortalClick = onOpenPortalClick, + ), + ), + ) + } + + fun getStateWithUpdatedCloreMigrationSignature(signature: String): TokenDetailsState { + val state = currentStateProvider() + val bottomSheetConfig = state.bottomSheetConfig ?: return state + val content = bottomSheetConfig.content as? CloreMigrationBottomSheetConfig ?: return state + + return state.copy( + bottomSheetConfig = bottomSheetConfig.copy( + content = content.copy( + signature = signature, + isSigningInProgress = false, + ), + ), + ) + } + + fun getStateWithCloreMigrationSigning(): TokenDetailsState { + val state = currentStateProvider() + val bottomSheetConfig = state.bottomSheetConfig ?: return state + val content = bottomSheetConfig.content as? CloreMigrationBottomSheetConfig ?: return state + + return state.copy( + bottomSheetConfig = bottomSheetConfig.copy( + content = content.copy(isSigningInProgress = true), + ), + ) + } + + // endregion Clore migration } \ 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 f3466d9025..4dc151587f 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 @@ -35,6 +35,8 @@ import com.tangem.feature.tokendetails.presentation.tokendetails.ui.components.T import com.tangem.feature.tokendetails.presentation.tokendetails.ui.components.TokenDetailsDialogs import com.tangem.feature.tokendetails.presentation.tokendetails.ui.components.TokenDetailsTopAppBar import com.tangem.feature.tokendetails.presentation.tokendetails.ui.components.TokenInfoBlock +import com.tangem.feature.tokendetails.presentation.tokendetails.ui.components.clore.CloreMigrationBottomSheet +import com.tangem.feature.tokendetails.presentation.tokendetails.ui.components.clore.CloreMigrationBottomSheetConfig import com.tangem.feature.tokendetails.presentation.tokendetails.ui.components.express.ExpressStatusBottomSheet import com.tangem.feature.tokendetails.presentation.tokendetails.ui.components.staking.TokenStakingBlock import com.tangem.features.markets.token.block.TokenMarketBlockComponent @@ -45,7 +47,7 @@ import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow // TODO: Split to blocks [REDACTED_JIRA] -@Suppress("LongMethod") +@Suppress("LongMethod", "CyclomaticComplexMethod") @Composable internal fun TokenDetailsScreen( state: TokenDetailsState, @@ -164,17 +166,25 @@ internal fun TokenDetailsScreen( TokenDetailsDialogs(state = state) - state.bottomSheetConfig?.let { config -> - when (config.content) { - is TokenReceiveBottomSheetConfig -> { - TokenReceiveBottomSheet(config = config) - } - is ChooseAddressBottomSheetConfig -> { - ChooseAddressBottomSheet(config = config) - } - is ExpressStatusBottomSheetConfig -> { - ExpressStatusBottomSheet(config = config) - } + TokenDetailsBottomSheets(state = state) + } +} + +@Composable +private fun TokenDetailsBottomSheets(state: TokenDetailsState) { + state.bottomSheetConfig?.let { config -> + when (config.content) { + is TokenReceiveBottomSheetConfig -> { + TokenReceiveBottomSheet(config = config) + } + is ChooseAddressBottomSheetConfig -> { + ChooseAddressBottomSheet(config = config) + } + is ExpressStatusBottomSheetConfig -> { + ExpressStatusBottomSheet(config = config) + } + is CloreMigrationBottomSheetConfig -> { + CloreMigrationBottomSheet(config = config) } } } diff --git a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/ui/components/clore/CloreMigrationBottomSheet.kt b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/ui/components/clore/CloreMigrationBottomSheet.kt new file mode 100644 index 0000000000..9875449e80 --- /dev/null +++ b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/ui/components/clore/CloreMigrationBottomSheet.kt @@ -0,0 +1,220 @@ +package com.tangem.feature.tokendetails.presentation.tokendetails.ui.components.clore + +import android.content.res.Configuration +import androidx.compose.foundation.border +import androidx.compose.foundation.layout.* +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import com.tangem.core.ui.R as CoreR +import com.tangem.core.ui.components.PrimaryButton +import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig +import com.tangem.core.ui.components.bottomsheets.modal.TangemModalBottomSheet +import com.tangem.core.ui.components.bottomsheets.modal.TangemModalBottomSheetTitle +import com.tangem.core.ui.components.buttons.SecondarySmallButton +import com.tangem.core.ui.components.buttons.SmallButtonConfig +import com.tangem.core.ui.components.fields.SimpleTextField +import com.tangem.core.ui.extensions.TextReference +import com.tangem.core.ui.extensions.stringResourceSafe +import com.tangem.core.ui.res.TangemTheme +import com.tangem.core.ui.res.TangemThemePreview +import com.tangem.features.tokendetails.impl.R + +@Composable +internal fun CloreMigrationBottomSheet(config: TangemBottomSheetConfig) { + TangemModalBottomSheet( + config = config, + title = { + TangemModalBottomSheetTitle( + endIconRes = CoreR.drawable.ic_close_24, + onEndClick = config.onDismissRequest, + ) + }, + ) { content: CloreMigrationBottomSheetConfig -> + CloreMigrationBottomSheetContent(content = content) + } +} + +@Composable +private fun CloreMigrationBottomSheetContent(content: CloreMigrationBottomSheetConfig) { + Column( + modifier = Modifier + .padding(bottom = 16.dp) + .padding(horizontal = 16.dp), + horizontalAlignment = Alignment.CenterHorizontally, + verticalArrangement = Arrangement.spacedBy(space = 16.dp), + ) { + Text( + text = stringResourceSafe(R.string.warning_clore_migration_sheet_title), + color = TangemTheme.colors.text.primary1, + textAlign = TextAlign.Center, + style = TangemTheme.typography.h3, + ) + + Text( + text = stringResourceSafe(R.string.warning_clore_migration_sheet_description), + color = TangemTheme.colors.text.secondary, + textAlign = TextAlign.Center, + style = TangemTheme.typography.body2, + modifier = Modifier.padding(horizontal = 8.dp), + ) + + Spacer(modifier = Modifier.height(8.dp)) + + MessageField( + placeholder = stringResourceSafe(R.string.warning_clore_migration_message_label), + value = content.message, + onValueChange = content.onMessageChange, + buttonText = stringResourceSafe(R.string.warning_clore_migration_sign_button), + onButtonClick = content.onSignClick, + isButtonEnabled = content.message.isNotBlank() && !content.isSigningInProgress, + isLoading = content.isSigningInProgress, + ) + + SignatureField( + placeholder = stringResourceSafe(R.string.warning_clore_migration_signature_label), + value = content.signature, + onCopyClick = content.onCopyClick, + ) + + Spacer(modifier = Modifier.height(8.dp)) + + PrimaryButton( + text = stringResourceSafe(R.string.warning_clore_migration_open_portal_button), + onClick = content.onOpenPortalClick, + modifier = Modifier.fillMaxWidth(), + ) + } +} + +@Suppress("LongParameterList") +@Composable +private fun MessageField( + placeholder: String, + value: String, + onValueChange: (String) -> Unit, + buttonText: String, + onButtonClick: () -> Unit, + isButtonEnabled: Boolean, + isLoading: Boolean, +) { + Row( + modifier = Modifier + .fillMaxWidth() + .border( + width = 1.dp, + color = TangemTheme.colors.stroke.primary, + shape = TangemTheme.shapes.roundedCornersSmall, + ) + .heightIn(min = 56.dp) + .padding(12.dp), + verticalAlignment = Alignment.CenterVertically, + ) { + SimpleTextField( + value = value, + onValueChange = onValueChange, + placeholder = TextReference.Str(placeholder), + modifier = Modifier.weight(1f), + singleLine = false, + ) + + Spacer(modifier = Modifier.width(8.dp)) + + SecondarySmallButton( + config = SmallButtonConfig( + text = TextReference.Str(buttonText), + onClick = onButtonClick, + isEnabled = isButtonEnabled, + isLoading = isLoading, + ), + ) + } +} + +@Suppress("FunctionSignature") +@Composable +private fun SignatureField(placeholder: String, value: String, onCopyClick: () -> Unit) { + Row( + modifier = Modifier + .fillMaxWidth() + .border( + width = 1.dp, + color = TangemTheme.colors.stroke.primary, + shape = TangemTheme.shapes.roundedCornersSmall, + ) + .heightIn(min = 56.dp) + .padding(12.dp), + verticalAlignment = Alignment.CenterVertically, + ) { + SimpleTextField( + value = value, + onValueChange = {}, + placeholder = TextReference.Str(placeholder), + modifier = Modifier.weight(1f), + readOnly = true, + singleLine = false, + ) + + Spacer(modifier = Modifier.width(8.dp)) + + SecondarySmallButton( + config = SmallButtonConfig( + text = TextReference.Res(R.string.warning_clore_migration_copy_button), + onClick = onCopyClick, + isEnabled = value.isNotBlank(), + ), + ) + } +} + +// region Preview +@Composable +@Preview(showBackground = true, widthDp = 360) +@Preview(showBackground = true, widthDp = 360, uiMode = Configuration.UI_MODE_NIGHT_YES) +private fun Preview_CloreMigrationBottomSheet() { + TangemThemePreview { + val config = TangemBottomSheetConfig( + isShown = true, + content = CloreMigrationBottomSheetConfig( + message = "Claim request for CLORE tokens to Ethereum address " + + "0x742d35Cc6634C0532925a3b844Bc9e7595f60126 from CEsMERNgBVPo9Dkc99pRDMPD3mxwPMxHZi", + signature = "H4sIAAAAAAAEAGNgGAWjYBSMglEwCkYBHQEA", + isSigningInProgress = false, + onMessageChange = {}, + onSignClick = {}, + onCopyClick = {}, + onOpenPortalClick = {}, + ), + onDismissRequest = {}, + ) + + CloreMigrationBottomSheet(config) + } +} + +@Composable +@Preview(showBackground = true, widthDp = 360) +private fun Preview_CloreMigrationBottomSheet_Empty() { + TangemThemePreview { + val config = TangemBottomSheetConfig( + isShown = true, + content = CloreMigrationBottomSheetConfig( + message = "", + signature = "", + isSigningInProgress = false, + onMessageChange = {}, + onSignClick = {}, + onCopyClick = {}, + onOpenPortalClick = {}, + ), + onDismissRequest = {}, + ) + + CloreMigrationBottomSheet(config) + } +} +// endregion Preview \ No newline at end of file diff --git a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/ui/components/clore/CloreMigrationBottomSheetConfig.kt b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/ui/components/clore/CloreMigrationBottomSheetConfig.kt new file mode 100644 index 0000000000..9adf70e4af --- /dev/null +++ b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/ui/components/clore/CloreMigrationBottomSheetConfig.kt @@ -0,0 +1,15 @@ +package com.tangem.feature.tokendetails.presentation.tokendetails.ui.components.clore + +import androidx.compose.runtime.Immutable +import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfigContent + +@Immutable +data class CloreMigrationBottomSheetConfig( + val message: String = "", + val signature: String = "", + val isSigningInProgress: Boolean = false, + val onMessageChange: (String) -> Unit, + val onSignClick: () -> Unit, + val onCopyClick: () -> Unit, + val onOpenPortalClick: () -> Unit, +) : TangemBottomSheetConfigContent \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/wallet/model/intents/WalletContentClickIntents.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/wallet/model/intents/WalletContentClickIntents.kt index d301a20f0c..2526056e5f 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/wallet/model/intents/WalletContentClickIntents.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/wallet/model/intents/WalletContentClickIntents.kt @@ -202,6 +202,7 @@ internal class WalletContentClickIntentsImplementor @Inject constructor( navigationAction = navigationAction, apy = apy, ) + is NavigationAction.CloreMigration -> Unit } } @@ -298,6 +299,7 @@ internal class WalletContentClickIntentsImplementor @Inject constructor( }, ) } + is NavigationAction.CloreMigration -> return } analyticsEventHandler.send(event) } diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/wallet/model/intents/WalletWarningsClickIntents.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/wallet/model/intents/WalletWarningsClickIntents.kt index ed19a09083..2f90546c14 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/wallet/model/intents/WalletWarningsClickIntents.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/wallet/model/intents/WalletWarningsClickIntents.kt @@ -18,6 +18,7 @@ import com.tangem.domain.feedback.GetWalletMetaInfoUseCase import com.tangem.domain.feedback.SendFeedbackEmailUseCase import com.tangem.domain.feedback.models.FeedbackEmailType import com.tangem.domain.models.currency.CryptoCurrency +import com.tangem.domain.models.currency.CryptoCurrencyStatus import com.tangem.domain.models.wallet.UserWallet import com.tangem.domain.models.wallet.UserWalletId import com.tangem.domain.models.wallet.requireColdWallet @@ -35,6 +36,7 @@ import com.tangem.domain.staking.multi.MultiStakingBalanceFetcher import com.tangem.domain.tokens.model.analytics.PromoAnalyticsEvent import com.tangem.domain.tokens.model.analytics.PromoAnalyticsEvent.Program import com.tangem.domain.tokens.model.analytics.PromoAnalyticsEvent.PromotionBannerClicked +import com.tangem.domain.tokens.model.details.NavigationAction import com.tangem.domain.wallets.legacy.UserWalletsListManager.Lockable.UnlockType import com.tangem.domain.wallets.models.UnlockWalletsError import com.tangem.domain.wallets.usecase.* @@ -65,6 +67,8 @@ internal interface WalletWarningsClickIntents { fun onAddBackupCardClick() + fun onCloreMigrationClick(cryptoCurrencyStatus: CryptoCurrencyStatus) + fun onCloseAlreadySignedHashesWarningClick() fun onGenerateMissedAddressesClick(missedAddressCurrencies: List) @@ -146,6 +150,16 @@ internal class WalletWarningsClickIntentsImplementor @Inject constructor( prepareAndStartOnboardingProcess() } + override fun onCloreMigrationClick(cryptoCurrencyStatus: CryptoCurrencyStatus) { + val userWallet = getSelectedUserWallet() ?: return + + router.openTokenDetails( + userWalletId = userWallet.walletId, + currencyStatus = cryptoCurrencyStatus, + navigationAction = NavigationAction.CloreMigration, + ) + } + private fun prepareAndStartOnboardingProcess() { modelScope.launch(dispatchers.main) { getSelectedUserWallet()?.requireColdWallet()?.let { diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/analytics/utils/WalletWarningsAnalyticsSender.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/analytics/utils/WalletWarningsAnalyticsSender.kt index 94ede58c14..62dc30de88 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/analytics/utils/WalletWarningsAnalyticsSender.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/analytics/utils/WalletWarningsAnalyticsSender.kt @@ -76,6 +76,7 @@ internal class WalletWarningsAnalyticsSender @Inject constructor( is WalletNotification.UsedOutdatedData, is WalletNotification.UnlockVisaAccess, is WalletNotification.Warning.YeildSupplyApprove, // TODO apply correct event + is WalletNotification.CloreMigration, -> null is WalletNotification.FinishWalletActivation -> { val activationState = if (warning.isBackupExists) { diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/domain/GetMultiWalletWarningsFactory.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/domain/GetMultiWalletWarningsFactory.kt index b757338349..ed279ff217 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/domain/GetMultiWalletWarningsFactory.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/domain/GetMultiWalletWarningsFactory.kt @@ -33,6 +33,7 @@ import com.tangem.feature.wallet.impl.R import com.tangem.feature.wallet.presentation.account.AccountDependencies import com.tangem.feature.wallet.presentation.wallet.state.model.WalletNotification import com.tangem.hot.sdk.model.HotWalletId +import com.tangem.lib.crypto.BlockchainUtils import com.tangem.utils.extensions.addIf import com.tangem.utils.extensions.isPositive import com.tangem.utils.extensions.orZero @@ -317,6 +318,29 @@ internal class GetMultiWalletWarningsFactory @Inject constructor( element = WalletNotification.Warning.SomeNetworksUnreachable, condition = flattenCurrencies.hasUnreachableNetworks(), ) + + addCloreMigrationNotification(flattenCurrencies, clickIntents) + } + + private fun MutableList.addCloreMigrationNotification( + flattenCurrencies: Lce>, + clickIntents: WalletClickIntents, + ) { + val cloreCurrency = flattenCurrencies.findCloreCurrency() ?: return + + add( + WalletNotification.CloreMigration( + onStartMigrationClick = { clickIntents.onCloreMigrationClick(cloreCurrency) }, + ), + ) + } + + private fun Lce>.findCloreCurrency(): CryptoCurrencyStatus? { + val currencies = getOrNull(isPartialContentAccepted = true) ?: return null + + return currencies.find { currencyStatus -> + BlockchainUtils.isClore(currencyStatus.currency.network.rawId) + } } private fun MutableList.addPushReminderNotification( diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/model/WalletNotification.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/model/WalletNotification.kt index e2dc6648fc..b0563ac622 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/model/WalletNotification.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/model/WalletNotification.kt @@ -414,4 +414,18 @@ sealed class WalletNotification(val config: NotificationConfig) { iconSize = 54.dp, ), ) + + data class CloreMigration( + val onStartMigrationClick: () -> Unit, + ) : WalletNotification( + config = NotificationConfig( + title = resourceReference(com.tangem.core.res.R.string.warning_clore_migration_title), + subtitle = resourceReference(com.tangem.core.res.R.string.warning_clore_migration_sheet_description), + iconResId = R.drawable.img_attention_20, + buttonsState = NotificationConfig.ButtonsState.SecondaryButtonConfig( + text = resourceReference(com.tangem.core.res.R.string.warning_clore_migration_button), + onClick = onStartMigrationClick, + ), + ), + ) } \ No newline at end of file diff --git a/gradle/tangem_dependencies.toml b/gradle/tangem_dependencies.toml index d8e493fcf3..d7281ac75f 100644 --- a/gradle/tangem_dependencies.toml +++ b/gradle/tangem_dependencies.toml @@ -5,7 +5,7 @@ # https://github.com/tangem/tangem-sdk-android/ # https://github.com/tangem/vico -tangemBlockchainSdk = "releases-5.32-1388" +tangemBlockchainSdk = "releases-5.32-1404" #tangemBlockchainSdk = "0.0.1" # Keep it! - used for local builds tangemCardSdk = "releases-5.32-574" #tangemCardSdk = "0.0.1" # Keep it! - used for local builds ^ diff --git a/tangem-android-tools b/tangem-android-tools index 28079b094a..cfc2c0ab47 160000 --- a/tangem-android-tools +++ b/tangem-android-tools @@ -1 +1 @@ -Subproject commit 28079b094a4518de068161226576747089519918 +Subproject commit cfc2c0ab47e2e821f38f463f32f120d620804c72