Updated on 2026-08-14

This commit is contained in:
Tangem 2023-08-04 17:00:04 +03:00
commit c2b14b6515
11 changed files with 125 additions and 69 deletions

View file

@ -39,14 +39,14 @@ class HomeFragment : Fragment(), StoreSubscriber<HomeState> {
super.onCreate(savedInstanceState)
store.dispatch(HomeAction.OnCreate)
store.dispatch(HomeAction.Init)
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)
}
}
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View {
// sync adding story before screen creation
if (learn2earnViewModel.uiState.storyScreenState.isVisible) {
store.dispatch(HomeAction.InsertStory(position = 0, Stories.OneInchPromo))
homeState.value = store.state.homeState
}
return ComposeView(inflater.context).apply {
setContent {
TangemTheme {

View file

@ -20,9 +20,7 @@ import com.tangem.tap.common.extensions.eraseContext
import com.tangem.tap.common.extensions.onUserWalletSelected
import com.tangem.tap.common.redux.AppState
import com.tangem.tap.common.redux.global.GlobalAction
import com.tangem.tap.features.home.BELARUS_COUNTRY_CODE
import com.tangem.tap.features.home.RUSSIA_COUNTRY_CODE
import com.tangem.tap.features.home.redux.HomeMiddleware.BUY_WALLET_URL
import com.tangem.tap.features.home.redux.HomeMiddleware.NEW_BUY_WALLET_URL
import com.tangem.tap.features.send.redux.states.ButtonState
import com.tangem.tap.features.signin.redux.SignInAction
import com.tangem.tap.preferencesStorage
@ -40,6 +38,7 @@ object HomeMiddleware {
val handler = homeMiddleware
const val BUY_WALLET_URL = "https://tangem.com/ru/resellers/"
const val NEW_BUY_WALLET_URL = "https://buy.tangem.com/ru/"
}
private val homeMiddleware: Middleware<AppState> = { _, _ ->
@ -67,10 +66,13 @@ private fun handleHomeAction(action: Action) {
}
is HomeAction.GoToShop -> {
Analytics.send(Shop.ScreenOpened())
when (action.userCountryCode) {
RUSSIA_COUNTRY_CODE, BELARUS_COUNTRY_CODE -> store.dispatchOpenUrl(BUY_WALLET_URL)
else -> store.dispatch(NavigationAction.NavigateTo(AppScreen.Shop))
}
store.dispatchOpenUrl(NEW_BUY_WALLET_URL)
// disabled for now in task [REDACTED_JIRA]
// when (action.userCountryCode) {
// RUSSIA_COUNTRY_CODE, BELARUS_COUNTRY_CODE -> store.dispatchOpenUrl(BUY_WALLET_URL)
// else -> store.dispatch(NavigationAction.NavigateTo(AppScreen.Shop))
// }
}
}
}

View file

@ -173,7 +173,7 @@
<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 токены будут зачислены на адрес вашего кошелька в сети %s в течение 24 часов</string>
<string name="main_promotion_credited">1INCH токены будут зачислены на адрес вашего кошелька в сети %s в течение 48 часов</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>

View file

@ -170,7 +170,7 @@
<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 %s wallet address within 24 hours</string>
<string name="main_promotion_credited">1INCH tokens will be credited to your %s wallet address within 48 hours</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>

View file

@ -20,13 +20,22 @@ class InputNumberFormatter(
val thousandsSeparator = symbols.groupingSeparator
val decimalSeparator = symbols.decimalSeparator
if (text.startsWith("0") && text.length > 1 && text[1] != decimalSeparator) {
val lastChar = text.lastOrNull()
val trimmedText = if (text.isNotEmpty() && (lastChar == thousandsSeparator || lastChar == POINT_SEPARATOR)) {
text.dropLast(1) + decimalSeparator
} else {
text
}
if (trimmedText.startsWith("0") && trimmedText.length > 1 && trimmedText[1] != decimalSeparator) {
return "0"
}
val filteredChars = text.replace(thousandsSeparator.toString(), "").filterIndexed { index, c ->
val isOneOrZeroPoint = c == decimalSeparator && index != 0 && text.count { it == decimalSeparator } <= 1
val isIndexPointIndex = c == decimalSeparator && index != 0 && text.indexOf(decimalSeparator) == index
val filteredChars = trimmedText.replace(thousandsSeparator.toString(), "").filterIndexed { index, c ->
val isOneOrZeroPoint =
c == decimalSeparator && index != 0 && trimmedText.count { it == decimalSeparator } <= 1
val isIndexPointIndex =
c == decimalSeparator && index != 0 && trimmedText.indexOf(decimalSeparator) == index
c.isDigit() || isIndexPointIndex || isOneOrZeroPoint
}
// If dot is present, take first digits before decimal and first decimals digits after decimal
@ -64,5 +73,6 @@ class InputNumberFormatter(
companion object {
private const val TEXT_CHUNK_THOUSAND = 3
private const val POINT_SEPARATOR = '.'
}
}

View file

@ -38,7 +38,10 @@ internal sealed class Learn2earnEvents(
params = mapOf(AnalyticsParam.CLIENT_TYPE to clientType.value),
)
class NoticeClaimSuccess : MainScreen("Notice - Claim Successed")
class NoticeClaimSuccess(clientType: AnalyticsParam.ClientType) : MainScreen(
event = "Notice - Successful Claim",
params = mapOf(AnalyticsParam.CLIENT_TYPE to clientType.value),
)
}
sealed class PromoScreen(
@ -65,6 +68,6 @@ internal sealed class AnalyticsParam {
}
companion object Key {
const val CLIENT_TYPE = "Client type"
const val CLIENT_TYPE = "Client Type"
}
}

View file

@ -29,7 +29,7 @@ import timber.log.Timber
/**
[REDACTED_AUTHOR]
*/
@Suppress("LongParameterList")
@Suppress("LongParameterList", "LargeClass")
internal class DefaultLearn2earnInteractor(
private val featureToggleManager: Learn2earnFeatureToggleManager,
private val repository: Learn2earnRepository,
@ -43,19 +43,22 @@ internal class DefaultLearn2earnInteractor(
override var webViewResultHandler: WebViewResultHandler? = null
private val promoUserData: PromoUserData
get() = repository.getUserData()
private lateinit var promotion: Promotion
private val scope = CoroutineScope(Job() + dispatchers.io + FeatureCoroutineExceptionHandler.create("mainScope"))
private val isTangemWalletFlow = MutableStateFlow(false)
private var isTangemWalletFlowJob: Job? = null
private var isTangemWalletFlowSync = false
private var isTangemWalletSync = false
private val webViewUriBuilder: WebViewUriBuilder by lazy {
WebViewUriBuilder(
authCredentialsProvider = dependencyProvider.getWebViewAuthCredentialsProvider(),
localeLanguageProvider = dependencyProvider.getLocaleProvider(),
promoCodeProvider = { repository.getUserData().promoCode },
promoCodeProvider = { promoUserData.promoCode },
)
}
@ -70,12 +73,13 @@ internal class DefaultLearn2earnInteractor(
override fun getIsTangemWalletFlow(): Flow<Boolean> = isTangemWalletFlow
override fun isUserHadPromoCode(): Boolean {
return repository.getUserData().promoCode != null
return promoUserData.promoCode != null
}
override suspend fun validateUserWallet(): Result<Unit> {
val promoCode = repository.getUserData().promoCode
val promoCode = promoUserData.promoCode
val userWalletId = userWalletManager.getWalletId()
if (userWalletId.isEmpty()) return Result.failure(PromotionError.EmptyUserWalletId)
val domainError = if (promoCode == null) {
repository.validate(userWalletId).error
@ -92,14 +96,13 @@ internal class DefaultLearn2earnInteractor(
}
override fun isUserRegisteredInPromotion(): Boolean {
return repository.getUserData().isRegisteredInPromotion
return promoUserData.isRegisteredInPromotion
}
override fun getAwardAmount(): Int = try {
val promoCode = repository.getUserData().promoCode
promotion.getPromotionInfo().getData(promoCode).award.toInt()
promotion.getPromotionInfo().getCardData().award.toInt()
} catch (ex: NullPointerException) {
Timber.e(ex)
Timber.w(ex)
0
}
@ -114,7 +117,7 @@ internal class DefaultLearn2earnInteractor(
?: return Result.failure(PromotionError.UnknownError("Currency for award is null"))
val walletId = userWalletManager.getWalletId()
val promoCode = repository.getUserData().promoCode
val promoCode = promoUserData.promoCode
val domainError = if (promoCode == null) {
requestAward(walletId, awardCurrency)
@ -199,11 +202,11 @@ internal class DefaultLearn2earnInteractor(
}
override fun buildUriForNewUser(): Uri {
return webViewUriBuilder.buildUriForNewUser(repository.getUserData().isLearningStageFinished)
return webViewUriBuilder.buildUriForNewUser(promoUserData.isLearningStageFinished)
}
override fun buildUriForOldUser(): Uri {
return webViewUriBuilder.buildUriForOldUser(repository.getUserData().isLearningStageFinished)
return webViewUriBuilder.buildUriForOldUser(promoUserData.isLearningStageFinished)
}
override fun getBasicAuthHeaders(): ArrayList<String> {
@ -251,8 +254,8 @@ internal class DefaultLearn2earnInteractor(
val isActive = when {
demoModeDatasource.isDemoModeActive -> false
!featureToggleManager.isLearn2earnEnabled -> false
repository.getUserData().isAlreadyReceivedAward -> false
else -> !promotion.isError()
promoUserData.isAlreadyReceivedAward -> false
else -> promotion.isReadyToUse()
}
return isActive
@ -267,10 +270,18 @@ internal class DefaultLearn2earnInteractor(
}
override fun isPromotionActiveOnMain(): Boolean {
return when {
!isPromotionActive() -> false
!isTangemWalletFlowSync -> false
else -> promotion.getPromotionInfo().oldCard.status == PromotionInfoResponse.Status.ACTIVE
if (!isPromotionActive() || !isTangemWalletSync) return false
val promotionInfo = promotion.getPromotionInfo()
return if (isUseLogicForOldCards()) {
promotionInfo.oldCard.status == PromotionInfoResponse.Status.ACTIVE
} else {
when (promotionInfo.newCard.status) {
PromotionInfoResponse.Status.PENDING -> !promoUserData.isAlreadyReceivedAward
PromotionInfoResponse.Status.ACTIVE -> true
// disable promotion for a new user only if they have received an award
PromotionInfoResponse.Status.FINISHED -> !promoUserData.isAlreadyReceivedAward
}
}
}
@ -325,25 +336,35 @@ internal class DefaultLearn2earnInteractor(
)
}
private fun updateUserData(updateBlock: (PromoUserData) -> PromoUserData): PromoUserData {
return updateBlock(repository.getUserData()).apply {
repository.updateUserData(this)
}
}
private fun subscribeToCardTypeResolverFlow() {
if (isTangemWalletFlowJob == null || isTangemWalletFlowJob?.isCancelled == true) {
isTangemWalletFlowJob = dependencyProvider.getCardTypeResolverFlow()
.onEach {
isTangemWalletFlowSync = it?.isTangemWallet() ?: false
isTangemWalletFlow.emit(isTangemWalletFlowSync)
val isTangemWallet = it?.isTangemWallet() ?: false
isTangemWalletSync = isTangemWallet
isTangemWalletFlow.emit(isTangemWallet)
}
.launchIn(scope)
}
}
private fun Promotion.PromotionInfo.getData(promoCode: String?): PromotionInfoResponse.Data {
return if (promoCode == null) {
// region Utils
private fun updateUserData(updateBlock: (PromoUserData) -> PromoUserData): PromoUserData {
return updateBlock(promoUserData).apply {
repository.updateUserData(this)
}
}
private fun isUseLogicForOldCards(): Boolean {
return promoUserData.promoCode == null
}
private fun Promotion.isReadyToUse(): Boolean {
return info != null
}
private fun Promotion.PromotionInfo.getCardData(): PromotionInfoResponse.Data {
return if (isUseLogicForOldCards()) {
oldCard
} else {
newCard
@ -360,4 +381,5 @@ internal class DefaultLearn2earnInteractor(
WebViewResult.Empty -> WebViewAction.PROCEED
}
}
// endregion Utils
}

View file

@ -13,8 +13,6 @@ internal data class Promotion(
@Throws(NullPointerException::class)
fun getPromotionInfo(): PromotionInfo = info!!
fun isError(): Boolean = error != null
data class PromotionInfo(
val newCard: PromotionInfoResponse.Data,
val oldCard: PromotionInfoResponse.Data,

View file

@ -20,6 +20,7 @@ sealed class PromotionError(val code: Int, val description: String) : Throwable(
class ProgramWasEnd(code: Int, description: String) : PromotionError(code, description)
class UnknownError(description: String) : PromotionError(code = -1, description)
object EmptyUserWalletId : PromotionError(code = -1, "UserWalletId is empty")
}
@Suppress("MagicNumber")

View file

@ -55,12 +55,10 @@ class Learn2earnViewModel @Inject constructor(
viewModelScope.launch(dispatchers.io) {
interactor.getIsTangemWalletFlow()
.collect { isTangemWallet ->
updateUi {
uiState
.updateGetBonusVisibility(
isVisible = isTangemWallet && interactor.isPromotionActiveOnMain(),
)
.changeGetBonusDescription(getBonusDescription())
if (isTangemWallet) {
updateMainScreenViews()
} else {
updateUi { uiState.updateGetBonusVisibility(isVisible = false) }
}
}
}
@ -103,7 +101,7 @@ class Learn2earnViewModel @Inject constructor(
)
}
.onFailure {
Timber.e(it)
Timber.w(it)
updateUi { uiState.updateGetBonusVisibility(isVisible = false) }
}
}
@ -119,7 +117,9 @@ class Learn2earnViewModel @Inject constructor(
-> {
updateUi { uiState.updateViewsVisibility(isVisible = false) }
}
is PromotionError.CodeWasNotAppliedInShop -> {
is PromotionError.CodeWasNotAppliedInShop,
is PromotionError.EmptyUserWalletId,
-> {
updateUi { uiState.updateGetBonusVisibility(isVisible = false) }
}
is PromotionError.CodeNotFound,
@ -141,11 +141,11 @@ class Learn2earnViewModel @Inject constructor(
}
private fun onButtonMainClick() {
if (interactor.isUserHadPromoCode() || interactor.isUserRegisteredInPromotion()) {
analytics.send(Learn2earnEvents.MainScreen.NoticeLear2earn(AnalyticsParam.ClientType.Old()))
val isUserHasPromo = interactor.isUserHadPromoCode()
analytics.send(Learn2earnEvents.MainScreen.NoticeLear2earn(getAnalyticsClientType(isUserHasPromo)))
if (isUserHasPromo || interactor.isUserRegisteredInPromotion()) {
requestAward()
} else {
analytics.send(Learn2earnEvents.MainScreen.NoticeLear2earn(AnalyticsParam.ClientType.New()))
subscribeToWebViewResultEvents()
router.openWebView(interactor.buildUriForOldUser(), interactor.getBasicAuthHeaders())
}
@ -159,7 +159,13 @@ class Learn2earnViewModel @Inject constructor(
.onSuccess { requestAwardResult ->
requestAwardResult.fold(
onSuccess = {
analytics.send(Learn2earnEvents.MainScreen.NoticeClaimSuccess())
analytics.send(
Learn2earnEvents.MainScreen.NoticeClaimSuccess(
clientType = getAnalyticsClientType(
isUserHasPromo = interactor.isUserHadPromoCode(),
),
),
)
val onHideDialog = {
uiState = uiState.updateViewsVisibility(isVisible = false)
.hideDialog()
@ -173,9 +179,13 @@ class Learn2earnViewModel @Inject constructor(
},
onFailure = {
val errorDialog = createErrorDialog(it.toDomainError())
updateUi {
uiState.updateProgress(showProgress = false)
.showDialog(errorDialog)
if (errorDialog == null) {
updateUi { uiState.updateProgress(showProgress = false) }
} else {
updateUi {
uiState.updateProgress(showProgress = false)
.showDialog(errorDialog)
}
}
},
)
@ -184,8 +194,9 @@ class Learn2earnViewModel @Inject constructor(
updateUi { uiState.updateProgress(showProgress = false) }
if (exception is UserCancelledException) return@launch
val errorDialog = createErrorDialog(exception.toDomainError())
updateUi { uiState.showDialog(errorDialog) }
createErrorDialog(exception.toDomainError())?.let {
updateUi { uiState.showDialog(it) }
}
}
}
}
@ -198,7 +209,15 @@ class Learn2earnViewModel @Inject constructor(
}
}
private fun createErrorDialog(error: PromotionError): MainScreenState.Dialog {
private fun getAnalyticsClientType(isUserHasPromo: Boolean): AnalyticsParam.ClientType {
return if (isUserHasPromo) {
AnalyticsParam.ClientType.New()
} else {
AnalyticsParam.ClientType.Old()
}
}
private fun createErrorDialog(error: PromotionError): MainScreenState.Dialog? {
return when (error) {
is PromotionError.CodeWasNotAppliedInShop -> {
val onHideDialog = { uiState = uiState.hideDialog() }
@ -237,6 +256,7 @@ class Learn2earnViewModel @Inject constructor(
onDismissRequest = onHideDialog,
)
}
is PromotionError.EmptyUserWalletId -> null
}
}

View file

@ -19,7 +19,7 @@ interface UserWalletManager {
fun getNativeTokenForNetwork(networkId: String): Currency
/**
* Returns user walletId
* Returns user walletId or empty string
*/
fun getWalletId(): String