Updated on 2026-08-14

This commit is contained in:
Tangem 2023-02-17 15:25:12 +03:00
commit 30210ca422
26 changed files with 243 additions and 71 deletions

View file

@ -194,6 +194,7 @@ dependencies {
}
implementation(Library.accompanistAppCompatTheme)
implementation(Library.accompanistSystemUiController)
implementation(Library.accompanistWebView)
implementation(Library.xmlShimmer)
implementation(Library.viewBindingDelegate)
implementation(Library.armadillo)

View file

@ -193,7 +193,7 @@ class TapApplication : Application(), ImageLoaderFactory {
private fun initWithConfigDependency(config: Config) {
shopService = TangemShopService(this, config.shopify!!)
initAnalytics(this, config)
initFeedbackManager(this, preferencesStorage, foregroundActivityObserver)
initFeedbackManager(this, preferencesStorage, foregroundActivityObserver, store)
}
private fun initAnalytics(application: Application, config: Config) {
@ -219,6 +219,7 @@ class TapApplication : Application(), ImageLoaderFactory {
context: Context,
preferencesStorage: PreferencesStorage,
foregroundActivityObserver: ForegroundActivityObserver,
store: Store<AppState>,
) {
fun initAdditionalFeedbackInfo(context: Context): AdditionalFeedbackInfo = AdditionalFeedbackInfo().apply {
appVersion = try {
@ -252,7 +253,7 @@ class TapApplication : Application(), ImageLoaderFactory {
val feedbackManager = FeedbackManager(
infoHolder = additionalFeedbackInfo,
logCollector = tangemLogCollector,
chatManager = ChatManager(preferencesStorage, foregroundActivityObserver),
chatManager = ChatManager(preferencesStorage, foregroundActivityObserver, store),
)
store.dispatch(GlobalAction.SetFeedbackManager(feedbackManager))
}

View file

@ -19,6 +19,10 @@ data class ZendeskConfig(
val url: String,
) : ChatConfig
@JsonClass(generateAdapter = true)
data class SprinklrConfig(
@Json(name = "appID")
val appId: String,
@Json(name = "baseURL")
val baseUrl: String,
) : ChatConfig

View file

@ -5,18 +5,21 @@ import com.tangem.tap.ForegroundActivityObserver
import com.tangem.tap.common.chat.opener.ChatOpener
import com.tangem.tap.common.chat.opener.implementation.SprinklrChatOpener
import com.tangem.tap.common.chat.opener.implementation.ZendeskChatOpener
import com.tangem.tap.common.redux.AppState
import com.tangem.tap.persistence.PreferencesStorage
import org.rekotlin.Store
class ChatManager(
private val preferencesStorage: PreferencesStorage,
private val foregroundActivityObserver: ForegroundActivityObserver,
private val store: Store<AppState>,
) {
private val openers = mutableMapOf<ChatConfig, ChatOpener>()
fun open(config: ChatConfig, feedbackDataBuilder: (Context) -> String) {
val opener = openers.getOrPut(config) {
when (config) {
is SprinklrConfig -> SprinklrChatOpener()
is SprinklrConfig -> SprinklrChatOpener(config, store)
is ZendeskConfig -> ZendeskChatOpener(config, preferencesStorage, foregroundActivityObserver)
}
}

View file

@ -1,10 +1,21 @@
package com.tangem.tap.common.chat.opener.implementation
import android.content.Context
import com.tangem.tap.common.chat.SprinklrConfig
import com.tangem.tap.common.chat.opener.ChatOpener
import com.tangem.tap.common.extensions.dispatchOnMain
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.sprinklr.redux.SprinklrAction
import org.rekotlin.Store
internal class SprinklrChatOpener : ChatOpener {
internal class SprinklrChatOpener(
private val config: SprinklrConfig,
private val store: Store<AppState>,
) : ChatOpener {
override fun open(feedbackDataBuilder: (Context) -> String) {
// TODO: Open activity with Sprinklr WebView, will be in further MRs
store.dispatch(SprinklrAction.SetConfig(config))
store.dispatchOnMain(NavigationAction.NavigateTo(AppScreen.Sprinklr))
}
}

View file

@ -12,9 +12,7 @@ import androidx.compose.material.Icon
import androidx.compose.material.IconButton
import androidx.compose.material.Scaffold
import androidx.compose.material.Text
import androidx.compose.material.ripple.LocalRippleTheme
import androidx.compose.runtime.Composable
import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
@ -133,18 +131,6 @@ fun ClearButton(
}
}
/**
* Used for disable ripple if button is enable = false
*/
@Composable
fun ToggledRippleTheme(
isEnabled: Boolean,
content: @Composable () -> Unit,
) {
val theme = LocalRippleTheme provides if (isEnabled) LocalRippleTheme.current else NoRippleTheme()
CompositionLocalProvider(theme) { content() }
}
@Preview
@Composable
fun ButtonTest() {

View file

@ -1,17 +0,0 @@
package com.tangem.tap.common.compose
import androidx.compose.material.ripple.RippleAlpha
import androidx.compose.material.ripple.RippleTheme
import androidx.compose.runtime.Composable
import androidx.compose.ui.graphics.Color
/**
[REDACTED_AUTHOR]
*/
class NoRippleTheme : RippleTheme {
@Composable
override fun defaultColor() = Color.Unspecified
@Composable
override fun rippleAlpha(): RippleAlpha = RippleAlpha(0.0f, 0.0f, 0.0f, 0.0f)
}

View file

@ -25,6 +25,7 @@ import com.tangem.tap.features.onboarding.products.wallet.ui.OnboardingWalletFra
import com.tangem.tap.features.saveWallet.ui.SaveWalletBottomSheetFragment
import com.tangem.tap.features.send.ui.SendFragment
import com.tangem.tap.features.shop.ui.ShopFragment
import com.tangem.tap.features.sprinklr.ui.SprinklrFragment
import com.tangem.tap.features.tokens.addCustomToken.AddCustomTokenFragment
import com.tangem.tap.features.tokens.ui.AddTokensFragment
import com.tangem.tap.features.wallet.ui.WalletDetailsFragment
@ -120,5 +121,6 @@ private fun fragmentFactory(screen: AppScreen): Fragment {
AppScreen.Welcome -> WelcomeFragment()
AppScreen.SaveWallet -> SaveWalletBottomSheetFragment()
AppScreen.WalletSelector -> WalletSelectorBottomSheetFragment()
AppScreen.Sprinklr -> SprinklrFragment()
}
}

View file

@ -13,11 +13,12 @@ import com.tangem.tap.features.onboarding.products.wallet.redux.OnboardingWallet
import com.tangem.tap.features.saveWallet.redux.SaveWalletReducer
import com.tangem.tap.features.send.redux.reducers.SendScreenReducer
import com.tangem.tap.features.shop.redux.ShopReducer
import com.tangem.tap.features.sprinklr.redux.SprinklrReducer
import com.tangem.tap.features.tokens.redux.TokensReducer
import com.tangem.tap.features.wallet.redux.reducers.WalletReducer
import com.tangem.tap.proxy.AppStateHolder
import com.tangem.tap.features.walletSelector.redux.WalletSelectorReducer
import com.tangem.tap.features.welcome.redux.WelcomeReducer
import com.tangem.tap.proxy.AppStateHolder
import org.rekotlin.Action
fun appReducer(action: Action, state: AppState?, appStateHolder: AppStateHolder): AppState {
@ -42,6 +43,7 @@ fun appReducer(action: Action, state: AppState?, appStateHolder: AppStateHolder)
welcomeState = WelcomeReducer.reduce(action, state),
saveWalletState = SaveWalletReducer.reduce(action, state),
walletSelectorState = WalletSelectorReducer.reduce(action, state),
sprinklrState = SprinklrReducer.reduce(action, state),
)
}

View file

@ -31,6 +31,8 @@ import com.tangem.tap.features.send.redux.middlewares.SendMiddleware
import com.tangem.tap.features.send.redux.states.SendState
import com.tangem.tap.features.shop.redux.ShopMiddleware
import com.tangem.tap.features.shop.redux.ShopState
import com.tangem.tap.features.sprinklr.redux.SprinklrMiddleware
import com.tangem.tap.features.sprinklr.redux.SprinklrState
import com.tangem.tap.features.tokens.redux.TokensMiddleware
import com.tangem.tap.features.tokens.redux.TokensState
import com.tangem.tap.features.wallet.redux.WalletState
@ -60,6 +62,7 @@ data class AppState(
val welcomeState: WelcomeState = WelcomeState(),
val saveWalletState: SaveWalletState = SaveWalletState(),
val walletSelectorState: WalletSelectorState = WalletSelectorState(),
val sprinklrState: SprinklrState = SprinklrState(),
) : StateType {
val domainState: DomainState
@ -94,6 +97,7 @@ data class AppState(
WalletSelectorMiddleware().middleware,
LockUserWalletsTimerMiddleware().middleware,
AccessCodeRequestPolicyMiddleware().middleware,
SprinklrMiddleware().middleware,
)
}
}

View file

@ -9,7 +9,6 @@ import com.tangem.domain.common.LogConfig
import com.tangem.domain.common.ProductType
import com.tangem.domain.common.ScanResponse
import com.tangem.domain.common.extensions.withMainContext
import com.tangem.tap.common.chat.SprinklrConfig
import com.tangem.tap.common.extensions.dispatchDebugErrorNotification
import com.tangem.tap.common.extensions.dispatchDialogShow
import com.tangem.tap.common.extensions.dispatchOnMain
@ -111,7 +110,7 @@ private fun handleAction(action: Action, appState: () -> AppState?, dispatch: Di
// if config not set -> try to get it based on a scanResponse.productType
val unsafeChatConfig = action.chatConfig ?: when {
scanResponse?.isSaltPay() == true -> config.saltPayConfig?.sprinklrAppID?.let(::SprinklrConfig)
scanResponse?.isSaltPay() == true -> config.saltPayConfig?.sprinklr
else -> config.zendesk
}

View file

@ -26,4 +26,5 @@ enum class AppScreen(
Welcome,
SaveWallet(isDialogFragment = true),
WalletSelector(isDialogFragment = true),
Sprinklr,
}

View file

@ -1,12 +1,13 @@
package com.tangem.tap.features.onboarding.products.wallet.saltPay
import com.tangem.tap.common.chat.SprinklrConfig
import org.spongycastle.util.encoders.Base64.toBase64String
/**
[REDACTED_AUTHOR]
*/
data class SaltPayConfig(
val sprinklrAppID: String,
val sprinklr: SprinklrConfig,
val kycProvider: KYCProvider,
val credentials: Credentials,
val blockscoutCredentials: Credentials,
@ -14,7 +15,7 @@ data class SaltPayConfig(
companion object {
fun stub(): SaltPayConfig {
return SaltPayConfig(
sprinklrAppID = "",
sprinklr = SprinklrConfig("", ""),
kycProvider = KYCProvider("", "", "", ""),
credentials = Credentials("", ""),
blockscoutCredentials = Credentials("", ""),

View file

@ -4,7 +4,6 @@ import android.app.Dialog
import android.content.Context
import androidx.appcompat.app.AlertDialog
import com.tangem.common.extensions.guard
import com.tangem.tap.common.chat.SprinklrConfig
import com.tangem.tap.common.extensions.dispatchDebugErrorNotification
import com.tangem.tap.common.feedback.SupportInfo
import com.tangem.tap.common.redux.global.GlobalAction
@ -21,14 +20,14 @@ class NoFundsForActivationDialog {
setTitle(R.string.saltpay_error_no_gas_title)
setMessage(R.string.saltpay_error_no_gas_message)
setPositiveButton(R.string.chat_button_title) { _, _ ->
val sprinklrAppId = store.state.globalState.configManager?.config
val sprinklrConfig = store.state.globalState.configManager?.config
?.saltPayConfig
?.sprinklrAppID
?.sprinklr
.guard {
store.dispatchDebugErrorNotification("SaltPayConfig not initialized")
return@setPositiveButton
}
store.dispatch(GlobalAction.OpenChat(SupportInfo(), SprinklrConfig(sprinklrAppId)))
store.dispatch(GlobalAction.OpenChat(SupportInfo(), sprinklrConfig))
}
setNegativeButton(R.string.common_cancel) { _, _ ->
}

View file

@ -0,0 +1,9 @@
package com.tangem.tap.features.sprinklr.redux
import com.tangem.tap.common.chat.SprinklrConfig
import org.rekotlin.Action
sealed interface SprinklrAction : Action {
data class SetConfig(val config: SprinklrConfig) : SprinklrAction
data class UpdateUrl(val url: String) : SprinklrAction
}

View file

@ -0,0 +1,34 @@
package com.tangem.tap.features.sprinklr.redux
import com.tangem.tap.common.extensions.dispatchOnMain
import com.tangem.tap.common.redux.AppState
import com.tangem.tap.store
import org.rekotlin.Middleware
internal class SprinklrMiddleware {
val middleware: Middleware<AppState> = { _, appStateProvider ->
{ next ->
{ action ->
val appState = appStateProvider()
if (action is SprinklrAction && appState != null) {
handleAction(action)
}
next(action)
}
}
}
private fun handleAction(action: SprinklrAction) {
when (action) {
is SprinklrAction.SetConfig -> createUrl()
is SprinklrAction.UpdateUrl -> Unit
}
}
private fun createUrl() {
// TODO: Create Sprinklr URL, will be in further MRs
val url = "https://prod-live-chat.sprinklr.com/" +
"page?appId=60c1d169c96beb5bf5a326f3_app_950954&device=MOBILE&enableClose=true&zoom=false"
store.dispatchOnMain(SprinklrAction.UpdateUrl(url))
}
}

View file

@ -0,0 +1,19 @@
package com.tangem.tap.features.sprinklr.redux
import com.tangem.tap.common.redux.AppState
import org.rekotlin.Action
internal object SprinklrReducer {
fun reduce(action: Action, state: AppState): SprinklrState {
return if (action is SprinklrAction) {
internalReduce(action, state.sprinklrState)
} else state.sprinklrState
}
private fun internalReduce(action: SprinklrAction, state: SprinklrState): SprinklrState {
return when (action) {
is SprinklrAction.SetConfig -> state
is SprinklrAction.UpdateUrl -> state.copy(url = action.url)
}
}
}

View file

@ -0,0 +1,5 @@
package com.tangem.tap.features.sprinklr.redux
data class SprinklrState(
val url: String = "",
)

View file

@ -0,0 +1,31 @@
package com.tangem.tap.features.sprinklr.ui
import androidx.activity.compose.BackHandler
import androidx.compose.foundation.layout.imePadding
import androidx.compose.foundation.layout.systemBarsPadding
import androidx.compose.runtime.Composable
import androidx.compose.runtime.State
import androidx.compose.runtime.collectAsState
import androidx.compose.ui.Modifier
import androidx.fragment.app.viewModels
import com.tangem.core.ui.fragments.ComposeFragment
internal class SprinklrFragment : ComposeFragment<SprinklrScreenState>() {
private val viewModel by viewModels<SprinklrViewModel>()
@Composable
override fun provideState(): State<SprinklrScreenState> {
return viewModel.state.collectAsState()
}
@Composable
override fun ScreenContent(modifier: Modifier, state: SprinklrScreenState) {
BackHandler(onBack = state.onNavigateBack)
SprinklrScreenContent(
modifier = modifier
.systemBarsPadding()
.imePadding(),
state = state,
)
}
}

View file

@ -0,0 +1,25 @@
package com.tangem.tap.features.sprinklr.ui
import android.annotation.SuppressLint
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import com.google.accompanist.web.WebView
import com.google.accompanist.web.rememberWebViewState
@SuppressLint("SetJavaScriptEnabled")
@Composable
internal fun SprinklrScreenContent(
state: SprinklrScreenState,
modifier: Modifier = Modifier,
) {
WebView(
modifier = modifier,
state = rememberWebViewState(url = state.initialUrl),
onCreated = { webView ->
with(webView.settings) {
javaScriptEnabled = true
domStorageEnabled = true
}
},
)
}

View file

@ -0,0 +1,6 @@
package com.tangem.tap.features.sprinklr.ui
internal data class SprinklrScreenState(
val initialUrl: String = "",
val onNavigateBack: () -> Unit = {},
)

View file

@ -0,0 +1,44 @@
package com.tangem.tap.features.sprinklr.ui
import androidx.lifecycle.ViewModel
import com.tangem.tap.common.extensions.dispatchOnMain
import com.tangem.tap.common.redux.navigation.NavigationAction
import com.tangem.tap.features.sprinklr.redux.SprinklrState
import com.tangem.tap.store
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.flow.update
import org.rekotlin.StoreSubscriber
internal class SprinklrViewModel : ViewModel(), StoreSubscriber<SprinklrState> {
private val stateInternal = MutableStateFlow(SprinklrScreenState())
val state = stateInternal.asStateFlow()
init {
subscribeToStoreChanges()
}
override fun newState(state: SprinklrState) {
stateInternal.update { prevState ->
prevState.copy(
initialUrl = state.url,
onNavigateBack = this::navigateBack,
)
}
}
override fun onCleared() {
store.unsubscribe(this)
}
private fun subscribeToStoreChanges() {
store.subscribe(this) { appState ->
appState.skip { old, new -> old.sprinklrState == new.sprinklrState }
.select { it.sprinklrState }
}
}
private fun navigateBack() {
store.dispatchOnMain(NavigationAction.PopBackTo())
}
}

View file

@ -55,7 +55,6 @@ import com.tangem.domain.redux.domainStore
import com.tangem.tap.common.compose.AddCustomTokenWarning
import com.tangem.tap.common.compose.ClosePopupTrigger
import com.tangem.tap.common.compose.ComposeDialogManager
import com.tangem.tap.common.compose.ToggledRippleTheme
import com.tangem.tap.common.compose.keyboardAsState
import com.tangem.tap.domain.moduleMessage.ModuleMessageConverter
import com.tangem.tap.features.tokens.addCustomToken.compose.test.TestCase
@ -66,7 +65,7 @@ import kotlinx.coroutines.launch
/**
[REDACTED_AUTHOR]
*/
private class AddCustomTokenScreen {} // for simple search
private class AddCustomTokenScreen // for simple search
@OptIn(ExperimentalMaterialApi::class)
@Composable
@ -91,11 +90,13 @@ fun AddCustomTokenScreen(
},
sheetPeekHeight = 0.dp,
) {
Column() {
TestCasesList(onItemClick = {
selectedTestCase.value = it
toggleBottomSheet()
})
Column {
TestCasesList(
onItemClick = {
selectedTestCase.value = it
toggleBottomSheet()
},
)
ScreenContent(state, closePopupTrigger)
}
}
@ -228,23 +229,21 @@ private fun AddCustomTokenFab(
FloatingActionButtonDefaults.elevation()
}
ToggledRippleTheme(isEnabled) {
ExtendedFloatingActionButton(
modifier = modifier,
icon = {
Icon(
imageVector = Icons.Filled.Add,
tint = contentColor,
contentDescription = "Add",
)
},
text = { Text(text = stringResource(id = R.string.common_add)) },
onClick = { if (isEnabled) onClick() },
backgroundColor = backgroundColor,
contentColor = contentColor,
elevation = elevation,
)
}
ExtendedFloatingActionButton(
modifier = modifier,
icon = {
Icon(
imageVector = Icons.Filled.Add,
tint = contentColor,
contentDescription = "Add",
)
},
text = { Text(text = stringResource(id = R.string.common_add)) },
onClick = { if (isEnabled) onClick() },
backgroundColor = backgroundColor,
contentColor = contentColor,
elevation = elevation,
)
}
data class ScreenFieldData(

View file

@ -16,4 +16,6 @@
</trust-anchors>
</domain-config>
</network-security-config>
<base-config cleartextTrafficPermitted="false" />
</network-security-config>

View file

@ -30,6 +30,7 @@ object Library {
const val accompanistAppCompatTheme = "com.google.accompanist:accompanist-appcompat-theme:" + Versions.accompanist
const val accompanistSystemUiController =
"com.google.accompanist:accompanist-systemuicontroller:" + Versions.accompanist
const val accompanistWebView = "com.google.accompanist:accompanist-webview:" + Versions.accompanist
const val amplitude = "com.amplitude:android-sdk:" + Versions.amplitude
const val appsflyer = "com.appsflyer:af-android-sdk:" + Versions.appsflyer
const val armadillo = "at.favre.lib:armadillo:" + Versions.armadillo

View file

@ -23,7 +23,7 @@ object Versions {
// endregion Compose
// region Other libraries
const val accompanist = "0.23.0"
const val accompanist = "0.28.0"
const val amplitude = "2.36.1"
const val appsflyer = "6.5.1"
const val armadillo = "0.9.0"