Updated on 2026-08-14

This commit is contained in:
Tangem 2023-06-28 17:42:30 +03:00
commit 15ee3075f2
59 changed files with 1115 additions and 523 deletions

View file

@ -138,7 +138,9 @@
android:name="com.tangem.tap.features.sprinklr.ui.SprinklrActivity"
android:theme="@style/AppTheme" />
<activity android:name="com.tangem.feature.learn2earn.presentation.Learn2earnWebViewActivity"/>
<activity
android:name="com.tangem.feature.learn2earn.presentation.webView.Learn2earnWebViewActivity"
android:theme="@style/Theme.MaterialComponents.Light.NoActionBar" />
<provider
android:name="androidx.core.content.FileProvider"

View file

@ -145,7 +145,7 @@ class TapApplication : Application(), ImageLoaderFactory {
lateinit var walletConnectSessionsRepository: WalletConnectSessionsRepository
@Inject
lateinit var learn2ernInteractor: Learn2earnInteractor
lateinit var learn2earnInteractor: Learn2earnInteractor
override fun onCreate() {
super.onCreate()
@ -204,7 +204,7 @@ class TapApplication : Application(), ImageLoaderFactory {
// [REDACTED_JIRA]
runBlocking {
featureTogglesManager.init()
learn2ernInteractor.init()
learn2earnInteractor.init()
}
initTopUpController()

View file

@ -12,7 +12,7 @@ import androidx.compose.ui.platform.ComposeView
import androidx.core.view.WindowCompat
import androidx.core.view.WindowInsetsControllerCompat
import androidx.fragment.app.Fragment
import androidx.fragment.app.viewModels
import androidx.fragment.app.activityViewModels
import com.tangem.core.analytics.Analytics
import com.tangem.core.ui.res.TangemTheme
import com.tangem.feature.learn2earn.presentation.Learn2earnViewModel
@ -33,13 +33,13 @@ class HomeFragment : Fragment(), StoreSubscriber<HomeState> {
private var homeState: MutableState<HomeState> = mutableStateOf(store.state.homeState)
private val learn2earnViewModel by viewModels<Learn2earnViewModel>()
private val learn2earnViewModel by activityViewModels<Learn2earnViewModel>()
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
store.dispatch(HomeAction.OnCreate)
store.dispatch(HomeAction.Init)
if (learn2earnViewModel.isNeedToShowViewOnStoriesScreen()) {
if (learn2earnViewModel.uiState.storyScreenState.isVisible) {
store.dispatch(HomeAction.InsertStory(position = 0, Stories.OneInchPromo))
// re init homeState after inserting learn2earn story
homeState = mutableStateOf(store.state.homeState)
@ -90,10 +90,8 @@ class HomeFragment : Fragment(), StoreSubscriber<HomeState> {
@Composable
private fun ScreenContent() {
StoriesScreen(
homeState,
onLearn2earnClick = {
learn2earnViewModel.onStoriesClick()
},
homeState = homeState,
onLearn2earnClick = learn2earnViewModel.uiState.storyScreenState.onClick,
onScanButtonClick = {
Analytics.send(IntroductionProcess.ButtonScanCard())
store.dispatch(HomeAction.ReadCard())

View file

@ -26,7 +26,7 @@ import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import com.google.accompanist.systemuicontroller.rememberSystemUiController
import com.tangem.core.ui.res.TangemTheme
import com.tangem.feature.learn2earn.presentation.ui.StoriesScreen
import com.tangem.feature.learn2earn.presentation.ui.Learn2earnStoriesScreen
import com.tangem.tap.features.home.compose.content.*
import com.tangem.tap.features.home.compose.views.HomeButtons
import com.tangem.tap.features.home.compose.views.StoriesProgressBar
@ -54,8 +54,11 @@ fun StoriesScreen(
currentStory = state.stories[max(0, currentStep() - 1)]
}
val goToNextScreen = {
currentStory =
if (currentStep() < state.stories.lastIndex) state.stories[currentStep() + 1] else state.firstStory
currentStory = if (currentStep() < state.stories.lastIndex) {
state.stories[currentStep() + 1]
} else {
state.firstStory
}
}
val isPressed = remember { mutableStateOf(false) }
@ -153,7 +156,7 @@ fun StoriesScreen(
colorFilter = if (currentStory.isDarkBackground) null else ColorFilter.tint(Color.Black),
)
when (currentStory) {
Stories.OneInchPromo -> StoriesScreen(onLearn2earnClick)
Stories.OneInchPromo -> Learn2earnStoriesScreen(onLearn2earnClick)
Stories.TangemIntro -> FirstStoriesContent(isPaused, currentStory.duration) { hideContent.value = it }
Stories.RevolutionaryWallet -> StoriesRevolutionaryWallet(currentStory.duration)
Stories.UltraSecureBackup -> StoriesUltraSecureBackup(isPaused, currentStory.duration)

View file

@ -8,7 +8,9 @@ import android.view.View
import androidx.activity.OnBackPressedCallback
import androidx.appcompat.app.AppCompatActivity
import androidx.compose.runtime.mutableStateOf
import androidx.compose.ui.platform.ViewCompositionStrategy
import androidx.fragment.app.Fragment
import androidx.fragment.app.activityViewModels
import androidx.fragment.app.viewModels
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.flowWithLifecycle
@ -22,12 +24,16 @@ import coil.size.Scale
import com.badoo.mvicore.modelWatcher
import com.tangem.core.analytics.Analytics
import com.tangem.core.ui.fragments.setStatusBarColor
import com.tangem.core.ui.res.TangemTheme
import com.tangem.core.ui.utils.OneTouchClickListener
import com.tangem.datasource.connection.NetworkConnectionManager
import com.tangem.feature.learn2earn.presentation.Learn2earnViewModel
import com.tangem.feature.learn2earn.presentation.ui.Learn2earnMainPageScreen
import com.tangem.feature.swap.api.SwapFeatureToggleManager
import com.tangem.feature.swap.domain.SwapInteractor
import com.tangem.tap.MainActivity
import com.tangem.tap.common.analytics.events.Portfolio
import com.tangem.tap.common.extensions.beginDelayedTransition
import com.tangem.tap.common.extensions.show
import com.tangem.tap.common.recyclerView.SpaceItemDecoration
import com.tangem.tap.common.redux.global.GlobalAction
@ -73,6 +79,7 @@ class WalletFragment : Fragment(R.layout.fragment_wallet), SafeStoreSubscriber<W
private var walletView: WalletView = MultiWalletView()
private val learn2earnViewModel by activityViewModels<Learn2earnViewModel>()
private val viewModel by viewModels<WalletViewModel>()
private val totalBalanceWatcher = modelWatcher {
@ -102,6 +109,7 @@ class WalletFragment : Fragment(R.layout.fragment_wallet), SafeStoreSubscriber<W
val inflater = TransitionInflater.from(requireContext())
enterTransition = inflater.inflateTransition(R.transition.slide_right)
exitTransition = inflater.inflateTransition(R.transition.fade)
learn2earnViewModel.onMainScreenCreated()
}
override fun onStart() {
@ -199,6 +207,27 @@ class WalletFragment : Fragment(R.layout.fragment_wallet), SafeStoreSubscriber<W
binding.toolbar.setNavigationIcon(
if (state.canSaveUserWallets) R.drawable.ic_wallet_24 else R.drawable.ic_tap_card_24,
)
showLearn2earnView()
}
private fun showLearn2earnView() {
val isShowing = learn2earnViewModel.uiState.mainScreenState.isVisible
if (!isShowing) return
binding.composeLearnToEarnContainer.show(true) { binding.llWarnings.beginDelayedTransition() }
binding.composeLearnToEarnContainer.apply {
setViewCompositionStrategy(
strategy = ViewCompositionStrategy.DisposeOnLifecycleDestroyed(
lifecycle = this@WalletFragment.lifecycle,
),
)
setContent {
TangemTheme {
Learn2earnMainPageScreen(learn2earnViewModel.uiState)
}
}
}
}
private fun setupPullToRefreshLayout(state: WalletState) {

View file

@ -1,12 +0,0 @@
package com.tangem.tap.network.auth
import com.tangem.lib.auth.BasicAuthProvider
class TangemBasicAuthProvider(private val credentials: String?) : BasicAuthProvider {
override fun getCredentials(): Map<String, String> = if (credentials == null) {
mapOf()
} else {
mapOf("Authorization" to "Basic $credentials")
}
}

View file

@ -1,10 +1,7 @@
package com.tangem.tap.network.auth.di
import com.tangem.datasource.config.ConfigManager
import com.tangem.lib.auth.AuthProvider
import com.tangem.lib.auth.BasicAuthProvider
import com.tangem.tap.network.auth.AuthProviderImpl
import com.tangem.tap.network.auth.TangemBasicAuthProvider
import com.tangem.tap.proxy.AppStateHolder
import dagger.Module
import dagger.Provides
@ -21,10 +18,4 @@ class AuthModule {
fun provideAuthProvider(appStateHolder: AppStateHolder): AuthProvider {
return AuthProviderImpl(appStateHolder)
}
@Provides
@Singleton
fun provideTangemBasicAuthProvider(configManager: ConfigManager): BasicAuthProvider {
return TangemBasicAuthProvider(configManager.config.tangemComAuthorization)
}
}

View file

@ -1,6 +1,8 @@
package com.tangem.tap.proxy.di
import androidx.compose.ui.text.intl.Locale
import com.tangem.core.analytics.api.AnalyticsEventHandler
import com.tangem.feature.learn2earn.domain.api.Learn2earnDependencyProvider
import com.tangem.lib.crypto.DerivationManager
import com.tangem.lib.crypto.TransactionManager
import com.tangem.lib.crypto.UserWalletManager
@ -48,4 +50,20 @@ class ProxyModule {
appStateHolder = appStateHolder,
)
}
// regions FeatureConsumers
@Provides
@Singleton
fun provideLear2earnDependencies(appStateHolder: AppStateHolder): Learn2earnDependencyProvider {
return object : Learn2earnDependencyProvider {
override fun getUserCountryCodeProvider(): () -> String = {
appStateHolder.mainStore?.state?.globalState?.userCountryCode ?: Locale.current.language
}
override fun getWebViewAuthCredentialsProvider(): () -> String? = {
appStateHolder.mainStore?.state?.globalState?.configManager?.config?.tangemComAuthorization
}
}
}
// endregion FeatureConsumers
}

View file

@ -23,7 +23,7 @@
android:focusable="true"
android:gravity="center_vertical"
android:padding="16dp"
android:text="@string/wallet_button_buy"
android:text="@string/common_buy"
android:textColor="@color/darkGray3"
android:textSize="14sp"
android:textStyle="bold"
@ -38,7 +38,7 @@
android:focusable="true"
android:gravity="center_vertical"
android:padding="16dp"
android:text="@string/wallet_button_sell"
android:text="@string/common_sell"
android:textColor="@color/darkGray3"
android:textSize="14sp"
android:textStyle="bold"

View file

@ -22,7 +22,7 @@
android:layout_width="match_parent"
android:layout_height="?attr/actionBarSize"
app:navigationIcon="@drawable/ic_baseline_arrow_back_24"
app:title="@string/send_title" />
app:title="@string/common_send" />
</com.google.android.material.appbar.AppBarLayout>
@ -115,7 +115,7 @@
style="@style/TapPrimaryIconButton"
android:layout_width="match_parent"
android:fontFamily="@font/saira_semi_condensed_regular"
android:text="@string/send_title"
android:text="@string/common_send"
app:icon="@drawable/ic_arrow_right" />
<ProgressBar

View file

@ -113,14 +113,11 @@
android:paddingTop="8dp"
app:layout_constraintTop_toBottomOf="@id/rv_warning_messages">
<FrameLayout
android:id="@+id/fl_learn_to_earn_container"
<androidx.compose.ui.platform.ComposeView
android:id="@+id/compose_learn_to_earn_container"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginStart="16dp"
android:layout_marginTop="8dp"
android:layout_marginEnd="16dp"
android:layout_marginBottom="8dp" />
android:visibility="gone" />
<include
android:id="@+id/l_wallet_rescan_warning"

View file

@ -25,14 +25,14 @@
<com.google.android.material.button.MaterialButton
android:id="@+id/btn_buy"
style="@style/TapPrimaryIconButton"
android:text="@string/wallet_button_buy"
android:text="@string/common_buy"
android:visibility="gone"
app:icon="@drawable/ic_arrow_up" />
<com.google.android.material.button.MaterialButton
android:id="@+id/btn_sell"
style="@style/TapPrimaryIconButton"
android:text="@string/wallet_button_sell"
android:text="@string/common_sell"
android:visibility="gone"
app:icon="@drawable/ic_arrow_down" />
@ -51,7 +51,7 @@
style="@style/TapPrimaryIconButton"
android:layout_width="0dp"
android:layout_weight="1"
android:text="@string/wallet_button_send"
android:text="@string/common_send"
app:icon="@drawable/ic_send" />
</merge>

View file

@ -24,6 +24,6 @@ interface PromotionApi {
@POST("promotion/validate")
suspend fun validate(@Body request: ValidateRequestBody): ValidateResponse
@GET("promotion/award")
@POST("promotion/award")
suspend fun requestAward(@Body request: AwardRequestBody): AwardResponse
}

View file

@ -13,6 +13,6 @@ abstract class AbstractPromotionResponse {
data class Error(
@Json(name = "code") val code: Int,
@Json(name = "description") val description: String,
@Json(name = "message") val message: String,
)
}

View file

@ -7,5 +7,6 @@ import com.squareup.moshi.Json
*/
data class AwardRequestBody(
@Json(name = "walletId") val walletId: String,
@Json(name = "address") val address: String,
@Json(name = "programName") val programName: String,
)

View file

@ -7,9 +7,9 @@ import com.squareup.moshi.Json
*/
data class PromotionInfoResponse(
@Json(name = "status") val status: Status?,
@Json(name = "awardForNewCard") val awardForNewCard: Float?,
@Json(name = "awardForOldCard") val awardForOldCard: Float?,
@Json(name = "awardPaymentToken") val awardPaymentToken: TokenInfo?,
@Json(name = "awardForNewCard") val awardForNewCard: Double?,
@Json(name = "awardForOldCard") val awardForOldCard: Double?,
@Json(name = "awardPaymentToken") val awardPaymentToken: TokenData?,
@Json(name = "error") override val error: Error? = null,
) : AbstractPromotionResponse() {
@ -24,18 +24,13 @@ data class PromotionInfoResponse(
FINISHED("finished"),
}
data class TokenInfo(
data class TokenData(
@Json(name = "id") val id: String,
@Json(name = "name") val name: String,
@Json(name = "symbol") val symbol: String,
@Json(name = "active") val active: Boolean,
@Json(name = "networks") val networks: List<Network>,
) {
data class Network(
@Json(name = "networkId") val networkId: String,
@Json(name = "exchangeable") val exchangeable: Boolean,
@Json(name = "contractAddress") val contractAddress: String,
@Json(name = "decimalCount") val decimalCount: Int,
)
}
@Json(name = "networkId") val networkId: String,
@Json(name = "contractAddress") val contractAddress: String,
@Json(name = "decimalCount") val decimalCount: Int,
)
}

View file

@ -8,6 +8,7 @@ import com.tangem.datasource.utils.RequestHeader.*
import com.tangem.datasource.utils.addHeaders
import com.tangem.datasource.utils.allowLogging
import com.tangem.lib.auth.AuthProvider
import com.tangem.lib.auth.BuildConfig
import dagger.Module
import dagger.Provides
import dagger.hilt.InstallIn
@ -77,7 +78,7 @@ class NetworkModule {
private fun createBasePromotionRetrofit(okHttpClient: OkHttpClient, moshi: Moshi): PromotionApi {
return Retrofit.Builder()
.addConverterFactory(MoshiConverterFactory.create(moshi))
.baseUrl(PROMOTION_BASE_URL)
.baseUrl(if (BuildConfig.DEBUG) DEV_TANGEM_TECH_BASE_URL else PROD_TANGEM_TECH_BASE_URL)
.client(okHttpClient)
.build()
.create(PromotionApi::class.java)
@ -87,8 +88,6 @@ class NetworkModule {
const val PROD_TANGEM_TECH_BASE_URL = "https://api.tangem-tech.com/v1/"
const val DEV_TANGEM_TECH_BASE_URL = "https://devapi.tangem-tech.com/v1/"
const val PROMOTION_BASE_URL = "https://tangem.com/v1/"
const val PAYMENTOLOGY_BASE_URL: String = "https://paymentologygate.oa.r.appspot.com/"
const val API_ONE_INCH_TIMEOUT_MS = 5000L
}

View file

@ -13,7 +13,7 @@ internal fun OkHttpClient.Builder.addHeaders(vararg requestHeaders: RequestHeade
val request = chain.request().newBuilder().apply {
requestHeaders
.flatMap(RequestHeader::values)
.forEach { addHeader(it.first, it.second) }
.forEach { addHeader(it.first, it.second.invoke()) }
}.build()
chain.proceed(request)

View file

@ -7,15 +7,15 @@ import com.tangem.lib.auth.AuthProvider
*
* @param pairs header name and header value pairs
*/
sealed class RequestHeader(vararg pairs: Pair<String, String>) {
sealed class RequestHeader(vararg pairs: Pair<String, () -> String>) {
/** Header list */
val values: List<Pair<String, String>> = pairs.toList()
val values: List<Pair<String, () -> String>> = pairs.toList()
object CacheControlHeader : RequestHeader("Cache-Control" to "max-age=600")
object CacheControlHeader : RequestHeader("Cache-Control" to { "max-age=600" })
class AuthenticationHeader(authProvider: AuthProvider) : RequestHeader(
"card_public_key" to authProvider.getCardPublicKey(),
"card_id" to authProvider.getCardId(),
"card_id" to { authProvider.getCardId() },
"card_public_key" to { authProvider.getCardPublicKey() },
)
}

View file

@ -39,7 +39,7 @@
<string name="card_settings_access_code_recovery_title">Восстановление кода доступа</string>
<string name="card_settings_change_access_code">Смена кода доступа</string>
<string name="card_settings_change_access_code_footer">Код доступа будет изменен только на данной карте</string>
<string name="card_settings_reset_card_to_factory">Сброс к заводским настройкам</string>
<string name="card_settings_reset_card_to_factory">Заводские настройки</string>
<string name="card_settings_security_mode">Тип безопасности</string>
<string name="card_settings_title">Настройки карты</string>
<string name="chat_bot_name">Tangem Bot</string>
@ -66,13 +66,17 @@
<string name="common_enable">Включить</string>
<string name="common_enabled">Включено</string>
<string name="common_exchange">Обменять</string>
<string name="common_explore_transaction_history">Посмотреть историю транзакций</string>
<string name="common_explorer">Обозреватель</string>
<string name="common_like">Нравится</string>
<string name="common_main_network">Основная сеть</string>
<string name="common_no">Нет</string>
<string name="common_no_data">Нет данных</string>
<string name="common_ok">Ок</string>
<string name="common_ok">OK</string>
<string name="common_origin_card">Основная карта</string>
<string name="common_receive">Получить</string>
<string name="common_reject">Отклонить</string>
<string name="common_reload">Перезагрузить</string>
<string name="common_save_changes">Сохранить изменения</string>
<string name="common_search">Искать</string>
<string name="common_seed_phrase">Секретная фраза</string>
@ -156,11 +160,22 @@
<string name="initial_message_tap_header">Приложите карту</string>
<string name="internal_error_wallet_manager_not_found">Внутренняя ошибка: не удается найти менеджер кошельков</string>
<string name="key_invalidated_warning_description">Вы обновили данные биометрии, отсканируйте свою карту для входа</string>
<string name="main_get_bonus_subtitle">Вы завершили обучение и можете получить свои токены 1inch</string>
<string name="main_get_bonus_title">Получите бонус</string>
<plurals name="main_learn_subtitle">
<item quantity="one">Пройдите обучение и получите %d токен 1inch на свой кошелек</item>
<item quantity="few">Пройдите обучение и получите %d токена 1inch на свой кошелек</item>
<item quantity="many">Пройдите обучение и получите %d токена 1inch на свой кошелек</item>
<item quantity="other">Пройдите обучение и получите %d токенов 1inch на свой кошелек</item>
</plurals>
<string name="main_learn_title">Бонус за обучение</string>
<string name="main_manage_tokens">Управление токенами</string>
<string name="main_no_backup_warning_subtitle">Чтобы защитить свои активы, мы советуем вам выполнить эту процедуру</string>
<string name="main_no_backup_warning_title">Бэкап кошелька не был произведен</string>
<string name="main_page_balance">Баланс</string>
<string name="main_processing_full_amount">В сумме учтены не все монеты</string>
<string name="main_promotion_credited">Токены 1inch будут зачислены на адрес вашего кошелька в течение 2 дней</string>
<string name="main_promotion_no_purchase">По вашему промокоду не было покупки кошелька, а значит вы не можете получить бонус. Купите кошелек Tangem, отсканируйте его в приложении и получите бонус.</string>
<string name="main_scan_card_warning_view_subtitle">Чтобы получить доступ ко всем сетям, вам необходимо отсканировать карту</string>
<string name="main_scan_card_warning_view_title">Отсканируйте карту</string>
<string name="onboarding_access_code_feature_1_description">Вам необходимо установить единый код доступа для защиты всех ваших карт</string>
@ -184,11 +199,8 @@
<string name="onboarding_button_claim">Запросить</string>
<string name="onboarding_button_continue_wallet">Перейти к моему кошельку</string>
<string name="onboarding_button_finalize_backup">Завершение бэкапа</string>
<string name="onboarding_button_kyc_start">Верифицировать (Utorg)</string>
<string name="onboarding_button_kyc_waiting">Обновить</string>
<string name="onboarding_button_pin">Установить Код доступа</string>
<string name="onboarding_button_receive_crypto">Получить криптовалюту</string>
<string name="onboarding_button_register_wallet">Зарегистрироваться</string>
<string name="onboarding_button_scan_origin_card">Сканировать основную карту</string>
<string name="onboarding_button_skip_backup">Пропустить</string>
<string name="onboarding_button_what_does_it_mean">Как это работает?</string>
@ -203,19 +215,9 @@
<string name="onboarding_exit_alert_message">В этом случае вам будет необходимо начать процесс заново.</string>
<string name="onboarding_exit_alert_title">Вы хотите выйти из процесса активации?</string>
<string name="onboarding_getting_started">Подготовка</string>
<string name="onboarding_linking_error_card_with_wallets">Другой кошелек уже был создан на карте, которую вы пытаетесь добавить. Хотите сбросить его и использовать карту для бэкапа?</string>
<string name="onboarding_navbar_kyc_progress">Подтвердите свою личность</string>
<string name="onboarding_navbar_kyc_start">Верификация клиента</string>
<string name="onboarding_navbar_pin">Код доступа</string>
<string name="onboarding_navbar_register_wallet">Подключиться</string>
<string name="onboarding_navbar_title_creating_backup">Резервная копия</string>
<string name="onboarding_saltpay_button_backup_origin">Приложите SaltPay карту</string>
<string name="onboarding_saltpay_subtitle_no_backup_cards">Для начала процесса бэкапа вам необходимо добавить Tangem карту</string>
<string name="onboarding_saltpay_subtitle_one_backup_card">Завершите процесс бэкапа создав код доступа</string>
<string name="onboarding_saltpay_title_backup_card">Приложите Tangem карту</string>
<string name="onboarding_saltpay_title_no_backup_card">Бэкап карта не добавлена</string>
<string name="onboarding_saltpay_title_one_backup_card">Бэкап карта создана</string>
<string name="onboarding_saltpay_title_prepare_origin">Приготовьте SaltPay карту</string>
<string name="onboarding_seed_button_read_more">Читать про секретную фразу</string>
<string name="onboarding_seed_generate_message">Запишите эти 12 слов в порядке, указанном ниже, и сохраните их в надежном месте.</string>
<string name="onboarding_seed_generate_title">Ваша секретная фраза</string>
@ -231,13 +233,8 @@
<string name="onboarding_seed_user_validation_title">Итак, проверим</string>
<string name="onboarding_subtitle_claim">Для начала работы просто запросите начисление wxDAI на свой кошелек</string>
<string name="onboarding_subtitle_claim_progress">Это займет несколько секунд</string>
<string name="onboarding_subtitle_kyc_retry">Более подробная информация отправлена на ваш адрес электронной почты.</string>
<string name="onboarding_subtitle_kyc_start">Для начала работы с картой вам необходимо завершить процесс подтверждения личности</string>
<string name="onboarding_subtitle_kyc_waiting">Пожалуйста дождитесь завершения процесса подтверждения личности. Вы будете уведомлены через e-mail. Обычно это занимает не более часа. Вы можете закрыть приложение и вернуться позже.</string>
<string name="onboarding_subtitle_no_backup_cards">Чтобы начать процесс резервного копирования, добавьте одну или две резервные карты.</string>
<string name="onboarding_subtitle_one_backup_card">Вы можете добавить еще одну карту или завершить процесс резервного копирования</string>
<string name="onboarding_subtitle_pin">Установите 4-х значный код.\nОн будет использован для платежей.</string>
<string name="onboarding_subtitle_register_wallet">Подключите вашу карту к децентрализованной платежной системе</string>
<string name="onboarding_subtitle_scan_backup_card_format">Подготовьте резервную карту с номером %s</string>
<string name="onboarding_subtitle_scan_origin_card">Подготовьте основную карту</string>
<string name="onboarding_subtitle_scan_primary_card_format">Подготовьте основную карту с номером %s</string>
@ -248,13 +245,9 @@
<string name="onboarding_title_backup_card_format">Резервная карта #%d</string>
<string name="onboarding_title_claim">Запросить %s</string>
<string name="onboarding_title_claim_progress">Запрашивается</string>
<string name="onboarding_title_kyc_retry">Что-то пошло не так</string>
<string name="onboarding_title_kyc_start">Подтвердите свою личность</string>
<string name="onboarding_title_kyc_waiting">Подтверждение личности в процессе</string>
<string name="onboarding_title_no_backup_cards">Нет резервных карт</string>
<string name="onboarding_title_one_backup_card">Добавлена ​​одна резервная карта</string>
<string name="onboarding_title_pin">Код доступа</string>
<string name="onboarding_title_register_wallet">Подключите свою карту</string>
<string name="onboarding_title_scan_origin_card">Подготовьте свою карту</string>
<string name="onboarding_title_two_backup_cards">Добавлены две резервные карты</string>
<string name="onboarding_top_up_body">Пополните кошелек на любую сумму, чтобы начать пользоваться картой</string>
@ -275,6 +268,9 @@
<string name="organize_tokens_group">Группировка</string>
<string name="organize_tokens_sort_by_balance">По балансу</string>
<string name="organize_tokens_title">Сортировка токенов</string>
<string name="organize_tokens_ungroup">Разгруппировать</string>
<string name="receive_bottom_sheet_title">%1$s %2$s адрес в сети %3$s</string>
<string name="receive_bottom_sheet_warning_message">%1$s (%2$s) в сети %3$s</string>
<string name="referral_button_participate">Участвовать</string>
<string name="referral_error_failed_to_load_info">Не удалось загрузить информацию по реферальной программе. Пожалуйста, попробуйте позже.</string>
<string name="referral_error_failed_to_load_info_with_reason">Не удалось загрузить информацию по реферальной программе. Код ошибки: %s. Пожалуйста, попробуйте позже.</string>
@ -306,12 +302,6 @@
<string name="reset_card_without_backup_to_factory_message">Сброс к заводским настройкам приведет к полному удалению кошелька с выбранной карты. Вы не сможете восстановить текущий кошелек.</string>
<string name="russian_bank_card_warning_subtitle">У вас есть карта банка другой страны или платежной системы UnionPay?</string>
<string name="russian_bank_card_warning_title">Карты банков РФ в данный момент не принимаются</string>
<string name="saltpay_error_empty_backup_message">Приложите карту с логотипом Visa</string>
<string name="saltpay_error_empty_backup_title">Внимание</string>
<string name="saltpay_error_no_gas_message">Пожалуйста обратитесь в службу поддержки</string>
<string name="saltpay_error_no_gas_title">Недостаточно средств для активации</string>
<string name="saltpay_error_pin_weak_message">Данный Код доступа может быть легко взломан</string>
<string name="saltpay_error_pin_weak_title">Ввод одинаковых цифр является не безопасным</string>
<string name="save_user_wallet_agreement_access_description">Войдите в приложение и следите за своим балансом без сканирования карты</string>
<string name="save_user_wallet_agreement_access_title">Доступ в приложение</string>
<string name="save_user_wallet_agreement_allow_biometrics">Использовать биометрию</string>
@ -344,7 +334,6 @@
<string name="send_fee_picker_priority">Приоритетная</string>
<string name="send_max_amount_label">Максимальная сумма</string>
<string name="send_network_fee_title">Сетевая комиссия</string>
<string name="send_title">Отправить</string>
<string name="send_title_currency_format">Отправка %s</string>
<string name="send_total_label">Всего</string>
<string name="send_total_subtitle_asset_format">%1$s и %2$s будет отправлено</string>
@ -361,6 +350,7 @@
<string name="solana_rent_warning">Сеть Solana взимает арендную плату в размере %1$s каждые 2 дня. Аккаунты, которые не могут позволить себе арендную плату, удаляются из сети. Пополните свой счет более чем на %2$s, чтобы не платить арендную плату.</string>
<string name="story_awe_description">Держите свои криптосбережения в безопасности. Приватные ключи надежно хранятся на карте.</string>
<string name="story_awe_title">Революционный аппаратный кошелек</string>
<string name="story_backup_description">До **трех карт** с одним кошельком</string>
<string name="story_backup_description_1">До</string>
<string name="story_backup_description_2_bold">трех карт</string>
<string name="story_backup_description_3">с одним кошельком</string>
@ -369,6 +359,9 @@
<string name="story_currencies_title">Тысячи криптовалют</string>
<string name="story_finish_description">Используйте его на ходу, в любом месте, в любое время. Без проводов и батареек. Как только понадобится крипта, просто приложите карту к телефону.</string>
<string name="story_finish_title">Кошелек для каждого</string>
<string name="story_learn_description">Пройдите обучение и получите возможность купить кошелек Tangem со скидкой и токены 1inch в качестве бонуса</string>
<string name="story_learn_learn">Пройти обучение</string>
<string name="story_learn_title">Получите свой бонус</string>
<string name="story_meet_borrow">Занимайте</string>
<string name="story_meet_buy">Покупайте</string>
<string name="story_meet_exchange">Обменивайте</string>
@ -421,6 +414,7 @@
<string name="token_details_hide_token">Скрыть токен</string>
<string name="token_details_send_blocked_fee_format">%1$s — это токен в сети %2$s. Чтобы отправить транзакцию %3$s, необходимо пополнить баланс %4$s (%5$s) для оплаты комиссии сети.</string>
<string name="token_details_send_blocked_tx_format">Пожалуйста, дождитесь завершения транзакции %s, чтобы иметь возможность отправить средства</string>
<string name="token_details_token_type_subtitle">%1$s токен в сети %%image%% %2$s</string>
<string name="token_details_unable_hide_alert_message">Токен %1$s является основной валютой в сети %2$s и не может быть скрыт до тех пор, пока у вас в списке есть другие токены этой сети.</string>
<string name="token_details_unable_hide_alert_title">Невозможно скрыть %s</string>
<string name="token_item_no_rate">Нет цены</string>
@ -435,6 +429,7 @@
<string name="transaction_history_error_failed_to_load">Не удалось загрузить историю транзакций.\nНажмите на кнопку перезагрузки, чтобы обновить информацию.</string>
<string name="transaction_history_not_supported_description">История транзакций в настоящее время не поддерживается для этого блокчейна. Но не волнуйтесь, мы работаем над этим! А пока вы можете проверить ее в обозревателе.</string>
<string name="transaction_history_transaction_from_address">от: %s</string>
<string name="transaction_history_transaction_to_address">на: %s</string>
<string name="transaction_history_tx_in_progress">В процессе…</string>
<string name="twin_error_same_card">Вы отсканировали ту же карту. Для создания twin-кошелька вам необходимо отсканировать карту с номером %d</string>
<string name="twins_onboarding_description_format">Это карта, которую вы держите в руках. У парной карты номер %s.\n\nОбе карты можно использовать для вывода средств из этого кошелька.</string>
@ -465,9 +460,6 @@
<string name="wallet_balance_tx_in_progress">Транзакция подтверждается…</string>
<string name="wallet_balance_verified">Подтвержденный баланс</string>
<string name="wallet_button_actions">Действия</string>
<string name="wallet_button_buy">Купить</string>
<string name="wallet_button_sell">Продать</string>
<string name="wallet_button_send">Отправить</string>
<string name="wallet_choose_trade_action">Вы хотите купить или продать криптовалюту?</string>
<string name="wallet_connect_alert_sign_message">Запрос на подпись сообщения.\n\n%s</string>
<string name="wallet_connect_bnb_sign_message">Dapp %1$s, запрос на\nподпись транзакции с BNB.\n\n%2$s</string>

View file

@ -171,11 +171,8 @@
<string name="onboarding_button_claim">獲取</string>
<string name="onboarding_button_continue_wallet">繼續至我的錢包</string>
<string name="onboarding_button_finalize_backup">完成備份過程</string>
<string name="onboarding_button_kyc_start">通過Utorg驗證</string>
<string name="onboarding_button_kyc_waiting">刷新</string>
<string name="onboarding_button_pin">設置PIN碼</string>
<string name="onboarding_button_receive_crypto">接收貨幣</string>
<string name="onboarding_button_register_wallet">註冊</string>
<string name="onboarding_button_scan_origin_card">掃描主卡</string>
<string name="onboarding_button_skip_backup">暫時略過</string>
<string name="onboarding_button_what_does_it_mean">它是如何運作的?</string>
@ -190,18 +187,9 @@
<string name="onboarding_exit_alert_message">這此情況,您必須要重新開始</string>
<string name="onboarding_exit_alert_title">您想要離開啟用程序嗎?</string>
<string name="onboarding_getting_started">開始</string>
<string name="onboarding_navbar_kyc_progress">驗證身分</string>
<string name="onboarding_navbar_kyc_start">KYC</string>
<string name="onboarding_navbar_pin">PIN 碼</string>
<string name="onboarding_navbar_register_wallet">連接</string>
<string name="onboarding_navbar_title_creating_backup">創建備份</string>
<string name="onboarding_saltpay_button_backup_origin">點擊 SaltPay 卡</string>
<string name="onboarding_saltpay_subtitle_no_backup_cards">要開始備份過程,您必須添加 Tangem 卡片作為備份</string>
<string name="onboarding_saltpay_subtitle_one_backup_card">通過創建訪問密碼完成備份</string>
<string name="onboarding_saltpay_title_backup_card">點擊 Tangem 卡片</string>
<string name="onboarding_saltpay_title_no_backup_card">沒有備份卡片</string>
<string name="onboarding_saltpay_title_one_backup_card">備份卡片已準備完成</string>
<string name="onboarding_saltpay_title_prepare_origin">準備SaltPay卡</string>
<string name="onboarding_seed_button_read_more">閱讀更多關於助記詞的訊息</string>
<string name="onboarding_seed_generate_message">按照下面給出的順序寫下這 12 個單詞,並將它們存放在安全秘密的地方。</string>
<string name="onboarding_seed_generate_title">您的助記詞</string>
@ -217,13 +205,8 @@
<string name="onboarding_seed_user_validation_title">那麼,讓我們檢查一下</string>
<string name="onboarding_subtitle_claim">要開始,只需將 wxDAI 獲取到您的錢包</string>
<string name="onboarding_subtitle_claim_progress">這會花上幾秒</string>
<string name="onboarding_subtitle_kyc_retry">請查看Email已獲得更多指示</string>
<string name="onboarding_subtitle_kyc_start">要開始使用您的卡,您必須通過 KYC驗證</string>
<string name="onboarding_subtitle_kyc_waiting">請等待驗證完成,您將收到電子郵件通知。通常最多需要 1 小時。您可以關閉該應用程序,稍後再回來</string>
<string name="onboarding_subtitle_no_backup_cards">要開始備份過程,最多可添加兩張備份卡。</string>
<string name="onboarding_subtitle_one_backup_card">您可以再添加一張卡或完成備份過程</string>
<string name="onboarding_subtitle_pin">設置一個 4 位密碼。 \n它將用在付款時使用。</string>
<string name="onboarding_subtitle_register_wallet">將您的卡片連接到去中心化支付系統</string>
<string name="onboarding_subtitle_scan_backup_card_format">準備編號為 %s 的備份卡</string>
<string name="onboarding_subtitle_scan_origin_card">準備主卡</string>
<string name="onboarding_subtitle_scan_primary_card_format">準備編號為 %s 的主卡</string>
@ -234,13 +217,9 @@
<string name="onboarding_title_backup_card_format">備份卡 #%d</string>
<string name="onboarding_title_claim">獲取%s</string>
<string name="onboarding_title_claim_progress">獲取中</string>
<string name="onboarding_title_kyc_retry">有東西出錯了</string>
<string name="onboarding_title_kyc_start">確認你的身分</string>
<string name="onboarding_title_kyc_waiting">KYC 認證中</string>
<string name="onboarding_title_no_backup_cards">沒有備用卡</string>
<string name="onboarding_title_one_backup_card">添加了一張備用卡</string>
<string name="onboarding_title_pin">PIN 碼</string>
<string name="onboarding_title_register_wallet">連接你的錢包</string>
<string name="onboarding_title_scan_origin_card">準備好你的卡片</string>
<string name="onboarding_title_two_backup_cards">添加了兩張備用卡</string>
<string name="onboarding_top_up_body">要開始使用,只需為錢包充值任意金額</string>
@ -286,12 +265,6 @@
<string name="reset_card_without_backup_to_factory_message">恢復原廠設置將從所選卡中完全刪除錢包並將其從應用程序中刪除。您將無法恢復當前錢包</string>
<string name="russian_bank_card_warning_subtitle">您有其他國家的銀行卡或銀聯卡嗎?</string>
<string name="russian_bank_card_warning_title">目前不接受俄羅斯銀行卡</string>
<string name="saltpay_error_empty_backup_message">輕觸帶有 visa 標誌的卡片</string>
<string name="saltpay_error_empty_backup_title">注意</string>
<string name="saltpay_error_no_gas_message">請聯繫客服</string>
<string name="saltpay_error_no_gas_title">沒有激活資金</string>
<string name="saltpay_error_pin_weak_message">這樣的 PIN 很容易被暴力破解</string>
<string name="saltpay_error_pin_weak_title">四個相同的數字不安全</string>
<string name="save_user_wallet_agreement_access_description">登錄應用程序並在不掃描卡片的情況下檢查您的資產</string>
<string name="save_user_wallet_agreement_access_title">訪問應用程序</string>
<string name="save_user_wallet_agreement_allow_biometrics">允許使用生物辨識</string>
@ -339,6 +312,7 @@
<string name="solana_rent_warning">Solana 網絡每 2 天收取 %1$s 的費用。無法付此費用的帳戶將從網絡中清除。向您的帳戶存入超過 %2$s 即可免費使用</string>
<string name="story_awe_description">安全地存儲您的加密貨幣,同時將私鑰保存在您的卡中</string>
<string name="story_awe_title">創新式的硬體錢包</string>
<string name="story_backup_description">最多 **3張實體卡片** 到一個錢包</string>
<string name="story_backup_description_1">最多</string>
<string name="story_backup_description_2_bold">3張實體卡片</string>
<string name="story_backup_description_3">到一個錢包</string>
@ -430,9 +404,6 @@
<string name="wallet_balance_tx_in_progress">交易進行中</string>
<string name="wallet_balance_verified">檢視餘額</string>
<string name="wallet_button_actions">動作</string>
<string name="wallet_button_buy">購買</string>
<string name="wallet_button_sell">銷售</string>
<string name="wallet_button_send">發送</string>
<string name="wallet_choose_trade_action">您想要購買或賣出交易貨幣?</string>
<string name="wallet_connect_alert_sign_message">請求籤署消息。%s</string>
<string name="wallet_connect_bnb_sign_message">Dapp %1$s請求\n簽署 BNB 交易。\n%2$s</string>

View file

@ -64,13 +64,17 @@
<string name="common_enable">Enable</string>
<string name="common_enabled">Enabled</string>
<string name="common_exchange">Exchange</string>
<string name="common_explore_transaction_history">Explore transaction history</string>
<string name="common_explorer">Explorer</string>
<string name="common_like">Like</string>
<string name="common_main_network">Main network</string>
<string name="common_no">No</string>
<string name="common_no_data">No data</string>
<string name="common_ok">OK</string>
<string name="common_origin_card">Primary Card</string>
<string name="common_receive">Receive</string>
<string name="common_reject">Reject</string>
<string name="common_reload">Reload</string>
<string name="common_save_changes">Save changes</string>
<string name="common_search">Search</string>
<string name="common_seed_phrase">Seed phrase</string>
@ -154,13 +158,20 @@
<string name="initial_message_tap_header">Tap the card</string>
<string name="internal_error_wallet_manager_not_found">Internal error: wallet manager not found</string>
<string name="key_invalidated_warning_description">You have updated biometrics, scan your card to enter</string>
<string name="main_learn_subtitle">You have completed the training and can get your 1inch tokens</string>
<string name="main_learn_title">Get a bonus</string>
<string name="main_get_bonus_subtitle">You have completed the training and can get your 1inch tokens</string>
<string name="main_get_bonus_title">Get a bonus</string>
<plurals name="main_learn_subtitle">
<item quantity="one">Complete the training and get %d 1inch token on your wallet</item>
<item quantity="other">Complete the training and get %d 1inch tokens on your wallet</item>
</plurals>
<string name="main_learn_title">Learn &amp; Earn</string>
<string name="main_manage_tokens">Manage tokens</string>
<string name="main_no_backup_warning_subtitle">To protect your assets, we advise you to carry out this procedure</string>
<string name="main_no_backup_warning_title">Your wallet has not been backed up</string>
<string name="main_page_balance">Total balance</string>
<string name="main_processing_full_amount">The amount does not include some of your funds</string>
<string name="main_promotion_credited">1inch tokens will be credited to your wallet address within 2 days</string>
<string name="main_promotion_no_purchase">There was no purchase of a wallet using your promo code, which means you cannot receive a bonus. Buy Tangem wallet, scan it in the app, and get the bonus.</string>
<string name="main_scan_card_warning_view_subtitle">To access all the networks you need to scan the card</string>
<string name="main_scan_card_warning_view_title">Scan your card</string>
<string name="onboarding_access_code_feature_1_description">You have to set up a single access code to protect all your wallets</string>
@ -184,11 +195,8 @@
<string name="onboarding_button_claim">Claim</string>
<string name="onboarding_button_continue_wallet">Continue to my wallet</string>
<string name="onboarding_button_finalize_backup">Finalize the backup</string>
<string name="onboarding_button_kyc_start">Verify via Utorg</string>
<string name="onboarding_button_kyc_waiting">Refresh</string>
<string name="onboarding_button_pin">Set PIN code</string>
<string name="onboarding_button_receive_crypto">Receive crypto</string>
<string name="onboarding_button_register_wallet">Register</string>
<string name="onboarding_button_scan_origin_card">Scan primary card</string>
<string name="onboarding_button_skip_backup">Skip for later</string>
<string name="onboarding_button_what_does_it_mean">How does it work?</string>
@ -204,18 +212,9 @@
<string name="onboarding_exit_alert_title">Do you want to exit the activation process?</string>
<string name="onboarding_getting_started">Getting started</string>
<string name="onboarding_linking_error_card_with_wallets">Another wallet has already been created on the card you\'re trying to add. Do you want to reset it and use the card for a new wallet?</string>
<string name="onboarding_navbar_kyc_progress">Verify your identity</string>
<string name="onboarding_navbar_kyc_start">KYC</string>
<string name="onboarding_navbar_pin">Pin code</string>
<string name="onboarding_navbar_register_wallet">Connect</string>
<string name="onboarding_navbar_title_creating_backup">Creating a backup</string>
<string name="onboarding_saltpay_button_backup_origin">Tap the SaltPay card</string>
<string name="onboarding_saltpay_subtitle_no_backup_cards">To start the backup process you have to add the Tangem card as your backup</string>
<string name="onboarding_saltpay_subtitle_one_backup_card">Finalize the backup process by creating an access code</string>
<string name="onboarding_saltpay_title_backup_card">Tap the Tangem card</string>
<string name="onboarding_saltpay_title_no_backup_card">No backup card</string>
<string name="onboarding_saltpay_title_one_backup_card">Backup card ready</string>
<string name="onboarding_saltpay_title_prepare_origin">Prepare the SaltPay card</string>
<string name="onboarding_seed_button_read_more">Read more about seed phrases</string>
<string name="onboarding_seed_generate_message">Write these 12 words down in the order given below and store them in a safe and secret place.</string>
<string name="onboarding_seed_generate_title">Your secret phrase</string>
@ -231,13 +230,8 @@
<string name="onboarding_seed_user_validation_title">So, lets check</string>
<string name="onboarding_subtitle_claim">To get started, simply claim wxDAI to your wallet</string>
<string name="onboarding_subtitle_claim_progress">It will take a few seconds</string>
<string name="onboarding_subtitle_kyc_retry">Please check your email for further instructions</string>
<string name="onboarding_subtitle_kyc_start">To start using your card you have to pass the KYC process</string>
<string name="onboarding_subtitle_kyc_waiting">Please wait until the verification is completed. You\'ll be notified via email. Usually it takes up to 1 hour. You can close the app and come back later.</string>
<string name="onboarding_subtitle_no_backup_cards">To start the backup process add up to two backup cards.</string>
<string name="onboarding_subtitle_one_backup_card">You can add one more card or finalize the backup process</string>
<string name="onboarding_subtitle_pin">Set up a 4-digit code.\nIt will be used for payments.</string>
<string name="onboarding_subtitle_register_wallet">Connect your card to the decentralized payment system</string>
<string name="onboarding_subtitle_scan_backup_card_format">Prepare the backup card with number %s</string>
<string name="onboarding_subtitle_scan_origin_card">Prepare the primary card</string>
<string name="onboarding_subtitle_scan_primary_card_format">Prepare the primary card with number %s</string>
@ -248,13 +242,9 @@
<string name="onboarding_title_backup_card_format">Backup card #%d</string>
<string name="onboarding_title_claim">Claim %s</string>
<string name="onboarding_title_claim_progress">Claiming</string>
<string name="onboarding_title_kyc_retry">Something went wrong</string>
<string name="onboarding_title_kyc_start">Verify your identity</string>
<string name="onboarding_title_kyc_waiting">KYC is in progress</string>
<string name="onboarding_title_no_backup_cards">No backup cards</string>
<string name="onboarding_title_one_backup_card">One backup card added</string>
<string name="onboarding_title_pin">PIN Code</string>
<string name="onboarding_title_register_wallet">Connect your card</string>
<string name="onboarding_title_scan_origin_card">Prepare your card</string>
<string name="onboarding_title_two_backup_cards">Two backup cards added</string>
<string name="onboarding_top_up_body">To get started, simply top up the wallet with any amount</string>
@ -275,6 +265,10 @@
<string name="organize_tokens_group">Group</string>
<string name="organize_tokens_sort_by_balance">By balance</string>
<string name="organize_tokens_title">Organize tokens</string>
<string name="organize_tokens_ungroup">Ungroup</string>
<string name="receive_bottom_sheet_title">%1$s %2$s address on %3$s network</string>
<string name="receive_bottom_sheet_warning_message">%1$s (%2$s) on %3$s network</string>
<string name="receive_bottom_sheet_warning_message_full">Send only %s to this address. Sending any other currency will result in its irreversible loss.</string>
<string name="referral_button_participate">Participate</string>
<string name="referral_error_failed_to_load_info">Failed to load the information about the referral program. Please try again later.</string>
<string name="referral_error_failed_to_load_info_with_reason">Failed to load the information about the referral program. Error code: %s. Please try again later.</string>
@ -304,12 +298,6 @@
<string name="reset_card_without_backup_to_factory_message">Factory Reset will completely delete the wallet from the selected card and remove it from the app. You will not be able to restore the current wallet.</string>
<string name="russian_bank_card_warning_subtitle">Do you have a bank card of another country or a UnionPay card?</string>
<string name="russian_bank_card_warning_title">Russian bank cards are not accepted at the moment</string>
<string name="saltpay_error_empty_backup_message">Tap the card with the visa logo</string>
<string name="saltpay_error_empty_backup_title">Attention</string>
<string name="saltpay_error_no_gas_message">Please contact support</string>
<string name="saltpay_error_no_gas_title">No funds for activation</string>
<string name="saltpay_error_pin_weak_message">Such a PIN can be brute-forced easily</string>
<string name="saltpay_error_pin_weak_title">Four identical digits isn\'t safe</string>
<string name="save_user_wallet_agreement_access_description">Log into the app and check your balance without scanning the card</string>
<string name="save_user_wallet_agreement_access_title">Access the app</string>
<string name="save_user_wallet_agreement_allow_biometrics">Allow to use biometrics</string>
@ -342,7 +330,6 @@
<string name="send_fee_picker_priority">Priority</string>
<string name="send_max_amount_label">Maximum amount</string>
<string name="send_network_fee_title">Network fee</string>
<string name="send_title">Send</string>
<string name="send_title_currency_format">Sending %s</string>
<string name="send_total_label">Total</string>
<string name="send_total_subtitle_asset_format">%1$s and %2$s will be sent</string>
@ -354,13 +341,12 @@
<string name="shop_i_have_a_promo_code">I have a promo code…</string>
<string name="shop_one_wallet">Tangem Wallet</string>
<string name="shop_other_payment_methods">Other payment methods</string>
<string name="shop_sold_out_description_prefix">Due to the high volume of orders we are receiving, Shipping and Local Delivery orders may be delayed.</string>
<string name="shop_pre_order_now">Pre-order now</string>
<string name="shop_sold_out_description">Due to the high volume of orders we are receiving shipping may be delayed up to 5 weeks depending on your location</string>
<string name="shop_total">Total</string>
<string name="solana_rent_warning">Solana network charges a rent of %1$s every 2 days. Accounts that can\'t afford the rent are purged from the network. Deposit your account with more than %2$s to use it for free.</string>
<string name="story_awe_description">Store your crypto assets secure while keeping private keys contained in your card</string>
<string name="story_awe_title">Revolutionary Hardware Wallet</string>
<string name="story_backup_description">Up to **3 physical cards** to one wallet</string>
<string name="story_backup_description_1">Up to</string>
<string name="story_backup_description_2_bold">3 physical cards</string>
<string name="story_backup_description_3">to one wallet</string>
@ -422,6 +408,7 @@
<string name="token_details_hide_token">Hide token</string>
<string name="token_details_send_blocked_fee_format">%1$s is a token in the %2$s network. To make a %3$s transaction you need to deposit some %4$s (%5$s) to cover the network fee.</string>
<string name="token_details_send_blocked_tx_format">Please wait for %s transaction to complete to be able to send funds</string>
<string name="token_details_token_type_subtitle">%1$s token in %%image%% %2$s network</string>
<string name="token_details_unable_hide_alert_message">The %1$s token is the main currency on the %2$s network and cannot be hidden as long as you have other tokens on this network in the list.</string>
<string name="token_details_unable_hide_alert_title">Unable to hide %s</string>
<string name="token_item_no_rate">No rate</string>
@ -465,9 +452,6 @@
<string name="wallet_balance_tx_in_progress">Transaction is in progress…</string>
<string name="wallet_balance_verified">Verified Balance</string>
<string name="wallet_button_actions">Actions</string>
<string name="wallet_button_buy">Buy</string>
<string name="wallet_button_sell">Sell</string>
<string name="wallet_button_send">Send</string>
<string name="wallet_choose_trade_action">Do you want to buy or sell crypto?</string>
<string name="wallet_connect_alert_sign_message">Requesting to sign a message.\n\n%s</string>
<string name="wallet_connect_bnb_sign_message">Dapp %1$s, requesting to\nsign BNB transaction.\n\n%2$s</string>

View file

@ -1,9 +1,11 @@
package com.tangem.core.ui.extensions
import androidx.annotation.PluralsRes
import androidx.annotation.StringRes
import androidx.compose.runtime.Composable
import androidx.compose.runtime.Immutable
import androidx.compose.runtime.ReadOnlyComposable
import androidx.compose.ui.res.pluralStringResource
import androidx.compose.ui.res.stringResource
/**
@ -24,6 +26,8 @@ sealed interface TextReference {
*/
data class Res(@StringRes val id: Int, val formatArgs: WrappedList<Any> = WrappedList(emptyList())) : TextReference
data class PluralRes(@PluralsRes val id: Int, val count: Int, val formatArgs: WrappedList<Any>) : TextReference
/**
* Text string
*
@ -38,6 +42,7 @@ sealed interface TextReference {
fun TextReference.resolveReference(): String {
return when (this) {
is TextReference.Res -> stringResource(id, *formatArgs.toTypedArray())
is TextReference.PluralRes -> pluralStringResource(id, count, *formatArgs.toTypedArray())
is TextReference.Str -> value
}
}

View file

@ -36,6 +36,7 @@ private class DomainGlobalReducer : ReStoreReducer<DomainGlobalState> {
return when (action) {
is DomainGlobalAction.SaveScanNoteResponse -> {
val card = action.scanResponse.card
// TODO: AuthHeaders: try to remove it, because now we can use headers with dynamic values
state.networkServices.tangemTechService.addAuthenticationHeader(
RequestHeader.AuthenticationHeader(
object : AuthProvider {

View file

@ -17,31 +17,28 @@ dependencies {
implementation(project(":core:res"))
implementation(project(":data:source:preferences"))
implementation(project(":libs:auth"))
implementation(project(":libs:crypto"))
implementation(deps.material)
/** AndroidX */
implementation(deps.androidx.core.ktx)
implementation(deps.androidx.appCompat)
implementation(deps.androidx.fragment.ktx)
implementation(deps.androidx.activity.compose)
implementation(deps.androidx.constraintLayout)
implementation(deps.androidx.browser)
/** Compose */
implementation(deps.compose.material)
implementation(deps.compose.material3)
implementation(deps.compose.foundation)
implementation(deps.compose.ui)
implementation(deps.compose.ui.tooling)
/** Tangem libraries */
// TODO: 1inch: delete if later it doesn't needed
// implementation(deps.tangem.card.core)
// implementation(deps.tangem.card.android) {
// exclude(module = "joda-time")
// }
/** Preferences */
implementation(deps.krateSharedPref)
/** Network */
implementation(deps.krateSharedPref)
implementation(deps.moshi)
implementation(deps.moshi.kotlin)
implementation(deps.retrofit)

View file

@ -5,7 +5,7 @@ import com.tangem.datasource.api.promotion.PromotionApi
import com.tangem.datasource.api.promotion.models.*
import com.tangem.feature.learn2earn.data.api.Learn2earnPreferenceStorage
import com.tangem.feature.learn2earn.data.api.Learn2earnRepository
import com.tangem.feature.learn2earn.data.api.UsedCardsPreferenceStorage
import com.tangem.feature.learn2earn.data.models.PromoUserData
import com.tangem.utils.coroutines.AppCoroutineDispatcherProvider
import kotlinx.coroutines.withContext
import timber.log.Timber
@ -15,38 +15,32 @@ import timber.log.Timber
*/
internal class DefaultLearn2earnRepository(
private val preferencesStorage: Learn2earnPreferenceStorage,
private val usedCardsPreferenceStorage: UsedCardsPreferenceStorage,
private val moshi: Moshi,
private val api: PromotionApi,
private val dispatchers: AppCoroutineDispatcherProvider,
moshi: Moshi,
) : Learn2earnRepository {
private val promotionInfoAdapter = moshi.adapter(PromotionInfoResponse::class.java)
private val userDataAdapter = moshi.adapter(PromoUserData::class.java)
override fun isHadActivatedCards(): Boolean {
return usedCardsPreferenceStorage.isHadActivatedCards()
private var userData: PromoUserData = restoreUserData()
override fun getUserData(): PromoUserData {
return userData
}
override fun getPromoCode(): String? {
return preferencesStorage.promoCode
}
override fun savePromoCode(code: String) {
preferencesStorage.promoCode = code
override fun updateUserData(userData: PromoUserData) {
this.userData = userData
saveUserData(userData)
}
override fun getProgramName(): String {
return PROGRAM_NAME
}
override fun isAlreadyReceivedAward(): Boolean {
return preferencesStorage.alreadyReceivedAward
}
override suspend fun getPromotionInfo(): Result<PromotionInfoResponse> {
return withContext(dispatchers.io) {
val info = restorePromotionInfo()
if (info?.status == PromotionInfoResponse.Status.FINISHED) {
Result.success(info)
} else {
@ -68,9 +62,9 @@ internal class DefaultLearn2earnRepository(
}
}
override suspend fun requestAward(walletId: String): AwardResponse {
override suspend fun requestAward(walletId: String, address: String): AwardResponse {
return withContext(dispatchers.io) {
api.requestAward(AwardRequestBody(walletId, getProgramName()))
api.requestAward(AwardRequestBody(walletId, address, getProgramName()))
}
}
@ -104,6 +98,30 @@ internal class DefaultLearn2earnRepository(
}
}
private fun restoreUserData(): PromoUserData {
return try {
preferencesStorage.userData?.let { userDataAdapter.fromJson(it) }
?: createEmptyUserData().apply { saveUserData(this) }
} catch (ex: Exception) {
Timber.e(ex)
createEmptyUserData().apply { saveUserData(this) }
}
}
private fun saveUserData(userData: PromoUserData) {
try {
preferencesStorage.userData = userDataAdapter.toJson(userData)
} catch (ex: Exception) {
Timber.e(ex)
}
}
private fun createEmptyUserData(): PromoUserData = PromoUserData(
promoCode = null,
isRegisteredInPromotion = false,
isAlreadyReceivedAward = false,
)
private companion object {
const val PROGRAM_NAME: String = "1inch"
}

View file

@ -3,7 +3,6 @@ package com.tangem.feature.learn2earn.data
import android.content.Context
import com.tangem.feature.learn2earn.data.api.Learn2earnPreferenceStorage
import hu.autsoft.krate.SimpleKrate
import hu.autsoft.krate.booleanPref
import hu.autsoft.krate.default.withDefault
import hu.autsoft.krate.stringPref
@ -14,9 +13,7 @@ internal class DefaultPreferenceStorage(
context: Context,
) : SimpleKrate(context = context, name = "Lear2earnPromotion"), Learn2earnPreferenceStorage {
override var promoCode: String? by stringPref().withDefault(null)
override var promotionInfo: String? by stringPref().withDefault(null)
override var alreadyReceivedAward: Boolean by booleanPref().withDefault(false)
override var userData: String? by stringPref().withDefault(null)
}

View file

@ -1,14 +0,0 @@
package com.tangem.feature.learn2earn.data
import com.tangem.data.source.preferences.storage.UsedCardsPrefStorage
import com.tangem.feature.learn2earn.data.api.UsedCardsPreferenceStorage
/**
[REDACTED_AUTHOR]
*/
class DefaultUsedCardsPreferenceStorage(
private val usedCardsPrefStorage: UsedCardsPrefStorage,
) : UsedCardsPreferenceStorage {
override fun isHadActivatedCards(): Boolean = usedCardsPrefStorage.hadFinishedActivation()
}

View file

@ -5,9 +5,7 @@ package com.tangem.feature.learn2earn.data.api
*/
interface Learn2earnPreferenceStorage {
var promoCode: String?
var promotionInfo: String?
var alreadyReceivedAward: Boolean
var userData: String?
}

View file

@ -1,27 +1,24 @@
package com.tangem.feature.learn2earn.data.api
import com.tangem.datasource.api.promotion.models.*
import com.tangem.feature.learn2earn.data.models.PromoUserData
/**
[REDACTED_AUTHOR]
*/
interface Learn2earnRepository {
fun isHadActivatedCards(): Boolean
fun getUserData(): PromoUserData
fun getPromoCode(): String?
fun savePromoCode(code: String)
fun updateUserData(userData: PromoUserData)
fun getProgramName(): String
fun isAlreadyReceivedAward(): Boolean
suspend fun getPromotionInfo(): Result<PromotionInfoResponse>
suspend fun validate(walletId: String): ValidateResponse
suspend fun requestAward(walletId: String): AwardResponse
suspend fun requestAward(walletId: String, address: String): AwardResponse
suspend fun validateCode(walletId: String, code: String?): CodeValidateResponse

View file

@ -1,9 +0,0 @@
package com.tangem.feature.learn2earn.data.api
/**
[REDACTED_AUTHOR]
* An interface that encapsulates access to deprecated PreferencesDataSource.usedCardsPrefStorage.
*/
interface UsedCardsPreferenceStorage {
fun isHadActivatedCards(): Boolean
}

View file

@ -2,16 +2,13 @@ package com.tangem.feature.learn2earn.data.di
import android.content.Context
import com.squareup.moshi.Moshi
import com.tangem.data.source.preferences.PreferencesDataSource
import com.tangem.datasource.api.promotion.PromotionApi
import com.tangem.datasource.di.NetworkMoshi
import com.tangem.datasource.di.PromotionOneInch
import com.tangem.feature.learn2earn.data.DefaultLearn2earnRepository
import com.tangem.feature.learn2earn.data.DefaultPreferenceStorage
import com.tangem.feature.learn2earn.data.DefaultUsedCardsPreferenceStorage
import com.tangem.feature.learn2earn.data.api.Learn2earnPreferenceStorage
import com.tangem.feature.learn2earn.data.api.Learn2earnRepository
import com.tangem.feature.learn2earn.data.api.UsedCardsPreferenceStorage
import com.tangem.utils.coroutines.AppCoroutineDispatcherProvider
import dagger.Module
import dagger.Provides
@ -30,29 +27,19 @@ class Learn2earnDataModule {
return DefaultPreferenceStorage(context)
}
@Provides
@Singleton
fun provideDeprecatedPreferenceDataSource(
preferenceDataSource: PreferencesDataSource,
): UsedCardsPreferenceStorage {
return DefaultUsedCardsPreferenceStorage(preferenceDataSource.usedCardsPrefStorage)
}
@Provides
@Singleton
fun provideRepository(
preferenceStorage: Learn2earnPreferenceStorage,
usedCardsPreferenceStorage: UsedCardsPreferenceStorage,
@NetworkMoshi moshi: Moshi,
@PromotionOneInch promotionApi: PromotionApi,
dispatchers: AppCoroutineDispatcherProvider,
): Learn2earnRepository {
return DefaultLearn2earnRepository(
preferencesStorage = preferenceStorage,
usedCardsPreferenceStorage = usedCardsPreferenceStorage,
moshi = moshi,
api = promotionApi,
dispatchers = dispatchers,
moshi = moshi,
)
}
}

View file

@ -0,0 +1,10 @@
package com.tangem.feature.learn2earn.data.models
/**
[REDACTED_AUTHOR]
*/
data class PromoUserData(
val promoCode: String?,
val isRegisteredInPromotion: Boolean,
val isAlreadyReceivedAward: Boolean,
)

View file

@ -1,77 +1,192 @@
package com.tangem.feature.learn2earn.domain
import android.net.Uri
import com.tangem.datasource.api.promotion.models.AbstractPromotionResponse
import com.tangem.datasource.api.promotion.models.PromotionInfoResponse
import com.tangem.feature.learn2earn.data.api.Learn2earnRepository
import com.tangem.feature.learn2earn.domain.api.Learn2earnInteractor
import com.tangem.feature.learn2earn.domain.models.CardType
import com.tangem.feature.learn2earn.data.models.PromoUserData
import com.tangem.feature.learn2earn.domain.api.*
import com.tangem.feature.learn2earn.domain.models.Promotion
import com.tangem.feature.learn2earn.domain.models.PromotionError
import com.tangem.feature.learn2earn.domain.models.PromotionError.Companion.toDomainError
import com.tangem.feature.learn2earn.domain.models.RedirectConsequences
import com.tangem.lib.auth.BasicAuthProvider
import com.tangem.feature.learn2earn.domain.models.toDomainError
import com.tangem.lib.crypto.DerivationManager
import com.tangem.lib.crypto.UserWalletManager
import com.tangem.lib.crypto.models.Currency
/**
[REDACTED_AUTHOR]
*/
class DefaultLearn2earnInteractor(
private val repository: Learn2earnRepository,
basicAuthProvider: BasicAuthProvider,
userCountryCodeProvider: () -> String,
private val userWalletManager: UserWalletManager,
private val derivationManager: DerivationManager,
dependencyProvider: Learn2earnDependencyProvider,
) : Learn2earnInteractor {
private val webViewUriBuilder = WebViewUriBuilder(
basicAuthProvider = basicAuthProvider,
userCountryCodeProvider = userCountryCodeProvider,
promoCodeProvider = { repository.getPromoCode() },
promoNameProvider = { repository.getProgramName() },
)
override var webViewResultHandler: WebViewResultHandler? = null
private lateinit var promotion: Promotion
private val webViewUriBuilder: WebViewUriBuilder = WebViewUriBuilder(
authCredentialsProvider = dependencyProvider.getWebViewAuthCredentialsProvider(),
userCountryCodeProvider = dependencyProvider.getUserCountryCodeProvider(),
promoCodeProvider = { repository.getUserData().promoCode },
)
override suspend fun init() {
initPromotionInfo()
}
override fun isUserHadPromoCode(): Boolean {
return repository.getUserData().promoCode != null
}
override fun isNeedToShowViewOnStoriesScreen(): Boolean {
return promotionIsActive()
}
override fun getBasicAuthHeaders(): Map<String, String> {
return webViewUriBuilder.getBasicAuthHeaders()
override suspend fun isNeedToShowViewOnMainScreen(): Boolean {
if (!promotionIsActive()) return false
val response = repository.validate(userWalletManager.getWalletId())
return response.valid == true
}
override suspend fun isNeedToShowViewOnWalletScreen(walletId: String): Boolean {
val response = repository.validate(walletId)
override fun isUserRegisteredInPromotion(): Boolean {
return repository.getUserData().isRegisteredInPromotion
}
return if (response.valid == false && response.isError()) {
false
override fun getAwardAmount(): Int {
val userInfo = repository.getUserData()
val promotionInfo = promotion.getPromotionInfo()
return if (userInfo.promoCode == null) {
promotionInfo.awardForOldCard
} else {
promotionIsActive()
promotionInfo.awardForNewCard
}.toInt()
}
@Throws(IllegalArgumentException::class)
override suspend fun requestAward(): Result<Unit> {
val awardCurrency = getCurrencyForAward()
?: return Result.failure(PromotionError.UnknownError("Currency for award is null"))
val walletId = userWalletManager.getWalletId()
val promoCode = repository.getUserData().promoCode
val error = if (promoCode == null) {
requestAward(walletId, awardCurrency)
} else {
requestAwardWithPromoCode(walletId, awardCurrency, promoCode)
}
return if (error == null) {
Result.success(Unit)
} else {
val domainError = error.toDomainError()
handlePromotionError(domainError)
Result.failure(domainError)
}
}
override fun buildUriForStories(): Uri {
val type = if (repository.isHadActivatedCards()) {
CardType.NEW
private suspend fun requestAward(walletId: String, awardCurrency: Currency): AbstractPromotionResponse.Error? {
val validateResponse = repository.validate(walletId)
return if (validateResponse.valid == true) {
val address = getWalletAddressForAward(awardCurrency)
val awardResponse = repository.requestAward(walletId, address)
if (awardResponse.status == true) {
updateUserData { it.copy(isAlreadyReceivedAward = true) }
null
} else {
awardResponse.error
}
} else {
CardType.EXISTED
validateResponse.error
}
return webViewUriBuilder.buildUriForStories(type)
}
override fun buildUriForMainPage(walletId: String, cardId: String, cardPubKey: String): Uri {
return webViewUriBuilder.buildUriForMainPage(walletId, cardId, cardPubKey)
private suspend fun requestAwardWithPromoCode(
walletId: String,
awardCurrency: Currency,
promoCode: String,
): AbstractPromotionResponse.Error? {
val codeValidateResponse = repository.validateCode(walletId, promoCode)
return if (codeValidateResponse.valid == true) {
val address = getWalletAddressForAward(awardCurrency)
val awardWithCodeResponse = repository.requestAwardByCode(walletId, address, promoCode)
if (awardWithCodeResponse.status == true) {
updateUserData { it.copy(isAlreadyReceivedAward = true) }
null
} else {
awardWithCodeResponse.error
}
} else {
codeValidateResponse.error
}
}
override fun handleWebViewRedirect(uri: Uri): RedirectConsequences {
private suspend fun getWalletAddressForAward(currency: Currency): String {
val derivationPath = deriveOrAddTokens(currency)
return userWalletManager.getWalletAddress(currency.networkId, derivationPath)
}
private suspend fun deriveOrAddTokens(currency: Currency): String {
val derivationPath = derivationManager.getDerivationPathForBlockchain(currency.networkId)
if (derivationPath.isNullOrEmpty()) error("derivationPath shouldn't be empty")
if (!derivationManager.hasDerivation(currency.networkId, derivationPath)) {
derivationManager.deriveMissingBlockchains(currency)
}
if (!userWalletManager.isTokenAdded(currency, derivationPath)) {
userWalletManager.addToken(currency, derivationPath)
}
return derivationPath
}
private fun handlePromotionError(error: PromotionError) {
when (error) {
is PromotionError.CodeNotFound -> {
updateUserData { it.copy(promoCode = null) }
}
is PromotionError.CardAlreadyHasAward, is PromotionError.WalletAlreadyHasAward,
is PromotionError.CodeWasAlreadyUsed,
-> {
updateUserData { it.copy(isAlreadyReceivedAward = true) }
}
else -> Unit
}
}
override fun buildUriForNewUser(): Uri {
return webViewUriBuilder.buildUriForNewUser()
}
override fun buildUriForOldUser(): Uri {
return webViewUriBuilder.buildUriForOldUser()
}
override fun getBasicAuthHeaders(): ArrayList<String> {
return HeadersConverter().convert(webViewUriBuilder.getBasicAuthHeaders())
}
override fun handleRedirect(uri: Uri): RedirectConsequences {
if (webViewUriBuilder.isReadyForExistedCardAwardRedirect(uri)) {
updateUserData { it.copy(isRegisteredInPromotion = true) }
webViewResultHandler?.handleResult(WebViewResult.ReadyForAward)
return RedirectConsequences.FINISH_SESSION
}
return if (webViewUriBuilder.isPromoCodeRedirect(uri)) {
webViewUriBuilder.extractPromoCode(uri)?.let { repository.savePromoCode(it) }
webViewUriBuilder.extractPromoCode(uri)?.let { code ->
updateUserData {
it.copy(
promoCode = code,
isRegisteredInPromotion = true,
)
}
webViewResultHandler?.handleResult(WebViewResult.PromoCodeReceived)
}
RedirectConsequences.NOTHING
} else {
RedirectConsequences.PROCEED
@ -80,9 +195,9 @@ class DefaultLearn2earnInteractor(
private fun promotionIsActive(): Boolean {
val isActive = when {
repository.isAlreadyReceivedAward() -> false
repository.getUserData().isAlreadyReceivedAward -> false
promotion.isError() -> false
else -> promotion.getInfo().status == PromotionInfoResponse.Status.ACTIVE
else -> promotion.getPromotionInfo().status == PromotionInfoResponse.Status.ACTIVE
}
return isActive
@ -118,4 +233,23 @@ class DefaultLearn2earnInteractor(
},
)
}
private fun getCurrencyForAward(): Currency? {
val token = promotion.info?.awardPaymentToken ?: return null
return Currency.NonNativeToken(
id = token.id,
name = token.name,
symbol = token.symbol,
networkId = token.networkId,
contractAddress = token.contractAddress,
decimalCount = token.decimalCount,
)
}
private fun updateUserData(updateBlock: (PromoUserData) -> PromoUserData): PromoUserData {
return updateBlock(repository.getUserData()).apply {
repository.updateUserData(this)
}
}
}

View file

@ -0,0 +1,28 @@
package com.tangem.feature.learn2earn.domain
import com.tangem.utils.converter.TwoWayConverter
/**
* A converter that helps prepare a list of headers to be added to the Bundle.
* ArrayList is used to prevent cast exceptions when the convert result is placed in a Bundle.
* @see Learn2earnRouter#openWebView() putStringArrayListExtra
*
[REDACTED_AUTHOR]
*/
internal class HeadersConverter : TwoWayConverter<Map<String, String>, ArrayList<String>> {
override fun convert(value: Map<String, String>): ArrayList<String> {
val list = value.toList().map {
"${it.first}$SPLIT_SIGN${it.second}"
}
return ArrayList(list)
}
override fun convertBack(value: ArrayList<String>): Map<String, String> {
return value.map { it.split(SPLIT_SIGN) }.associate { it[0] to it[1] }
}
private companion object {
const val SPLIT_SIGN = ":"
}
}

View file

@ -1,23 +1,20 @@
package com.tangem.feature.learn2earn.domain
import android.net.Uri
import com.tangem.feature.learn2earn.domain.models.CardType
import com.tangem.feature.learn2earn.impl.BuildConfig
import com.tangem.lib.auth.BasicAuthProvider
/**
[REDACTED_AUTHOR]
*/
class WebViewUriBuilder(
private val basicAuthProvider: BasicAuthProvider,
internal class WebViewUriBuilder(
private val authCredentialsProvider: () -> String?,
private val userCountryCodeProvider: () -> String,
private val promoCodeProvider: () -> String?,
private val promoNameProvider: () -> String,
) {
fun buildUriForStories(type: CardType): Uri {
fun buildUriForNewUser(): Uri {
val builder = makeWebViewUriBuilder()
.appendQueryParameter("type", type.typeName)
.appendQueryParameter("type", QUERY_NEW_CARD)
promoCodeProvider.invoke()?.let {
builder.appendQueryParameter("code", it)
@ -26,23 +23,20 @@ class WebViewUriBuilder(
return builder.build()
}
fun buildUriForMainPage(walletId: String, cardId: String, cardPubKey: String): Uri {
fun buildUriForOldUser(): Uri {
val builder = makeWebViewUriBuilder()
.appendQueryParameter("type", CardType.EXISTED.typeName)
.appendQueryParameter("cardPublicKey", cardPubKey)
.appendQueryParameter("cardId", cardId)
.appendQueryParameter("walletId", walletId)
.appendQueryParameter("programName", promoNameProvider.invoke())
promoCodeProvider.invoke()?.let {
builder.appendQueryParameter("code", it)
}
.appendQueryParameter("type", QUERY_EXISTED_CARD)
return builder.build()
}
fun getBasicAuthHeaders(): Map<String, String> {
return basicAuthProvider.getCredentials()
val credentials = authCredentialsProvider.invoke()
return if (credentials == null) {
mapOf()
} else {
mapOf("Authorization" to "Basic $credentials")
}
}
fun isPromoCodeRedirect(uri: Uri): Boolean {
@ -67,30 +61,34 @@ class WebViewUriBuilder(
private fun makeWebViewUriBuilder(): Uri.Builder {
val builder = Uri.Builder()
builder.scheme("https")
builder.scheme(SCHEME)
if (BuildConfig.DEBUG) {
builder.authority(DEV_WEB_VIEW_BASE_URL)
builder.authority(DEV_BASE_URL)
builder.appendPath(userCountryCodeProvider.invoke())
builder.appendPath("promotion-test")
builder.appendPath(DEV_PATH_PROMOTION)
} else {
builder.authority(WEB_VIEW_BASE_URL)
builder.authority(BASE_URL)
builder.appendPath(userCountryCodeProvider.invoke())
builder.appendPath("promotion")
builder.appendPath(PATH_PROMOTION)
}
return builder
}
private companion object {
const val WEB_VIEW_BASE_URL = "tangem.com"
const val SCHEME = "https"
const val DEV_WEB_VIEW_BASE_URL = "devweb.tangem.com"
const val DEV_WEB_VIEW_BASIC_AUTH = "Basic dGFuZ2VtOnRhbmdlbWRldjc="
// TODO: 1inch: replace by invalid auth
// const val DEV_WEB_VIEW_BASIC_AUTH = "Basic paste valid value"
const val BASE_URL = "tangem.com"
const val PATH_PROMOTION = "promotion"
const val QUERY_NEW_CARD = "new-card"
const val QUERY_EXISTED_CARD = "existed-card"
const val DEV_BASE_URL = "devweb.tangem.com"
const val DEV_PATH_PROMOTION = "promotion-test"
const val SUFFIX_CODE_CREATED = "/code-created?code="
const val SUFFIX_READY_FOR_AWARD = "/ready-for-exsited-card-award"
const val SUFFIX_READY_FOR_AWARD = "/ready-for-existed-card-award"
}
}

View file

@ -0,0 +1,13 @@
package com.tangem.feature.learn2earn.domain.api
/**
* Interface, a wrapper that allows us to get values from the AppStateHolder located in the app module
*
[REDACTED_AUTHOR]
*/
interface Learn2earnDependencyProvider {
fun getUserCountryCodeProvider(): () -> String
fun getWebViewAuthCredentialsProvider(): () -> String?
}

View file

@ -1,24 +1,32 @@
package com.tangem.feature.learn2earn.domain.api
import android.net.Uri
import com.tangem.feature.learn2earn.domain.models.RedirectConsequences
/**
[REDACTED_AUTHOR]
*/
interface Learn2earnInteractor {
interface Learn2earnInteractor : WebViewRedirectHandler {
var webViewResultHandler: WebViewResultHandler?
suspend fun init()
fun isUserHadPromoCode(): Boolean
fun isNeedToShowViewOnStoriesScreen(): Boolean
suspend fun isNeedToShowViewOnWalletScreen(walletId: String): Boolean
suspend fun isNeedToShowViewOnMainScreen(): Boolean
fun getBasicAuthHeaders(): Map<String, String>
fun isUserRegisteredInPromotion(): Boolean
fun buildUriForStories(): Uri
fun getAwardAmount(): Int
fun buildUriForMainPage(walletId: String, cardId: String, cardPubKey: String): Uri
@Throws(IllegalArgumentException::class)
suspend fun requestAward(): Result<Unit>
fun handleWebViewRedirect(uri: Uri): RedirectConsequences
fun buildUriForNewUser(): Uri
fun buildUriForOldUser(): Uri
fun getBasicAuthHeaders(): ArrayList<String>
}

View file

@ -0,0 +1,19 @@
package com.tangem.feature.learn2earn.domain.api
import android.net.Uri
/**
* Handler that helps determine what actions to take inside the WebView of the Learn2earnWebViewActivity after
* a redirect has been processed by WebViewRedirectHandler
*
[REDACTED_AUTHOR]
*/
interface WebViewRedirectHandler {
fun handleRedirect(uri: Uri): RedirectConsequences
}
enum class RedirectConsequences {
NOTHING,
PROCEED,
FINISH_SESSION,
}

View file

@ -0,0 +1,15 @@
package com.tangem.feature.learn2earn.domain.api
/**
* Handler that helps determine a result of the Learn2earnWebViewActivity webView actions.
*
[REDACTED_AUTHOR]
*/
interface WebViewResultHandler {
fun handleResult(result: WebViewResult)
}
sealed class WebViewResult {
object PromoCodeReceived : WebViewResult()
object ReadyForAward : WebViewResult()
}

View file

@ -3,9 +3,12 @@ package com.tangem.feature.learn2earn.domain.di
import android.content.Context
import com.tangem.feature.learn2earn.data.api.Learn2earnRepository
import com.tangem.feature.learn2earn.domain.DefaultLearn2earnInteractor
import com.tangem.feature.learn2earn.domain.api.Learn2earnDependencyProvider
import com.tangem.feature.learn2earn.domain.api.Learn2earnInteractor
import com.tangem.feature.learn2earn.domain.api.WebViewRedirectHandler
import com.tangem.feature.learn2earn.presentation.Learn2earnRouter
import com.tangem.lib.auth.BasicAuthProvider
import com.tangem.lib.crypto.DerivationManager
import com.tangem.lib.crypto.UserWalletManager
import dagger.Module
import dagger.Provides
import dagger.hilt.InstallIn
@ -27,13 +30,21 @@ class Learn2earnDomainModule {
@Singleton
fun provideInteractor(
repository: Learn2earnRepository,
basicAuthProvider: BasicAuthProvider,
dependencyProvider: Learn2earnDependencyProvider,
userWalletManager: UserWalletManager,
derivationManager: DerivationManager,
): Learn2earnInteractor {
return DefaultLearn2earnInteractor(
repository = repository,
basicAuthProvider = basicAuthProvider,
// TODO: 1inch: paste locale provider instead of hardcode
userCountryCodeProvider = { "ru" },
userWalletManager = userWalletManager,
derivationManager = derivationManager,
dependencyProvider = dependencyProvider,
)
}
@Provides
@Singleton
fun provideWebViewHandler(interactor: Learn2earnInteractor): WebViewRedirectHandler {
return interactor
}
}

View file

@ -1,9 +0,0 @@
package com.tangem.feature.learn2earn.domain.models
/**
[REDACTED_AUTHOR]
*/
enum class CardType(val typeName: String) {
NEW("new-card"),
EXISTED("existed-card"),
}

View file

@ -1,76 +0,0 @@
package com.tangem.feature.learn2earn.domain.models
import com.tangem.datasource.api.promotion.models.PromotionInfoResponse
/**
[REDACTED_AUTHOR]
*/
internal fun Promotion.Companion.dummyPending(): Promotion {
return Promotion(
info = Promotion.PromotionInfo.dummy().copy(
status = PromotionInfoResponse.Status.PENDING,
),
error = null,
)
}
internal fun Promotion.Companion.dummyActive(): Promotion {
return Promotion(
info = Promotion.PromotionInfo.dummy().copy(
status = PromotionInfoResponse.Status.ACTIVE,
),
error = null,
)
}
internal fun Promotion.Companion.dummyFinished(): Promotion {
return Promotion(
info = Promotion.PromotionInfo.dummy().copy(
status = PromotionInfoResponse.Status.FINISHED,
),
error = null,
)
}
internal fun Promotion.Companion.dummyError(): Promotion {
return Promotion(
info = null,
error = PromotionError.Error(
code = 105,
description = "any",
),
)
}
internal fun Promotion.Companion.dummyErrorUnreachable(): Promotion {
return Promotion(
info = null,
error = PromotionError.NetworkUnreachable,
)
}
private fun Promotion.PromotionInfo.Companion.dummy(): Promotion.PromotionInfo {
return Promotion.PromotionInfo(
status = PromotionInfoResponse.Status.ACTIVE,
awardForNewCard = 10f,
awardForOldCard = 5.5f,
awardPaymentToken = Promotion.tokenInfo(),
)
}
private fun Promotion.Companion.tokenInfo(): PromotionInfoResponse.TokenInfo {
return PromotionInfoResponse.TokenInfo(
id = "1inch",
name = "1inch",
symbol = "1INCH",
active = true,
networks = listOf(
PromotionInfoResponse.TokenInfo.Network(
networkId = "polygon-pos",
exchangeable = false,
contractAddress = "0x9c2c5fd7b07e95ee044ddeba0e97a665f142394f",
decimalCount = 1,
),
),
)
}

View file

@ -11,7 +11,7 @@ data class Promotion(
) {
@Throws(NullPointerException::class)
fun getInfo(): PromotionInfo = info!!
fun getPromotionInfo(): PromotionInfo = info!!
fun isError(): Boolean = error != null
@ -19,9 +19,9 @@ data class Promotion(
data class PromotionInfo(
val status: PromotionInfoResponse.Status,
val awardForNewCard: Float,
val awardForOldCard: Float,
val awardPaymentToken: PromotionInfoResponse.TokenInfo,
val awardForNewCard: Double,
val awardForOldCard: Double,
val awardPaymentToken: PromotionInfoResponse.TokenData,
) {
companion object
}

View file

@ -5,11 +5,31 @@ import com.tangem.datasource.api.promotion.models.AbstractPromotionResponse
/**
[REDACTED_AUTHOR]
*/
sealed class PromotionError(val code: Int, val description: String) {
object NetworkUnreachable : PromotionError(-1, "Network or service is unreachable")
class Error(code: Int, description: String) : PromotionError(code, description)
sealed class PromotionError(val code: Int, val description: String) : Throwable(
message = "code: $code, description: $description",
) {
companion object {
fun AbstractPromotionResponse.Error.toDomainError(): PromotionError = Error(code, description)
}
object NetworkUnreachable : PromotionError(code = 0, "Network or service not available")
class CodeNotFound(code: Int, description: String) : PromotionError(code, description)
class CodeWasNotAppliedInShop(code: Int, description: String) : PromotionError(code, description)
class CodeWasAlreadyUsed(code: Int, description: String) : PromotionError(code, description)
class WalletAlreadyHasAward(code: Int, description: String) : PromotionError(code, description)
class CardAlreadyHasAward(code: Int, description: String) : PromotionError(code, description)
class ProgramNotFound(code: Int, description: String) : PromotionError(code, description)
class ProgramWasEnd(code: Int, description: String) : PromotionError(code, description)
class UnknownError(description: String) : PromotionError(code = -1, description)
}
@Suppress("MagicNumber")
internal fun AbstractPromotionResponse.Error.toDomainError(): PromotionError = when (code) {
101 -> PromotionError.CodeNotFound(code, message)
102 -> PromotionError.CodeWasNotAppliedInShop(code, message)
103 -> PromotionError.CodeWasAlreadyUsed(code, message)
104 -> PromotionError.WalletAlreadyHasAward(code, message)
105 -> PromotionError.CardAlreadyHasAward(code, message)
106 -> PromotionError.ProgramNotFound(code, message)
107 -> PromotionError.ProgramWasEnd(code, message)
else -> PromotionError.UnknownError("$code:$message")
}

View file

@ -1,17 +0,0 @@
package com.tangem.feature.learn2earn.domain.models
import android.net.Uri
/**
[REDACTED_AUTHOR]
*/
interface WebViewHelper {
fun handleWebViewRedirect(uri: Uri): RedirectConsequences
fun getWebViewHeaders(): Map<String, String>
}
enum class RedirectConsequences {
NOTHING,
PROCEED,
FINISH_SESSION,
}

View file

@ -2,6 +2,8 @@ package com.tangem.feature.learn2earn.presentation
import android.content.Context
import android.content.Intent
import android.net.Uri
import com.tangem.feature.learn2earn.presentation.webView.Learn2earnWebViewActivity
/**
[REDACTED_AUTHOR]
@ -10,7 +12,12 @@ class Learn2earnRouter(
private val context: Context,
) {
fun openWebView() {
context.startActivity(Intent(context, Learn2earnWebViewActivity::class.java))
fun openWebView(uri: Uri, rawHeaders: ArrayList<String>) {
val intent = Intent(context, Learn2earnWebViewActivity::class.java)
.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
.putExtra(Learn2earnWebViewActivity.EXTRA_WEB_URI, uri.toString())
.putStringArrayListExtra(Learn2earnWebViewActivity.EXTRA_WEB_HEADERS, rawHeaders)
context.startActivity(intent)
}
}

View file

@ -1,11 +1,16 @@
package com.tangem.feature.learn2earn.presentation
import android.net.Uri
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.setValue
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import com.tangem.feature.learn2earn.domain.api.Learn2earnInteractor
import com.tangem.feature.learn2earn.domain.models.RedirectConsequences
import com.tangem.feature.learn2earn.domain.models.WebViewHelper
import com.tangem.feature.learn2earn.domain.api.WebViewResult
import com.tangem.feature.learn2earn.domain.api.WebViewResultHandler
import com.tangem.feature.learn2earn.domain.models.PromotionError
import com.tangem.feature.learn2earn.presentation.ui.state.*
import com.tangem.lib.crypto.models.errors.UserCancelledException
import com.tangem.utils.coroutines.AppCoroutineDispatcherProvider
import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.launch
@ -20,39 +25,156 @@ class Learn2earnViewModel @Inject constructor(
private val interactor: Learn2earnInteractor,
private val router: Learn2earnRouter,
private val dispatchers: AppCoroutineDispatcherProvider,
) : ViewModel(), WebViewHelper {
) : ViewModel() {
var webViewUri: Uri = Uri.parse("https://localhost")
var uiState: Learn2earnState by mutableStateOf(
Learn2earnState.init(
uiActions = Learn2earnUiActions(
onButtonStoryClick = ::onButtonStoryClick,
onButtonMainClick = ::onButtonMainClick,
),
),
)
private set
fun isNeedToShowViewOnStoriesScreen(): Boolean = interactor.isNeedToShowViewOnStoriesScreen()
fun onStoriesClick() {
webViewUri = interactor.buildUriForStories()
router.openWebView()
init {
uiState = uiState.updateStoriesVisibility(interactor.isNeedToShowViewOnStoriesScreen())
}
fun isNeedToShowViewOnWalletScreen(walletId: String, callback: (Boolean) -> Unit) {
fun onMainScreenCreated() {
updateMainScreenViews()
}
private fun updateMainScreenViews() {
viewModelScope.launch(dispatchers.io) {
val isNeedToShow = interactor.isNeedToShowViewOnWalletScreen(walletId)
withContext(dispatchers.main) {
callback(isNeedToShow)
if (interactor.isNeedToShowViewOnMainScreen()) {
updateUi {
uiState.changeGetBounsDescription(getBonusDescription())
.updateGetBonusVisibility(isVisible = true)
}
} else {
updateUi { uiState.updateGetBonusVisibility(isVisible = false) }
}
}
}
fun onMainPageClick(walletId: String, cardId: String, cardPubKey: String) {
webViewUri = interactor.buildUriForMainPage(walletId, cardId, cardPubKey)
router.openWebView()
private fun onButtonStoryClick() {
router.openWebView(interactor.buildUriForNewUser(), interactor.getBasicAuthHeaders())
}
// region WebViewHelper
override fun handleWebViewRedirect(uri: Uri): RedirectConsequences {
return interactor.handleWebViewRedirect(uri)
private fun onButtonMainClick() {
if (!interactor.isUserHadPromoCode() && !interactor.isUserRegisteredInPromotion()) {
subscribeToWebViewResultEvents()
router.openWebView(interactor.buildUriForOldUser(), interactor.getBasicAuthHeaders())
return
}
viewModelScope.launch(dispatchers.io) {
updateUi { uiState.updateProgress(showProgress = true) }
runCatching { interactor.requestAward() }
.onSuccess { requestAwardResult ->
requestAwardResult.fold(
onSuccess = {
val onHideDialog = {
uiState = uiState.updateGetBonusVisibility(isVisible = false)
.hideDialog()
}
val successDialog = MainScreenState.Dialog.Claimed(
onOk = onHideDialog,
onDismissRequest = onHideDialog,
)
updateUi { uiState.showDialog(successDialog) }
},
onFailure = {
val errorDialog = createErrorDialog(it.toDomainError())
updateUi {
uiState.updateProgress(showProgress = false)
.showDialog(errorDialog)
}
},
)
}
.onFailure { exception ->
updateUi { uiState.updateProgress(showProgress = false) }
if (exception is UserCancelledException) return@launch
val errorDialog = createErrorDialog(exception.toDomainError())
updateUi { uiState.showDialog(errorDialog) }
}
}
}
override fun getWebViewHeaders(): Map<String, String> {
return interactor.getBasicAuthHeaders()
private fun getBonusDescription(): MainScreenState.Description {
return if (interactor.isUserRegisteredInPromotion()) {
MainScreenState.Description.GetBonus
} else {
MainScreenState.Description.Learn(interactor.getAwardAmount())
}
}
// endregion WebViewHelper
private fun createErrorDialog(error: PromotionError): MainScreenState.Dialog {
return when (error) {
is PromotionError.CodeWasNotAppliedInShop -> {
val onHideDialog = { uiState = uiState.hideDialog() }
MainScreenState.Dialog.PromoCodeNotRegistered(
onOk = {
uiState = uiState.hideDialog()
subscribeToWebViewResultEvents()
router.openWebView(interactor.buildUriForNewUser(), interactor.getBasicAuthHeaders())
},
onCancel = onHideDialog,
onDismissRequest = onHideDialog,
)
}
is PromotionError.CodeNotFound,
is PromotionError.CodeWasAlreadyUsed,
is PromotionError.CardAlreadyHasAward,
is PromotionError.WalletAlreadyHasAward,
is PromotionError.ProgramNotFound,
is PromotionError.ProgramWasEnd,
-> {
val onHideDialog = {
uiState = uiState.hideDialog()
.updateGetBonusVisibility(isVisible = false)
}
MainScreenState.Dialog.Error(
error = error,
onOk = onHideDialog,
onDismissRequest = onHideDialog,
)
}
is PromotionError.UnknownError, PromotionError.NetworkUnreachable -> {
val onHideDialog = { uiState = uiState.hideDialog() }
MainScreenState.Dialog.Error(
error = error,
onOk = onHideDialog,
onDismissRequest = onHideDialog,
)
}
}
}
private fun subscribeToWebViewResultEvents() {
interactor.webViewResultHandler = object : WebViewResultHandler {
override fun handleResult(result: WebViewResult) {
interactor.webViewResultHandler = null
when (result) {
WebViewResult.ReadyForAward -> updateMainScreenViews()
WebViewResult.PromoCodeReceived -> Unit
}
}
}
}
private suspend fun updateUi(updateBlock: suspend () -> Learn2earnState) {
withContext(dispatchers.io) {
val newState = updateBlock.invoke()
withContext(dispatchers.main) { uiState = newState }
}
}
}
private fun Throwable.toDomainError(): PromotionError {
return this as? PromotionError ?: PromotionError.UnknownError(message ?: "Unknown error")
}

View file

@ -4,6 +4,7 @@ import androidx.compose.foundation.Image
import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.*
import androidx.compose.material.CircularProgressIndicator
import androidx.compose.material.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
@ -13,22 +14,30 @@ import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.ColorFilter
import androidx.compose.ui.graphics.Shape
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.tooling.preview.PreviewParameter
import androidx.compose.ui.tooling.preview.datasource.CollectionPreviewParameterProvider
import androidx.compose.ui.unit.dp
import com.tangem.core.ui.components.SpacerH4
import com.tangem.core.ui.extensions.resolveReference
import com.tangem.core.ui.res.TangemColorPalette
import com.tangem.core.ui.res.TangemTheme
import com.tangem.core.ui.res.TangemTypography
import com.tangem.feature.learn2earn.impl.R
import com.tangem.feature.learn2earn.presentation.ui.component.GradientCircle
import com.tangem.feature.learn2earn.presentation.ui.state.MainScreenState
/**
[REDACTED_AUTHOR]
*/
@Composable
fun GetBonusAlert(onClick: () -> Unit, modifier: Modifier = Modifier) {
internal fun GetBonusView(state: MainScreenState, modifier: Modifier = Modifier) {
if (!state.isVisible) return
val shape = TangemTheme.shapes.roundedCornersMedium
Box(
modifier = modifier
.clickable(onClick = state.onClick)
.height(TangemTheme.dimens.size96)
.background(
color = TangemTheme.colors.background.action,
@ -40,21 +49,33 @@ fun GetBonusAlert(onClick: () -> Unit, modifier: Modifier = Modifier) {
modifier = Modifier.fillMaxHeight(),
verticalAlignment = Alignment.CenterVertically,
) {
Image(
modifier = Modifier.padding(horizontal = TangemTheme.dimens.size16),
painter = painterResource(id = R.drawable.img_1inch_logo_42_40),
contentDescription = null,
)
Box(modifier = Modifier.padding(horizontal = TangemTheme.dimens.size16)) {
Image(
painter = painterResource(id = R.drawable.img_1inch_logo_42_40),
alpha = state.logoState.alpha,
contentDescription = null,
)
if (state.showProgress) {
CircularProgressIndicator(
modifier = Modifier
.align(Alignment.Center)
.size(size = TangemTheme.dimens.size28),
color = TangemColorPalette.White,
strokeWidth = TangemTheme.dimens.size2,
)
}
}
Column(
modifier = Modifier.padding(end = TangemTheme.dimens.size40),
) {
Text(
text = stringResource(id = R.string.main_learn_title),
text = state.description.title.resolveReference(),
style = TangemTypography.body1,
color = TangemTheme.colors.text.primary2,
)
SpacerH4()
Text(
text = stringResource(id = R.string.main_learn_subtitle),
text = state.description.subtitle.resolveReference(),
style = TangemTypography.caption,
color = TangemTheme.colors.text.secondary,
)
@ -64,8 +85,7 @@ fun GetBonusAlert(onClick: () -> Unit, modifier: Modifier = Modifier) {
modifier = Modifier
.size(TangemTheme.dimens.size40)
.padding(end = TangemTheme.dimens.size16)
.align(Alignment.CenterEnd)
.clickable(onClick = onClick),
.align(Alignment.CenterEnd),
painter = painterResource(id = R.drawable.ic_chevron_right_24),
contentDescription = null,
colorFilter = ColorFilter.tint(Color.Gray),
@ -108,24 +128,46 @@ private fun ContentBackground(shape: Shape) {
@Preview
@Composable
private fun OneInchStoriesContentPreview_Light() {
private fun GetBonusViewPreview_Light(@PreviewParameter(GetBonusViewPreviewProvider::class) state: MainScreenState) {
TangemTheme(
isDark = false,
) {
GetBonusAlert(
onClick = {},
)
GetBonusView(state)
}
}
@Preview
@Composable
private fun OneInchStoriesContentPreview_Dark() {
private fun GetBonusViewPreview_Dark(@PreviewParameter(GetBonusViewPreviewProvider::class) state: MainScreenState) {
TangemTheme(
isDark = true,
) {
GetBonusAlert(
onClick = {},
)
GetBonusView(state)
}
}
}
private class GetBonusViewPreviewProvider : CollectionPreviewParameterProvider<MainScreenState>(
collection = listOf(
MainScreenState(
isVisible = true,
onClick = {},
description = MainScreenState.Description.Learn(1),
logoState = MainScreenState.LogoState.Idle,
showProgress = false,
),
MainScreenState(
isVisible = true,
onClick = {},
description = MainScreenState.Description.Learn(2),
logoState = MainScreenState.LogoState.InProgress,
showProgress = true,
),
MainScreenState(
isVisible = true,
onClick = {},
description = MainScreenState.Description.GetBonus,
logoState = MainScreenState.LogoState.Idle,
showProgress = false,
),
),
)

View file

@ -0,0 +1,85 @@
package com.tangem.feature.learn2earn.presentation.ui
import androidx.compose.material.AlertDialog
import androidx.compose.material.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.res.stringResource
import com.tangem.core.ui.components.TextButton
import com.tangem.feature.learn2earn.impl.R
import com.tangem.feature.learn2earn.presentation.ui.state.MainScreenState
/**
[REDACTED_AUTHOR]
*/
@Composable
internal fun Learn2earnDialogs(dialog: MainScreenState.Dialog?) {
when (dialog) {
is MainScreenState.Dialog.Claimed -> ClaimedDialog(dialog)
is MainScreenState.Dialog.PromoCodeNotRegistered -> PromoCodeNotRegisteredDialog(dialog)
is MainScreenState.Dialog.Error -> ErrorDialog(dialog)
else -> Unit
}
}
@Composable
private fun ClaimedDialog(dialog: MainScreenState.Dialog.Claimed) {
AlertDialog(
title = {
Text(text = stringResource(id = R.string.common_success))
},
text = {
Text(text = stringResource(id = R.string.main_promotion_credited))
},
confirmButton = {
TextButton(
text = stringResource(id = R.string.common_ok),
onClick = dialog.onOk,
)
},
onDismissRequest = dialog.onDismissRequest,
)
}
@Composable
private fun PromoCodeNotRegisteredDialog(dialog: MainScreenState.Dialog.PromoCodeNotRegistered) {
AlertDialog(
title = {
Text(text = stringResource(id = R.string.common_error))
},
text = {
Text(text = stringResource(id = R.string.main_promotion_no_purchase))
},
dismissButton = {
TextButton(
text = stringResource(id = R.string.common_cancel),
onClick = dialog.onCancel,
)
},
confirmButton = {
TextButton(
text = stringResource(id = R.string.common_buy),
onClick = dialog.onOk,
)
},
onDismissRequest = dialog.onDismissRequest,
)
}
@Composable
private fun ErrorDialog(dialog: MainScreenState.Dialog.Error) {
AlertDialog(
title = {
Text(text = stringResource(id = R.string.common_error))
},
text = {
Text(text = dialog.error.description)
},
confirmButton = {
TextButton(
text = stringResource(id = R.string.common_ok),
onClick = dialog.onOk,
)
},
onDismissRequest = dialog.onDismissRequest,
)
}

View file

@ -0,0 +1,21 @@
package com.tangem.feature.learn2earn.presentation.ui
import androidx.compose.foundation.layout.padding
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import com.tangem.core.ui.res.TangemTheme
import com.tangem.feature.learn2earn.presentation.ui.state.Learn2earnState
/**
[REDACTED_AUTHOR]
*/
@Composable
fun Learn2earnMainPageScreen(state: Learn2earnState, modifier: Modifier = Modifier) {
GetBonusView(
state = state.mainScreenState,
modifier = modifier
.padding(horizontal = TangemTheme.dimens.size16)
.padding(vertical = TangemTheme.dimens.size8),
)
Learn2earnDialogs(state.mainScreenState.dialog)
}

View file

@ -24,9 +24,8 @@ import com.tangem.feature.learn2earn.presentation.ui.component.GradientCircle
/**
[REDACTED_AUTHOR]
*/
// TODO: 1inch: make function as internal after adding feature interface
@Composable
fun StoriesScreen(onLearnClick: () -> Unit, modifier: Modifier = Modifier) {
fun Learn2earnStoriesScreen(onLearnClick: () -> Unit, modifier: Modifier = Modifier) {
Box(modifier = modifier.fillMaxSize()) {
ContentBackground(
modifier = Modifier
@ -107,11 +106,11 @@ private fun StoryDescription(headerText: String, bodyText: String, modifier: Mod
@Preview
@Composable
private fun OneInchStoriesContentPreview_Light() {
private fun Learn2earnStoriesScreenPreview_Light() {
TangemTheme(
isDark = false,
) {
StoriesScreen(
Learn2earnStoriesScreen(
onLearnClick = {},
)
}
@ -119,11 +118,11 @@ private fun OneInchStoriesContentPreview_Light() {
@Preview
@Composable
private fun OneInchStoriesContentPreview_Dark() {
private fun Learn2earnStoriesScreenPreview_Dark() {
TangemTheme(
isDark = true,
) {
StoriesScreen(
Learn2earnStoriesScreen(
onLearnClick = {},
)
}

View file

@ -0,0 +1,74 @@
package com.tangem.feature.learn2earn.presentation.ui.state
import com.tangem.core.ui.extensions.TextReference
import com.tangem.core.ui.extensions.WrappedList
import com.tangem.feature.learn2earn.domain.models.PromotionError
import com.tangem.feature.learn2earn.impl.R
/**
[REDACTED_AUTHOR]
*/
data class Learn2earnState(
val storyScreenState: StoryScreenState,
val mainScreenState: MainScreenState,
) {
companion object
}
data class StoryScreenState(
val isVisible: Boolean,
val onClick: () -> Unit,
)
data class MainScreenState(
val isVisible: Boolean,
val onClick: () -> Unit,
val description: Description,
val showProgress: Boolean,
val logoState: LogoState,
val dialog: Dialog? = null,
) {
sealed class LogoState(val alpha: Float) {
object Idle : LogoState(alpha = 1.0f)
object InProgress : LogoState(alpha = 0.2f)
}
sealed class Description(val title: TextReference, val subtitle: TextReference) {
class Learn(award: Int) : Description(
TextReference.Res(R.string.main_learn_title),
TextReference.PluralRes(R.plurals.main_learn_subtitle, award, WrappedList(listOf(award))),
)
object GetBonus : Description(
TextReference.Res(R.string.main_get_bonus_title),
TextReference.Res(R.string.main_get_bonus_subtitle),
)
}
sealed class Dialog {
data class Claimed(
val onOk: () -> Unit,
val onDismissRequest: () -> Unit,
) : Dialog()
data class PromoCodeNotRegistered(
val onOk: () -> Unit,
val onCancel: () -> Unit,
val onDismissRequest: () -> Unit,
) : Dialog()
data class Error(
val error: PromotionError,
val onOk: () -> Unit,
val onDismissRequest: () -> Unit,
) : Dialog()
}
}
class Learn2earnUiActions(
val onButtonStoryClick: () -> Unit,
val onButtonMainClick: () -> Unit,
)

View file

@ -0,0 +1,97 @@
package com.tangem.feature.learn2earn.presentation.ui.state
/**
[REDACTED_AUTHOR]
*/
internal fun Learn2earnState.Companion.init(uiActions: Learn2earnUiActions): Learn2earnState {
return Learn2earnState(
storyScreenState = StoryScreenState(
isVisible = false,
onClick = uiActions.onButtonStoryClick,
),
mainScreenState = MainScreenState(
isVisible = false,
onClick = uiActions.onButtonMainClick,
description = MainScreenState.Description.Learn(0),
logoState = MainScreenState.LogoState.Idle,
showProgress = false,
),
)
}
internal fun Learn2earnState.updateStoriesVisibility(isVisible: Boolean): Learn2earnState {
return if (storyScreenState.isVisible == isVisible) {
this
} else {
copy(
storyScreenState = storyScreenState.copy(
isVisible = isVisible,
),
)
}
}
internal fun Learn2earnState.updateGetBonusVisibility(isVisible: Boolean): Learn2earnState {
return if (mainScreenState.isVisible == isVisible) {
this
} else {
copy(
mainScreenState = mainScreenState.copy(
isVisible = isVisible,
),
)
}
}
internal fun Learn2earnState.changeGetBounsDescription(description: MainScreenState.Description): Learn2earnState {
return if (mainScreenState.description == description) {
this
} else {
copy(
mainScreenState = mainScreenState.copy(
description = description,
),
)
}
}
internal fun Learn2earnState.showDialog(dialog: MainScreenState.Dialog): Learn2earnState {
return if (mainScreenState.dialog == dialog) {
this
} else {
copy(
mainScreenState = mainScreenState.copy(
dialog = dialog,
),
)
}
}
internal fun Learn2earnState.hideDialog(): Learn2earnState {
return if (mainScreenState.dialog == null) {
this
} else {
copy(
mainScreenState = mainScreenState.copy(
dialog = null,
),
)
}
}
internal fun Learn2earnState.updateProgress(showProgress: Boolean): Learn2earnState {
return if (mainScreenState.showProgress == showProgress) {
this
} else {
copy(
mainScreenState = mainScreenState.copy(
showProgress = showProgress,
logoState = if (showProgress) {
MainScreenState.LogoState.InProgress
} else {
MainScreenState.LogoState.Idle
},
),
)
}
}

View file

@ -1,15 +1,11 @@
package com.tangem.feature.learn2earn.presentation
package com.tangem.feature.learn2earn.presentation.webView
import android.os.Bundle
import android.view.MenuItem
import android.webkit.WebResourceRequest
import android.webkit.WebView
import android.webkit.WebViewClient
import androidx.activity.viewModels
import androidx.appcompat.app.ActionBar
import androidx.appcompat.app.AppCompatActivity
import com.tangem.feature.learn2earn.domain.models.RedirectConsequences
import com.tangem.feature.learn2earn.domain.models.WebViewHelper
import com.tangem.feature.learn2earn.impl.R
import dagger.hilt.android.AndroidEntryPoint
@ -19,7 +15,7 @@ import dagger.hilt.android.AndroidEntryPoint
@AndroidEntryPoint
class Learn2earnWebViewActivity : AppCompatActivity() {
private val learn2earnViewModel by viewModels<Learn2earnViewModel>()
private val viewModel by viewModels<WebViewViewModel>()
private lateinit var actionBar: ActionBar
private lateinit var webView: WebView
@ -28,24 +24,27 @@ class Learn2earnWebViewActivity : AppCompatActivity() {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_web_view)
val webViewData = viewModel.extractData(intent)
setSupportActionBar(findViewById(R.id.toolbar))
actionBar = supportActionBar!!
actionBar.setDisplayHomeAsUpEnabled(true)
actionBar.setDisplayShowHomeEnabled(true)
actionBar.title = learn2earnViewModel.webViewUri.authority
actionBar.title = webViewData.uri.authority
webView = findViewById(R.id.web_view)
webView.settings.javaScriptEnabled = true
webView.settings.domStorageEnabled = true
webView.settings.loadsImagesAutomatically = true
webView.webViewClient = Learn2earnWebViewClient(
helper = learn2earnViewModel,
helper = viewModel,
headers = webViewData.headers,
finishSessionHandler = { finish() },
)
webView.loadUrl(
learn2earnViewModel.webViewUri.toString(),
learn2earnViewModel.getWebViewHeaders(),
webViewData.uri.toString(),
webViewData.headers,
)
}
@ -66,25 +65,9 @@ class Learn2earnWebViewActivity : AppCompatActivity() {
super.onDestroy()
}
}
private class Learn2earnWebViewClient(
private val helper: WebViewHelper,
private val finishSessionHandler: () -> Unit,
) : WebViewClient() {
override fun shouldOverrideUrlLoading(view: WebView, request: WebResourceRequest): Boolean {
when (helper.handleWebViewRedirect(request.url)) {
RedirectConsequences.NOTHING -> Unit
RedirectConsequences.PROCEED -> {
val headers = helper.getWebViewHeaders().toMutableMap()
request.requestHeaders?.let { headers.putAll(it) }
view.loadUrl(request.url.toString(), headers)
}
RedirectConsequences.FINISH_SESSION -> {
finishSessionHandler.invoke()
}
}
return true
companion object {
const val EXTRA_WEB_URI = "webViewUri"
const val EXTRA_WEB_HEADERS = "webViewHeaders"
}
}

View file

@ -0,0 +1,31 @@
package com.tangem.feature.learn2earn.presentation.webView
import android.webkit.WebResourceRequest
import android.webkit.WebView
import android.webkit.WebViewClient
import com.tangem.feature.learn2earn.domain.api.RedirectConsequences
import com.tangem.feature.learn2earn.domain.api.WebViewRedirectHandler
import timber.log.Timber
internal class Learn2earnWebViewClient(
private val helper: WebViewRedirectHandler,
private val headers: Map<String, String>,
private val finishSessionHandler: () -> Unit,
) : WebViewClient() {
override fun shouldOverrideUrlLoading(view: WebView, request: WebResourceRequest): Boolean {
Timber.d("url: ${request.url}")
when (helper.handleRedirect(request.url)) {
RedirectConsequences.PROCEED -> {
val mergedHeaders = headers.toMutableMap()
request.requestHeaders?.let { mergedHeaders.putAll(it) }
view.loadUrl(request.url.toString(), mergedHeaders)
}
RedirectConsequences.FINISH_SESSION -> {
finishSessionHandler.invoke()
}
else -> Unit
}
return true
}
}

View file

@ -0,0 +1,40 @@
package com.tangem.feature.learn2earn.presentation.webView
import android.content.Intent
import android.net.Uri
import androidx.lifecycle.ViewModel
import com.tangem.feature.learn2earn.domain.HeadersConverter
import com.tangem.feature.learn2earn.domain.api.RedirectConsequences
import com.tangem.feature.learn2earn.domain.api.WebViewRedirectHandler
import dagger.hilt.android.lifecycle.HiltViewModel
import javax.inject.Inject
/**
[REDACTED_AUTHOR]
*/
@HiltViewModel
internal class WebViewViewModel @Inject constructor(
private val handler: WebViewRedirectHandler,
) : ViewModel(), WebViewRedirectHandler {
override fun handleRedirect(uri: Uri): RedirectConsequences {
return handler.handleRedirect(uri)
}
fun extractData(intent: Intent): WebViewData {
val uriString = requireNotNull(intent.getStringExtra(Learn2earnWebViewActivity.EXTRA_WEB_URI)) {
"The Intent property EXTRA_WEB_URI is null"
}
val rawHeaders = intent.getStringArrayListExtra(Learn2earnWebViewActivity.EXTRA_WEB_HEADERS) ?: ArrayList()
return WebViewData(
uri = Uri.parse(uriString),
headers = HeadersConverter().convertBack(rawHeaders),
)
}
}
data class WebViewData(
val uri: Uri,
val headers: Map<String, String>,
)

View file

@ -10,6 +10,7 @@
android:id="@+id/app_bar"
style="@style/Widget.MaterialComponents.Toolbar.Surface"
android:layout_width="match_parent"
app:liftOnScroll="true"
android:layout_height="wrap_content">
<com.google.android.material.appbar.MaterialToolbar

View file

@ -1,9 +0,0 @@
package com.tangem.lib.auth
/**
* Provides auth for Tangem WebView
*/
interface BasicAuthProvider {
fun getCredentials(): Map<String, String>
}