Updated on 2026-08-14

This commit is contained in:
Tangem 2023-06-19 08:51:46 +03:00
parent 7e765758b4
commit efe271e1a0
24 changed files with 751 additions and 75 deletions

View file

@ -138,6 +138,8 @@
android:name="com.tangem.tap.features.sprinklr.ui.SprinklrActivity" android:name="com.tangem.tap.features.sprinklr.ui.SprinklrActivity"
android:theme="@style/AppTheme" /> android:theme="@style/AppTheme" />
<activity android:name="com.tangem.feature.learn2earn.presentation.Learn2earnWebViewActivity"/>
<provider <provider
android:name="androidx.core.content.FileProvider" android:name="androidx.core.content.FileProvider"
android:authorities="${applicationId}.provider" android:authorities="${applicationId}.provider"

View file

@ -1,59 +1,67 @@
package com.tangem.tap package com.tangem.tap
import android.app.* import android.app.Application
import android.content.* import android.content.Context
import android.content.pm.* import android.content.pm.PackageManager
import coil.* import coil.ImageLoader
import com.tangem.* import coil.ImageLoaderFactory
import com.tangem.blockchain.common.* import com.tangem.Log
import com.tangem.blockchain.network.* import com.tangem.LogFormat
import com.tangem.core.analytics.* import com.tangem.blockchain.common.BlockchainSdkConfig
import com.tangem.core.featuretoggle.manager.* import com.tangem.blockchain.common.WalletManagerFactory
import com.tangem.data.source.preferences.* import com.tangem.blockchain.network.BlockchainSdkRetrofitBuilder
import com.tangem.datasource.api.common.* import com.tangem.core.analytics.Analytics
import com.tangem.datasource.asset.* import com.tangem.core.featuretoggle.manager.FeatureTogglesManager
import com.tangem.datasource.config.* import com.tangem.data.source.preferences.PreferencesDataSource
import com.tangem.datasource.config.models.* import com.tangem.datasource.api.common.MoshiConverter
import com.tangem.datasource.connection.* import com.tangem.datasource.asset.AssetReader
import com.tangem.domain.* import com.tangem.datasource.config.ConfigManager
import com.tangem.domain.common.* import com.tangem.datasource.config.FeaturesLocalLoader
import com.tangem.features.wallet.featuretoggles.* import com.tangem.datasource.config.models.Config
import com.tangem.tap.common.* import com.tangem.datasource.connection.NetworkConnectionManager
import com.tangem.tap.common.analytics.* import com.tangem.domain.DomainLayer
import com.tangem.tap.common.analytics.api.* import com.tangem.domain.common.LogConfig
import com.tangem.tap.common.analytics.handlers.amplitude.* import com.tangem.feature.learn2earn.domain.api.Learn2earnInteractor
import com.tangem.tap.common.analytics.handlers.appsFlyer.* import com.tangem.features.wallet.featuretoggles.WalletFeatureToggles
import com.tangem.tap.common.analytics.handlers.firebase.* import com.tangem.tap.common.analytics.AnalyticsFactory
import com.tangem.tap.common.analytics.topup.* import com.tangem.tap.common.analytics.api.AnalyticsHandlerBuilder
import com.tangem.tap.common.chat.* import com.tangem.tap.common.analytics.handlers.amplitude.AmplitudeAnalyticsHandler
import com.tangem.tap.common.feedback.* import com.tangem.tap.common.analytics.handlers.appsFlyer.AppsFlyerAnalyticsHandler
import com.tangem.tap.common.images.* import com.tangem.tap.common.analytics.handlers.firebase.FirebaseAnalyticsHandler
import com.tangem.tap.common.log.* import com.tangem.tap.common.analytics.topup.TopUpController
import com.tangem.tap.common.redux.* import com.tangem.tap.common.chat.ChatManager
import com.tangem.tap.common.redux.global.* import com.tangem.tap.common.feedback.AdditionalFeedbackInfo
import com.tangem.tap.common.shop.* import com.tangem.tap.common.feedback.FeedbackManager
import com.tangem.tap.domain.configurable.warningMessage.* import com.tangem.tap.common.images.createCoilImageLoader
import com.tangem.tap.domain.tokens.* import com.tangem.tap.common.log.TangemLogCollector
import com.tangem.tap.domain.totalBalance.* import com.tangem.tap.common.redux.AppState
import com.tangem.tap.domain.totalBalance.di.* import com.tangem.tap.common.redux.appReducer
import com.tangem.tap.domain.walletCurrencies.* import com.tangem.tap.common.redux.global.GlobalAction
import com.tangem.tap.domain.walletCurrencies.di.* import com.tangem.tap.common.shop.TangemShopService
import com.tangem.tap.domain.walletStores.* import com.tangem.tap.domain.configurable.warningMessage.WarningMessagesManager
import com.tangem.tap.domain.walletStores.di.* import com.tangem.tap.domain.tokens.UserTokensRepository
import com.tangem.tap.domain.walletStores.repository.* import com.tangem.tap.domain.totalBalance.TotalFiatBalanceCalculator
import com.tangem.tap.domain.walletStores.repository.di.* import com.tangem.tap.domain.totalBalance.di.provideDefaultImplementation
import com.tangem.tap.domain.walletCurrencies.WalletCurrenciesManager
import com.tangem.tap.domain.walletCurrencies.di.provideDefaultImplementation
import com.tangem.tap.domain.walletStores.WalletStoresManager
import com.tangem.tap.domain.walletStores.di.provideDefaultImplementation
import com.tangem.tap.domain.walletStores.repository.WalletAmountsRepository
import com.tangem.tap.domain.walletStores.repository.WalletManagersRepository
import com.tangem.tap.domain.walletStores.repository.WalletStoresRepository
import com.tangem.tap.domain.walletStores.repository.di.provideDefaultImplementation
import com.tangem.tap.domain.walletconnect.WalletConnectRepository import com.tangem.tap.domain.walletconnect.WalletConnectRepository
import com.tangem.tap.domain.walletconnect2.domain.* import com.tangem.tap.domain.walletconnect2.domain.WalletConnectSessionsRepository
import com.tangem.tap.features.customtoken.api.featuretoggles.* import com.tangem.tap.features.customtoken.api.featuretoggles.CustomTokenFeatureToggles
import com.tangem.tap.proxy.* import com.tangem.tap.proxy.AppStateHolder
import com.tangem.tap.proxy.redux.* import com.tangem.tap.proxy.redux.DaggerGraphState
import com.tangem.wallet.BuildConfig import com.tangem.wallet.BuildConfig
import dagger.hilt.android.* import dagger.hilt.android.HiltAndroidApp
import kotlinx.coroutines.* import kotlinx.coroutines.runBlocking
import okhttp3.logging.* import okhttp3.logging.HttpLoggingInterceptor
import org.rekotlin.* import org.rekotlin.Store
import timber.log.* import timber.log.Timber
import javax.inject.* import javax.inject.Inject
import com.tangem.tap.domain.walletconnect2.domain.WalletConnectRepository as WalletConnect2Repository import com.tangem.tap.domain.walletconnect2.domain.WalletConnectRepository as WalletConnect2Repository
lateinit var store: Store<AppState> lateinit var store: Store<AppState>
@ -136,6 +144,9 @@ class TapApplication : Application(), ImageLoaderFactory {
@Inject @Inject
lateinit var walletConnectSessionsRepository: WalletConnectSessionsRepository lateinit var walletConnectSessionsRepository: WalletConnectSessionsRepository
@Inject
lateinit var learn2ernInteractor: Learn2earnInteractor
override fun onCreate() { override fun onCreate() {
super.onCreate() super.onCreate()
@ -189,8 +200,9 @@ class TapApplication : Application(), ImageLoaderFactory {
appStateHolder.userTokensRepository = userTokensRepository appStateHolder.userTokensRepository = userTokensRepository
appStateHolder.walletStoresManager = walletStoresManager appStateHolder.walletStoresManager = walletStoresManager
scope.launch { runBlocking {
featureTogglesManager.init() featureTogglesManager.init()
learn2ernInteractor.init()
} }
initTopUpController() initTopUpController()

View file

@ -12,26 +12,38 @@ import androidx.compose.ui.platform.ComposeView
import androidx.core.view.WindowCompat import androidx.core.view.WindowCompat
import androidx.core.view.WindowInsetsControllerCompat import androidx.core.view.WindowInsetsControllerCompat
import androidx.fragment.app.Fragment import androidx.fragment.app.Fragment
import androidx.fragment.app.viewModels
import com.tangem.core.analytics.Analytics import com.tangem.core.analytics.Analytics
import com.tangem.core.ui.res.TangemTheme import com.tangem.core.ui.res.TangemTheme
import com.tangem.feature.learn2earn.presentation.Learn2earnViewModel
import com.tangem.tap.common.analytics.events.IntroductionProcess import com.tangem.tap.common.analytics.events.IntroductionProcess
import com.tangem.tap.common.redux.navigation.AppScreen import com.tangem.tap.common.redux.navigation.AppScreen
import com.tangem.tap.common.redux.navigation.NavigationAction import com.tangem.tap.common.redux.navigation.NavigationAction
import com.tangem.tap.features.home.compose.StoriesScreen import com.tangem.tap.features.home.compose.StoriesScreen
import com.tangem.tap.features.home.redux.HomeAction import com.tangem.tap.features.home.redux.HomeAction
import com.tangem.tap.features.home.redux.HomeState import com.tangem.tap.features.home.redux.HomeState
import com.tangem.tap.features.home.redux.Stories
import com.tangem.tap.features.tokens.legacy.redux.TokensAction import com.tangem.tap.features.tokens.legacy.redux.TokensAction
import com.tangem.tap.store import com.tangem.tap.store
import dagger.hilt.android.AndroidEntryPoint
import org.rekotlin.StoreSubscriber import org.rekotlin.StoreSubscriber
@AndroidEntryPoint
class HomeFragment : Fragment(), StoreSubscriber<HomeState> { class HomeFragment : Fragment(), StoreSubscriber<HomeState> {
private var homeState: MutableState<HomeState> = mutableStateOf(store.state.homeState) private var homeState: MutableState<HomeState> = mutableStateOf(store.state.homeState)
private val learn2earnViewModel by viewModels<Learn2earnViewModel>()
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
store.dispatch(HomeAction.OnCreate) store.dispatch(HomeAction.OnCreate)
store.dispatch(HomeAction.Init) store.dispatch(HomeAction.Init)
if (learn2earnViewModel.isNeedToShowViewOnStoriesScreen()) {
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 { override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View {
@ -79,6 +91,9 @@ class HomeFragment : Fragment(), StoreSubscriber<HomeState> {
private fun ScreenContent() { private fun ScreenContent() {
StoriesScreen( StoriesScreen(
homeState, homeState,
onLearn2earnClick = {
learn2earnViewModel.onStoriesClick()
},
onScanButtonClick = { onScanButtonClick = {
Analytics.send(IntroductionProcess.ButtonScanCard()) Analytics.send(IntroductionProcess.ButtonScanCard())
store.dispatch(HomeAction.ReadCard()) store.dispatch(HomeAction.ReadCard())

View file

@ -26,7 +26,7 @@ import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp import androidx.compose.ui.unit.sp
import com.google.accompanist.systemuicontroller.rememberSystemUiController import com.google.accompanist.systemuicontroller.rememberSystemUiController
import com.tangem.core.ui.res.TangemTheme import com.tangem.core.ui.res.TangemTheme
import com.tangem.feature.learn2earn.presentation.ui.OneInchStoriesScreen import com.tangem.feature.learn2earn.presentation.ui.StoriesScreen
import com.tangem.tap.features.home.compose.content.* 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.HomeButtons
import com.tangem.tap.features.home.compose.views.StoriesProgressBar import com.tangem.tap.features.home.compose.views.StoriesProgressBar
@ -39,6 +39,7 @@ import kotlin.math.max
@Composable @Composable
fun StoriesScreen( fun StoriesScreen(
homeState: MutableState<HomeState>, homeState: MutableState<HomeState>,
onLearn2earnClick: () -> Unit,
onScanButtonClick: () -> Unit, onScanButtonClick: () -> Unit,
onShopButtonClick: () -> Unit, onShopButtonClick: () -> Unit,
onSearchTokensClick: () -> Unit, onSearchTokensClick: () -> Unit,
@ -152,7 +153,7 @@ fun StoriesScreen(
colorFilter = if (currentStory.isDarkBackground) null else ColorFilter.tint(Color.Black), colorFilter = if (currentStory.isDarkBackground) null else ColorFilter.tint(Color.Black),
) )
when (currentStory) { when (currentStory) {
Stories.OneInchPromo -> OneInchStoriesScreen({}) Stories.OneInchPromo -> StoriesScreen(onLearn2earnClick)
Stories.TangemIntro -> FirstStoriesContent(isPaused, currentStory.duration) { hideContent.value = it } Stories.TangemIntro -> FirstStoriesContent(isPaused, currentStory.duration) { hideContent.value = it }
Stories.RevolutionaryWallet -> StoriesRevolutionaryWallet(currentStory.duration) Stories.RevolutionaryWallet -> StoriesRevolutionaryWallet(currentStory.duration)
Stories.UltraSecureBackup -> StoriesUltraSecureBackup(isPaused, currentStory.duration) Stories.UltraSecureBackup -> StoriesUltraSecureBackup(isPaused, currentStory.duration)
@ -215,6 +216,7 @@ fun StoriesScreen(
@Composable @Composable
private fun StoriesScreenPreview() { private fun StoriesScreenPreview() {
StoriesScreen( StoriesScreen(
onLearn2earnClick = {},
onScanButtonClick = {}, onScanButtonClick = {},
onShopButtonClick = {}, onShopButtonClick = {},
onSearchTokensClick = {}, onSearchTokensClick = {},

View file

@ -11,6 +11,8 @@ sealed class HomeAction : Action {
object OnCreate : HomeAction() object OnCreate : HomeAction()
object Init : HomeAction() object Init : HomeAction()
data class InsertStory(val position: Int, val story: Stories) : HomeAction()
data class ReadCard( data class ReadCard(
val analyticsEvent: AnalyticsEvent? = Basic.CardWasScanned(AnalyticsParam.ScannedFrom.Introduction), val analyticsEvent: AnalyticsEvent? = Basic.CardWasScanned(AnalyticsParam.ScannedFrom.Introduction),
) : HomeAction() ) : HomeAction()

View file

@ -12,6 +12,13 @@ private fun internalReduce(action: Action, state: AppState): HomeState {
var state = state.homeState var state = state.homeState
when (action) { when (action) {
is HomeAction.InsertStory -> {
state = state.copy(
stories = state.stories.toMutableList().apply {
add(action.position, action.story)
},
)
}
is HomeAction.ScanInProgress -> { is HomeAction.ScanInProgress -> {
state = state.copy(scanInProgress = action.scanInProgress) state = state.copy(scanInProgress = action.scanInProgress)
} }

View file

@ -11,7 +11,8 @@ data class HomeState(
val stories: List<Stories> = initDefaultStories(), val stories: List<Stories> = initDefaultStories(),
) : StateType { ) : StateType {
val firstStory: Stories = stories[0] val firstStory: Stories
get() = stories[0]
val btnScanStateInProgress: Boolean val btnScanStateInProgress: Boolean
get() = btnScanState.progressState == ProgressState.Loading get() = btnScanState.progressState == ProgressState.Loading
@ -20,7 +21,6 @@ data class HomeState(
companion object { companion object {
fun initDefaultStories(): List<Stories> = listOf( fun initDefaultStories(): List<Stories> = listOf(
Stories.OneInchPromo,
Stories.TangemIntro, Stories.TangemIntro,
Stories.RevolutionaryWallet, Stories.RevolutionaryWallet,
Stories.UltraSecureBackup, Stories.UltraSecureBackup,

View file

@ -1,8 +1,10 @@
package com.tangem.tap.features.welcome.redux package com.tangem.tap.features.welcome.redux
import android.content.Intent
import com.tangem.common.*
import com.tangem.common.core.TangemSdkError import com.tangem.common.core.TangemSdkError
import com.tangem.common.doOnFailure
import com.tangem.common.doOnResult
import com.tangem.common.doOnSuccess
import com.tangem.common.flatMap
import com.tangem.domain.models.scan.ScanResponse import com.tangem.domain.models.scan.ScanResponse
import com.tangem.tap.* import com.tangem.tap.*
import com.tangem.tap.common.analytics.events.AnalyticsParam import com.tangem.tap.common.analytics.events.AnalyticsParam
@ -17,7 +19,6 @@ import com.tangem.tap.domain.model.builders.UserWalletBuilder
import com.tangem.tap.domain.scanCard.ScanCardProcessor import com.tangem.tap.domain.scanCard.ScanCardProcessor
import com.tangem.tap.domain.userWalletList.unlockIfLockable import com.tangem.tap.domain.userWalletList.unlockIfLockable
import com.tangem.tap.features.intentHandler.handlers.WalletConnectLinkIntentHandler import com.tangem.tap.features.intentHandler.handlers.WalletConnectLinkIntentHandler
import com.tangem.tap.features.onboarding.products.wallet.saltPay.message.SaltPayActivationError
import com.tangem.tap.features.signin.redux.SignInAction import com.tangem.tap.features.signin.redux.SignInAction
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import org.rekotlin.Middleware import org.rekotlin.Middleware
@ -98,15 +99,6 @@ internal class WelcomeMiddleware {
} }
} }
private fun handleInitialIntent(intent: Intent?) {
val isBackgroundScanNotHandled = !intentHandler.handleBackgroundScan(intent, hasSavedUserWallets = true)
val hasNotIncompletedBackup = !backupService.hasIncompletedBackup
if (isBackgroundScanNotHandled && hasNotIncompletedBackup) {
store.dispatchOnMain(WelcomeAction.ProceedWithBiometrics)
}
}
private suspend inline fun scanCardInternal(crossinline onCardScanned: suspend (ScanResponse) -> Unit) { private suspend inline fun scanCardInternal(crossinline onCardScanned: suspend (ScanResponse) -> Unit) {
tangemSdkManager.setAccessCodeRequestPolicy( tangemSdkManager.setAccessCodeRequestPolicy(
useBiometricsForAccessCode = preferencesStorage.shouldSaveAccessCodes, useBiometricsForAccessCode = preferencesStorage.shouldSaveAccessCodes,

View file

@ -46,8 +46,8 @@
android:id="@+id/cl_wallet" android:id="@+id/cl_wallet"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:clipToPadding="false"
android:clipChildren="false" android:clipChildren="false"
android:clipToPadding="false"
android:paddingBottom="92dp"> android:paddingBottom="92dp">
<ImageView <ImageView
@ -113,6 +113,15 @@
android:paddingTop="8dp" android:paddingTop="8dp"
app:layout_constraintTop_toBottomOf="@id/rv_warning_messages"> app:layout_constraintTop_toBottomOf="@id/rv_warning_messages">
<FrameLayout
android:id="@+id/fl_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" />
<include <include
android:id="@+id/l_wallet_rescan_warning" android:id="@+id/l_wallet_rescan_warning"
layout="@layout/layout_wallet_rescan_warning" layout="@layout/layout_wallet_rescan_warning"
@ -219,8 +228,8 @@
android:id="@+id/btn_add_token" android:id="@+id/btn_add_token"
style="@style/TapPrimaryButton" style="@style/TapPrimaryButton"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_marginTop="8dp"
android:layout_marginStart="16dp" android:layout_marginStart="16dp"
android:layout_marginTop="8dp"
android:layout_marginEnd="16dp" android:layout_marginEnd="16dp"
android:text="@string/main_manage_tokens" android:text="@string/main_manage_tokens"
android:visibility="gone" android:visibility="gone"

View file

@ -0,0 +1,10 @@
package com.tangem.utils.extensions
import java.lang.ref.WeakReference
/**
[REDACTED_AUTHOR]
*/
fun <T> T.toWeakReference(): WeakReference<T> {
return WeakReference(this)
}

View file

@ -0,0 +1,123 @@
package com.tangem.feature.learn2earn.domain
import android.net.Uri
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.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
/**
[REDACTED_AUTHOR]
*/
class DefaultLearn2earnInteractor(
private val repository: Learn2earnRepository,
basicAuthProvider: BasicAuthProvider,
userCountryCodeProvider: () -> String,
) : Learn2earnInteractor {
private val webViewUriBuilder = WebViewUriBuilder(
basicAuthProvider = basicAuthProvider,
userCountryCodeProvider = userCountryCodeProvider,
promoCodeProvider = { repository.getPromoCode() },
promoNameProvider = { repository.getProgramName() },
)
private lateinit var promotion: Promotion
override suspend fun init() {
initPromotionInfo()
}
override fun isNeedToShowViewOnStoriesScreen(): Boolean {
return promotionIsActive()
}
override fun getBasicAuthHeaders(): Map<String, String> {
return webViewUriBuilder.getBasicAuthHeaders()
}
override suspend fun isNeedToShowViewOnWalletScreen(walletId: String): Boolean {
val response = repository.validate(walletId)
return if (response.valid == false && response.isError()) {
false
} else {
promotionIsActive()
}
}
override fun buildUriForStories(): Uri {
val type = if (repository.isHadActivatedCards()) {
CardType.NEW
} else {
CardType.EXISTED
}
return webViewUriBuilder.buildUriForStories(type)
}
override fun buildUriForMainPage(walletId: String, cardId: String, cardPubKey: String): Uri {
return webViewUriBuilder.buildUriForMainPage(walletId, cardId, cardPubKey)
}
override fun handleWebViewRedirect(uri: Uri): RedirectConsequences {
if (webViewUriBuilder.isReadyForExistedCardAwardRedirect(uri)) {
return RedirectConsequences.FINISH_SESSION
}
return if (webViewUriBuilder.isPromoCodeRedirect(uri)) {
webViewUriBuilder.extractPromoCode(uri)?.let { repository.savePromoCode(it) }
RedirectConsequences.NOTHING
} else {
RedirectConsequences.PROCEED
}
}
private fun promotionIsActive(): Boolean {
val isActive = when {
repository.isAlreadyReceivedAward() -> false
promotion.isError() -> false
else -> promotion.getInfo().status == PromotionInfoResponse.Status.ACTIVE
}
return isActive
}
private suspend fun initPromotionInfo() {
// return Promotion.dummyActive()
promotion = repository.getPromotionInfo()
.fold(
onSuccess = { response ->
val responseError = response.error
if (responseError == null) {
Promotion(
info = Promotion.PromotionInfo(
status = response.status!!,
awardForNewCard = response.awardForNewCard!!,
awardForOldCard = response.awardForOldCard!!,
awardPaymentToken = response.awardPaymentToken!!,
),
error = null,
)
} else {
Promotion(
info = null,
error = responseError.toDomainError(),
)
}
},
onFailure = {
Promotion(
info = null,
error = PromotionError.NetworkUnreachable,
)
},
)
}
}

View file

@ -0,0 +1,98 @@
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,
private val userCountryCodeProvider: () -> String,
private val promoCodeProvider: () -> String?,
private val promoNameProvider: () -> String,
) {
fun buildUriForStories(type: CardType): Uri {
val builder = makeWebViewUriBuilder()
.appendQueryParameter("type", type.typeName)
promoCodeProvider.invoke()?.let {
builder.appendQueryParameter("code", it)
}
return builder.build()
}
fun buildUriForMainPage(walletId: String, cardId: String, cardPubKey: String): 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)
}
return builder.build()
}
fun getBasicAuthHeaders(): Map<String, String> {
return basicAuthProvider.getCredentials()?.let {
mapOf("Authorization" to "Basic $it")
} ?: mapOf()
}
fun isPromoCodeRedirect(uri: Uri): Boolean {
return uri.toString().contains(SUFFIX_CODE_CREATED)
}
fun isReadyForExistedCardAwardRedirect(uri: Uri): Boolean {
return uri.toString().endsWith(SUFFIX_READY_FOR_AWARD)
}
fun extractPromoCode(uri: Uri): String? {
val url = uri.toString()
return if (isPromoCodeRedirect(uri)) {
val replaceString = makeWebViewUriBuilder().build().toString() + SUFFIX_CODE_CREATED
val code = url.replace(replaceString, "")
code
} else {
null
}
}
private fun makeWebViewUriBuilder(): Uri.Builder {
val builder = Uri.Builder()
builder.scheme("https")
if (BuildConfig.DEBUG) {
builder.authority(DEV_WEB_VIEW_BASE_URL)
builder.appendPath(userCountryCodeProvider.invoke())
builder.appendPath("promotion-test")
} else {
builder.authority(WEB_VIEW_BASE_URL)
builder.appendPath(userCountryCodeProvider.invoke())
builder.appendPath("promotion")
}
return builder
}
private companion object {
const val WEB_VIEW_BASE_URL = "tangem.com"
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 SUFFIX_CODE_CREATED = "/code-created?code="
const val SUFFIX_READY_FOR_AWARD = "/ready-for-exsited-card-award"
}
}

View file

@ -0,0 +1,18 @@
package com.tangem.feature.learn2earn.domain.api
import android.net.Uri
import com.tangem.feature.learn2earn.domain.models.RedirectConsequences
/**
[REDACTED_AUTHOR]
*/
interface Learn2earnInteractor {
suspend fun init()
fun isNeedToShowViewOnStoriesScreen(): Boolean
suspend fun isNeedToShowViewOnWalletScreen(walletId: String): Boolean
fun getBasicAuthHeaders(): Map<String, String>
fun buildUriForStories(): Uri
fun buildUriForMainPage(walletId: String, cardId: String, cardPubKey: String): Uri
fun handleWebViewRedirect(uri: Uri): RedirectConsequences
}

View file

@ -0,0 +1,39 @@
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.Learn2earnInteractor
import com.tangem.feature.learn2earn.presentation.Learn2earnRouter
import com.tangem.lib.auth.BasicAuthProvider
import com.tangem.utils.extensions.toWeakReference
import dagger.Module
import dagger.Provides
import dagger.hilt.InstallIn
import dagger.hilt.android.qualifiers.ApplicationContext
import dagger.hilt.components.SingletonComponent
import javax.inject.Singleton
@Module
@InstallIn(SingletonComponent::class)
class Learn2earnDomainModule {
@Provides
@Singleton
fun provideRouter(@ApplicationContext context: Context): Learn2earnRouter {
return Learn2earnRouter(context.toWeakReference())
}
@Provides
@Singleton
fun provideInteractor(
repository: Learn2earnRepository,
basicAuthProvider: BasicAuthProvider,
): Learn2earnInteractor {
return DefaultLearn2earnInteractor(
repository = repository,
basicAuthProvider = basicAuthProvider,
userCountryCodeProvider = { "ru" },
)
}
}

View file

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

View file

@ -0,0 +1,76 @@
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

@ -0,0 +1,30 @@
package com.tangem.feature.learn2earn.domain.models
import com.tangem.datasource.api.promotion.models.PromotionInfoResponse
/**
[REDACTED_AUTHOR]
*/
data class Promotion(
val info: PromotionInfo?,
val error: PromotionError?,
) {
@Throws(NullPointerException::class)
fun getInfo(): PromotionInfo = info!!
fun isError(): Boolean = error != null
fun isUnreachable(): Boolean = error == PromotionError.NetworkUnreachable
data class PromotionInfo(
val status: PromotionInfoResponse.Status,
val awardForNewCard: Float,
val awardForOldCard: Float,
val awardPaymentToken: PromotionInfoResponse.TokenInfo,
) {
companion object
}
companion object
}

View file

@ -0,0 +1,15 @@
package com.tangem.feature.learn2earn.domain.models
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)
companion object {
fun AbstractPromotionResponse.Error.toDomainError(): PromotionError = Error(code, description)
}
}

View file

@ -0,0 +1,17 @@
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

@ -0,0 +1,23 @@
package com.tangem.feature.learn2earn.presentation
import android.content.Context
import android.content.Intent
import timber.log.Timber
import java.lang.ref.WeakReference
/**
[REDACTED_AUTHOR]
*/
class Learn2earnRouter(
private val wContext: WeakReference<Context>,
) {
fun openWebView() {
val context = wContext.get()
if (context == null) {
Timber.e("Can't open the Learn2earnWebViewActivity")
} else {
context.startActivity(Intent(context, Learn2earnWebViewActivity::class.java))
}
}
}

View file

@ -0,0 +1,58 @@
package com.tangem.feature.learn2earn.presentation
import android.net.Uri
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.utils.coroutines.AppCoroutineDispatcherProvider
import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import javax.inject.Inject
/**
[REDACTED_AUTHOR]
*/
@HiltViewModel
class Learn2earnViewModel @Inject constructor(
private val interactor: Learn2earnInteractor,
private val router: Learn2earnRouter,
private val dispatchers: AppCoroutineDispatcherProvider,
) : ViewModel(), WebViewHelper {
var webViewUri: Uri = Uri.parse("https://localhost")
private set
fun isNeedToShowViewOnStoriesScreen(): Boolean = interactor.isNeedToShowViewOnStoriesScreen()
fun onStoriesClick() {
webViewUri = interactor.buildUriForStories()
router.openWebView()
}
fun isNeedToShowViewOnWalletScreen(walletId: String, callback: (Boolean) -> Unit) {
viewModelScope.launch(dispatchers.io) {
val isNeedToShow = interactor.isNeedToShowViewOnWalletScreen(walletId)
withContext(dispatchers.main) {
callback(isNeedToShow)
}
}
}
fun onMainPageClick(walletId: String, cardId: String, cardPubKey: String) {
webViewUri = interactor.buildUriForMainPage(walletId, cardId, cardPubKey)
router.openWebView()
}
// region WebViewHelper
override fun handleWebViewRedirect(uri: Uri): RedirectConsequences {
return interactor.handleWebViewRedirect(uri)
}
override fun getWebViewHeaders(): Map<String, String> {
return interactor.getBasicAuthHeaders()
}
// endregion WebViewHelper
}

View file

@ -0,0 +1,90 @@
package com.tangem.feature.learn2earn.presentation
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
/**
[REDACTED_AUTHOR]
*/
@AndroidEntryPoint
class Learn2earnWebViewActivity : AppCompatActivity() {
private val learn2earnViewModel by viewModels<Learn2earnViewModel>()
private lateinit var actionBar: ActionBar
private lateinit var webView: WebView
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_web_view)
setSupportActionBar(findViewById(R.id.toolbar))
actionBar = supportActionBar!!
actionBar.setDisplayHomeAsUpEnabled(true)
actionBar.setDisplayShowHomeEnabled(true)
actionBar.title = learn2earnViewModel.webViewUri.authority
webView = findViewById(R.id.web_view)
webView.settings.javaScriptEnabled = true
webView.settings.domStorageEnabled = true
webView.settings.loadsImagesAutomatically = true
webView.webViewClient = Learn2earnWebViewClient(
helper = learn2earnViewModel,
finishSessionHandler = { finish() },
)
webView.loadUrl(
learn2earnViewModel.webViewUri.toString(),
learn2earnViewModel.getWebViewHeaders(),
)
}
override fun onOptionsItemSelected(item: MenuItem): Boolean {
return when (item.itemId) {
android.R.id.home -> {
finish()
true
}
else -> super.onOptionsItemSelected(item)
}
}
override fun onDestroy() {
webView.clearHistory()
webView.clearCache(true)
webView.destroy()
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
}
}

View file

@ -24,9 +24,9 @@ import com.tangem.feature.learn2earn.presentation.ui.component.GradientCircle
/** /**
[REDACTED_AUTHOR] [REDACTED_AUTHOR]
*/ */
// TODO: fixme: make function as internal after adding feature interface // TODO: 1inch: make function as internal after adding feature interface
@Composable @Composable
fun OneInchStoriesScreen(onLearnClick: () -> Unit, modifier: Modifier = Modifier) { fun StoriesScreen(onLearnClick: () -> Unit, modifier: Modifier = Modifier) {
Box(modifier = modifier.fillMaxSize()) { Box(modifier = modifier.fillMaxSize()) {
ContentBackground( ContentBackground(
modifier = Modifier modifier = Modifier
@ -111,7 +111,7 @@ private fun OneInchStoriesContentPreview_Light() {
TangemTheme( TangemTheme(
isDark = false, isDark = false,
) { ) {
OneInchStoriesScreen( StoriesScreen(
onLearnClick = {}, onLearnClick = {},
) )
} }
@ -123,7 +123,7 @@ private fun OneInchStoriesContentPreview_Dark() {
TangemTheme( TangemTheme(
isDark = true, isDark = true,
) { ) {
OneInchStoriesScreen( StoriesScreen(
onLearnClick = {}, onLearnClick = {},
) )
} }

View file

@ -0,0 +1,29 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.coordinatorlayout.widget.CoordinatorLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:id="@+id/coordinator_wallet"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">
<com.google.android.material.appbar.AppBarLayout
android:id="@+id/app_bar"
style="@style/Widget.MaterialComponents.Toolbar.Surface"
android:layout_width="match_parent"
android:layout_height="wrap_content">
<com.google.android.material.appbar.MaterialToolbar
android:id="@+id/toolbar"
android:layout_width="match_parent"
android:layout_height="?attr/actionBarSize"
app:navigationIcon="@drawable/ic_close_24" />
</com.google.android.material.appbar.AppBarLayout>
<WebView
android:id="@+id/web_view"
android:layout_width="match_parent"
android:layout_height="match_parent"
app:layout_behavior="@string/appbar_scrolling_view_behavior" />
</androidx.coordinatorlayout.widget.CoordinatorLayout>