diff --git a/app/src/main/java/com/tangem/tap/features/onboarding/products/wallet/redux/OnboardingWalletMiddleware.kt b/app/src/main/java/com/tangem/tap/features/onboarding/products/wallet/redux/OnboardingWalletMiddleware.kt index dc9e77a48d..58d13a9b0e 100644 --- a/app/src/main/java/com/tangem/tap/features/onboarding/products/wallet/redux/OnboardingWalletMiddleware.kt +++ b/app/src/main/java/com/tangem/tap/features/onboarding/products/wallet/redux/OnboardingWalletMiddleware.kt @@ -4,6 +4,7 @@ import android.net.Uri import com.google.firebase.crashlytics.FirebaseCrashlytics import com.tangem.common.CompletionResult import com.tangem.common.card.Card +import com.tangem.common.core.TangemError import com.tangem.common.core.TangemSdkError import com.tangem.common.extensions.ifNotNull import com.tangem.common.extensions.toHexString @@ -226,7 +227,7 @@ private fun navigateToWalletScreen() { } } -private suspend fun readCard(onSuccess: suspend (ScanResponse) -> Unit) { +private suspend fun readCard(onSuccess: suspend (ScanResponse) -> Unit, onFailure: (TangemError) -> Unit) { val shouldSaveAccessCodes = store.inject(DaggerGraphState::settingsRepository).shouldSaveAccessCodes() store.inject(DaggerGraphState::cardSdkConfigRepository).setAccessCodeRequestPolicy( @@ -247,6 +248,7 @@ private suspend fun readCard(onSuccess: suspend (ScanResponse) -> Unit) { Timber.e(it, "Unable to scan card") delay(HIDE_PROGRESS_DELAY) store.dispatch(HomeAction.ScanInProgress(scanInProgress = false)) + onFailure(it) }, onSuccess = onSuccess, ) @@ -605,9 +607,14 @@ private fun handleBackupAction(appState: () -> AppState?, action: BackupAction) } if (scanResponse == null) { scope.launch { - readCard { newScanResponse -> - handleFinishBackup(newScanResponse) - } + readCard( + onSuccess = { newScanResponse -> + handleFinishBackup(newScanResponse) + }, + onFailure = { + store.dispatchNavigationAction(AppRouter::pop) + }, + ) } } else { handleFinishBackup(scanResponse) @@ -643,10 +650,15 @@ private fun handleBackupAction(appState: () -> AppState?, action: BackupAction) } else { delay(HIDE_PROGRESS_DELAY) - readCard { newScanResponse -> - scanResponse = newScanResponse - userWallet = createUserWallet(newScanResponse, backupState) - } + readCard( + onSuccess = { newScanResponse -> + scanResponse = newScanResponse + userWallet = createUserWallet(newScanResponse, backupState) + }, + onFailure = { + store.dispatchNavigationAction(AppRouter::pop) + }, + ) } val notActivatedCardIds = gatherCardIds(backupState, card).mapNotNull { diff --git a/core/res/src/main/res/values-de/strings.xml b/core/res/src/main/res/values-de/strings.xml index 65a4990969..d8c34ccdc5 100644 --- a/core/res/src/main/res/values-de/strings.xml +++ b/core/res/src/main/res/values-de/strings.xml @@ -180,6 +180,7 @@ Nutzungsbedingungen Heute Transaktion fehlgeschlagen + Transaktionsstatus Transaktionen Überweisung Ich verstehe @@ -187,6 +188,7 @@ Nicht erreichbar staking beenden woche + mit Ja Vertragsadresse kopiert! Verfügbare Netzwerke @@ -292,6 +294,9 @@ Für dieses Paar nicht verfügbar Erlaubnis erforderlich Empfohlen + Gekauft %s + Kauf %s + Kauf %s... Keine Token gefunden. Bitte versuche eine andere Anfrage ID: %s Transaktions-ID kopiert @@ -568,13 +573,14 @@ Keine verfügbaren Anbieter für diese Währung Bezahlen mit Du kannst Deine Transaktion beim Drittanbieter %s abschließen. - Umleitung auf %s … + Umleitung auf %s… Unsere Dienstleistungen sind in diesem Land nicht verfügbar Änder oder bestätige bitte Dein Wohnsitz wurde identifiziert als Residenz Bitte wähle das richtige Land aus, um korrekte Zahlungsoptionen und Dienstleistungen zu gewährleisten. Einstellungen + Du kannst den Transaktionsstatus auf der Token-Seite überprüfen. Über Gruppe erstellen Nach Guthaben @@ -853,14 +859,14 @@ Angezeigte Salden Rückgängig machen Dieser Vorgang ist derzeit nicht verfügbar. Bitte versuche es später noch einmal. - Der Kauf von %s ist im Moment nicht verfügbar. Bitte prüfe später ob es Updates gibt. + Der Kauf von %s wird von aktuellen Anbietern nicht unterstützt, aber wir arbeiten daran, weitere Optionen hinzuzufügen. Du hast kein Guthaben um dies zu Verkaufen. Lade dein Konto auf, um Geld von dort zu verkaufen. Du hast kein Guthaben zum Versenden. Lade dein Konto auf, um Geld von dort aus senden zu können. - Swappen %s ist im Moment nicht verfügbar. Bitte prüfe später ob es Updates gibt. + Das Tauschen von %s wird von aktuellen Anbietern nicht unterstützt, aber wir arbeiten daran, weitere Optionen hinzuzufügen. Das Geld für den Verkauf steht zur Verfügung, sobald die ausstehende(n) Transaktion(en) im Netzwerk %s abgeschlossen ist/sind. Die Geldsendung wird verfügbar, sobald die ausstehende(n) Transaktion(en) im Netzwerk %s abgeschlossen ist/sind. - Der Verkauf von %s ist im Moment nicht verfügbar. Bitte prüfe später ob es Updates gibt. - Staking %s ist aktuell nicht verfügbar. Prüfe bitte ob es ein neues Update gibt. + Der Verkauf von %s wird von aktuellen Anbietern nicht unterstützt, aber wir arbeiten daran, weitere Optionen hinzuzufügen. + Staking %s wird von aktuellen Anbietern nicht unterstützt, aber wir arbeiten daran, weitere Optionen hinzuzufügen. Generiere XPUB Ausblenden Du bist dabei, dieses Token vom Hauptbildschirm auszublenden. Du kannst es jederzeit über die Seite „Token verwalten“ wieder hinzufügen. @@ -953,6 +959,9 @@ %s Netzwerk Die Adresse wurde erfolgreich kopiert Keine Internetverbindung + Jetzt mit 10 %% Rabatt kaufen + Greife auf 13.000+ Kryptowährungen zu. Kaufe, verkaufe, tausche und stake Sie mit einem einzigen Fingertipp.\nVerbinde bis zu drei Karten für ein Backup. + Entdecke die Tangem Wallet Wallet-Einstellungen Tangem Verwende %s oder scanne eine Karte oder Ring, um den Zugriff auf deine Wallet freizuschalten. @@ -998,6 +1007,9 @@ Dieses Token muss mit deinem Hedera-Konto verknüpft sein, bevor du ihn erhalten kannst. Verknüpfe deinen Token Nicht genug %s. Lade dein Hedera-Konto auf, um dieses Token zuzuordnen + Bist Du sicher, dass Du die Transaktion stornieren willst? Du kannst die Transaktion nicht erneut starten. + Deine Transaktion mit einem Betrag von %1$s %2$s wurde nicht abgeschlossen. Du kannst versuchen, diese erneut abzuschließen. + Du hast eine nicht abgeschlossene Transaktion Auf dieser Karte oder Ring sind nur noch %s Unterschriften übrig. Du musst dein gesamtes Guthaben abheben. Geringe Anzahl von Unterschriften Token in verschiedenen Netzwerken können unterschiedliche Adressen haben. Überprüfe bei der Überweisung noch einmal, ob deine Adresse mit der des Netzwerks übereinstimmt. diff --git a/core/res/src/main/res/values-es/strings.xml b/core/res/src/main/res/values-es/strings.xml index ee04d97a2f..b77f4a6456 100644 --- a/core/res/src/main/res/values-es/strings.xml +++ b/core/res/src/main/res/values-es/strings.xml @@ -858,14 +858,14 @@ Saldos mostrados Cancelar Esta operación no está disponible actualmente. Por favor, inténtalo de nuevo más tarde. - Comprar %s no está disponible en este momento. Por favor revise sus actualizaciones. + Comprar %s no está disponible con los proveedores actuales pero estamos trabajando para agregar más opciones. No tiene fondos para vender. Recargue su cuenta para poder vender fondos desde ella. No tienes fondos para enviar. Recarga tu cuenta para poder enviar fondos desde ella. - El servicio de intercambio %s no está disponible en este momento. Por favor consulte nuestras actualizaciones. + El servicio de intercambio %s no está compatible con los proveedores actuales pero estamos trabajando para agregar más opciones. La venta de fondos estará disponible una vez que la(s) transacción(es) pendiente(s) en la red %s se complete El envío de fondos estará disponible una vez que se completen las transacciones pendientes en la red %s. - La venta de %s no está disponible en este momento. Por favor consulte nuestras actualizaciones. - El staking %s no está disponible en este momento. Por favor consulte nuestras actualizaciones. + Vender %s no es compatible con los proveedores actuales, pero estamos trabajando para agregar más opciones. + El staking de %s no es compatible con los proveedores actuales, pero estamos trabajando para agregar más opciones. Generar XPUB Ocultar Está a punto de ocultar este token de la pantalla principal. Puede volver a agregarlo en cualquier momento a través de la página de gestión de tokens. diff --git a/core/res/src/main/res/values-fr/strings.xml b/core/res/src/main/res/values-fr/strings.xml index c9983efdb7..346d3081e5 100644 --- a/core/res/src/main/res/values-fr/strings.xml +++ b/core/res/src/main/res/values-fr/strings.xml @@ -850,14 +850,14 @@ Soldes affichés Annuler Cette opération est actuellement indisponible. Veuillez réessayer plus tard. - L\'achat de %s n\'est pas disponible pour le moment. Veuillez vérifier vos mises à jour. + L\'achat de %s n\'est pas pris en charge par les fournisseurs actuels mais nous travaillons à ajouter davantage d\'options. Vous n\'avez pas de fonds à vendre. Renflouez votre compte pour pouvoir vendre des fonds à partir de celui-ci. Vous n\'avez pas de fonds à envoyer. Renflouez votre compte pour pouvoir envoyer des fonds à partir de celui-ci. - Le service d\'échange %s n\'est pas disponible pour le moment. Veuillez consulter nos mises à jour. + Le service d\'échange %s n\'est pas pris en charge par les fournisseurs actuels mais nous travaillons à ajouter plus d\'options. La vente de fonds sera disponible une fois que la ou les transactions en attente dans le réseau %s seront terminées L\'envoi de fonds sera disponible une fois la ou les transactions en attente dans le réseau %s terminées. - La vente de %s n\'est pas disponible pour le moment. Veuillez consulter nos mises à jour. - Le staking %s n’est pas disponible pour le moment. Veuillez consulter nos mises à jour. + L\'achat de %s n\'est pas pris en charge par les fournisseurs actuels mais nous travaillons à ajouter plus d\'options. + Le staking de %s n\'est pas pris en charge par les fournisseurs actuels, mais nous travaillons à ajouter plus d\'options. Générer XPUB Masquer Vous êtes sur le point de masquer ce jeton de l\'écran principal. Vous pouvez le rajouter à tout moment via la page de gestion des jetons. diff --git a/core/res/src/main/res/values-ja/strings.xml b/core/res/src/main/res/values-ja/strings.xml index e32e1a6fcb..0c5037ec1c 100644 --- a/core/res/src/main/res/values-ja/strings.xml +++ b/core/res/src/main/res/values-ja/strings.xml @@ -291,6 +291,9 @@ このペアは利用できません 許可が必要です 推奨 + %sを買い付けました + %sを買い付けています + %sを買い付けています... トークンが見つかりません。別のリクエストをお試しください。 ID: %s 取引IDをコピーしました @@ -830,7 +833,7 @@ 金額には以下が含まれます: \n • サービス プロバイダーの手数料\n • 取引所からユーザーのアドレスに%1$sを送金するためのネットワーク手数料。 \n\nプロバイダーのスリッページは最大%2$sです この金額には、サービスプロバイダーの手数料が含まれています。 金額にはサービスプロバイダーの手数料が含まれます。 \n\nプロバイダーのスリッページは最大%s です - 手数料 + 情報 すべての分散型取引所は、スマートコントラクトがあなたの許可なくウォレットにアクセスするのを防ぐために承認を必要とします。設計上、スマートコントラクトは承認なしでトークンにアクセスできません。トークンを「ロック解除」することで、あなたは1-inchのスマートコントラクトがトークンを使うことを承認します。ネットワークのマイナーは、このアクションをブロックチェーンに記録するためのガス料金(あなたが支払う)を受け取ります。承認後、トークンを交換することができます。 承認 手数料見積りエラーです。サポートにフィードバックをお送りください。 @@ -846,14 +849,14 @@ 残高表示 元に戻す この操作は現在利用できません。しばらくしてからもう一度お試しください。 - 現在、 %sの買付はご利用いただけません。アップデート情報をご確認ください。 + %sの買付は、現在のプロバイダーではサポートされていませんが、より多くのオプションを追加できるよう取り組んでいます。 売却できる資金がありません。アカウントに入金して、売却できるようにしてください。 送金する資金がありません。アカウントに入金して、送金できるようにしてください。 - 現在、 %sの交換はご利用いただけません。アップデート情報を確認してください。 + %sのスワップは、現在のプロバイダーではサポートされていませんが、より多くのオプションを追加できるよう取り組んでいます。 ネットワーク%s内の保留中の取引が完了すると、資金の売却が可能になります。 ネットワーク%s内の保留中の取引が完了すると、送金が可能になります。 - 現在、 %sの売却はご利用いただけません。アップデート情報をご確認ください。 - %s のステーキングは現在ご利用いただけません。最新情報をご確認ください。 + %sの売却は、現在のプロバイダーではサポートされていませんが、より多くのオプションを追加できるよう取り組んでいます。 + %sのステーキングは、現在のプロバイダーではサポートされていませんが、より多くのオプションを追加できるよう取り組んでいます。 XPUBを生成する 非表示 このトークンをメイン画面から非表示にします。トークンの管理ページからいつでも再度追加できます。 @@ -946,6 +949,7 @@ %sネットワーク アドレスがクリップボードにコピーされました インターネット接続がありません + 今すぐ10 %%オフで購入 1.3万種類以上の暗号資産にアクセス。ワンタップで買付、売却、スワップ、ステーキングが可能です。\nバックアップ用に最大3枚のカードを連携できます。 Tangemウォレットを見る ウォレット設定 @@ -993,6 +997,9 @@ このトークンを受け取るには、Hederaアカウントに関連付ける必要があります。 トークンを関連付ける %sが不足しています。このトークンを関連付けるには、Hederaアカウントに資金を追加してください。 + 取引をキャンセルしてもよろしいですか? 取引を再試行することはできません + %1$s %2$sの取引が完了しませんでした。もう一度試して完了してください。 + 未完了の取引があります このカードには%sの署名のみが残っています。資金をすべて引き出す必要があります。 署名数が少ないです 異なるネットワーク上のトークンは、異なるアドレスを持つ場合があります。資金を送金する際には、アドレスがネットワークと一致していることを再確認してください。 @@ -1018,7 +1025,7 @@ Solanaネットワークが混雑しています。2分以内に取引が完了しない場合は、再度取引を繰り返してください。 Solanaネットワークアラート Solana ネットワークは 2 日ごとに%1$sのレンタル料を請求します。レンタル料を支払えないアカウントはネットワークから削除されます。アカウントに%2$s以上入金すると、無料で使用できます。 - 現在、一部のネットワークにアクセスできません。しばらくしてからもう一度お試しください。 + 下にスワイプして更新するか、後でもう一度お試しください。 一部のネットワークにアクセスできません これはテストネットカードです。取引処理はできませんので、テストおよび開発目的でのみご利用ください。 テスト目的のみ diff --git a/core/res/src/main/res/values-ru/strings.xml b/core/res/src/main/res/values-ru/strings.xml index 9f00f71c2c..280a951d4b 100644 --- a/core/res/src/main/res/values-ru/strings.xml +++ b/core/res/src/main/res/values-ru/strings.xml @@ -183,6 +183,7 @@ Условиями использования Сегодня Ошибка транзакции + Статус транзакции Транзакции Перевод Я понял @@ -580,7 +581,7 @@ Для данной валюты нет доступных провайдеров Оплата с Вы сможете завершить транзакцию через сервис стороннего провайдера, %s - Переход к %s ... + Переход к %s... Наши сервисы недоступны в данной стране Настройки Через @@ -965,7 +966,7 @@ Нет соединения с интернетом Получить с 10%% скидкой Получите доступ к более чем 13 000 криптовалют. Покупайте, продавайте, обменяйте и стейкайте в один клик. Свяжите до трех карт для резервного копирования. - Откройте Tangem Wallet + Откройте Tangem Wallet Настройки кошелька Tangem Используйте %s или отсканируйте карту/кольцо, чтобы разблокировать доступ к вашему кошельку @@ -1011,6 +1012,9 @@ Этот токен должен быть ассоциирован с вашей учетной записью Hedera, прежде чем вы сможете его принять Ассоциируете свой токен Недостаточно %s. Пополните ваш аккаунт Hedera для ассоциации этого токена + Вы уверены что хотите отменить эту транзакцию? Вы не сможете отправить ее еще раз + Ваша транзакция %1$s %2$s не была завершена. Вы можете попробовать отправить ее еще раз. + У вас есть незавершенная транзакция На этой карте осталось всего %s подписей. Вам следует вывести все ваши средства. Малое количество подписей Токены на разных сетях могут иметь разные адреса. Пожалуйста, убедитесь при переводе средств, что ваш адрес соответствует сети. 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 8f07d981bf..c446190dcc 100644 --- a/core/res/src/main/res/values-uk-rUA/strings.xml +++ b/core/res/src/main/res/values-uk-rUA/strings.xml @@ -300,6 +300,9 @@ Недоступно для цієї пари Потрібен дозвіл Рекомендовано + Купив %s + Купівля %s... + Купівля %s... Токенів не знайдено. Будь ласка, спробуйте інший запит ID: %s ID транзакції скопійовано @@ -975,6 +978,9 @@ Мережа %s Адреса скопійована в буфер обміну Немає підключення до інтернету + Отримати з 10%% знижкою + Доступ до 13,000+ криптовалют. Купуйте, продавайте, обмінюйте та стейкайте одним дотиком.\nЗ’єднайте до трьох карток для бекапу. + Відкрийте Tangem Wallet Налаштування гаманця Tangem Використовуйте %s або відскануйте картку/кільце, щоб розблокувати доступ до вашого гаманця @@ -1020,6 +1026,9 @@ Цей токен повинен бути асоційований з вашим обліковим записом Hedera, перш ніж ви зможете його прийняти Асоціюйте свій токен Недостатньо %s. Поповніть ваш обліковий запис Hedera для асоціації цього токена + Ви впевнені, що хочете скасувати цю транзакцію? Ви не зможете відправити її ще раз + Ваша транзакція %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 6978552ae0..08a7924ae5 100644 --- a/core/res/src/main/res/values/strings.xml +++ b/core/res/src/main/res/values/strings.xml @@ -294,6 +294,9 @@ Unavailable for this pair Permission Required Recommended + Bought %s + Buying %s + Buying %s... No tokens found. Please try another request ID: %s Transaction ID copied @@ -1004,6 +1007,9 @@ This token must be associated with your Hedera account before you can receive it Associate your token Not enough %s. Top up your Hedera account to associate this token + Are you sure you want to cancel the transaction? You will not be able to retry the transaction again + Your transaction with an amount of %1$s %2$s was not completed. You can try again to complete it. + You have unfinished transaction Only %s signatures are left on this card. You must withdraw all of your funds. Low signature count Tokens on different networks can have different addresses. Double-check that your address matches the network when you transfer funds. diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/notifications/NoteMigrationNotification.kt b/core/ui/src/main/java/com/tangem/core/ui/components/notifications/NoteMigrationNotification.kt new file mode 100644 index 0000000000..6b5f396f85 --- /dev/null +++ b/core/ui/src/main/java/com/tangem/core/ui/components/notifications/NoteMigrationNotification.kt @@ -0,0 +1,111 @@ +package com.tangem.core.ui.components.notifications + +import androidx.compose.foundation.Image +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.* +import androidx.compose.material.Text +import androidx.compose.runtime.* +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.layout.ContentScale +import androidx.compose.ui.res.painterResource +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 +import com.tangem.core.ui.components.buttons.common.TangemButton +import com.tangem.core.ui.components.buttons.common.TangemButtonColors +import com.tangem.core.ui.components.buttons.common.TangemButtonIconPosition +import com.tangem.core.ui.extensions.resolveReference +import com.tangem.core.ui.extensions.stringReference +import com.tangem.core.ui.res.TangemColorPalette +import com.tangem.core.ui.res.TangemColorPalette.Dark6 +import com.tangem.core.ui.res.TangemColorPalette.Light2 +import com.tangem.core.ui.res.TangemTheme +import com.tangem.core.ui.res.TangemThemePreview + +@Composable +fun NoteMigrationNotification(config: NotificationConfig, modifier: Modifier = Modifier) { + val button = config.buttonsState as? NotificationConfig.ButtonsState.SecondaryButtonConfig + button ?: return + + Column( + modifier = modifier + .fillMaxWidth() + .clip(shape = TangemTheme.shapes.roundedCornersXMedium) + .background(TangemColorPalette.Dark6), + horizontalAlignment = Alignment.CenterHorizontally, + ) { + Image( + modifier = Modifier.heightIn(max = 146.dp), + painter = painterResource(id = R.drawable.banner_note_migration), + contentDescription = null, + contentScale = ContentScale.Fit, + ) + Column( + modifier = Modifier.padding( + start = 12.dp, + end = 12.dp, + bottom = 12.dp, + ), + verticalArrangement = Arrangement.spacedBy(10.dp), + horizontalAlignment = Alignment.CenterHorizontally, + ) { + config.title?.let { + Text( + text = it.resolveReference(), + color = TangemColorPalette.White, + style = TangemTheme.typography.h3, + ) + } + + Text( + text = config.subtitle.resolveReference(), + textAlign = TextAlign.Center, + color = TangemColorPalette.Light5, + style = TangemTheme.typography.body2, + ) + TangemButton( + modifier = Modifier.fillMaxWidth(), + text = button.text.resolveReference(), + icon = TangemButtonIconPosition.None, + onClick = button.onClick, + colors = TangemButtonColors( + backgroundColor = Light2, + contentColor = Dark6, + disabledBackgroundColor = TangemTheme.colors.button.disabled, + disabledContentColor = TangemTheme.colors.text.disabled, + ), + enabled = true, + textStyle = TangemTheme.typography.subtitle1, + showProgress = false, + ) + } + } +} + +// region Preview +@Preview(showBackground = true, widthDp = 328) +@Composable +private fun NoteMigrationNotification_Preview() { + TangemThemePreview { + NoteMigrationNotification( + NotificationConfig( + title = stringReference("Discover Tangem Wallet"), + subtitle = stringReference( + "Access 13,000+ cryptocurrencies. Buy, sell, swap, and stake with a single tap. Link up to " + + "three cards for a backup.", + ), + iconResId = R.drawable.ic_empty_64, + onCloseClick = { }, + buttonsState = NotificationConfig.ButtonsState.SecondaryButtonConfig( + text = stringReference("Get now"), + onClick = {}, + ), + ), + ) + } +} + +// endregion \ No newline at end of file diff --git a/core/ui/src/main/res/drawable/banner_note_migration.webp b/core/ui/src/main/res/drawable/banner_note_migration.webp new file mode 100644 index 0000000000..6f4f2a0ec6 Binary files /dev/null and b/core/ui/src/main/res/drawable/banner_note_migration.webp differ diff --git a/data/markets/src/main/java/com/tangem/data/markets/DefaultMarketsTokenRepository.kt b/data/markets/src/main/java/com/tangem/data/markets/DefaultMarketsTokenRepository.kt index 2a8c0ed1d7..a329974a0d 100644 --- a/data/markets/src/main/java/com/tangem/data/markets/DefaultMarketsTokenRepository.kt +++ b/data/markets/src/main/java/com/tangem/data/markets/DefaultMarketsTokenRepository.kt @@ -64,14 +64,10 @@ internal class DefaultMarketsTokenRepository( // we shouldn't infinitely retry on the first batch request val res = if (isFirstBatchFetching) { - catchApiErrorAndSendEvent(errorEvent = MarketsDataAnalyticsEvent.List.Error) { - requestCall() - } + catchListErrorAndSendEvent { requestCall() } } else { retryOnError(priority = true) { - catchApiErrorAndSendEvent(errorEvent = MarketsDataAnalyticsEvent.List.Error) { - requestCall() - } + catchListErrorAndSendEvent { requestCall() } } } @@ -100,7 +96,7 @@ internal class DefaultMarketsTokenRepository( marketsApi = marketsApi, analyticsEventHandler = analyticsEventHandler, onApiError = { - analyticsEventHandler.send(MarketsDataAnalyticsEvent.List.Error.toEvent()) + analyticsEventHandler.send(createListErrorEvent(it).toEvent()) }, ) @@ -128,11 +124,9 @@ internal class DefaultMarketsTokenRepository( interval = interval.toRequestParam(), ) - val result = catchApiErrorAndSendEvent( - errorEvent = MarketsDataAnalyticsEvent.Details.Error( - request = MarketsDataAnalyticsEvent.Details.Error.Request.Chart, - tokenSymbol = tokenSymbol, - ), + val result = catchDetailsErrorAndSendEvent( + request = MarketsDataAnalyticsEvent.Details.Error.Request.Chart, + tokenSymbol = tokenSymbol, ) { response.getOrThrow() } @@ -146,6 +140,7 @@ internal class DefaultMarketsTokenRepository( analyticsEventHandler.send( MarketsDataAnalyticsEvent.ChartNullValuesError( requestPath = "coins/history", + errorType = MarketsDataAnalyticsEvent.Type.Custom, ), ) }, @@ -159,18 +154,16 @@ internal class DefaultMarketsTokenRepository( tokenSymbol: String, ) = withContext(dispatcherProvider.io) { val mappedTokenId = getTokenIdIfL2Network(tokenId) - val response = marketsApi.getCoinsListCharts( - coinIds = mappedTokenId, - currency = fiatCurrencyCode, - interval = interval.toRequestParam(), - ) - val chart = catchApiErrorAndSendEvent(errorEvent = MarketsDataAnalyticsEvent.List.Error) { - response.getOrThrow()[mappedTokenId] ?: error( + val chart = catchListErrorAndSendEvent { + marketsApi.getCoinsListCharts( + coinIds = mappedTokenId, + currency = fiatCurrencyCode, + interval = interval.toRequestParam(), + ).getOrThrow()[mappedTokenId] ?: error( "No chart preview data for the token $mappedTokenId", ) } - return@withContext TokenChartConverter.convert( interval = interval, value = chart, @@ -180,6 +173,7 @@ internal class DefaultMarketsTokenRepository( analyticsEventHandler.send( MarketsDataAnalyticsEvent.ChartNullValuesError( requestPath = "coins/history_preview", + errorType = MarketsDataAnalyticsEvent.Type.Custom, ), ) }, @@ -192,19 +186,15 @@ internal class DefaultMarketsTokenRepository( tokenSymbol: String, languageCode: String, ) = withContext(dispatcherProvider.io) { - val response = marketsApi.getCoinMarketData( - currency = fiatCurrencyCode, - coinId = tokenId, - language = languageCode, - ) - - val result = catchApiErrorAndSendEvent( - errorEvent = MarketsDataAnalyticsEvent.Details.Error( - request = MarketsDataAnalyticsEvent.Details.Error.Request.Info, - tokenSymbol = tokenSymbol, - ), + val result = catchDetailsErrorAndSendEvent( + request = MarketsDataAnalyticsEvent.Details.Error.Request.Info, + tokenSymbol = tokenSymbol, ) { - response.getOrThrow() + marketsApi.getCoinMarketData( + currency = fiatCurrencyCode, + coinId = tokenId, + language = languageCode, + ).getOrThrow() } val resultResponse = result.applyL2Compatibility(tokenId) @@ -214,19 +204,16 @@ internal class DefaultMarketsTokenRepository( override suspend fun getTokenQuotes(fiatCurrencyCode: String, tokenId: String, tokenSymbol: String) = withContext(dispatcherProvider.io) { // for second markets iteration we should use extended api method with all required fields - val response = tangemTechApi.getQuotes( - currencyId = fiatCurrencyCode, - coinIds = tokenId, - fields = marketsQuoteFields.joinToString(separator = ","), - ) - val result = catchApiErrorAndSendEvent( - errorEvent = MarketsDataAnalyticsEvent.Details.Error( - request = MarketsDataAnalyticsEvent.Details.Error.Request.Info, - tokenSymbol = tokenSymbol, - ), + val result = catchDetailsErrorAndSendEvent( + request = MarketsDataAnalyticsEvent.Details.Error.Request.Info, + tokenSymbol = tokenSymbol, ) { - response.getOrThrow() + tangemTechApi.getQuotes( + currencyId = fiatCurrencyCode, + coinIds = tokenId, + fields = marketsQuoteFields.joinToString(separator = ","), + ).getOrThrow() } return@withContext TokenQuotesShortConverter.convert(tokenId, result).toFull() @@ -272,15 +259,75 @@ internal class DefaultMarketsTokenRepository( } } - private inline fun catchApiErrorAndSendEvent(errorEvent: MarketsDataAnalyticsEvent, block: () -> T): T { + inline fun catchListErrorAndSendEvent(block: () -> T): T { + return catchErrorAndSendEvent(block, ::createListErrorEvent) + } + + private inline fun catchDetailsErrorAndSendEvent( + request: MarketsDataAnalyticsEvent.Details.Error.Request, + tokenSymbol: String, + block: () -> T, + ): T { + return catchErrorAndSendEvent(block) { error -> + createDetailsErrorEvent(error, request, tokenSymbol) + } + } + + private inline fun catchErrorAndSendEvent( + block: () -> T, + createErrorEvent: (ApiResponseError) -> MarketsDataAnalyticsEvent, + ): T { return try { block() - } catch (e: ApiResponseError.HttpException) { - analyticsEventHandler.send(errorEvent.toEvent()) - throw e - } catch (e: ApiResponseError.TimeoutException) { + } catch (e: ApiResponseError) { + val errorEvent = createErrorEvent(e) + analyticsEventHandler.send(errorEvent.toEvent()) throw e } } + + private fun createListErrorEvent(error: ApiResponseError): MarketsDataAnalyticsEvent.List.Error { + return createErrorEvent(error) { errorType, errorCode -> + MarketsDataAnalyticsEvent.List.Error( + errorType = errorType, + errorCode = errorCode, + ) + } + } + + private fun createDetailsErrorEvent( + error: ApiResponseError, + request: MarketsDataAnalyticsEvent.Details.Error.Request, + tokenSymbol: String, + ): MarketsDataAnalyticsEvent.Details.Error { + return createErrorEvent(error) { errorType, errorCode -> + MarketsDataAnalyticsEvent.Details.Error( + errorType = errorType, + errorCode = errorCode, + request = request, + tokenSymbol = tokenSymbol, + ) + } + } + + private inline fun createErrorEvent( + error: ApiResponseError, + createEvent: (MarketsDataAnalyticsEvent.Type, Int?) -> T, + ): T { + return when (error) { + is ApiResponseError.HttpException -> { + createEvent(MarketsDataAnalyticsEvent.Type.Http, error.code.code) + } + is ApiResponseError.TimeoutException -> { + createEvent(MarketsDataAnalyticsEvent.Type.Timeout, null) + } + is ApiResponseError.NetworkException -> { + createEvent(MarketsDataAnalyticsEvent.Type.Network, null) + } + is ApiResponseError.UnknownException -> { + createEvent(MarketsDataAnalyticsEvent.Type.Unknown, null) + } + } + } } \ No newline at end of file diff --git a/data/markets/src/main/java/com/tangem/data/markets/MarketsBatchUpdateFetcher.kt b/data/markets/src/main/java/com/tangem/data/markets/MarketsBatchUpdateFetcher.kt index f1a92028fb..6a9952d71f 100644 --- a/data/markets/src/main/java/com/tangem/data/markets/MarketsBatchUpdateFetcher.kt +++ b/data/markets/src/main/java/com/tangem/data/markets/MarketsBatchUpdateFetcher.kt @@ -25,7 +25,7 @@ internal class MarketsBatchUpdateFetcher( private val marketsApi: TangemTechMarketsApi, private val tangemTechApi: TangemTechApi, private val analyticsEventHandler: AnalyticsEventHandler, - private val onApiError: () -> Unit, + private val onApiError: (ApiResponseError) -> Unit, ) : BatchUpdateFetcher, TokenMarketUpdateRequest> { override suspend fun BatchUpdateFetcher.UpdateContext>.fetchUpdateAsync( @@ -124,6 +124,7 @@ internal class MarketsBatchUpdateFetcher( analyticsEventHandler.send( MarketsDataAnalyticsEvent.ChartNullValuesError( requestPath = "coins/history_preview", + errorType = MarketsDataAnalyticsEvent.Type.Custom, ), ) @@ -133,11 +134,11 @@ internal class MarketsBatchUpdateFetcher( } } - private inline fun catchApiError(onError: () -> Unit, block: () -> T): T { + private inline fun catchApiError(onError: (ApiResponseError) -> Unit, block: () -> T): T { return try { block() } catch (e: ApiResponseError) { - onError() + onError(e) throw e } } diff --git a/data/markets/src/main/java/com/tangem/data/markets/analytics/MarketsDataAnalyticsEvent.kt b/data/markets/src/main/java/com/tangem/data/markets/analytics/MarketsDataAnalyticsEvent.kt index 1c131a3cd4..e0d27728a0 100644 --- a/data/markets/src/main/java/com/tangem/data/markets/analytics/MarketsDataAnalyticsEvent.kt +++ b/data/markets/src/main/java/com/tangem/data/markets/analytics/MarketsDataAnalyticsEvent.kt @@ -9,7 +9,16 @@ sealed interface MarketsDataAnalyticsEvent { params: Map = mapOf(), ) : AnalyticsEvent(category = "Markets", event = event, params = params), MarketsDataAnalyticsEvent { - data object Error : List(event = "Data Error") + data class Error( + val errorType: Type, + val errorCode: Int? = null, + ) : List( + event = "Data Error", + params = buildMap { + put("Error Type", errorType.value) + errorCode?.let { put("Error Code", it.toString()) } + }, + ) } sealed class Details( @@ -20,12 +29,16 @@ sealed interface MarketsDataAnalyticsEvent { data class Error( val request: Request, val tokenSymbol: String, + val errorType: Type, + val errorCode: Int? = null, ) : Details( event = "Data Error", - params = mapOf( - "Source" to request.source, - "Token" to tokenSymbol, - ), + params = buildMap { + put("Source", request.source) + put("Token", tokenSymbol) + errorCode?.let { put("Error Code", it.toString()) } + put("Error Type", errorType.value) + }, ) { enum class Request(val source: String) { @@ -43,13 +56,25 @@ sealed interface MarketsDataAnalyticsEvent { data class ChartNullValuesError( val requestPath: String, + val errorType: Type, + val errorCode: Int? = null, ) : AnalyticsEvent( category = "Markets / Chart", event = "Data Error", - params = mapOf("Request path" to requestPath), - error = IllegalStateException( - "Chart data contains null values from the API", - ), + params = buildMap { + put("Request path", requestPath) + errorCode?.let { put("Error Code", it.toString()) } + put("Error Type", errorType.value) + put("Error Description", "Chart data contains null values from the API") + }, ), MarketsDataAnalyticsEvent + + enum class Type(val value: String) { + Http("Http"), + Timeout("Timeout"), + Network("Network"), + Custom("Custom"), + Unknown("Unknown"), + } } \ No newline at end of file diff --git a/data/tokens/src/main/kotlin/com/tangem/data/tokens/repository/DefaultQuotesRepository.kt b/data/tokens/src/main/kotlin/com/tangem/data/tokens/repository/DefaultQuotesRepository.kt index 13e1c3e368..a680263464 100644 --- a/data/tokens/src/main/kotlin/com/tangem/data/tokens/repository/DefaultQuotesRepository.kt +++ b/data/tokens/src/main/kotlin/com/tangem/data/tokens/repository/DefaultQuotesRepository.kt @@ -45,7 +45,7 @@ internal class DefaultQuotesRepository( .filterNotNull() .flatMapLatest { appCurrency -> fetchExpiredQuotes(currenciesIds, appCurrency.id, refresh = refresh) - quotesStore.get(currenciesIds).map(quotesConverter::convertSet) + quotesStore.get(currenciesIds).map { quotesConverter.convert(currenciesIds to it) } } .cancellable() .flowOn(dispatchers.io) @@ -77,14 +77,15 @@ internal class DefaultQuotesRepository( val quotes = quotesStore.getSync(currenciesIds) - quotesConverter.convertSet(quotes) + quotesConverter.convert(currenciesIds to quotes) } } override suspend fun getQuoteSync(currencyId: CryptoCurrency.ID): Quote? { return withContext(dispatchers.io) { - val quote = quotesStore.getSync(setOf(currencyId)).firstOrNull() - quote?.let { quotesConverter.convert(it) } + val setOfCurrencyId = setOf(currencyId) + val quote = quotesStore.getSync(setOfCurrencyId).firstOrNull() + quote?.let { quotesConverter.convert(setOfCurrencyId to setOf(it)).firstOrNull() } } } diff --git a/data/tokens/src/main/kotlin/com/tangem/data/tokens/utils/QuotesConverter.kt b/data/tokens/src/main/kotlin/com/tangem/data/tokens/utils/QuotesConverter.kt index 85c903ceec..79af4417b7 100644 --- a/data/tokens/src/main/kotlin/com/tangem/data/tokens/utils/QuotesConverter.kt +++ b/data/tokens/src/main/kotlin/com/tangem/data/tokens/utils/QuotesConverter.kt @@ -1,16 +1,28 @@ package com.tangem.data.tokens.utils import com.tangem.datasource.local.quote.model.StoredQuote +import com.tangem.domain.tokens.model.CryptoCurrency import com.tangem.domain.tokens.model.Quote import com.tangem.utils.converter.Converter import java.math.BigDecimal -internal class QuotesConverter : Converter { +typealias QuotesConverterValue = Pair, Set> - override fun convert(value: StoredQuote): Quote { - val (rawCurrencyId, responseQuote) = value +internal class QuotesConverter : Converter> { - return Quote( + override fun convert(value: QuotesConverterValue): Set { + val (setOfCurrencyId, setOfStoredQuote) = value + return setOfCurrencyId.mapTo(hashSetOf()) { id -> + setOfStoredQuote.find { id.rawCurrencyId == it.rawCurrencyId } + ?.let(::convertExistStoredQuote) + ?: Quote.Empty(id.rawCurrencyId) + } + } + + private fun convertExistStoredQuote(storedQuote: StoredQuote): Quote { + val (rawCurrencyId, responseQuote) = storedQuote + + return Quote.Value( rawCurrencyId = rawCurrencyId, fiatRate = responseQuote.price ?: BigDecimal.ZERO, priceChange = (responseQuote.priceChange24h ?: BigDecimal.ZERO).movePointLeft(2), diff --git a/domain/markets/src/main/java/com/tangem/domain/markets/GetCurrencyQuotesUseCase.kt b/domain/markets/src/main/java/com/tangem/domain/markets/GetCurrencyQuotesUseCase.kt index 1ec184fbdc..0a8d0d931d 100644 --- a/domain/markets/src/main/java/com/tangem/domain/markets/GetCurrencyQuotesUseCase.kt +++ b/domain/markets/src/main/java/com/tangem/domain/markets/GetCurrencyQuotesUseCase.kt @@ -15,10 +15,10 @@ class GetCurrencyQuotesUseCase( currencyID: CryptoCurrency.ID, interval: PriceChangeInterval, refresh: Boolean, - ): Flow> { + ): Flow> { return quotesRepository.getQuotesUpdates( currenciesIds = setOf(currencyID), refresh = refresh, - ).map { it.firstOrNull().toOption() }.catch { emit(None) } + ).map { it.filterIsInstance().firstOrNull().toOption() }.catch { emit(None) } } } \ No newline at end of file diff --git a/domain/tokens/models/src/main/java/com/tangem/domain/tokens/model/Quote.kt b/domain/tokens/models/src/main/java/com/tangem/domain/tokens/model/Quote.kt index 3db9280b76..d54b9a2342 100644 --- a/domain/tokens/models/src/main/java/com/tangem/domain/tokens/model/Quote.kt +++ b/domain/tokens/models/src/main/java/com/tangem/domain/tokens/model/Quote.kt @@ -2,15 +2,27 @@ package com.tangem.domain.tokens.model import java.math.BigDecimal -/** - * Represents financial information for a specific cryptocurrency, including its fiat exchange rate and price change. - * - * @property rawCurrencyId The unique identifier of the cryptocurrency for which the financial information is provided. - * @property fiatRate The current fiat exchange rate for the cryptocurrency. - * @property priceChange The price change for the cryptocurrency. - */ -data class Quote( - val rawCurrencyId: String, - val fiatRate: BigDecimal, - val priceChange: BigDecimal, -) \ No newline at end of file +sealed interface Quote { + + val rawCurrencyId: String? + + /** + * Represents unknown financial information for a specific cryptocurrency. + * + * @property rawCurrencyId The raw cryptocurrency ID. If it is a custom token, the value will be `null`. + */ + data class Empty(override val rawCurrencyId: String?) : Quote + + /** + * Represents financial information for a specific cryptocurrency, including its fiat exchange rate and price change. + * + * @property rawCurrencyId The unique identifier of the cryptocurrency for which the financial information is provided. + * @property fiatRate The current fiat exchange rate for the cryptocurrency. + * @property priceChange The price change for the cryptocurrency. + */ + data class Value( + override val rawCurrencyId: String, + val fiatRate: BigDecimal, + val priceChange: BigDecimal, + ) : Quote +} \ No newline at end of file diff --git a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/operations/CurrencyStatusOperations.kt b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/operations/CurrencyStatusOperations.kt index ff31ccc64a..3e4f3a77ae 100644 --- a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/operations/CurrencyStatusOperations.kt +++ b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/operations/CurrencyStatusOperations.kt @@ -12,6 +12,18 @@ internal class CurrencyStatusOperations( private val ignoreQuote: Boolean, ) { + private val Quote?.fiatRate: BigDecimal? + get() = when (this) { + is Quote.Value -> this.fiatRate + is Quote.Empty, null -> null + } + + private val Quote?.priceChange: BigDecimal? + get() = when (this) { + is Quote.Value -> this.priceChange + is Quote.Empty, null -> null + } + fun createTokenStatus(): CryptoCurrencyStatus = CryptoCurrencyStatus(currency, createStatus()) private fun createStatus(): CryptoCurrencyStatus.Value { @@ -74,7 +86,7 @@ internal class CurrencyStatusOperations( null } return when { - ignoreQuote -> CryptoCurrencyStatus.NoQuote( + quote is Quote.Empty || ignoreQuote -> CryptoCurrencyStatus.NoQuote( amount = amount, hasCurrentNetworkTransactions = hasCurrentNetworkTransactions, pendingTransactions = currentTransactions, @@ -91,8 +103,7 @@ internal class CurrencyStatusOperations( networkAddress = status.address, yieldBalance = currentYieldBalance, ) - quote == null -> CryptoCurrencyStatus.Loading - else -> CryptoCurrencyStatus.Loaded( + quote is Quote.Value -> CryptoCurrencyStatus.Loaded( amount = amount, fiatAmount = calculateFiatAmount(amount, quote.fiatRate), fiatRate = quote.fiatRate, @@ -102,6 +113,7 @@ internal class CurrencyStatusOperations( networkAddress = status.address, yieldBalance = currentYieldBalance, ) + else -> CryptoCurrencyStatus.Loading } } diff --git a/domain/tokens/src/test/kotlin/com/tangem/domain/tokens/mock/MockQuotes.kt b/domain/tokens/src/test/kotlin/com/tangem/domain/tokens/mock/MockQuotes.kt index 68de5e729a..002f80eae8 100644 --- a/domain/tokens/src/test/kotlin/com/tangem/domain/tokens/mock/MockQuotes.kt +++ b/domain/tokens/src/test/kotlin/com/tangem/domain/tokens/mock/MockQuotes.kt @@ -7,65 +7,71 @@ import java.math.BigDecimal @Suppress("MemberVisibilityCanBePrivate") internal object MockQuotes { - val quote1 = Quote( + val quote1 = Quote.Value( rawCurrencyId = MockTokens.token1.id.rawCurrencyId!!, fiatRate = BigDecimal("1.23"), priceChange = BigDecimal("0.01"), ) - val quote2 = Quote( + val quote2 = Quote.Value( rawCurrencyId = MockTokens.token2.id.rawCurrencyId!!, fiatRate = BigDecimal("2.34"), priceChange = BigDecimal("-0.02"), ) - val quote3 = Quote( + val quote3 = Quote.Value( rawCurrencyId = MockTokens.token3.id.rawCurrencyId!!, fiatRate = BigDecimal("3.45"), priceChange = BigDecimal("0.03"), ) - val quote4 = Quote( + val quote4 = Quote.Value( rawCurrencyId = MockTokens.token4.id.rawCurrencyId!!, fiatRate = BigDecimal("4.56"), priceChange = BigDecimal("-0.04"), ) - val quote5 = Quote( + val quote5 = Quote.Value( rawCurrencyId = MockTokens.token5.id.rawCurrencyId!!, fiatRate = BigDecimal("5.67"), priceChange = BigDecimal("0.05"), ) - val quote6 = Quote( + val quote6 = Quote.Value( rawCurrencyId = MockTokens.token6.id.rawCurrencyId!!, fiatRate = BigDecimal("6.78"), priceChange = BigDecimal("-0.06"), ) - val quote7 = Quote( + val quote7 = Quote.Value( rawCurrencyId = MockTokens.token7.id.rawCurrencyId!!, fiatRate = BigDecimal("7.89"), priceChange = BigDecimal("0.07"), ) - val quote8 = Quote( + val quote8 = Quote.Value( rawCurrencyId = MockTokens.token8.id.rawCurrencyId!!, fiatRate = BigDecimal("8.90"), priceChange = BigDecimal("-0.08"), ) - val quote9 = Quote( + val quote9 = Quote.Value( rawCurrencyId = MockTokens.token9.id.rawCurrencyId!!, fiatRate = BigDecimal("9.01"), priceChange = BigDecimal("0.09"), ) - val quote10 = Quote( + val quote10 = Quote.Value( rawCurrencyId = MockTokens.token10.id.rawCurrencyId!!, fiatRate = BigDecimal("10.12"), priceChange = BigDecimal("-0.10"), ) - val quotes = nonEmptySetOf(quote1, quote2, quote3, quote4, quote5, quote6, quote7, quote8, quote9, quote10) + val quote11 = Quote.Empty(null) + val quote12 = Quote.Empty("null") + + val quotes = nonEmptySetOf( + quote1, quote2, quote3, quote4, quote5, quote6, quote7, quote8, quote9, quote10, + quote11, quote12, + ) } \ No newline at end of file diff --git a/domain/tokens/src/test/kotlin/com/tangem/domain/tokens/mock/MockTokensStates.kt b/domain/tokens/src/test/kotlin/com/tangem/domain/tokens/mock/MockTokensStates.kt index 45c2f537d4..a9d704b56f 100644 --- a/domain/tokens/src/test/kotlin/com/tangem/domain/tokens/mock/MockTokensStates.kt +++ b/domain/tokens/src/test/kotlin/com/tangem/domain/tokens/mock/MockTokensStates.kt @@ -1,10 +1,7 @@ package com.tangem.domain.tokens.mock import arrow.core.nonEmptyListOf -import com.tangem.domain.tokens.model.CryptoCurrencyAmountStatus -import com.tangem.domain.tokens.model.CryptoCurrencyStatus -import com.tangem.domain.tokens.model.NetworkAddress -import com.tangem.domain.tokens.model.NetworkStatus +import com.tangem.domain.tokens.model.* import java.math.BigDecimal @Suppress("MemberVisibilityCanBePrivate") @@ -140,20 +137,29 @@ internal object MockTokensStates { as? CryptoCurrencyAmountStatus.Loaded )?.value ?: BigDecimal.ZERO val quote = MockQuotes.quotes.first { it.rawCurrencyId == status.currency.id.rawCurrencyId } - val fiatAmount = amount * quote.fiatRate - status.copy( - value = CryptoCurrencyStatus.Loaded( + val value = when (quote) { + is Quote.Empty -> CryptoCurrencyStatus.NoQuote( + amount = status.value.amount!!, + pendingTransactions = emptySet(), + hasCurrentNetworkTransactions = false, + networkAddress = requireNotNull( + value = MockNetworks.verifiedNetworksStatuses.first { it.network == status.currency.network }.value as? NetworkStatus.Verified, + ).address, + yieldBalance = null, + ) + is Quote.Value -> CryptoCurrencyStatus.Loaded( amount = amount, - fiatAmount = fiatAmount, + fiatAmount = amount * quote.fiatRate, fiatRate = quote.fiatRate, priceChange = quote.priceChange, pendingTransactions = emptySet(), hasCurrentNetworkTransactions = false, networkAddress = requireNotNull(networkStatus.value as? NetworkStatus.Verified).address, yieldBalance = null, - ), - ) + ) + } + status.copy(value = value) } val noQuotesTokensStatuses = loadedTokensStates.map { status -> diff --git a/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/SwapInteractorImpl.kt b/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/SwapInteractorImpl.kt index 1d5e225d91..63b4b823e7 100644 --- a/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/SwapInteractorImpl.kt +++ b/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/SwapInteractorImpl.kt @@ -2178,8 +2178,8 @@ internal class SwapInteractorImpl @AssistedInject constructor( ) } - private suspend fun getQuotes(vararg ids: CryptoCurrency.ID): Map { - val set = ids.toSet().getQuotesOrEmpty(false) + private suspend fun getQuotes(vararg ids: CryptoCurrency.ID): Map { + val set = ids.toSet().getQuotesOrEmpty(false).filterIsInstance() return ids .mapNotNull { id -> set.find { it.rawCurrencyId == id.rawCurrencyId }?.let { id to it } } diff --git a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/TokenDetailsSwapTransactionsStateConverter.kt b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/TokenDetailsSwapTransactionsStateConverter.kt index 2ec87f5ed8..fc0cfa0541 100644 --- a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/TokenDetailsSwapTransactionsStateConverter.kt +++ b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/TokenDetailsSwapTransactionsStateConverter.kt @@ -3,9 +3,9 @@ package com.tangem.feature.tokendetails.presentation.tokendetails.state.factory import com.tangem.core.analytics.api.AnalyticsEventHandler import com.tangem.core.ui.components.currency.icon.converter.CryptoCurrencyToIconStateConverter import com.tangem.core.ui.extensions.TextReference +import com.tangem.core.ui.extensions.resourceReference import com.tangem.core.ui.format.bigdecimal.crypto import com.tangem.core.ui.format.bigdecimal.format -import com.tangem.core.ui.extensions.resourceReference import com.tangem.core.ui.utils.BigDecimalFormatter import com.tangem.core.ui.utils.toDateFormatWithTodayYesterday import com.tangem.core.ui.utils.toTimeFormat @@ -55,16 +55,22 @@ internal class TokenDetailsSwapTransactionsStateConverter( .forEach { swapTransaction -> val toCryptoCurrency = swapTransaction.toCryptoCurrency val fromCryptoCurrency = swapTransaction.fromCryptoCurrency + val toCryptoCurrencyRawId = swapTransaction.toCryptoCurrency.id.rawCurrencyId + val fromCryptoCurrencyRawId = swapTransaction.fromCryptoCurrency.id.rawCurrencyId swapTransaction.transactions.forEach { transaction -> val toAmount = transaction.toCryptoAmount val fromAmount = transaction.fromCryptoAmount - val toFiatAmount = quotes.firstOrNull { - it.rawCurrencyId == swapTransaction.toCryptoCurrency.id.rawCurrencyId - }?.fiatRate?.multiply(toAmount) - val fromFiatAmount = quotes.firstOrNull { - it.rawCurrencyId == swapTransaction.fromCryptoCurrency.id.rawCurrencyId - }?.fiatRate?.multiply(fromAmount) + var toFiatAmount: BigDecimal? = null + var fromFiatAmount: BigDecimal? = null + quotes.forEach { quote -> + if (quote is Quote.Value && quote.rawCurrencyId == toCryptoCurrencyRawId) { + toFiatAmount = quote.fiatRate.multiply(toAmount) + } + if (quote is Quote.Value && quote.rawCurrencyId == fromCryptoCurrencyRawId) { + fromFiatAmount = quote.fiatRate.multiply(fromAmount) + } + } val timestamp = transaction.timestamp val notifications = getNotification(transaction.status?.status, transaction.status?.txExternalUrl, null) diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/analytics/WalletScreenAnalyticsEvent.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/analytics/WalletScreenAnalyticsEvent.kt index bfb7efaf45..39d99aaf63 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/analytics/WalletScreenAnalyticsEvent.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/analytics/WalletScreenAnalyticsEvent.kt @@ -114,6 +114,10 @@ sealed class WalletScreenAnalyticsEvent { data object BackupError : MainScreen(event = "Notice - Backup Error") + data object NotePromo : MainScreen(event = "Notice - Note Promo") + + data object NotePromoButton : MainScreen(event = "Note Promo Button") + data object UnlockAllWithBiometrics : MainScreen(event = "Button - Unlock All With Biometrics") data object UnlockWithCardScan : MainScreen(event = "Button - Unlock With Card Scan") 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 fecbf47463..89d0467012 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 @@ -35,6 +35,7 @@ internal class WalletWarningsAnalyticsSender @Inject constructor( return warnings.mapNotNullTo(mutableSetOf(), ::getEvent) } + @Suppress("CyclomaticComplexMethod") private fun getEvent(warning: WalletNotification): AnalyticsEvent? { return when (warning) { is WalletNotification.Critical.DevCard -> MainScreen.DevelopmentCard @@ -46,6 +47,7 @@ internal class WalletWarningsAnalyticsSender @Inject constructor( is WalletNotification.Informational.MissingAddresses -> MainScreen.MissingAddresses is WalletNotification.RateApp -> MainScreen.HowDoYouLikeTangem is WalletNotification.Critical.BackupError -> MainScreen.BackupError + is WalletNotification.NoteMigration -> MainScreen.NotePromo is WalletNotification.SwapPromo -> TokenSwapPromoAnalyticsEvent.NoticePromotionBanner( source = AnalyticsParam.ScreensSources.Main, programName = TokenSwapPromoAnalyticsEvent.ProgramName.OKX, diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/domain/GetSingleWalletWarningsFactory.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/domain/GetSingleWalletWarningsFactory.kt index c502e65890..81e435fa5e 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/domain/GetSingleWalletWarningsFactory.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/domain/GetSingleWalletWarningsFactory.kt @@ -41,7 +41,7 @@ internal class GetSingleWalletWarningsFactory @Inject constructor( buildList { addCriticalNotifications(cardTypesResolver) - addInformationalNotifications(cardTypesResolver) + addInformationalNotifications(cardTypesResolver, clickIntents) addWarningNotifications( userWallet, @@ -75,7 +75,17 @@ internal class GetSingleWalletWarningsFactory @Inject constructor( } } - private fun MutableList.addInformationalNotifications(cardTypesResolver: CardTypesResolver) { + private fun MutableList.addInformationalNotifications( + cardTypesResolver: CardTypesResolver, + clickIntents: WalletClickIntents, + ) { + addIf( + element = WalletNotification.NoteMigration( + onClick = { clickIntents.onNoteMigrationButtonClick(NOTE_MIGRATION_URL) }, + ), + condition = cardTypesResolver.isTangemNote(), + ) + addIf( element = WalletNotification.Informational.DemoCard, condition = isDemoCardUseCase(cardId = cardTypesResolver.getCardId()), @@ -160,13 +170,17 @@ internal class GetSingleWalletWarningsFactory @Inject constructor( private fun MutableList.addIf(element: WalletNotification, condition: Boolean) { if (condition) { add(element = element) - if (element is WalletNotification.Critical || element is WalletNotification.Warning) { + if (element is WalletNotification.Critical || + element is WalletNotification.Warning || + element is WalletNotification.NoteMigration + ) { readyForRateAppNotification = false } } } private companion object { + const val NOTE_MIGRATION_URL = "https://tangem.com/en/?promocode=Note10" const val MAX_REMAINING_SIGNATURES_COUNT = 10 } } \ No newline at end of file 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 2741cc56ca..feb796546d 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 @@ -2,10 +2,7 @@ package com.tangem.feature.wallet.presentation.wallet.state.model import androidx.compose.runtime.Immutable import com.tangem.core.ui.components.notifications.NotificationConfig -import com.tangem.core.ui.extensions.TextReference -import com.tangem.core.ui.extensions.pluralReference -import com.tangem.core.ui.extensions.resourceReference -import com.tangem.core.ui.extensions.wrappedList +import com.tangem.core.ui.extensions.* import com.tangem.feature.wallet.impl.R import org.joda.time.DateTime @@ -143,7 +140,7 @@ sealed class WalletNotification(val config: NotificationConfig) { ), ) - object DemoCard : Informational( + data object DemoCard : Informational( title = resourceReference(id = R.string.warning_demo_mode_title), subtitle = resourceReference(id = R.string.warning_demo_mode_message), ) @@ -204,4 +201,16 @@ sealed class WalletNotification(val config: NotificationConfig) { onCloseClick = onCloseClick, ), ) + + data class NoteMigration(val onClick: () -> Unit) : WalletNotification( + config = NotificationConfig( + title = resourceReference(R.string.wallet_promo_banner_title), + subtitle = resourceReference(R.string.wallet_promo_banner_description), + iconResId = R.drawable.banner_note_migration, + buttonsState = NotificationConfig.ButtonsState.SecondaryButtonConfig( + text = resourceReference(R.string.wallet_promo_banner_button_title), + onClick = onClick, + ), + ), + ) } \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/components/common/WalletNotifications.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/components/common/WalletNotifications.kt index 39dbbb2618..de41263c1e 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/components/common/WalletNotifications.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/components/common/WalletNotifications.kt @@ -1,9 +1,9 @@ package com.tangem.feature.wallet.presentation.wallet.ui.components.common -import androidx.compose.foundation.ExperimentalFoundationApi import androidx.compose.foundation.lazy.LazyListScope import androidx.compose.foundation.lazy.items import androidx.compose.ui.Modifier +import com.tangem.core.ui.components.notifications.NoteMigrationNotification import com.tangem.core.ui.components.notifications.Notification import com.tangem.core.ui.components.notifications.OkxPromoNotification import com.tangem.core.ui.components.notifications.RingPromoNotification @@ -19,7 +19,6 @@ import kotlinx.collections.immutable.ImmutableList * [REDACTED_AUTHOR] */ -@OptIn(ExperimentalFoundationApi::class) internal fun LazyListScope.notifications(configs: ImmutableList, modifier: Modifier = Modifier) { items( items = configs, @@ -31,16 +30,25 @@ internal fun LazyListScope.notifications(configs: ImmutableList { OkxPromoNotification( config = it.config, - modifier = modifier.animateItemPlacement(), + modifier = modifier.animateItem(), ) } is WalletNotification.RingPromo -> { - RingPromoNotification(config = it.config, modifier = modifier.animateItemPlacement()) + RingPromoNotification( + config = it.config, + modifier = modifier.animateItem(), + ) + } + is WalletNotification.NoteMigration -> { + NoteMigrationNotification( + config = it.config, + modifier = modifier.animateItem(), + ) } else -> { Notification( config = it.config, - modifier = modifier.animateItemPlacement(), + modifier = modifier.animateItem(), iconTint = when (it) { is WalletNotification.Critical -> TangemTheme.colors.icon.warning is WalletNotification.Informational -> TangemTheme.colors.icon.accent diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/viewmodels/intents/WalletWarningsClickIntents.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/viewmodels/intents/WalletWarningsClickIntents.kt index 2db549db07..7669529ac3 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/viewmodels/intents/WalletWarningsClickIntents.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/viewmodels/intents/WalletWarningsClickIntents.kt @@ -67,6 +67,8 @@ internal interface WalletWarningsClickIntents { fun onCloseTravalaPromoClick() fun onSupportClick() + + fun onNoteMigrationButtonClick(url: String) } @Suppress("LongParameterList") @@ -296,6 +298,13 @@ internal class WalletWarningsClickIntentsImplementor @Inject constructor( } } + override fun onNoteMigrationButtonClick(url: String) { + analyticsEventHandler.send(MainScreen.NotePromoButton) + viewModelScope.launch(dispatchers.main) { + router.openUrl(url) + } + } + private fun getSelectedUserWallet(): UserWallet? { val userWalletId = stateHolder.getSelectedWalletId() return getUserWalletUseCase(userWalletId).getOrElse { diff --git a/gradle.properties b/gradle.properties index c4b0b8ac32..a312f8c1a2 100644 --- a/gradle.properties +++ b/gradle.properties @@ -6,7 +6,7 @@ # http://www.gradle.org/docs/current/userguide/build_environment.html # Specifies the JVM arguments used for the daemon process. # The setting is particularly useful for tweaking memory settings. -org.gradle.jvmargs = -Xmx6144m -XX:MaxMetaspaceSize=768m -XX:+HeapDumpOnOutOfMemoryError -Dfile.encoding=UTF-8 +org.gradle.jvmargs = -Xmx6144m -XX:MaxMetaspaceSize=768m -XX:+HeapDumpOnOutOfMemoryError -Dfile.encoding=UTF-8 # When configured, Gradle will run in incubating parallel mode. # This option should only be used with decoupled projects. More details, visit # http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects