Updated on 2026-08-14
This commit is contained in:
parent
db08399066
commit
ae809ece30
28 changed files with 939 additions and 10 deletions
|
|
@ -248,6 +248,17 @@
|
|||
android:host="swap"
|
||||
android:scheme="tangem" />
|
||||
</intent-filter>
|
||||
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.VIEW" />
|
||||
|
||||
<category android:name="android.intent.category.DEFAULT" />
|
||||
<category android:name="android.intent.category.BROWSABLE" />
|
||||
|
||||
<data
|
||||
android:host="promo"
|
||||
android:scheme="tangem" />
|
||||
</intent-filter>
|
||||
</activity>
|
||||
|
||||
<!-- Disable android.startup completely. Used for Worker according doc -->
|
||||
|
|
|
|||
|
|
@ -15,6 +15,7 @@ import com.tangem.features.onramp.deeplink.SwapDeepLinkHandler
|
|||
import com.tangem.features.send.v2.api.deeplink.SellRedirectDeepLinkHandler
|
||||
import com.tangem.features.staking.api.deeplink.StakingDeepLinkHandler
|
||||
import com.tangem.features.tokendetails.deeplink.TokenDetailsDeepLinkHandler
|
||||
import com.tangem.features.wallet.deeplink.PromoDeeplinkHandler
|
||||
import com.tangem.features.wallet.deeplink.WalletDeepLinkHandler
|
||||
import com.tangem.features.walletconnect.components.deeplink.WalletConnectDeepLinkHandler
|
||||
import com.tangem.utils.coroutines.JobHolder
|
||||
|
|
@ -46,6 +47,7 @@ internal class DeepLinkFactory @Inject constructor(
|
|||
private val buyDeepLink: BuyDeepLinkHandler.Factory,
|
||||
private val sellDeepLink: SellDeepLinkHandler.Factory,
|
||||
private val swapDeepLink: SwapDeepLinkHandler.Factory,
|
||||
private val promoDeepLink: PromoDeeplinkHandler.Factory,
|
||||
) {
|
||||
private val permittedAppRoute = MutableStateFlow(false)
|
||||
|
||||
|
|
@ -131,6 +133,7 @@ internal class DeepLinkFactory @Inject constructor(
|
|||
DeepLinkRoute.Sell.host -> sellDeepLink.create()
|
||||
DeepLinkRoute.Swap.host -> swapDeepLink.create()
|
||||
DeepLinkRoute.WalletConnect.host -> walletConnectDeepLink.create(deeplinkUri)
|
||||
DeepLinkRoute.Promo.host -> promoDeepLink.create(coroutineScope, queryParams)
|
||||
else -> {
|
||||
Timber.i(
|
||||
"""
|
||||
|
|
|
|||
|
|
@ -13,6 +13,7 @@ import com.tangem.features.onramp.deeplink.SwapDeepLinkHandler
|
|||
import com.tangem.features.send.v2.api.deeplink.SellRedirectDeepLinkHandler
|
||||
import com.tangem.features.staking.api.deeplink.StakingDeepLinkHandler
|
||||
import com.tangem.features.tokendetails.deeplink.TokenDetailsDeepLinkHandler
|
||||
import com.tangem.features.wallet.deeplink.PromoDeeplinkHandler
|
||||
import com.tangem.features.wallet.deeplink.WalletDeepLinkHandler
|
||||
import com.tangem.features.walletconnect.components.deeplink.WalletConnectDeepLinkHandler
|
||||
import io.mockk.every
|
||||
|
|
@ -68,6 +69,10 @@ class DeepLinkFactoryTest {
|
|||
every { create() } returns mockk()
|
||||
}
|
||||
|
||||
private val promoDeepLinkFactory = mockk<PromoDeeplinkHandler.Factory>(relaxed = true) {
|
||||
every { create(any(), any()) } returns mockk()
|
||||
}
|
||||
|
||||
private val cardSdkProvider = mockk<CardSdkProvider>(relaxed = true) {
|
||||
every { sdk.uiVisibility() } returns MutableStateFlow(false)
|
||||
}
|
||||
|
|
@ -91,6 +96,7 @@ class DeepLinkFactoryTest {
|
|||
buyDeepLink = buyDeepLinkFactory,
|
||||
sellDeepLink = sellDeepLinkFactory,
|
||||
swapDeepLink = swapDeepLinkFactory,
|
||||
promoDeepLink = promoDeepLinkFactory,
|
||||
)
|
||||
|
||||
@OptIn(ExperimentalCoroutinesApi::class)
|
||||
|
|
@ -303,13 +309,19 @@ class DeepLinkFactoryTest {
|
|||
every { mockedUri.host } returns "swap"
|
||||
deepLinkFactory.handleDeeplink(mockedUri, testScope, isFromOnNewIntent)
|
||||
advanceUntilIdle()
|
||||
verify { sellDeepLinkFactory.create() }
|
||||
verify { swapDeepLinkFactory.create() }
|
||||
|
||||
// Test Buy
|
||||
every { mockedUri.host } returns "buy"
|
||||
deepLinkFactory.handleDeeplink(mockedUri, testScope, isFromOnNewIntent)
|
||||
advanceUntilIdle()
|
||||
verify { buyDeepLinkFactory.create() }
|
||||
|
||||
// Test Promo
|
||||
every { mockedUri.host } returns "promo"
|
||||
deepLinkFactory.handleDeeplink(mockedUri, testScope, isFromOnNewIntent)
|
||||
advanceUntilIdle()
|
||||
verify { promoDeepLinkFactory.create(eq(testScope), eq(emptyMap())) }
|
||||
}
|
||||
|
||||
@Test
|
||||
|
|
@ -332,6 +344,7 @@ class DeepLinkFactoryTest {
|
|||
buyDeepLinkFactory.create()
|
||||
sellDeepLinkFactory.create()
|
||||
swapDeepLinkFactory.create()
|
||||
promoDeepLinkFactory.create(any(), any())
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -400,4 +413,22 @@ class DeepLinkFactoryTest {
|
|||
advanceUntilIdle()
|
||||
verify { onrampDeepLinkFactory.create(eq(testScope), eq(emptyMap())) }
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `handleTangemDeepLinks routes to promo handler`() = runTest {
|
||||
every { mockedUri.scheme } returns "tangem"
|
||||
every { mockedUri.host } returns "promo"
|
||||
every { mockedUri.query } returns "promo_code=PROMO123"
|
||||
every { mockedUri.queryParameterNames } returns setOf("promo_code")
|
||||
every { mockedUri.getQueryParameter("promo_code") } returns "PROMO123"
|
||||
|
||||
deepLinkFactory.checkRoutingReadiness(AppRoute.Wallet)
|
||||
deepLinkFactory.handleDeeplink(mockedUri, testScope, isFromOnNewIntent)
|
||||
|
||||
advanceUntilIdle()
|
||||
|
||||
verify {
|
||||
promoDeepLinkFactory.create(eq(testScope), eq(mapOf("promo_code" to "PROMO123")))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -55,6 +55,10 @@ sealed class DeepLinkRoute {
|
|||
data object WalletConnect : DeepLinkRoute() {
|
||||
override val host: String = "wc"
|
||||
}
|
||||
|
||||
data object Promo : DeepLinkRoute() {
|
||||
override val host: String = "promo"
|
||||
}
|
||||
}
|
||||
|
||||
enum class DeepLinkScheme(val scheme: String) {
|
||||
|
|
|
|||
|
|
@ -11,5 +11,6 @@ object DeeplinkConst {
|
|||
const val TOKEN_ID_KEY = "token_id"
|
||||
const val DERIVATION_PATH_KEY = "derivation_path"
|
||||
const val TRANSACTION_ID_KEY = "transaction_id"
|
||||
const val PROMO_CODE_KEY = "promo_code"
|
||||
const val NAME_KEY = "name"
|
||||
}
|
||||
|
|
@ -176,4 +176,9 @@ interface TangemTechApi {
|
|||
@GET("v1/user-wallets/wallets/by-app/{app_id}")
|
||||
suspend fun getWallets(@Path("app_id") appId: String): ApiResponse<List<WalletResponse>>
|
||||
// endregion
|
||||
|
||||
// promo
|
||||
@POST("promo/v1/promo-codes/activate")
|
||||
suspend fun activatePromoCode(@Body body: PromocodeActivationBody): ApiResponse<PromocodeActivationResponse>
|
||||
// endregion
|
||||
}
|
||||
|
|
@ -0,0 +1,10 @@
|
|||
package com.tangem.datasource.api.tangemTech.models
|
||||
|
||||
import com.squareup.moshi.Json
|
||||
import com.squareup.moshi.JsonClass
|
||||
|
||||
@JsonClass(generateAdapter = true)
|
||||
data class PromocodeActivationBody(
|
||||
@Json(name = "promoCode") val promoCode: String,
|
||||
@Json(name = "address") val address: String,
|
||||
)
|
||||
|
|
@ -0,0 +1,9 @@
|
|||
package com.tangem.datasource.api.tangemTech.models
|
||||
|
||||
import com.squareup.moshi.Json
|
||||
import com.squareup.moshi.JsonClass
|
||||
|
||||
@JsonClass(generateAdapter = true)
|
||||
data class PromocodeActivationResponse(
|
||||
@Json(name = "status") val status: String,
|
||||
)
|
||||
|
|
@ -202,7 +202,7 @@
|
|||
<string name="common_nft">NFT</string>
|
||||
<string name="common_no">No</string>
|
||||
<string name="common_no_address">Ninguna dirección</string>
|
||||
<string name="common_not_added">No aregada</string>
|
||||
<string name="common_not_added">No agregada</string>
|
||||
<string name="common_not_now">Ahora no</string>
|
||||
<string name="common_now">Ahora</string>
|
||||
<string name="common_ok">OK</string>
|
||||
|
|
@ -250,8 +250,8 @@
|
|||
<string name="common_terms_of_use">Condiciones de uso</string>
|
||||
<string name="common_today">Hoy</string>
|
||||
<plurals name="common_tokens_count">
|
||||
<item quantity="one">%d ficha</item>
|
||||
<item quantity="other">%d fichas</item>
|
||||
<item quantity="one">%d token</item>
|
||||
<item quantity="other">%d tokens</item>
|
||||
</plurals>
|
||||
<string name="common_transaction_failed">Transacción fallida</string>
|
||||
<string name="common_transaction_status">Estado de la transacción</string>
|
||||
|
|
@ -1348,7 +1348,7 @@
|
|||
<string name="wc_alert_network_not_connected_description">Para continuar, vuelva a conectar su sesión de dApp con la red requerida %s.</string>
|
||||
<string name="wc_alert_network_not_connected_title">Red no conectada</string>
|
||||
<string name="wc_alert_request_timeout_description">Verifique su conexión de red</string>
|
||||
<string name="wc_alert_request_timeout_title">Tiempo de espera de la solicitud agotado</string>
|
||||
<string name="wc_alert_request_timeout_title">Tiempo de espera agotado</string>
|
||||
<string name="wc_alert_session_disconnected_description">Vuelva a su navegador y vuelva a conectarse a través de WalletConnect.</string>
|
||||
<string name="wc_alert_session_disconnected_title">La sesión de Wallet Connect se desconectó</string>
|
||||
<string name="wc_alert_sign_anyway">Firmar de todos modos</string>
|
||||
|
|
|
|||
|
|
@ -12,6 +12,7 @@
|
|||
<string name="access_code_create_description">ウォレットのロックを解除するには、 %s桁のアクセスコードを設定します。</string>
|
||||
<string name="access_code_create_title">アクセスコードの作成</string>
|
||||
<string name="access_code_navtitle">アクセスコード</string>
|
||||
<string name="account_archived_accounts">アーカイブされたアカウント</string>
|
||||
<string name="account_archived_recover">回復する</string>
|
||||
<string name="account_archived_title">アーカイブ済み</string>
|
||||
<string name="account_details_archive">アカウントをアーカイブする</string>
|
||||
|
|
@ -26,6 +27,7 @@
|
|||
<string name="account_form_placeholder_new_account">新しいアカウント</string>
|
||||
<string name="account_form_title_create">アカウントを追加</string>
|
||||
<string name="account_form_title_edit">アカウントを編集</string>
|
||||
<string name="account_reorder_description">アカウントを長押しして並べ替える</string>
|
||||
<string name="account_unsaved_dialog_action_first">編集を続ける</string>
|
||||
<string name="account_unsaved_dialog_action_second">破棄</string>
|
||||
<string name="account_unsaved_dialog_message_create">新しいアカウントを破棄してもよろしいですか?</string>
|
||||
|
|
@ -71,10 +73,8 @@
|
|||
<string name="app_settings_enable_biometrics_description">設定に移動して、Tangemアプリで生体認証を有効にします。</string>
|
||||
<string name="app_settings_enable_biometrics_title">生体認証を有効にする</string>
|
||||
<string name="app_settings_off_biometrics_alert_message">%1$sが無効になると、アプリのロックを解除してウォレットを操作するために、パスコードを入力する必要があります。</string>
|
||||
<string name="app_settings_off_require_access_code_alert_message">後でウォレットのアクセスコードを入力してもらいます。それを安全に保存し、今後の利用に備えるためです。</string>
|
||||
<string name="app_settings_off_saved_access_code_alert_message">これにより、保存されているウォレットアクセスコードがすべて削除されます。ウォレットでの今後の操作には、アクセスコードの送信が必要になります。</string>
|
||||
<string name="app_settings_off_saved_wallet_alert_message">保存したデバイスを削除すると、保存されているすべてのウォレットとそのアクセスコードがアプリから削除されます。</string>
|
||||
<string name="app_settings_on_require_access_code_alert_message">これにより、保存されているウォレットアクセスコードがすべて削除されます。今後ウォレットを操作するには、アクセスコードの送信が必要になります。</string>
|
||||
<string name="app_settings_require_access_code">アクセスコードを要求する</string>
|
||||
<string name="app_settings_require_access_code_footer">このオプションを選択すると、機密性の高い操作における生体認証が無効になります。取引の署名時などには、毎回アクセスコードの入力が必要になります。</string>
|
||||
<string name="app_settings_saved_access_codes">アクセスコードを保存</string>
|
||||
|
|
@ -116,6 +116,12 @@
|
|||
<string name="biometric_lockout_warning_description">30秒後に再試行するか、カードまたはリングをスキャンしてください</string>
|
||||
<string name="biometric_lockout_warning_title">試行回数が多すぎます</string>
|
||||
<string name="biometric_unavailable_warning">お使いの携帯電話で生体認証が無効になっているため、アプリにウォレットを保存できません。ウォレットを保存するには、携帯電話の設定で生体認証機能を有効にしてください。</string>
|
||||
<string name="bitcoin_promo_activation_error">プロモーションコードの処理中にエラーが発生しました。しばらくしてからもう一度お試しください。</string>
|
||||
<string name="bitcoin_promo_activation_success">プロモーションコードが正常に有効化されました。14日以内に10 USDT相当のビットコインボーナスが付与されます。</string>
|
||||
<string name="bitcoin_promo_activation_success_title">プロモーションコードが有効になりました</string>
|
||||
<string name="bitcoin_promo_already_activated">このプロモーションコードは既に使用されているため、再度使うことはできません。</string>
|
||||
<string name="bitcoin_promo_invalid_code">このプロモーションコードは無効であり、有効化できません。</string>
|
||||
<string name="bitcoin_promo_no_address">ボーナスを受け取るにはビットコインアドレスが必要です。ウォレットにビットコインアドレスを追加し、再度アクティベーションをお試しください。</string>
|
||||
<string name="button_start_backup_process">バックアップ処理を開始する</string>
|
||||
<string name="buy_token_description">銀行カードまたはその他の支払い方法を使用する</string>
|
||||
<plurals name="card_label_card_count">
|
||||
|
|
@ -149,6 +155,7 @@
|
|||
<string name="cardano_max_amount_has_token_title">ADAが不足しています。</string>
|
||||
<string name="common_accept">受け入れる</string>
|
||||
<string name="common_access_denied">アクセスが拒否されました</string>
|
||||
<string name="common_accounts">アカウント</string>
|
||||
<string name="common_add">追加</string>
|
||||
<string name="common_add_to_portfolio">ポートフォリオに追加</string>
|
||||
<string name="common_add_token">トークンを追加</string>
|
||||
|
|
@ -489,7 +496,6 @@
|
|||
<string name="hw_create_seed_description">最新の機能とニュースをお届けします</string>
|
||||
<string name="hw_create_seed_title">シードフレーズのバックアップ</string>
|
||||
<string name="hw_create_title">モバイルウォレットを作成する</string>
|
||||
<string name="hw_import_seed_phrase_already_imported">このリカバリーフレーズはすでにインポートされています。</string>
|
||||
<string name="hw_mobile_wallet">モバイルウォレット</string>
|
||||
<string name="information_generated_with_ai">この情報はAIで生成されました。 \nエラーが見つかった場合は、ここをタップしてください。</string>
|
||||
<string name="initial_message_change_access_code_body">アクセスコードを変更するには、上図のようにカードまたはリングをタップし、操作が終了するまで取り外さないでください。</string>
|
||||
|
|
|
|||
|
|
@ -59,6 +59,16 @@
|
|||
<string name="biometric_lockout_warning_description">Пожалуйста, попробуйте снова через 30 секунд или отсканируйте карту или кольцо</string>
|
||||
<string name="biometric_lockout_warning_title">Слишком много попыток</string>
|
||||
<string name="biometric_unavailable_warning">Вы отключили биометрическую аутентификацию на вашем телефоне и не сможете сохранять кошельки в приложении. Для сохранения кошельков, пожалуйста, включите функцию биометрической аутентификации в настройках телефона.</string>
|
||||
<string name="bitcoin_promo_activation_error">При обработке промокода произошла ошибка. Пожалуйста, попробуйте позже.</string>
|
||||
<string name="bitcoin_promo_activation_error_title">Ошибка активации</string>
|
||||
<string name="bitcoin_promo_activation_success">Ваш промокод успешно активирован. Бонус 10 USDT в Bitcoin будет зачислен через 14 дней.</string>
|
||||
<string name="bitcoin_promo_activation_success_title">Промокод активирован</string>
|
||||
<string name="bitcoin_promo_already_activated">Этот промокод уже был использован и не может быть активирован повторно.</string>
|
||||
<string name="bitcoin_promo_already_activated_title">Код недоступен</string>
|
||||
<string name="bitcoin_promo_invalid_code">Этот промокод недействителен и не может быть активирован.</string>
|
||||
<string name="bitcoin_promo_invalid_code_title">Неверный код</string>
|
||||
<string name="bitcoin_promo_no_address">Для зачисления бонуса нужен Bitcoin-адрес. Добавьте его в портфель и повторите активацию.</string>
|
||||
<string name="bitcoin_promo_no_address_title">Требуется Bitcoin-адрес</string>
|
||||
<string name="button_start_backup_process">Начать резервное копирование</string>
|
||||
<string name="buy_token_description">Используйте банковскую карту или другие методы оплаты</string>
|
||||
<plurals name="card_label_card_count">
|
||||
|
|
|
|||
|
|
@ -59,6 +59,16 @@
|
|||
<string name="biometric_lockout_warning_description">Будь ласка, спробуйте знову через 30 секунд або відскануйте картку або кільце</string>
|
||||
<string name="biometric_lockout_warning_title">Забагато спроб</string>
|
||||
<string name="biometric_unavailable_warning">Ви вимкнули біометричну автентифікацію на своєму телефоні і не зможете зберігати гаманці в додатку. Щоб зберегти гаманці, будь ласка, увімкніть функцію біометричної автентифікації в налаштуваннях телефону.</string>
|
||||
<string name="bitcoin_promo_activation_error">Під час обробки промокоду сталася помилка. Будь ласка, спробуйте пізніше.</string>
|
||||
<string name="bitcoin_promo_activation_error_title">Помилка активації</string>
|
||||
<string name="bitcoin_promo_activation_success">Ваш промокод успішно активовано. Бонус 10 USDT у Bitcoin буде зараховано через 14 днів.</string>
|
||||
<string name="bitcoin_promo_activation_success_title">Промокод активовано</string>
|
||||
<string name="bitcoin_promo_already_activated">Цей промокод уже був використаний і не може бути активований повторно.</string>
|
||||
<string name="bitcoin_promo_already_activated_title">Код недоступний</string>
|
||||
<string name="bitcoin_promo_invalid_code">Цей промокод недійсний і не може бути активований.</string>
|
||||
<string name="bitcoin_promo_invalid_code_title">Невірний код</string>
|
||||
<string name="bitcoin_promo_no_address">Для зарахування бонусу потрібна Bitcoin-адреса. Додайте її до портфеля та повторіть активацію.</string>
|
||||
<string name="bitcoin_promo_no_address_title">Потрібна Bitcoin-адреса</string>
|
||||
<string name="button_start_backup_process">Почніть процес резервного копіювання</string>
|
||||
<string name="buy_token_description">Використовуйте банківську картку або інші способи оплати</string>
|
||||
<plurals name="card_label_card_count">
|
||||
|
|
|
|||
|
|
@ -12,7 +12,12 @@
|
|||
<string name="access_code_create_description">Set a %s-digit Access Code to unlock your wallet.</string>
|
||||
<string name="access_code_create_title">Create Access Code</string>
|
||||
<string name="access_code_navtitle">Access code</string>
|
||||
<string name="account_add_limit_dialog_description">You cannot create more than %1$s accounts. Archive one to add new.</string>
|
||||
<string name="account_add_limit_dialog_title">Can’t add new account</string>
|
||||
<string name="account_archived_accounts">Archived accounts</string>
|
||||
<string name="account_archived_recover">Recover</string>
|
||||
<string name="account_archived_recover_dialog_description">You’re about to recover “%1$s”.</string>
|
||||
<string name="account_archived_recover_dialog_title">Recover account</string>
|
||||
<string name="account_archived_title">Archived</string>
|
||||
<string name="account_details_archive">Archive account</string>
|
||||
<string name="account_details_archive_action">Archive</string>
|
||||
|
|
@ -26,6 +31,8 @@
|
|||
<string name="account_form_placeholder_new_account">New account</string>
|
||||
<string name="account_form_title_create">Add account</string>
|
||||
<string name="account_form_title_edit">Edit account</string>
|
||||
<string name="account_label_tokens_info">%1$s in %2$s</string>
|
||||
<string name="account_reorder_description">Long tap on an account to reorder accounts</string>
|
||||
<string name="account_unsaved_dialog_action_first">Keep Editing</string>
|
||||
<string name="account_unsaved_dialog_action_second">Discard</string>
|
||||
<string name="account_unsaved_dialog_message_create">Are you sure you want to discard new account?</string>
|
||||
|
|
@ -117,6 +124,16 @@
|
|||
<string name="biometric_lockout_warning_description">Please try again in 30 seconds or scan the card or ring</string>
|
||||
<string name="biometric_lockout_warning_title">Too many attempts</string>
|
||||
<string name="biometric_unavailable_warning">You have disabled biometric authentication on your phone and will not be able to save wallets in the app. To save wallets, please enable the biometric authentication function in your phone settings.</string>
|
||||
<string name="bitcoin_promo_activation_error">An error occurred while processing your promo code. Please try again later.</string>
|
||||
<string name="bitcoin_promo_activation_error_title">Activation error</string>
|
||||
<string name="bitcoin_promo_activation_success">Your promo code has been successfully activated. A bonus of 10 USDT in Bitcoin will be credited in 14 days.</string>
|
||||
<string name="bitcoin_promo_activation_success_title">Promo code activated</string>
|
||||
<string name="bitcoin_promo_already_activated">This promo code has already been used and cannot be activated again.</string>
|
||||
<string name="bitcoin_promo_already_activated_title">Code unavailable</string>
|
||||
<string name="bitcoin_promo_invalid_code">This promo code is not valid and cannot be activated.</string>
|
||||
<string name="bitcoin_promo_invalid_code_title">Invalid code</string>
|
||||
<string name="bitcoin_promo_no_address">A Bitcoin address is required to receive the bonus. Please add one to your wallet and retry the activation.</string>
|
||||
<string name="bitcoin_promo_no_address_title">Bitcoin address required</string>
|
||||
<string name="button_start_backup_process">Start backup process</string>
|
||||
<string name="buy_token_description">Use a bank card or other payment methods</string>
|
||||
<plurals name="card_label_card_count">
|
||||
|
|
@ -152,6 +169,7 @@
|
|||
<string name="cardano_max_amount_has_token_title">Not enough ADA</string>
|
||||
<string name="common_accept">Accept</string>
|
||||
<string name="common_access_denied">Access denied</string>
|
||||
<string name="common_accounts">Accounts</string>
|
||||
<string name="common_add">Add</string>
|
||||
<string name="common_add_to_portfolio">Add to portfolio</string>
|
||||
<string name="common_add_token">Add token</string>
|
||||
|
|
@ -239,6 +257,10 @@
|
|||
<string name="common_month">month</string>
|
||||
<string name="common_network_fee_title">Network fee</string>
|
||||
<string name="common_network_fee_warning_content">Amount sent will be reduced by %1$s (%2$s) to cover the selected fee level</string>
|
||||
<plurals name="common_networks_count">
|
||||
<item quantity="one">%d network</item>
|
||||
<item quantity="other">%d networks</item>
|
||||
</plurals>
|
||||
<string name="common_next">Next</string>
|
||||
<string name="common_nft">NFT</string>
|
||||
<string name="common_no">No</string>
|
||||
|
|
@ -485,6 +507,8 @@
|
|||
<string name="hw_backup_close_description">If you do, you\'ll need to start over.</string>
|
||||
<string name="hw_backup_google_drive_description">Recover an existing wallet stored in your Google Drive backup</string>
|
||||
<string name="hw_backup_google_drive_title">Google Drive Backup</string>
|
||||
<string name="hw_backup_hardware_description">Upgrade your security right away with a best in class hardware wallet from Tangem.</string>
|
||||
<string name="hw_backup_hardware_title">Hardware Wallet</string>
|
||||
<string name="hw_backup_need_action">Go to backup</string>
|
||||
<string name="hw_backup_need_description">To secure your wallet with a Access Code, complete the backup first.</string>
|
||||
<string name="hw_backup_need_title">Finish Backup First</string>
|
||||
|
|
@ -498,6 +522,19 @@
|
|||
<string name="hw_create_title">Create Mobile Wallet</string>
|
||||
<string name="hw_import_seed_phrase_already_imported">This recovery phrase has already been imported</string>
|
||||
<string name="hw_mobile_wallet">Mobile Wallet</string>
|
||||
<string name="hw_upgrade_funds_access_description">Your funds stay safe and fully accessible during the process</string>
|
||||
<string name="hw_upgrade_funds_access_title">Funds access</string>
|
||||
<string name="hw_upgrade_general_security_description">All private wallet data will be removed from the mobile app and stored securely on your Tangem device only</string>
|
||||
<string name="hw_upgrade_general_security_title">General Security</string>
|
||||
<string name="hw_upgrade_key_migration_description">Private keys will be moved from the app to your Tangem card or ring</string>
|
||||
<string name="hw_upgrade_key_migration_title">Key Migration</string>
|
||||
<string name="hw_upgrade_scan_device">Scan device</string>
|
||||
<string name="hw_upgrade_start_action">Start upgrade</string>
|
||||
<string name="hw_upgrade_start_description">You’re about to upgrade your wallet to Tangem Wallet. This will keep your assets safe with cold storage.</string>
|
||||
<string name="hw_upgrade_start_title">Tangem Wallet</string>
|
||||
<string name="hw_upgrade_title">Upgrade to Hardware Wallet</string>
|
||||
<string name="hw_upgrade_to_cold_banner_description">Keep your crypto safe with Tangem’s best-in-class hardware wallet.</string>
|
||||
<string name="hw_upgrade_to_cold_banner_title">Upgrade wallet with a hardware
backup</string>
|
||||
<string name="information_generated_with_ai">This information was generated with AI.\nTap here, if you find any errors.</string>
|
||||
<string name="initial_message_change_access_code_body">To change the access code tap the card or ring as shown above and do not remove until the end of the operation</string>
|
||||
<string name="initial_message_change_passcode_body">To change the passcode tap the card as shown above and do not remove until the end of the operation</string>
|
||||
|
|
@ -702,6 +739,9 @@
|
|||
<string name="notification_referral_promo_button">Join Now</string>
|
||||
<string name="notification_referral_promo_text">Share your code - earn 5 USDT per sale. Your friend gets 10% OFF.</string>
|
||||
<string name="notification_referral_promo_title">Get REWARDS for every friend!</string>
|
||||
<string name="notification_sepa_button">Buy crypto</string>
|
||||
<string name="notification_sepa_text">Enjoy zero fees when purchasing crypto via SEPA transfer.</string>
|
||||
<string name="notification_sepa_title">Buy Crypto with SEPA</string>
|
||||
<string name="onboarding_access_code_feature_1_description">Set up a single access code to protect all your devices.</string>
|
||||
<string name="onboarding_access_code_feature_1_title">Protect</string>
|
||||
<string name="onboarding_access_code_feature_2_description">Set an individual access code for each card or ring later.</string>
|
||||
|
|
|
|||
|
|
@ -80,4 +80,29 @@ fun String.capitalize(): String = replaceFirstChar { if (it.isLowerCase()) it.ti
|
|||
|
||||
fun String.orMaskWithStars(maskWithStars: Boolean): String {
|
||||
return if (maskWithStars) THREE_STARS else this
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a masked representation of the string for safe display.
|
||||
*
|
||||
* Rules:
|
||||
* - empty string: returned unchanged
|
||||
* - length <= 2: all characters are replaced with '*'
|
||||
* - length <= 4: keep the first and last characters, mask the middle
|
||||
* - length > 4: keep the first two and last two characters, mask the middle with "**"
|
||||
*
|
||||
* @return masked string according to the rules above
|
||||
*/
|
||||
@Suppress("MagicNumber")
|
||||
fun String.mask(): String {
|
||||
return when {
|
||||
this.isEmpty() -> this
|
||||
this.length <= 2 -> "*".repeat(this.length) // mask all if too short
|
||||
this.length <= 4 -> this.first() + "*".repeat(this.length - 2) + this.last()
|
||||
else -> {
|
||||
val prefix = this.take(2)
|
||||
val suffix = this.takeLast(2)
|
||||
"$prefix**$suffix"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -147,6 +147,8 @@ data class DialogMessage(
|
|||
}
|
||||
}
|
||||
|
||||
data class GlobalLoadingMessage(val isShow: Boolean) : EventMessage
|
||||
|
||||
/**
|
||||
* Shows a bottom sheet.
|
||||
*
|
||||
|
|
|
|||
|
|
@ -2,13 +2,23 @@ package com.tangem.core.ui.message
|
|||
|
||||
import android.content.Context
|
||||
import android.widget.Toast
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.material3.CircularProgressIndicator
|
||||
import androidx.compose.material3.SnackbarDuration
|
||||
import androidx.compose.material3.SnackbarHostState
|
||||
import androidx.compose.material3.SnackbarResult
|
||||
import androidx.compose.runtime.*
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.window.Dialog
|
||||
import androidx.compose.ui.window.DialogProperties
|
||||
import com.tangem.core.ui.components.BasicDialog
|
||||
import com.tangem.core.ui.components.DialogButtonUM
|
||||
import com.tangem.core.ui.components.SpacerHMax
|
||||
import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig
|
||||
import com.tangem.core.ui.components.bottomsheets.message.MessageBottomSheet
|
||||
import com.tangem.core.ui.components.bottomsheets.message.MessageBottomSheetUM
|
||||
|
|
@ -17,6 +27,7 @@ import com.tangem.core.ui.event.EventEffect
|
|||
import com.tangem.core.ui.extensions.resolveReference
|
||||
import com.tangem.core.ui.res.LocalEventMessageHandler
|
||||
import com.tangem.core.ui.res.LocalSnackbarHostState
|
||||
import com.tangem.core.ui.res.TangemTheme
|
||||
|
||||
@Composable
|
||||
fun EventMessageEffect(
|
||||
|
|
@ -33,6 +44,7 @@ fun EventMessageEffect(
|
|||
var dialogMessage: DialogMessage? by remember { mutableStateOf(value = null) }
|
||||
var bottomSheetMessage: BottomSheetMessage? by remember { mutableStateOf(value = null) }
|
||||
var bottomSheetMessageV2: BottomSheetMessageV2? by remember { mutableStateOf(value = null) }
|
||||
var loadingMessage: GlobalLoadingMessage? by remember { mutableStateOf(value = null) }
|
||||
|
||||
EventEffect(event = messageEvent) { message ->
|
||||
when (message) {
|
||||
|
|
@ -51,6 +63,13 @@ fun EventMessageEffect(
|
|||
is ToastMessage -> {
|
||||
onShowToast(message, context)
|
||||
}
|
||||
is GlobalLoadingMessage -> {
|
||||
loadingMessage = if (message.isShow) {
|
||||
message
|
||||
} else {
|
||||
null
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -80,6 +99,34 @@ fun EventMessageEffect(
|
|||
onDismissRequest = { bottomSheetMessageV2 = null },
|
||||
)
|
||||
}
|
||||
|
||||
loadingMessage?.let {
|
||||
LoadingDialog()
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun LoadingDialog() {
|
||||
Dialog(
|
||||
onDismissRequest = {},
|
||||
properties = DialogProperties(
|
||||
dismissOnBackPress = false,
|
||||
dismissOnClickOutside = false,
|
||||
),
|
||||
) {
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxSize(),
|
||||
verticalArrangement = Arrangement.spacedBy(16.dp),
|
||||
) {
|
||||
SpacerHMax()
|
||||
CircularProgressIndicator(
|
||||
modifier = Modifier.align(Alignment.CenterHorizontally),
|
||||
color = TangemTheme.colors.icon.primary1,
|
||||
)
|
||||
SpacerHMax()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
|
|
|
|||
|
|
@ -0,0 +1,91 @@
|
|||
package com.tangem.core.ui.extensions
|
||||
|
||||
import com.google.common.truth.Truth.assertThat
|
||||
import org.junit.Test
|
||||
|
||||
class StringMaskTest {
|
||||
|
||||
@Test
|
||||
fun GIVEN_empty_string_WHEN_mask_THEN_returns_empty_string() {
|
||||
// GIVEN
|
||||
val input = ""
|
||||
|
||||
// WHEN
|
||||
val actual = input.mask()
|
||||
|
||||
// THEN
|
||||
assertThat(actual).isEqualTo("")
|
||||
}
|
||||
|
||||
@Test
|
||||
fun GIVEN_one_char_WHEN_mask_THEN_returns_asterisk() {
|
||||
// GIVEN
|
||||
val input = "a"
|
||||
|
||||
// WHEN
|
||||
val actual = input.mask()
|
||||
|
||||
// THEN
|
||||
assertThat(actual).isEqualTo("*")
|
||||
}
|
||||
|
||||
@Test
|
||||
fun GIVEN_two_chars_WHEN_mask_THEN_returns_two_asterisks() {
|
||||
// GIVEN
|
||||
val input = "ab"
|
||||
|
||||
// WHEN
|
||||
val actual = input.mask()
|
||||
|
||||
// THEN
|
||||
assertThat(actual).isEqualTo("**")
|
||||
}
|
||||
|
||||
@Test
|
||||
fun GIVEN_three_chars_WHEN_mask_THEN_masks_middle_only() {
|
||||
// GIVEN
|
||||
val input = "abc"
|
||||
|
||||
// WHEN
|
||||
val actual = input.mask()
|
||||
|
||||
// THEN
|
||||
assertThat(actual).isEqualTo("a*c")
|
||||
}
|
||||
|
||||
@Test
|
||||
fun GIVEN_four_chars_WHEN_mask_THEN_masks_middle_two() {
|
||||
// GIVEN
|
||||
val input = "abcd"
|
||||
|
||||
// WHEN
|
||||
val actual = input.mask()
|
||||
|
||||
// THEN
|
||||
assertThat(actual).isEqualTo("a**d")
|
||||
}
|
||||
|
||||
@Test
|
||||
fun GIVEN_five_chars_WHEN_mask_THEN_keeps_edges_and_two_stars_in_middle() {
|
||||
// GIVEN
|
||||
val input = "abcde"
|
||||
|
||||
// WHEN
|
||||
val actual = input.mask()
|
||||
|
||||
// THEN
|
||||
assertThat(actual).isEqualTo("ab**de")
|
||||
}
|
||||
|
||||
@Test
|
||||
fun GIVEN_seven_chars_WHEN_mask_THEN_keeps_two_on_each_side_and_two_stars() {
|
||||
// GIVEN
|
||||
val input = "abcdefg"
|
||||
|
||||
// WHEN
|
||||
val actual = input.mask()
|
||||
|
||||
// THEN
|
||||
assertThat(actual).isEqualTo("ab**fg")
|
||||
}
|
||||
}
|
||||
|
|
@ -1,12 +1,17 @@
|
|||
package com.tangem.data.wallets
|
||||
|
||||
import arrow.core.Either
|
||||
import arrow.core.left
|
||||
import arrow.core.right
|
||||
import com.tangem.data.wallets.converters.UserWalletRemoteInfoConverter
|
||||
import com.tangem.data.wallets.converters.WalletIdBodyConverter
|
||||
import com.tangem.datasource.api.common.AuthProvider
|
||||
import com.tangem.datasource.api.common.response.ApiResponseError.HttpException
|
||||
import com.tangem.datasource.api.common.response.fold
|
||||
import com.tangem.datasource.api.common.response.getOrThrow
|
||||
import com.tangem.datasource.api.tangemTech.TangemTechApi
|
||||
import com.tangem.datasource.api.tangemTech.models.MarkUserWalletWasCreatedBody
|
||||
import com.tangem.datasource.api.tangemTech.models.PromocodeActivationBody
|
||||
import com.tangem.datasource.api.tangemTech.models.SeedPhraseNotificationDTO
|
||||
import com.tangem.datasource.api.tangemTech.models.SeedPhraseNotificationDTO.Status
|
||||
import com.tangem.datasource.api.tangemTech.models.WalletBody
|
||||
|
|
@ -23,6 +28,7 @@ import com.tangem.domain.models.wallet.UserWallet
|
|||
import com.tangem.domain.models.wallet.UserWalletId
|
||||
import com.tangem.domain.wallets.models.SeedPhraseNotificationsStatus
|
||||
import com.tangem.domain.wallets.models.UserWalletRemoteInfo
|
||||
import com.tangem.domain.wallets.models.errors.ActivatePromoCodeError
|
||||
import com.tangem.domain.wallets.repository.WalletsRepository
|
||||
import com.tangem.utils.WEEK_MILLIS
|
||||
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
|
||||
|
|
@ -308,4 +314,28 @@ internal class DefaultWalletsRepository(
|
|||
body = walletsBody,
|
||||
).getOrThrow()
|
||||
}
|
||||
|
||||
override suspend fun activatePromoCode(
|
||||
promoCode: String,
|
||||
bitcoinAddress: String,
|
||||
): Either<ActivatePromoCodeError, String> = withContext(dispatchers.io) {
|
||||
tangemTechApi.activatePromoCode(
|
||||
body = PromocodeActivationBody(
|
||||
promoCode = promoCode,
|
||||
address = bitcoinAddress,
|
||||
),
|
||||
).fold({
|
||||
return@fold it.status.right()
|
||||
}, { error ->
|
||||
val error = when (error) {
|
||||
is HttpException -> when (error.code) {
|
||||
HttpException.Code.NOT_FOUND -> ActivatePromoCodeError.InvalidPromoCode
|
||||
HttpException.Code.CONFLICT -> ActivatePromoCodeError.PromocodeAlreadyUsed
|
||||
else -> ActivatePromoCodeError.ActivationFailed
|
||||
}
|
||||
else -> ActivatePromoCodeError.ActivationFailed
|
||||
}
|
||||
return@fold error.left()
|
||||
},)
|
||||
}
|
||||
}
|
||||
|
|
@ -6,12 +6,16 @@ import com.google.common.truth.Truth.assertThat
|
|||
import com.squareup.moshi.Moshi
|
||||
import com.tangem.datasource.api.common.AuthProvider
|
||||
import com.tangem.datasource.api.common.response.ApiResponse
|
||||
import com.tangem.datasource.api.common.response.ApiResponseError.HttpException
|
||||
import com.tangem.datasource.api.tangemTech.TangemTechApi
|
||||
import com.tangem.datasource.api.tangemTech.models.WalletResponse
|
||||
import com.tangem.datasource.api.tangemTech.models.PromocodeActivationBody
|
||||
import com.tangem.datasource.api.tangemTech.models.PromocodeActivationResponse
|
||||
import com.tangem.datasource.local.preferences.AppPreferencesStore
|
||||
import com.tangem.datasource.local.preferences.PreferencesKeys
|
||||
import com.tangem.domain.models.wallet.UserWallet
|
||||
import com.tangem.domain.models.wallet.UserWalletId
|
||||
import com.tangem.domain.wallets.models.errors.ActivatePromoCodeError
|
||||
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
|
||||
import com.tangem.utils.coroutines.TestingCoroutineDispatcherProvider
|
||||
import io.mockk.coEvery
|
||||
|
|
@ -235,4 +239,64 @@ class DefaultWalletsRepositoryTest {
|
|||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `GIVEN valid data WHEN activatePromoCode THEN returns Right with status and calls API`() = runTest {
|
||||
// GIVEN
|
||||
val promoCode = "PROMO123"
|
||||
val address = "bc1qexampleaddress"
|
||||
coEvery { tangemTechApi.activatePromoCode(any()) } returns ApiResponse.Success(
|
||||
PromocodeActivationResponse(status = "activated"),
|
||||
)
|
||||
|
||||
// WHEN
|
||||
val result = repository.activatePromoCode(promoCode = promoCode, bitcoinAddress = address)
|
||||
|
||||
// THEN
|
||||
var right: String? = null
|
||||
var left: ActivatePromoCodeError? = null
|
||||
result.fold({ left = it }, { right = it })
|
||||
assertThat(left).isNull()
|
||||
assertThat(right).isEqualTo("activated")
|
||||
|
||||
coVerify(exactly = 1) {
|
||||
tangemTechApi.activatePromoCode(
|
||||
match { it is PromocodeActivationBody && it.promoCode == promoCode && it.address == address },
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `GIVEN NOT_FOUND error WHEN activatePromoCode THEN returns Left InvalidPromoCode`() = runTest {
|
||||
// GIVEN
|
||||
coEvery { tangemTechApi.activatePromoCode(any()) } returns
|
||||
ApiResponse.Error(
|
||||
HttpException(code = HttpException.Code.NOT_FOUND, message = null, errorBody = null),
|
||||
) as ApiResponse<PromocodeActivationResponse>
|
||||
|
||||
// WHEN
|
||||
val result = repository.activatePromoCode(promoCode = "PROMO", bitcoinAddress = "addr")
|
||||
|
||||
// THEN
|
||||
var error: ActivatePromoCodeError? = null
|
||||
result.fold({ error = it }, { })
|
||||
assertThat(error).isEqualTo(ActivatePromoCodeError.InvalidPromoCode)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `GIVEN CONFLICT error WHEN activatePromoCode THEN returns Left PromocodeAlreadyUsed`() = runTest {
|
||||
// GIVEN
|
||||
coEvery { tangemTechApi.activatePromoCode(any()) } returns
|
||||
ApiResponse.Error(
|
||||
HttpException(code = HttpException.Code.CONFLICT, message = null, errorBody = null),
|
||||
) as ApiResponse<PromocodeActivationResponse>
|
||||
|
||||
// WHEN
|
||||
val result = repository.activatePromoCode(promoCode = "PROMO", bitcoinAddress = "addr")
|
||||
|
||||
// THEN
|
||||
var error: ActivatePromoCodeError? = null
|
||||
result.fold({ error = it }, { })
|
||||
assertThat(error).isEqualTo(ActivatePromoCodeError.PromocodeAlreadyUsed)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,9 @@
|
|||
package com.tangem.domain.wallets
|
||||
|
||||
enum class PromoCodeActivationResult {
|
||||
Failed,
|
||||
InvalidPromoCode,
|
||||
NoBitcoinAddress,
|
||||
PromoCodeAlreadyUsed,
|
||||
Activated,
|
||||
}
|
||||
|
|
@ -0,0 +1,12 @@
|
|||
package com.tangem.domain.wallets.models.errors
|
||||
|
||||
sealed class ActivatePromoCodeError {
|
||||
|
||||
data object InvalidPromoCode : ActivatePromoCodeError()
|
||||
|
||||
data object ActivationFailed : ActivatePromoCodeError()
|
||||
|
||||
data object PromocodeAlreadyUsed : ActivatePromoCodeError()
|
||||
|
||||
data object NoBitcoinAddress : ActivatePromoCodeError()
|
||||
}
|
||||
|
|
@ -1,9 +1,11 @@
|
|||
package com.tangem.domain.wallets.repository
|
||||
|
||||
import arrow.core.Either
|
||||
import com.tangem.domain.wallets.models.SeedPhraseNotificationsStatus
|
||||
import com.tangem.domain.models.wallet.UserWallet
|
||||
import com.tangem.domain.models.wallet.UserWalletId
|
||||
import com.tangem.domain.wallets.models.UserWalletRemoteInfo
|
||||
import com.tangem.domain.wallets.models.errors.ActivatePromoCodeError
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
|
||||
@Suppress("TooManyFunctions")
|
||||
|
|
@ -58,4 +60,6 @@ interface WalletsRepository {
|
|||
|
||||
@Throws
|
||||
suspend fun associateWallets(applicationId: String, wallets: List<UserWallet>)
|
||||
|
||||
suspend fun activatePromoCode(promoCode: String, bitcoinAddress: String): Either<ActivatePromoCodeError, String>
|
||||
}
|
||||
|
|
@ -0,0 +1,14 @@
|
|||
package com.tangem.domain.wallets.usecase
|
||||
|
||||
import arrow.core.Either
|
||||
import com.tangem.domain.wallets.models.errors.ActivatePromoCodeError
|
||||
import com.tangem.domain.wallets.repository.WalletsRepository
|
||||
import javax.inject.Inject
|
||||
|
||||
class ActivateBitcoinPromocodeUseCase @Inject constructor(
|
||||
private val walletsRepository: WalletsRepository,
|
||||
) {
|
||||
|
||||
suspend operator fun invoke(address: String, promoCode: String): Either<ActivatePromoCodeError, String> =
|
||||
walletsRepository.activatePromoCode(promoCode = promoCode, bitcoinAddress = address)
|
||||
}
|
||||
|
|
@ -0,0 +1,10 @@
|
|||
package com.tangem.features.wallet.deeplink
|
||||
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
|
||||
interface PromoDeeplinkHandler {
|
||||
|
||||
interface Factory {
|
||||
fun create(coroutineScope: CoroutineScope, queryParams: Map<String, String>): PromoDeeplinkHandler
|
||||
}
|
||||
}
|
||||
|
|
@ -121,6 +121,8 @@ dependencies {
|
|||
implementation(projects.common.ui)
|
||||
|
||||
/** Test libraries */
|
||||
implementation(deps.test.junit)
|
||||
implementation(deps.test.truth)
|
||||
testImplementation(deps.test.junit)
|
||||
testImplementation(deps.test.coroutine)
|
||||
testImplementation(deps.test.truth)
|
||||
testImplementation(deps.test.mockk)
|
||||
}
|
||||
|
|
@ -0,0 +1,142 @@
|
|||
package com.tangem.feature.wallet.deeplink
|
||||
|
||||
import com.tangem.blockchain.common.Blockchain
|
||||
import com.tangem.common.routing.deeplink.DeeplinkConst.PROMO_CODE_KEY
|
||||
import com.tangem.core.decompose.di.GlobalUiMessageSender
|
||||
import com.tangem.core.decompose.ui.UiMessageSender
|
||||
import com.tangem.core.ui.extensions.mask
|
||||
import com.tangem.core.ui.extensions.resourceReference
|
||||
import com.tangem.core.ui.message.DialogMessage
|
||||
import com.tangem.core.ui.message.GlobalLoadingMessage
|
||||
import com.tangem.domain.models.wallet.UserWallet
|
||||
import com.tangem.domain.tokens.GetMultiCryptoCurrencyStatusUseCase
|
||||
import com.tangem.domain.wallets.PromoCodeActivationResult
|
||||
import com.tangem.domain.wallets.PromoCodeActivationResult.*
|
||||
import com.tangem.domain.wallets.models.errors.ActivatePromoCodeError
|
||||
import com.tangem.domain.wallets.usecase.ActivateBitcoinPromocodeUseCase
|
||||
import com.tangem.domain.wallets.usecase.GetSelectedWalletSyncUseCase
|
||||
import com.tangem.feature.wallet.impl.R
|
||||
import com.tangem.features.wallet.deeplink.PromoDeeplinkHandler
|
||||
import dagger.assisted.Assisted
|
||||
import dagger.assisted.AssistedFactory
|
||||
import dagger.assisted.AssistedInject
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.launch
|
||||
import timber.log.Timber
|
||||
|
||||
internal class DefaultPromoDeeplinkHandler @AssistedInject constructor(
|
||||
@Assisted private val scope: CoroutineScope,
|
||||
@Assisted private val queryParams: Map<String, String>,
|
||||
@GlobalUiMessageSender private val uiMessageSender: UiMessageSender,
|
||||
private val getMultiCryptoCurrencyStatusUseCase: GetMultiCryptoCurrencyStatusUseCase,
|
||||
private val activateBitcoinPromocodeUseCase: ActivateBitcoinPromocodeUseCase,
|
||||
private val getSelectedWalletSyncUseCase: GetSelectedWalletSyncUseCase,
|
||||
) : PromoDeeplinkHandler {
|
||||
|
||||
init {
|
||||
val promoCode = queryParams[PROMO_CODE_KEY].orEmpty()
|
||||
if (promoCode.isEmpty()) {
|
||||
showAlert(InvalidPromoCode)
|
||||
} else {
|
||||
findSelectedWallet(promoCode)
|
||||
}
|
||||
}
|
||||
|
||||
private fun findSelectedWallet(promoCode: String) {
|
||||
getSelectedWalletSyncUseCase().fold(
|
||||
ifLeft = {
|
||||
Timber.tag(LOG_TAG).e("Error on getting user wallet: $it")
|
||||
showAlert(Failed)
|
||||
},
|
||||
ifRight = { userWallet ->
|
||||
Timber.tag(LOG_TAG).d("SelectedUserWallet ${userWallet.walletId.stringValue.mask()}")
|
||||
findBitcoinAddress(userWallet = userWallet, promoCode = promoCode)
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
private fun findBitcoinAddress(userWallet: UserWallet, promoCode: String) {
|
||||
scope.launch {
|
||||
getMultiCryptoCurrencyStatusUseCase.invokeMultiWalletSync(userWallet.walletId).onRight { currencies ->
|
||||
val bitcoinAddress = currencies.find { it.currency.id.rawNetworkId == Blockchain.Bitcoin.id }
|
||||
?.value
|
||||
?.networkAddress
|
||||
?.defaultAddress
|
||||
?.value
|
||||
|
||||
if (bitcoinAddress != null) {
|
||||
Timber.tag(LOG_TAG).d(
|
||||
"Start activation promoCode ${promoCode.mask()} address ${bitcoinAddress.mask()}",
|
||||
)
|
||||
activatePromoCode(bitcoinAddress = bitcoinAddress, promoCode = promoCode)
|
||||
} else {
|
||||
Timber.tag(LOG_TAG).d("no Bitcoin address")
|
||||
showAlert(NoBitcoinAddress)
|
||||
}
|
||||
}.onLeft {
|
||||
Timber.tag(LOG_TAG).e("Error on getting userWallet currencies: $it")
|
||||
showAlert(Failed)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun activatePromoCode(bitcoinAddress: String, promoCode: String) {
|
||||
uiMessageSender.send(GlobalLoadingMessage(true))
|
||||
activateBitcoinPromocodeUseCase(bitcoinAddress, promoCode).onRight {
|
||||
uiMessageSender.send(GlobalLoadingMessage(false))
|
||||
delay(DEFAULT_MESSAGE_SENDER_DELAY)
|
||||
Timber.tag(LOG_TAG).d("${promoCode.mask()} activation success on address ${bitcoinAddress.mask()}")
|
||||
showAlert(Activated)
|
||||
}.onLeft { error ->
|
||||
uiMessageSender.send(GlobalLoadingMessage(false))
|
||||
delay(DEFAULT_MESSAGE_SENDER_DELAY)
|
||||
Timber.tag(LOG_TAG).d("${promoCode.mask()} activation failed $error")
|
||||
val alertType = when (error) {
|
||||
ActivatePromoCodeError.ActivationFailed -> Failed
|
||||
ActivatePromoCodeError.InvalidPromoCode -> InvalidPromoCode
|
||||
ActivatePromoCodeError.NoBitcoinAddress -> NoBitcoinAddress
|
||||
ActivatePromoCodeError.PromocodeAlreadyUsed -> PromoCodeAlreadyUsed
|
||||
}
|
||||
showAlert(alertType)
|
||||
}
|
||||
}
|
||||
|
||||
private fun showAlert(type: PromoCodeActivationResult) {
|
||||
val (title, message) = when (type) {
|
||||
Failed -> resourceReference(R.string.bitcoin_promo_activation_error_title) to
|
||||
resourceReference(R.string.bitcoin_promo_activation_error)
|
||||
InvalidPromoCode -> resourceReference(R.string.bitcoin_promo_invalid_code_title) to
|
||||
resourceReference(R.string.bitcoin_promo_invalid_code)
|
||||
NoBitcoinAddress -> resourceReference(R.string.bitcoin_promo_no_address_title) to
|
||||
resourceReference(R.string.bitcoin_promo_no_address)
|
||||
PromoCodeAlreadyUsed -> resourceReference(R.string.bitcoin_promo_already_activated_title) to
|
||||
resourceReference(R.string.bitcoin_promo_already_activated)
|
||||
Activated -> resourceReference(R.string.bitcoin_promo_activation_success_title) to
|
||||
resourceReference(R.string.bitcoin_promo_activation_success)
|
||||
}
|
||||
uiMessageSender.send(
|
||||
DialogMessage(
|
||||
title = title,
|
||||
message = message,
|
||||
dismissOnFirstAction = true,
|
||||
firstActionBuilder = {
|
||||
okAction { }
|
||||
},
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
@AssistedFactory
|
||||
interface Factory : PromoDeeplinkHandler.Factory {
|
||||
override fun create(
|
||||
coroutineScope: CoroutineScope,
|
||||
queryParams: Map<String, String>,
|
||||
): DefaultPromoDeeplinkHandler
|
||||
}
|
||||
|
||||
companion object {
|
||||
private const val LOG_TAG = "PromoCodeActivation"
|
||||
private const val DEFAULT_MESSAGE_SENDER_DELAY = 500L
|
||||
}
|
||||
}
|
||||
|
|
@ -1,7 +1,9 @@
|
|||
package com.tangem.feature.wallet.deeplink.di
|
||||
|
||||
import com.tangem.feature.wallet.deeplink.DefaultPromoDeeplinkHandler
|
||||
import com.tangem.feature.wallet.deeplink.DefaultWalletDeepLinkActionTrigger
|
||||
import com.tangem.feature.wallet.deeplink.DefaultWalletDeepLinkHandler
|
||||
import com.tangem.features.wallet.deeplink.PromoDeeplinkHandler
|
||||
import com.tangem.features.wallet.deeplink.WalletDeepLinkActionListener
|
||||
import com.tangem.features.wallet.deeplink.WalletDeepLinkActionTrigger
|
||||
import com.tangem.features.wallet.deeplink.WalletDeepLinkHandler
|
||||
|
|
@ -19,6 +21,10 @@ internal interface WalletDeepLinkModule {
|
|||
@Singleton
|
||||
fun bindWalletDeepLinkHandlerFactory(impl: DefaultWalletDeepLinkHandler.Factory): WalletDeepLinkHandler.Factory
|
||||
|
||||
@Binds
|
||||
@Singleton
|
||||
fun bindPromoDeepLinkHandlerFactory(impl: DefaultPromoDeeplinkHandler.Factory): PromoDeeplinkHandler.Factory
|
||||
|
||||
@Binds
|
||||
@Singleton
|
||||
fun bindWalletDeepLinkActionTrigger(impl: DefaultWalletDeepLinkActionTrigger): WalletDeepLinkActionTrigger
|
||||
|
|
|
|||
|
|
@ -0,0 +1,331 @@
|
|||
package com.tangem.feature.wallet.presentation.wallet.deeplink
|
||||
|
||||
import arrow.core.Either
|
||||
import com.google.common.truth.Truth
|
||||
import com.tangem.blockchain.common.Blockchain
|
||||
import com.tangem.common.routing.deeplink.DeeplinkConst
|
||||
import com.tangem.core.decompose.ui.UiMessage
|
||||
import com.tangem.core.decompose.ui.UiMessageSender
|
||||
import com.tangem.core.ui.extensions.TextReference
|
||||
import com.tangem.core.ui.extensions.resourceReference
|
||||
import com.tangem.core.ui.message.DialogMessage
|
||||
import com.tangem.domain.models.currency.CryptoCurrency
|
||||
import com.tangem.domain.models.network.Network
|
||||
import com.tangem.domain.models.network.NetworkAddress
|
||||
import com.tangem.domain.models.wallet.UserWallet
|
||||
import com.tangem.domain.models.wallet.UserWalletId
|
||||
import com.tangem.domain.tokens.GetMultiCryptoCurrencyStatusUseCase
|
||||
import com.tangem.domain.tokens.error.TokenListError
|
||||
import com.tangem.domain.tokens.model.CryptoCurrencyStatus
|
||||
import com.tangem.domain.wallets.models.GetUserWalletError
|
||||
import com.tangem.domain.wallets.models.errors.ActivatePromoCodeError
|
||||
import com.tangem.domain.wallets.usecase.ActivateBitcoinPromocodeUseCase
|
||||
import com.tangem.domain.wallets.usecase.GetSelectedWalletSyncUseCase
|
||||
import com.tangem.feature.wallet.deeplink.DefaultPromoDeeplinkHandler
|
||||
import com.tangem.feature.wallet.impl.R
|
||||
import io.mockk.CapturingSlot
|
||||
import io.mockk.MockKAnnotations
|
||||
import io.mockk.coEvery
|
||||
import io.mockk.every
|
||||
import io.mockk.impl.annotations.MockK
|
||||
import io.mockk.just
|
||||
import io.mockk.mockk
|
||||
import io.mockk.runs
|
||||
import io.mockk.slot
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||
import kotlinx.coroutines.cancel
|
||||
import kotlinx.coroutines.test.StandardTestDispatcher
|
||||
import kotlinx.coroutines.test.TestDispatcher
|
||||
import kotlinx.coroutines.test.TestScope
|
||||
import kotlinx.coroutines.test.advanceUntilIdle
|
||||
import kotlinx.coroutines.test.resetMain
|
||||
import kotlinx.coroutines.test.runTest
|
||||
import kotlinx.coroutines.test.setMain
|
||||
import org.junit.After
|
||||
import org.junit.Before
|
||||
import org.junit.Test
|
||||
import timber.log.Timber
|
||||
import java.math.BigDecimal
|
||||
|
||||
class DefaultPromoDeeplinkHandlerTest {
|
||||
|
||||
@MockK(relaxed = true)
|
||||
private lateinit var uiMessageSender: UiMessageSender
|
||||
|
||||
@MockK
|
||||
private lateinit var getMultiCryptoCurrencyStatusUseCase: GetMultiCryptoCurrencyStatusUseCase
|
||||
|
||||
@MockK
|
||||
private lateinit var activateBitcoinPromocodeUseCase: ActivateBitcoinPromocodeUseCase
|
||||
|
||||
@MockK
|
||||
private lateinit var getSelectedWalletSyncUseCase: GetSelectedWalletSyncUseCase
|
||||
|
||||
private lateinit var messageSlot: CapturingSlot<UiMessage>
|
||||
private lateinit var testDispatcher: TestDispatcher
|
||||
private lateinit var testScope: TestScope
|
||||
|
||||
@OptIn(ExperimentalCoroutinesApi::class)
|
||||
@Before
|
||||
fun setUp() {
|
||||
testDispatcher = StandardTestDispatcher()
|
||||
testScope = TestScope(testDispatcher)
|
||||
|
||||
Dispatchers.setMain(testDispatcher)
|
||||
|
||||
MockKAnnotations.init(this)
|
||||
messageSlot = slot()
|
||||
every { uiMessageSender.send(capture(messageSlot)) } just runs
|
||||
|
||||
Timber.uprootAll() // Disable Timber logging for tests
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalCoroutinesApi::class)
|
||||
@After
|
||||
fun tearDown() {
|
||||
Dispatchers.resetMain()
|
||||
testScope.cancel()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `GIVEN empty promo code WHEN init THEN invalid promo code dialog is shown`() = runTest {
|
||||
val queryParams = emptyMap<String, String>()
|
||||
|
||||
DefaultPromoDeeplinkHandler(
|
||||
scope = this,
|
||||
queryParams = queryParams,
|
||||
uiMessageSender = uiMessageSender,
|
||||
getMultiCryptoCurrencyStatusUseCase = getMultiCryptoCurrencyStatusUseCase,
|
||||
activateBitcoinPromocodeUseCase = activateBitcoinPromocodeUseCase,
|
||||
getSelectedWalletSyncUseCase = getSelectedWalletSyncUseCase,
|
||||
)
|
||||
|
||||
val sent = messageSlot.captured as DialogMessage
|
||||
Truth.assertThat(sent.title).isEqualTo(resourceReference(R.string.bitcoin_promo_invalid_code_title))
|
||||
Truth.assertThat(sent.message).isEqualTo(resourceReference(R.string.bitcoin_promo_invalid_code))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `GIVEN wallet fetch error WHEN getSelectedWalletUseCase THEN failed dialog is shown`() = runTest {
|
||||
val queryParams = mapOf(DeeplinkConst.PROMO_CODE_KEY to "PROMO123")
|
||||
every { getSelectedWalletSyncUseCase.invoke() } returns Either.Left(GetUserWalletError.UserWalletNotFound)
|
||||
|
||||
DefaultPromoDeeplinkHandler(
|
||||
scope = this,
|
||||
queryParams = queryParams,
|
||||
uiMessageSender = uiMessageSender,
|
||||
getMultiCryptoCurrencyStatusUseCase = getMultiCryptoCurrencyStatusUseCase,
|
||||
activateBitcoinPromocodeUseCase = activateBitcoinPromocodeUseCase,
|
||||
getSelectedWalletSyncUseCase = getSelectedWalletSyncUseCase,
|
||||
)
|
||||
|
||||
val sent = messageSlot.captured as DialogMessage
|
||||
Truth.assertThat(sent.title).isEqualTo(resourceReference(R.string.bitcoin_promo_activation_error_title))
|
||||
Truth.assertThat(sent.message).isEqualTo(resourceReference(R.string.bitcoin_promo_activation_error))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `GIVEN token status error WHEN getWalletCurrenciesScreen THEN failed dialog is shown`() = runTest {
|
||||
val queryParams = mapOf(DeeplinkConst.PROMO_CODE_KEY to "PROMO123")
|
||||
val userWallet = mockUserWallet("ABCDEF")
|
||||
every { getSelectedWalletSyncUseCase.invoke() } returns Either.Right(userWallet)
|
||||
coEvery { getMultiCryptoCurrencyStatusUseCase.invokeMultiWalletSync(userWallet.walletId) } returns Either.Left(
|
||||
TokenListError.DataError(Exception("boom")),
|
||||
)
|
||||
|
||||
DefaultPromoDeeplinkHandler(
|
||||
scope = this,
|
||||
queryParams = queryParams,
|
||||
uiMessageSender = uiMessageSender,
|
||||
getMultiCryptoCurrencyStatusUseCase = getMultiCryptoCurrencyStatusUseCase,
|
||||
activateBitcoinPromocodeUseCase = activateBitcoinPromocodeUseCase,
|
||||
getSelectedWalletSyncUseCase = getSelectedWalletSyncUseCase,
|
||||
)
|
||||
|
||||
advanceUntilIdle()
|
||||
|
||||
val sent = messageSlot.captured as DialogMessage
|
||||
Truth.assertThat(sent.title).isEqualTo(resourceReference(R.string.bitcoin_promo_activation_error_title))
|
||||
Truth.assertThat(sent.message).isEqualTo(resourceReference(R.string.bitcoin_promo_activation_error))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `GIVEN no bitcoin address WHEN getWalletCurrenciesScreen THEN no bitcoin address dialog is shown`() = runTest {
|
||||
val queryParams = mapOf(DeeplinkConst.PROMO_CODE_KEY to "PROMO123")
|
||||
val userWallet = mockUserWallet("ABCDEF")
|
||||
every { getSelectedWalletSyncUseCase.invoke() } returns Either.Right(userWallet)
|
||||
// Return only ETH status so BTC is not found
|
||||
val ethStatus = buildStatusForNetwork(rawNetworkId = "ethereum", address = "0x123")
|
||||
coEvery { getMultiCryptoCurrencyStatusUseCase.invokeMultiWalletSync(userWallet.walletId) } returns Either.Right(
|
||||
listOf(ethStatus),
|
||||
)
|
||||
|
||||
DefaultPromoDeeplinkHandler(
|
||||
scope = this,
|
||||
queryParams = queryParams,
|
||||
uiMessageSender = uiMessageSender,
|
||||
getMultiCryptoCurrencyStatusUseCase = getMultiCryptoCurrencyStatusUseCase,
|
||||
activateBitcoinPromocodeUseCase = activateBitcoinPromocodeUseCase,
|
||||
getSelectedWalletSyncUseCase = getSelectedWalletSyncUseCase,
|
||||
)
|
||||
|
||||
advanceUntilIdle()
|
||||
|
||||
val sent = messageSlot.captured as DialogMessage
|
||||
Truth.assertThat(sent.title).isEqualTo(resourceReference(R.string.bitcoin_promo_no_address_title))
|
||||
Truth.assertThat(sent.message).isEqualTo(resourceReference(R.string.bitcoin_promo_no_address))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `GIVEN activation success WHEN activatePromoCode THEN activated dialog is shown`() = runTest {
|
||||
val promoCode = "PROMO123"
|
||||
val queryParams = mapOf(DeeplinkConst.PROMO_CODE_KEY to promoCode)
|
||||
val userWallet = mockUserWallet("ABCDEF")
|
||||
every { getSelectedWalletSyncUseCase.invoke() } returns Either.Right(userWallet)
|
||||
val btcStatus = buildStatusForNetwork(rawNetworkId = Blockchain.Bitcoin.id, address = "bc1qxyz")
|
||||
coEvery { getMultiCryptoCurrencyStatusUseCase.invokeMultiWalletSync(userWallet.walletId) } returns Either.Right(
|
||||
listOf(btcStatus),
|
||||
)
|
||||
coEvery { activateBitcoinPromocodeUseCase.invoke("bc1qxyz", promoCode) } returns Either.Right("ok")
|
||||
|
||||
DefaultPromoDeeplinkHandler(
|
||||
scope = this,
|
||||
queryParams = queryParams,
|
||||
uiMessageSender = uiMessageSender,
|
||||
getMultiCryptoCurrencyStatusUseCase = getMultiCryptoCurrencyStatusUseCase,
|
||||
activateBitcoinPromocodeUseCase = activateBitcoinPromocodeUseCase,
|
||||
getSelectedWalletSyncUseCase = getSelectedWalletSyncUseCase,
|
||||
)
|
||||
|
||||
advanceUntilIdle()
|
||||
|
||||
val sent = messageSlot.captured as DialogMessage
|
||||
Truth.assertThat(sent.title).isEqualTo(resourceReference(R.string.bitcoin_promo_activation_success_title))
|
||||
Truth.assertThat(sent.message).isEqualTo(resourceReference(R.string.bitcoin_promo_activation_success))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `GIVEN activation invalid code WHEN activatePromoCode THEN invalid promo code dialog is shown`() = runTest {
|
||||
runActivationErrorCase(
|
||||
error = ActivatePromoCodeError.InvalidPromoCode,
|
||||
expectedTitle = resourceReference(R.string.bitcoin_promo_invalid_code_title),
|
||||
expectedMessage = resourceReference(R.string.bitcoin_promo_invalid_code),
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `GIVEN activation failed WHEN activatePromoCode THEN failed dialog is shown`() = runTest {
|
||||
runActivationErrorCase(
|
||||
error = ActivatePromoCodeError.ActivationFailed,
|
||||
expectedTitle = resourceReference(R.string.bitcoin_promo_activation_error_title),
|
||||
expectedMessage = resourceReference(R.string.bitcoin_promo_activation_error),
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `GIVEN activation no address WHEN activatePromoCode THEN no bitcoin address dialog is shown`() = runTest {
|
||||
runActivationErrorCase(
|
||||
error = ActivatePromoCodeError.NoBitcoinAddress,
|
||||
expectedTitle = resourceReference(R.string.bitcoin_promo_no_address_title),
|
||||
expectedMessage = resourceReference(R.string.bitcoin_promo_no_address),
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `GIVEN activation already used WHEN activatePromoCode THEN already activated dialog is shown`() = runTest {
|
||||
runActivationErrorCase(
|
||||
error = ActivatePromoCodeError.PromocodeAlreadyUsed,
|
||||
expectedTitle = resourceReference(R.string.bitcoin_promo_already_activated_title),
|
||||
expectedMessage = resourceReference(R.string.bitcoin_promo_already_activated),
|
||||
)
|
||||
}
|
||||
|
||||
private fun runActivationErrorCase(
|
||||
error: ActivatePromoCodeError,
|
||||
expectedTitle: TextReference,
|
||||
expectedMessage: TextReference,
|
||||
) = runTest {
|
||||
val promoCode = "PROMO123"
|
||||
val queryParams = mapOf(DeeplinkConst.PROMO_CODE_KEY to promoCode)
|
||||
val userWallet = mockUserWallet("ABCDEF")
|
||||
every { getSelectedWalletSyncUseCase.invoke() } returns Either.Right(userWallet)
|
||||
val btcStatus = buildStatusForNetwork(rawNetworkId = Blockchain.Bitcoin.id, address = "bc1qxyz")
|
||||
coEvery { getMultiCryptoCurrencyStatusUseCase.invokeMultiWalletSync(userWallet.walletId) } returns Either.Right(
|
||||
listOf(btcStatus),
|
||||
)
|
||||
coEvery { activateBitcoinPromocodeUseCase.invoke("bc1qxyz", promoCode) } returns Either.Left(error)
|
||||
|
||||
DefaultPromoDeeplinkHandler(
|
||||
scope = this,
|
||||
queryParams = queryParams,
|
||||
uiMessageSender = uiMessageSender,
|
||||
getMultiCryptoCurrencyStatusUseCase = getMultiCryptoCurrencyStatusUseCase,
|
||||
activateBitcoinPromocodeUseCase = activateBitcoinPromocodeUseCase,
|
||||
getSelectedWalletSyncUseCase = getSelectedWalletSyncUseCase,
|
||||
)
|
||||
|
||||
advanceUntilIdle()
|
||||
|
||||
val sent = messageSlot.captured as DialogMessage
|
||||
Truth.assertThat(sent.title).isEqualTo(expectedTitle)
|
||||
Truth.assertThat(sent.message).isEqualTo(expectedMessage)
|
||||
}
|
||||
|
||||
private fun mockUserWallet(id: String): UserWallet {
|
||||
val userWallet = mockk<UserWallet>(relaxed = true)
|
||||
every { userWallet.walletId } returns UserWalletId(id)
|
||||
return userWallet
|
||||
}
|
||||
|
||||
private fun buildStatusForNetwork(rawNetworkId: String, address: String): CryptoCurrencyStatus {
|
||||
val networkId = Network.ID(Network.RawID(rawNetworkId), Network.DerivationPath.None)
|
||||
val network = Network(
|
||||
id = networkId,
|
||||
backendId = rawNetworkId,
|
||||
name = rawNetworkId,
|
||||
currencySymbol = rawNetworkId.take(3).uppercase(),
|
||||
derivationPath = Network.DerivationPath.None,
|
||||
isTestnet = false,
|
||||
standardType = Network.StandardType.Unspecified("UNSPECIFIED"),
|
||||
hasFiatFeeRate = false,
|
||||
canHandleTokens = false,
|
||||
transactionExtrasType = Network.TransactionExtrasType.NONE,
|
||||
)
|
||||
|
||||
val currencyId = CryptoCurrency.ID(
|
||||
prefix = CryptoCurrency.ID.Prefix.TOKEN_PREFIX,
|
||||
body = CryptoCurrency.ID.Body.NetworkId(rawNetworkId),
|
||||
suffix = CryptoCurrency.ID.Suffix.RawID(rawNetworkId),
|
||||
)
|
||||
|
||||
val coin = CryptoCurrency.Coin(
|
||||
id = currencyId,
|
||||
network = network,
|
||||
name = rawNetworkId,
|
||||
symbol = network.currencySymbol,
|
||||
decimals = 8,
|
||||
iconUrl = null,
|
||||
isCustom = false,
|
||||
)
|
||||
|
||||
val value = CryptoCurrencyStatus.NoQuote(
|
||||
amount = BigDecimal.ZERO,
|
||||
yieldBalance = null,
|
||||
hasCurrentNetworkTransactions = false,
|
||||
pendingTransactions = emptySet(),
|
||||
networkAddress = NetworkAddress.Single(
|
||||
defaultAddress = NetworkAddress.Address(
|
||||
value = address,
|
||||
type = NetworkAddress.Address.Type.Primary,
|
||||
),
|
||||
),
|
||||
sources = CryptoCurrencyStatus.Sources(),
|
||||
)
|
||||
|
||||
return CryptoCurrencyStatus(
|
||||
currency = coin,
|
||||
value = value,
|
||||
)
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue