Updated on 2026-08-14

This commit is contained in:
Tangem 2022-10-25 15:46:12 +05:00
parent 26f476bcd0
commit 7913e52216
12 changed files with 181 additions and 1430 deletions

File diff suppressed because it is too large Load diff

View file

@ -78,8 +78,7 @@ class DetailsViewModel(private val store: Store<AppState>) {
store.dispatch(DetailsAction.CreateBackup)
}
SettingsElement.TermsOfService -> {
store.dispatch(DisclaimerAction.ShowAcceptedDisclaimer)
store.dispatch(NavigationAction.NavigateTo(AppScreen.Disclaimer))
store.dispatch(DisclaimerAction.Show())
}
SettingsElement.TermsOfUse -> {
store.dispatch(DetailsAction.ShowDisclaimer)

View file

@ -1,8 +1,17 @@
package com.tangem.tap.features.disclaimer.redux
import com.tangem.common.extensions.VoidCallback
import org.rekotlin.Action
sealed class DisclaimerAction: Action {
object AcceptDisclaimer : DisclaimerAction()
object ShowAcceptedDisclaimer : DisclaimerAction()
sealed class DisclaimerAction : Action {
data class SetDisclaimerType(
val type: DisclaimerType,
) : DisclaimerAction()
data class Show(val onAcceptCallback: VoidCallback? = null) : DisclaimerAction()
data class AcceptDisclaimer(val type: DisclaimerType) : DisclaimerAction()
internal data class UpdateState(val type: DisclaimerType, val accepted: Boolean) : DisclaimerAction()
internal data class SetOnAcceptCallback(val onAcceptCallback: VoidCallback? = null) : DisclaimerAction()
}

View file

@ -1,28 +1,65 @@
package com.tangem.tap.features.disclaimer.redux
import com.tangem.tap.common.post
import com.tangem.tap.common.redux.AppState
import com.tangem.tap.common.redux.navigation.AppScreen
import com.tangem.tap.common.redux.navigation.NavigationAction
import com.tangem.tap.features.home.redux.HomeAction
import com.tangem.tap.preferencesStorage
import com.tangem.tap.store
import org.rekotlin.Action
import org.rekotlin.Middleware
class DisclaimerMiddleware {
val disclaimerMiddleware: Middleware<AppState> = { dispatch, state ->
{ next ->
{ action ->
when (action) {
is DisclaimerAction.AcceptDisclaimer -> {
preferencesStorage.saveDisclaimerAccepted()
store.dispatch(NavigationAction.PopBackTo())
//delayed sending used for better animation
post(550) { store.dispatch(HomeAction.ReadCard) }
}
}
state()?.let { handleDisclaimerMiddleware(action, it) }
next(action)
}
}
}
}
}
private fun handleDisclaimerMiddleware(action: Action, appState: AppState) {
val state = appState.disclaimerState
when (action) {
is DisclaimerAction.SetDisclaimerType -> {
handleUpdateState = action.type.createUpdateState()
}
is DisclaimerAction.Show -> {
handleUpdateState = state.type.createUpdateState()
store.dispatch(DisclaimerAction.SetOnAcceptCallback(action.onAcceptCallback))
store.dispatch(NavigationAction.NavigateTo(AppScreen.Disclaimer))
}
is DisclaimerAction.AcceptDisclaimer -> {
action.type.accept()
handleUpdateState = action.type.createUpdateState()
store.dispatch(NavigationAction.PopBackTo())
state.onAcceptCallback?.invoke()
store.dispatch(DisclaimerAction.SetOnAcceptCallback(null))
}
}
}
private fun DisclaimerType.accept() = when (this) {
DisclaimerType.SaltPay -> preferencesStorage.disclaimerPrefStorage.hasSaltPayTosAccepted = true
DisclaimerType.Tangem -> preferencesStorage.disclaimerPrefStorage.hasTangemTosAccepted = true
}
fun DisclaimerType.isAccepted(): Boolean = when (this) {
DisclaimerType.SaltPay -> preferencesStorage.disclaimerPrefStorage.hasSaltPayTosAccepted
DisclaimerType.Tangem -> preferencesStorage.disclaimerPrefStorage.hasTangemTosAccepted
}
private fun DisclaimerType.createUpdateState(): DisclaimerAction.UpdateState {
return DisclaimerAction.UpdateState(this, this.isAccepted())
}
private var handleUpdateState: DisclaimerAction.UpdateState = DisclaimerAction.UpdateState(
type = DisclaimerType.Tangem,
accepted = false,
)
set(value) {
field = value
store.dispatch(value)
}

View file

@ -13,9 +13,15 @@ private fun internalReduce(action: Action, state: AppState): DisclaimerState {
if (action !is DisclaimerAction) return state.disclaimerState
val disclaimerState = state.disclaimerState
return when (action) {
is DisclaimerAction.AcceptDisclaimer, DisclaimerAction.ShowAcceptedDisclaimer -> {
disclaimerState.copy(accepted = true)
}
is DisclaimerAction.UpdateState -> disclaimerState.copy(
type = action.type,
accepted = action.accepted,
)
is DisclaimerAction.SetOnAcceptCallback -> disclaimerState.copy(
onAcceptCallback = action.onAcceptCallback,
)
else -> disclaimerState
}
}

View file

@ -1,7 +1,30 @@
package com.tangem.tap.features.disclaimer.redux
import android.net.Uri
import com.tangem.common.card.Card
import com.tangem.common.extensions.VoidCallback
import com.tangem.domain.common.ScanResponse
import com.tangem.domain.common.TapWorkarounds.isSaltPay
import org.rekotlin.StateType
data class DisclaimerState(
val accepted: Boolean = false
val accepted: Boolean = false,
val type: DisclaimerType = DisclaimerType.Tangem,
val onAcceptCallback: VoidCallback? = null,
) : StateType
sealed class DisclaimerType(
val uri: Uri,
) {
object Tangem : DisclaimerType(Uri.parse("https://tangem.com/tangem_tos.html"))
object SaltPay : DisclaimerType(Uri.parse("https://tangem.com/soltpay_tos.html"))
companion object {
fun get(scanResponse: ScanResponse): DisclaimerType = get(scanResponse.card)
fun get(card: Card): DisclaimerType = when {
card.isSaltPay -> SaltPay
else -> Tangem
}
}
}

View file

@ -55,25 +55,25 @@ class DisclaimerFragment : Fragment(R.layout.fragment_disclaimer),
binding.toolbar.setNavigationOnClickListener {
store.dispatch(NavigationAction.PopBackTo())
}
initAndLoadTOS()
setOnClickListeners()
initViews()
}
private fun initAndLoadTOS() {
private fun initViews() {
binding.webView.configureSettings()
binding.webView.loadUrl("file:///android_asset/tos.html")
}
private fun setOnClickListeners() {
binding.btnAccept.setOnClickListener { store.dispatch(DisclaimerAction.AcceptDisclaimer) }
}
override fun newState(state: DisclaimerState) {
override fun newState(state: DisclaimerState) = with(binding) {
if (activity == null || view == null) return
if (state.accepted) {
binding.halfTransparentOverlay.hide()
binding.btnAccept.hide()
halfTransparentOverlay.hide()
btnAccept.hide()
}
btnAccept.setOnClickListener {
store.dispatch(DisclaimerAction.AcceptDisclaimer(state.type))
}
webView.loadUrl(state.type.uri.toString())
}
}

View file

@ -13,6 +13,5 @@ sealed class HomeAction : Action {
data class ShouldScanCardOnResume(val shouldScanCard: Boolean) : HomeAction()
object Init : HomeAction()
data class SetTermsOfUseState(val isDisclaimerAccepted: Boolean) : HomeAction()
data class ChangeScanCardButtonState(val state: IndeterminateProgressButton) : HomeAction()
}

View file

@ -20,6 +20,9 @@ import com.tangem.tap.common.redux.global.GlobalAction
import com.tangem.tap.common.redux.navigation.AppScreen
import com.tangem.tap.common.redux.navigation.FragmentShareTransition
import com.tangem.tap.common.redux.navigation.NavigationAction
import com.tangem.tap.features.disclaimer.redux.DisclaimerAction
import com.tangem.tap.features.disclaimer.redux.DisclaimerType
import com.tangem.tap.features.disclaimer.redux.isAccepted
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.Companion.BUY_WALLET_URL
@ -63,7 +66,6 @@ private fun handleHomeAction(appState: () -> AppState?, action: Action, dispatch
is HomeAction.Init -> {
store.dispatch(GlobalAction.RestoreAppCurrency)
store.dispatch(GlobalAction.ExchangeManager.Init)
store.dispatch(HomeAction.SetTermsOfUseState(preferencesStorage.wasDisclaimerAccepted()))
store.dispatch(GlobalAction.FetchUserCountry)
}
is HomeAction.ShouldScanCardOnResume -> {
@ -73,17 +75,16 @@ private fun handleHomeAction(appState: () -> AppState?, action: Action, dispatch
}
}
is HomeAction.ReadCard -> {
if (!preferencesStorage.wasDisclaimerAccepted()) {
store.dispatch(NavigationAction.NavigateTo(AppScreen.Disclaimer))
} else {
changeButtonState(ButtonState.PROGRESS)
val scanCardAction = GlobalAction.ScanCard(
onSuccess = ::onScanSuccess,
onFailure = ::onScanFailure,
)
store.dispatch(HomeAction.ScanInProgress(true))
postUiDelayBg(300) { store.dispatch(scanCardAction) }
}
changeButtonState(ButtonState.PROGRESS)
val scanCardAction = GlobalAction.ScanCard(
onSuccess = {
store.dispatch(HomeAction.ScanInProgress(false))
checkForDisclaimer(it, ::onScanSuccess)
},
onFailure = ::onScanFailure,
)
store.dispatch(HomeAction.ScanInProgress(true))
postUiDelayBg(300) { store.dispatch(scanCardAction) }
}
is HomeAction.GoToShop -> {
when (action.userCountryCode) {
@ -98,12 +99,28 @@ private fun handleHomeAction(appState: () -> AppState?, action: Action, dispatch
}
}
private fun checkForDisclaimer(scanResponse: ScanResponse, onDisclaimerAccepted: (ScanResponse) -> Unit) {
val disclaimerType = DisclaimerType.get(scanResponse)
store.dispatch(DisclaimerAction.SetDisclaimerType(disclaimerType))
if (disclaimerType.isAccepted()) {
onDisclaimerAccepted((scanResponse))
} else {
changeButtonState(ButtonState.ENABLED)
store.dispatch(
DisclaimerAction.Show {
changeButtonState(ButtonState.PROGRESS)
onDisclaimerAccepted(scanResponse)
},
)
}
}
private fun onScanSuccess(scanResponse: ScanResponse) {
val globalState = store.state.globalState
val tapWalletManager = globalState.tapWalletManager
tapWalletManager.updateConfigManager(scanResponse)
store.dispatch(HomeAction.ScanInProgress(false))
store.dispatch(TwinCardsAction.IfTwinsPrepareState(scanResponse))
if (scanResponse.isSaltPay()) {

View file

@ -0,0 +1,25 @@
package com.tangem.tap.persistence
import android.content.SharedPreferences
import androidx.core.content.edit
/**
[REDACTED_AUTHOR]
*/
class DisclaimerPrefStorage(
private val preferences: SharedPreferences,
) {
var hasTangemTosAccepted: Boolean
get() = preferences.getBoolean(KEY_TANGEM_TOS, false)
set(value) = preferences.edit { putBoolean(KEY_TANGEM_TOS, value) }
var hasSaltPayTosAccepted: Boolean
get() = preferences.getBoolean(KEY_SALT_PAY_TOS, false)
set(value) = preferences.edit { putBoolean(KEY_SALT_PAY_TOS, value) }
companion object {
private const val KEY_TANGEM_TOS = "tangem_tos_accepted"
private const val KEY_SALT_PAY_TOS = "saltPay_tos_accepted"
}
}

View file

@ -7,15 +7,16 @@ import androidx.core.content.edit
import com.tangem.common.json.MoshiJsonConverter
import java.util.*
class PreferencesStorage(applicationContext: Application) {
private val preferences: SharedPreferences = applicationContext.getSharedPreferences(PREFERENCES_NAME, Context.MODE_PRIVATE)
private val preferences: SharedPreferences =
applicationContext.getSharedPreferences(PREFERENCES_NAME, Context.MODE_PRIVATE)
val appRatingLaunchObserver: AppRatingLaunchObserver
val usedCardsPrefStorage: UsedCardsPrefStorage
val fiatCurrenciesPrefStorage: FiatCurrenciesPrefStorage
val saltPayActivationStorage: SaltPayActivationStorage
val disclaimerPrefStorage: DisclaimerPrefStorage
init {
incrementLaunchCounter()
@ -25,6 +26,7 @@ class PreferencesStorage(applicationContext: Application) {
fiatCurrenciesPrefStorage = FiatCurrenciesPrefStorage(preferences, MoshiJsonConverter.INSTANCE)
fiatCurrenciesPrefStorage.migrate()
saltPayActivationStorage = SaltPayActivationPrefStorage(applicationContext, MoshiJsonConverter.INSTANCE)
disclaimerPrefStorage = DisclaimerPrefStorage(preferences)
}
var chatFirstLaunchTime: Long?
@ -38,14 +40,6 @@ class PreferencesStorage(applicationContext: Application) {
return usedCardsPrefStorage.wasScanned(cardId)
}
fun saveDisclaimerAccepted() {
preferences.edit { putBoolean(DISCLAIMER_ACCEPTED_KEY, true) }
}
fun wasDisclaimerAccepted(): Boolean {
return preferences.getBoolean(DISCLAIMER_ACCEPTED_KEY, false)
}
fun saveTwinsOnboardingShown() {
preferences.edit { putBoolean(TWINS_ONBOARDING_SHOWN_KEY, true) }
}
@ -61,12 +55,10 @@ class PreferencesStorage(applicationContext: Application) {
companion object {
private const val PREFERENCES_NAME = "tapPrefs"
private const val DISCLAIMER_ACCEPTED_KEY = "disclaimerAccepted"
private const val TWINS_ONBOARDING_SHOWN_KEY = "twinsOnboardingShown"
private const val APP_LAUNCH_COUNT_KEY = "launchCount"
private const val CHAT_FIRST_LAUNCH_KEY = "chatFirstLaunchKey"
}
}
class AppRatingLaunchObserver(

View file

@ -22,6 +22,7 @@
android:layout_height="?attr/actionBarSize"
app:navigationIcon="@drawable/ic_baseline_arrow_back_24"
app:title="@string/disclaimer_title" />
</com.google.android.material.appbar.AppBarLayout>
<androidx.constraintlayout.widget.ConstraintLayout
@ -31,17 +32,26 @@
android:background="@color/white"
app:layout_behavior="@string/appbar_scrolling_view_behavior">
<WebView
android:id="@+id/web_view"
android:layout_width="0dp"
android:layout_height="0dp"
<androidx.core.widget.NestedScrollView
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_marginStart="16dp"
android:layout_marginTop="16dp"
android:layout_marginEnd="24dp"
android:clipChildren="false"
android:clipToPadding="false"
android:paddingTop="16dp"
android:paddingBottom="120dp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
app:layout_constraintTop_toTopOf="parent">
<WebView
android:id="@+id/web_view"
android:layout_width="match_parent"
android:layout_height="wrap_content" />
</androidx.core.widget.NestedScrollView>
<androidx.constraintlayout.widget.Guideline
android:id="@+id/guideline"