Updated on 2026-08-14

This commit is contained in:
Tangem 2023-01-09 00:35:28 +03:00
parent f968430c59
commit 905792c77e
20 changed files with 384 additions and 197 deletions

View file

@ -0,0 +1,80 @@
package com.tangem.tap.features.disclaimer
import android.net.Uri
/**
[REDACTED_AUTHOR]
*/
interface Disclaimer {
fun type(): DisclaimerType
fun getUri(): Uri
fun accept()
fun isAccepted(): Boolean
}
abstract class BaseDisclaimer(
protected val dataProvider: DisclaimerDataProvider,
) : Disclaimer {
val baseUrl = "https://tangem.com"
override fun accept() {
dataProvider.storage().accept(getPreferenceKey())
}
override fun isAccepted(): Boolean = dataProvider.storage().isAccepted(getPreferenceKey())
protected open fun getPreferenceKey(): String = type().name
}
class DummyDisclaimer : Disclaimer {
override fun type(): DisclaimerType = DisclaimerType.Tangem
override fun getUri(): Uri = Uri.parse("https://tangem.com/tangem_tos.html")
override fun accept() {}
override fun isAccepted(): Boolean = false
}
class TangemDisclaimer(dataProvider: DisclaimerDataProvider) : BaseDisclaimer(dataProvider) {
override fun type(): DisclaimerType = DisclaimerType.Tangem
override fun getUri(): Uri = Uri.parse("$baseUrl/tangem_tos.html")
override fun getPreferenceKey(): String = "tangem_tos_accepted"
}
class SaltPayDisclaimer(dataProvider: DisclaimerDataProvider) : BaseDisclaimer(dataProvider) {
override fun type(): DisclaimerType = DisclaimerType.SaltPay
override fun getUri(): Uri = Uri.parse("$baseUrl/soltpay_tos.html")
override fun getPreferenceKey(): String = "saltPay_tos_accepted"
}
class Start2CoinDisclaimer(dataProvider: DisclaimerDataProvider) : BaseDisclaimer(dataProvider) {
override fun type(): DisclaimerType = DisclaimerType.Start2Coin
override fun getUri(): Uri = Uri.parse("$baseUrl/" + filename(dataProvider.getLanguage(), getRegion()))
override fun getPreferenceKey(): String = "start2Coin_tos_accepted_${getRegion()}"
private fun filename(languageCode: String, regionCode: String?): String {
return when {
languageCode == "fr" && regionCode == "ch" -> "Start2Coin-fr-ch-tangem.html"
languageCode == "de" && regionCode == "ch" -> "Start2Coin-de-ch-tangem.html"
languageCode == "en" && regionCode == "ch" -> "Start2Coin-en-ch-tangem.html"
languageCode == "it" && regionCode == "ch" -> "Start2Coin-it-ch-tangem.html"
languageCode == "fr" && regionCode == "fr" -> "Start2Coin-fr-fr-atangem.html"
languageCode == "de" && regionCode == "at" -> "Start2Coin-de-at-tangem.html"
regionCode == "fr" -> "Start2Coin-fr-fr-atangem.html"
regionCode == "ch" -> "Start2Coin-en-ch-tangem.html"
regionCode == "at" -> "Start2Coin-de-at-tangem.html"
else -> "Start2Coin-fr-fr-atangem.html"
}
}
private fun getRegion(): String? {
val cardId = dataProvider.getCardId()
if (cardId.isEmpty()) return null
return when (cardId[1]) {
'0' -> "fr"
'1' -> "ch"
'2' -> "at"
else -> null
}
}
}

View file

@ -0,0 +1,12 @@
package com.tangem.tap.features.disclaimer
import com.tangem.tap.persistence.DisclaimerPrefStorage
/**
[REDACTED_AUTHOR]
*/
interface DisclaimerDataProvider {
fun getLanguage(): String
fun getCardId(): String
fun storage(): DisclaimerPrefStorage
}

View file

@ -0,0 +1,44 @@
package com.tangem.tap.features.disclaimer
import com.tangem.domain.common.CardDTO
import com.tangem.domain.common.TapWorkarounds.isSaltPay
import com.tangem.domain.common.TapWorkarounds.isStart2Coin
import com.tangem.tap.persistence.DisclaimerPrefStorage
import com.tangem.tap.preferencesStorage
import java.util.*
/**
[REDACTED_AUTHOR]
*/
enum class DisclaimerType {
Tangem,
SaltPay,
Start2Coin;
companion object {
fun get(cardDTO: CardDTO): DisclaimerType {
return when {
cardDTO.isSaltPay -> SaltPay
cardDTO.isStart2Coin -> Start2Coin
else -> Tangem
}
}
}
}
fun DisclaimerType.createDisclaimer(cardDTO: CardDTO): Disclaimer {
val dataProvider = provideDisclaimerDataProvider(cardDTO.cardId)
return when (this) {
DisclaimerType.Tangem -> TangemDisclaimer(dataProvider)
DisclaimerType.SaltPay -> SaltPayDisclaimer(dataProvider)
DisclaimerType.Start2Coin -> Start2CoinDisclaimer(dataProvider)
}
}
private fun provideDisclaimerDataProvider(cardId: String): DisclaimerDataProvider {
return object : DisclaimerDataProvider {
override fun getLanguage(): String = Locale.getDefault().language
override fun getCardId(): String = cardId
override fun storage(): DisclaimerPrefStorage = preferencesStorage.disclaimerPrefStorage
}
}

View file

@ -1,26 +1,21 @@
package com.tangem.tap.features.disclaimer.redux
import com.tangem.common.extensions.VoidCallback
import com.tangem.tap.common.redux.navigation.AppScreen
import com.tangem.tap.features.disclaimer.Disclaimer
import com.tangem.tap.features.wallet.redux.ProgressState
import org.rekotlin.Action
sealed class DisclaimerAction : Action {
data class SetDisclaimerType(
val type: DisclaimerType,
) : DisclaimerAction()
data class SetDisclaimer(val disclaimer: Disclaimer) : DisclaimerAction()
data class Show(
val fromScreen: AppScreen,
val callback: DisclaimerCallback? = null,
) : DisclaimerAction()
data class AcceptDisclaimer(val type: DisclaimerType) : DisclaimerAction()
internal data class UpdateState(val type: DisclaimerType, val accepted: Boolean) : DisclaimerAction()
object AcceptDisclaimer : DisclaimerAction()
object OnBackPressed : DisclaimerAction()
}
data class DisclaimerCallback(
val onAccept: VoidCallback? = null,
val onDismiss: VoidCallback? = null,
)
data class OnProgressStateChanged(val state: ProgressState?) : DisclaimerAction()
}

View file

@ -3,7 +3,6 @@ package com.tangem.tap.features.disclaimer.redux
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.preferencesStorage
import com.tangem.tap.store
import org.rekotlin.Action
import org.rekotlin.Middleware
@ -23,16 +22,11 @@ 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(NavigationAction.NavigateTo(AppScreen.Disclaimer))
}
is DisclaimerAction.AcceptDisclaimer -> {
action.type.accept()
handleUpdateState = action.type.createUpdateState()
state.disclaimer.accept()
store.dispatch(NavigationAction.PopBackTo())
state.callback?.onAccept?.invoke()
}
@ -41,29 +35,4 @@ private fun handleDisclaimerMiddleware(action: Action, appState: AppState) {
state.callback?.onDismiss?.invoke()
}
}
}
private fun DisclaimerType.accept() = when (this) {
DisclaimerType.Tangem -> preferencesStorage.disclaimerPrefStorage.hasTangemTosAccepted = true
DisclaimerType.Start2Coin -> preferencesStorage.disclaimerPrefStorage.hasStart2CoinTosAccepted = true
DisclaimerType.SaltPay -> preferencesStorage.disclaimerPrefStorage.hasSaltPayTosAccepted = true
}
fun DisclaimerType.isAccepted(): Boolean = when (this) {
DisclaimerType.Tangem -> preferencesStorage.disclaimerPrefStorage.hasTangemTosAccepted
DisclaimerType.Start2Coin -> preferencesStorage.disclaimerPrefStorage.hasStart2CoinTosAccepted
DisclaimerType.SaltPay -> preferencesStorage.disclaimerPrefStorage.hasSaltPayTosAccepted
}
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

@ -15,15 +15,19 @@ private fun internalReduce(action: Action, state: AppState): DisclaimerState {
val disclaimerState = state.disclaimerState
return when (action) {
is DisclaimerAction.Show -> disclaimerState.copy(
callback = action.callback,
is DisclaimerAction.SetDisclaimer -> disclaimerState.copy(
disclaimer = action.disclaimer,
)
is DisclaimerAction.UpdateState -> disclaimerState.copy(
type = action.type,
accepted = action.accepted,
is DisclaimerAction.Show -> disclaimerState.copy(
showedFromScreen = action.fromScreen,
callback = action.callback,
)
is DisclaimerAction.AcceptDisclaimer, is DisclaimerAction.OnBackPressed -> disclaimerState.copy(
callback = null,
progressState = null,
)
is DisclaimerAction.OnProgressStateChanged -> disclaimerState.copy(
progressState = action.state,
)
else -> disclaimerState
}

View file

@ -1,32 +1,21 @@
package com.tangem.tap.features.disclaimer.redux
import android.net.Uri
import com.tangem.domain.common.CardDTO
import com.tangem.domain.common.ScanResponse
import com.tangem.domain.common.TapWorkarounds.isSaltPay
import com.tangem.domain.common.TapWorkarounds.isStart2Coin
import com.tangem.common.extensions.VoidCallback
import com.tangem.tap.common.redux.navigation.AppScreen
import com.tangem.tap.features.disclaimer.Disclaimer
import com.tangem.tap.features.disclaimer.DummyDisclaimer
import com.tangem.tap.features.wallet.redux.ProgressState
import org.rekotlin.StateType
data class DisclaimerState(
val accepted: Boolean = false,
val type: DisclaimerType = DisclaimerType.Tangem,
val disclaimer: Disclaimer = DummyDisclaimer(),
val showedFromScreen: AppScreen = AppScreen.Home,
val callback: DisclaimerCallback? = null,
) : StateType
val progressState: ProgressState? = null,
) : StateType {
}
sealed class DisclaimerType(
val uri: Uri,
) {
object Tangem : DisclaimerType(Uri.parse("https://tangem.com/tangem_tos.html"))
object Start2Coin : 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: CardDTO): DisclaimerType = when {
card.isSaltPay -> SaltPay
card.isStart2Coin -> Start2Coin
else -> Tangem
}
}
}
data class DisclaimerCallback(
val onAccept: VoidCallback? = null,
val onDismiss: VoidCallback? = null,
)

View file

@ -5,13 +5,17 @@ import android.view.View
import androidx.transition.TransitionInflater
import by.kirich1409.viewbindingdelegate.viewBinding
import com.tangem.core.ui.fragments.setStatusBarColor
import com.tangem.tap.common.extensions.configureSettings
import com.tangem.tap.common.extensions.beginDelayedTransition
import com.tangem.tap.common.extensions.hide
import com.tangem.tap.common.extensions.show
import com.tangem.tap.common.redux.AppState
import com.tangem.tap.common.redux.navigation.AppScreen
import com.tangem.tap.features.BaseFragment
import com.tangem.tap.features.addBackPressHandler
import com.tangem.tap.features.disclaimer.Disclaimer
import com.tangem.tap.features.disclaimer.redux.DisclaimerAction
import com.tangem.tap.features.disclaimer.redux.DisclaimerState
import com.tangem.tap.features.wallet.redux.ProgressState
import com.tangem.tap.store
import com.tangem.wallet.R
import com.tangem.wallet.databinding.FragmentDisclaimerBinding
@ -20,17 +24,52 @@ import org.rekotlin.StoreSubscriber
class DisclaimerFragment : BaseFragment(R.layout.fragment_disclaimer), StoreSubscriber<DisclaimerState> {
private val binding: FragmentDisclaimerBinding by viewBinding(FragmentDisclaimerBinding::bind)
private val webViewClient = DisclaimerWebViewClient()
override fun configureTransitions() {
val inflater = TransitionInflater.from(requireContext())
when (store.state.disclaimerState.showedFromScreen) {
AppScreen.Home -> {
enterTransition = inflater.inflateTransition(android.R.transition.slide_bottom)
exitTransition = inflater.inflateTransition(android.R.transition.slide_top)
}
AppScreen.Details -> {
enterTransition = inflater.inflateTransition(android.R.transition.fade)
exitTransition = inflater.inflateTransition(android.R.transition.fade)
}
}
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
addBackPressHandler(handler = this)
binding.toolbar.setNavigationOnClickListener { handleOnBackPressed() }
binding.webView.configureSettings()
binding.apply {
toolbar.setNavigationOnClickListener { handleOnBackPressed() }
webView.apply {
settings.allowFileAccess = false
settings.javaScriptEnabled = false
webViewClient = this@DisclaimerFragment.webViewClient
}
webView.hide()
groupError.hide()
groupAccept.hide()
groupLoading.hide()
btnAccept.setOnClickListener {
store.dispatch(DisclaimerAction.AcceptDisclaimer)
}
btnRepeat.setOnClickListener {
webViewClient.reset()
}
}
}
override fun onStart() {
super.onStart()
setStatusBarColor(R.color.backgroundLightGray)
webViewClient.onProgressStateChanged = { store.dispatch(DisclaimerAction.OnProgressStateChanged(it)) }
store.subscribe(subscriber = this) { state ->
state
.skipRepeats { oldState, newState -> oldState.disclaimerState == newState.disclaimerState }
@ -39,30 +78,43 @@ class DisclaimerFragment : BaseFragment(R.layout.fragment_disclaimer), StoreSubs
}
override fun onStop() {
super.onStop()
webViewClient.onProgressStateChanged = null
store.unsubscribe(this)
}
override fun configureTransitions() {
super.configureTransitions()
val inflater = TransitionInflater.from(requireContext())
enterTransition = inflater.inflateTransition(android.R.transition.fade)
exitTransition = inflater.inflateTransition(android.R.transition.fade)
super.onStop()
}
override fun newState(state: DisclaimerState) = with(binding) {
if (activity == null || view == null) return
if (state.accepted) {
halfTransparentOverlay.hide()
btnAccept.hide()
}
updateUiVisibility(state.disclaimer, state.progressState)
binding.webView.loadUrl(state.disclaimer.getUri().toString())
}
btnAccept.setOnClickListener {
store.dispatch(DisclaimerAction.AcceptDisclaimer(state.type))
private fun updateUiVisibility(disclaimer: Disclaimer, progressState: ProgressState?) = with(binding) {
when (progressState) {
ProgressState.Loading -> {
root.beginDelayedTransition()
webView.hide()
groupError.hide()
groupAccept.hide()
groupLoading.show()
}
ProgressState.Done -> {
root.beginDelayedTransition()
groupError.hide()
groupLoading.hide()
webView.show()
groupAccept.show(!disclaimer.isAccepted())
}
ProgressState.Error -> {
root.beginDelayedTransition()
webView.hide()
groupAccept.hide()
groupLoading.hide()
groupError.show()
}
else -> {}
}
webView.loadUrl(state.type.uri.toString())
}
override fun handleOnBackPressed() {

View file

@ -0,0 +1,68 @@
package com.tangem.tap.features.disclaimer.ui
import android.graphics.Bitmap
import android.webkit.WebResourceError
import android.webkit.WebResourceRequest
import android.webkit.WebResourceResponse
import android.webkit.WebView
import android.webkit.WebViewClient
import com.tangem.common.extensions.ifNotNull
import com.tangem.tap.features.wallet.redux.ProgressState
class DisclaimerWebViewClient : WebViewClient() {
var onProgressStateChanged: ((ProgressState) -> Unit)? = null
private var loadingUrl: String? = null
private var loadedUrl: String? = null
private var progressState: ProgressState = ProgressState.Loading
set(value) {
field = value
onProgressStateChanged?.invoke(value)
}
fun reset() {
loadingUrl = null
loadedUrl = null
progressState = ProgressState.Loading
}
override fun onPageStarted(view: WebView?, url: String?, favicon: Bitmap?) {
super.onPageStarted(view, url, favicon)
if (loadingUrl != url) progressState = ProgressState.Loading
loadingUrl = url
}
override fun onPageFinished(view: WebView?, url: String?) {
super.onPageFinished(view, url)
if (loadedUrl != url) progressState = ProgressState.Done
loadedUrl = url
}
override fun onReceivedError(
view: WebView?,
resourceRequest: WebResourceRequest?,
error: WebResourceError?,
) {
super.onReceivedError(view, resourceRequest, error)
error?.let { progressState = ProgressState.Error }
}
override fun onReceivedHttpError(
view: WebView?,
resourceRequest: WebResourceRequest?,
errorResponse: WebResourceResponse?,
) {
super.onReceivedHttpError(view, resourceRequest, errorResponse)
ifNotNull(resourceRequest, errorResponse) { request, response ->
if (request.url?.toString() != loadingUrl || response.statusCode < 400) return
if (progressState != ProgressState.Done) return
progressState = ProgressState.Error
}
}
}