Updated on 2026-08-14
This commit is contained in:
parent
2561a7eef6
commit
e95243ab79
28 changed files with 422 additions and 112 deletions
|
|
@ -627,5 +627,7 @@ internal class DefaultUserWalletsListRepository(
|
|||
private suspend fun onAllWalletsDeleted() {
|
||||
// reset flag (that is set from AF deeplink) after removing the last wallet
|
||||
mobileWalletPromoRepository.setShouldShowMobileWalletPromo(false)
|
||||
// wipe the Usedesk support-chat clientId so a fresh UUID is generated for the next wallet
|
||||
appPreferencesStore.editData { it.remove(PreferencesKeys.USEDESK_CLIENT_ID_KEY) }
|
||||
}
|
||||
}
|
||||
|
|
@ -487,10 +487,12 @@ internal class ChildFactory @Inject constructor(
|
|||
componentFactory = feedEntryComponentFactory,
|
||||
)
|
||||
}
|
||||
is AppRoute.Usedesk -> { // TODO [REDACTED_TASK_KEY] pass params
|
||||
is AppRoute.Usedesk -> {
|
||||
createComponentChild(
|
||||
context = context,
|
||||
params = UsedeskComponent.Params(),
|
||||
params = UsedeskComponent.Params(
|
||||
userWalletId = route.walletMetaInfo.userWalletId?.stringValue,
|
||||
),
|
||||
componentFactory = usedeskComponentFactory,
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -16,7 +16,7 @@
|
|||
"version": "5.39"
|
||||
},
|
||||
{
|
||||
"name": "USEDESK_ENABLED",
|
||||
"name": "TWI_485_USEDESK_ENABLED",
|
||||
"version": "undefined"
|
||||
},
|
||||
{
|
||||
|
|
|
|||
|
|
@ -50,7 +50,6 @@ internal class FeatureTogglesNamingConventionTest {
|
|||
"SOLANA_TX_HISTORY_ENABLED",
|
||||
"STAKING_ETH_ENABLED",
|
||||
"SWAP_AB_ENABLED",
|
||||
"USEDESK_ENABLED",
|
||||
"VIRTUAL_ACCOUNTS_ENABLED",
|
||||
"VISA_ONBOARDING_ENABLED",
|
||||
"WALLET_CONNECT_BITCOIN_ENABLED",
|
||||
|
|
|
|||
|
|
@ -41,6 +41,8 @@ object PreferencesKeys {
|
|||
|
||||
val USED_CARDS_INFO_KEY by lazy { stringPreferencesKey(name = "usedCardsInfo_v2") }
|
||||
|
||||
val USEDESK_CLIENT_ID_KEY by lazy { stringPreferencesKey(name = "usedeskClientId") }
|
||||
|
||||
val APP_THEME_MODE_KEY by lazy { stringPreferencesKey(name = "appThemeMode") }
|
||||
|
||||
val SELECTED_APP_CURRENCY_KEY by lazy { stringPreferencesKey(name = "selectedAppCurrency") }
|
||||
|
|
|
|||
|
|
@ -9,5 +9,5 @@ internal class DefaultFeedbackFeatureToggles(
|
|||
) : FeedbackFeatureToggles {
|
||||
|
||||
override val isUsedeskEnabled: Boolean
|
||||
get() = featureTogglesManager.isFeatureEnabled(toggle = FeatureToggles.USEDESK_ENABLED)
|
||||
get() = featureTogglesManager.isFeatureEnabled(toggle = FeatureToggles.TWI_485_USEDESK_ENABLED)
|
||||
}
|
||||
|
|
@ -13,6 +13,8 @@ sealed class Settings(
|
|||
|
||||
class ButtonManageTokens : Settings(event = "Button - Manage Tokens")
|
||||
|
||||
class ButtonOpenChat : Settings(event = "Button - Open Chat")
|
||||
|
||||
class ColdWalletAdded(
|
||||
source: AnalyticsParam.ScreensSources?,
|
||||
) : Settings(
|
||||
|
|
|
|||
|
|
@ -30,11 +30,9 @@ internal class PreviewDetailsComponent : DetailsComponent {
|
|||
).buildAll(
|
||||
isWalletConnectAvailable = true,
|
||||
isAddressBookAvailable = true,
|
||||
isSupportChatAvailable = true,
|
||||
hasAnyMobileWallet = true,
|
||||
userWalletId = UserWalletId(""),
|
||||
onSupportEmailClick = {},
|
||||
onSupportChatClick = {},
|
||||
onSupportClick = {},
|
||||
onBuyClick = {},
|
||||
)
|
||||
}
|
||||
|
|
@ -48,6 +46,7 @@ internal class PreviewDetailsComponent : DetailsComponent {
|
|||
items = previewBlocks,
|
||||
footer = previewFooter,
|
||||
selectFeedbackEmailTypeBSConfig = TangemBottomSheetConfig.Empty,
|
||||
selectContactSupportTypeBSConfig = TangemBottomSheetConfig.Empty,
|
||||
popBack = { /* no-op */ },
|
||||
)
|
||||
|
||||
|
|
|
|||
|
|
@ -7,5 +7,6 @@ internal data class DetailsUM(
|
|||
val items: ImmutableList<DetailsItemUM>,
|
||||
val footer: DetailsFooterUM,
|
||||
val selectFeedbackEmailTypeBSConfig: TangemBottomSheetConfig,
|
||||
val selectContactSupportTypeBSConfig: TangemBottomSheetConfig,
|
||||
val popBack: () -> Unit,
|
||||
)
|
||||
|
|
@ -0,0 +1,16 @@
|
|||
package com.tangem.features.details.entity
|
||||
|
||||
import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfigContent
|
||||
import com.tangem.core.ui.extensions.TextReference
|
||||
import com.tangem.core.ui.extensions.resourceReference
|
||||
import com.tangem.features.details.impl.R
|
||||
|
||||
internal data class SelectContactSupportTypeBS(
|
||||
val onOptionClick: (Option) -> Unit,
|
||||
) : TangemBottomSheetConfigContent {
|
||||
|
||||
enum class Option(val text: TextReference) {
|
||||
Mail(resourceReference(R.string.support_selector_view_email_button)),
|
||||
Chat(resourceReference(R.string.support_selector_view_chat_button)),
|
||||
}
|
||||
}
|
||||
|
|
@ -24,6 +24,7 @@ import com.tangem.domain.tangempay.GetTangemPayCustomerIdUseCase
|
|||
import com.tangem.domain.tangempay.TangemPayAnalyticsEvents
|
||||
import com.tangem.domain.walletconnect.CheckIsWalletConnectAvailableUseCase
|
||||
import com.tangem.domain.wallets.usecase.GenerateBuyTangemCardLinkUseCase
|
||||
import com.tangem.domain.wallets.analytics.Settings
|
||||
import com.tangem.domain.wallets.usecase.GetSelectedWalletSyncUseCase
|
||||
import com.tangem.domain.wallets.usecase.GetWalletsUseCase
|
||||
import com.tangem.features.addressbook.AddressBookFeatureToggles
|
||||
|
|
@ -31,6 +32,7 @@ import com.tangem.features.details.component.DetailsComponent
|
|||
import com.tangem.features.details.entity.DetailsFooterUM
|
||||
import com.tangem.features.details.entity.DetailsItemUM
|
||||
import com.tangem.features.details.entity.DetailsUM
|
||||
import com.tangem.features.details.entity.SelectContactSupportTypeBS
|
||||
import com.tangem.features.details.entity.SelectEmailFeedbackTypeBS
|
||||
import com.tangem.features.details.utils.ItemsBuilder
|
||||
import com.tangem.features.details.utils.SocialsBuilder
|
||||
|
|
@ -71,6 +73,8 @@ internal class DetailsModel @Inject constructor(
|
|||
|
||||
private val params: DetailsComponent.Params = paramsContainer.require()
|
||||
|
||||
private val isUsedeskEnabled = feedbackFeatureToggles.isUsedeskEnabled
|
||||
|
||||
private val items: MutableStateFlow<ImmutableList<DetailsItemUM>>
|
||||
|
||||
val state: MutableStateFlow<DetailsUM>
|
||||
|
|
@ -89,11 +93,9 @@ internal class DetailsModel @Inject constructor(
|
|||
itemsBuilder.buildAll(
|
||||
isWalletConnectAvailable = isWalletConnectAvailable,
|
||||
isAddressBookAvailable = addressBookFeatureToggles.isAddressBookEnabled,
|
||||
isSupportChatAvailable = feedbackFeatureToggles.isUsedeskEnabled,
|
||||
hasAnyMobileWallet = getWalletsUseCase.invokeSync().any { it is UserWallet.Hot },
|
||||
userWalletId = params.userWalletId,
|
||||
onSupportEmailClick = ::sendFeedback,
|
||||
onSupportChatClick = ::openUseDesk,
|
||||
onSupportClick = ::onContactSupportClick,
|
||||
onBuyClick = ::onBuyClick,
|
||||
),
|
||||
)
|
||||
|
|
@ -108,6 +110,7 @@ internal class DetailsModel @Inject constructor(
|
|||
appVersion = getAppVersion(),
|
||||
),
|
||||
selectFeedbackEmailTypeBSConfig = TangemBottomSheetConfig.Empty,
|
||||
selectContactSupportTypeBSConfig = TangemBottomSheetConfig.Empty,
|
||||
popBack = router::pop,
|
||||
),
|
||||
)
|
||||
|
|
@ -159,6 +162,46 @@ internal class DetailsModel @Inject constructor(
|
|||
}
|
||||
}
|
||||
|
||||
private fun onContactSupportClick() {
|
||||
// Offer the mail/chat choice only when the chat is available; otherwise open mail directly.
|
||||
if (isUsedeskEnabled) {
|
||||
showContactSupportChooserBS()
|
||||
} else {
|
||||
sendFeedback()
|
||||
}
|
||||
}
|
||||
|
||||
private fun showContactSupportChooserBS() {
|
||||
state.update { current ->
|
||||
current.copy(
|
||||
selectContactSupportTypeBSConfig = TangemBottomSheetConfig(
|
||||
isShown = true,
|
||||
onDismissRequest = ::hideContactSupportChooserBS,
|
||||
content = SelectContactSupportTypeBS(
|
||||
onOptionClick = { option ->
|
||||
hideContactSupportChooserBS()
|
||||
when (option) {
|
||||
SelectContactSupportTypeBS.Option.Mail -> sendFeedback()
|
||||
SelectContactSupportTypeBS.Option.Chat -> {
|
||||
analyticsEventHandler.send(Settings.ButtonOpenChat())
|
||||
openUseDesk()
|
||||
}
|
||||
}
|
||||
},
|
||||
),
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private fun hideContactSupportChooserBS() {
|
||||
state.update { current ->
|
||||
current.copy(
|
||||
selectContactSupportTypeBSConfig = current.selectContactSupportTypeBSConfig.copy(isShown = false),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private fun openUseDesk() {
|
||||
modelScope.launch {
|
||||
val userWallet = getSelectedWalletSyncUseCase().getOrNull() ?: error("Selected wallet is null")
|
||||
|
|
|
|||
|
|
@ -76,6 +76,7 @@ internal fun DetailsScreen(
|
|||
}
|
||||
|
||||
SelectFeedbackEmailTypeBottomSheet(state.selectFeedbackEmailTypeBSConfig)
|
||||
SelectContactSupportTypeBottomSheet(state.selectContactSupportTypeBSConfig)
|
||||
}
|
||||
|
||||
@Composable
|
||||
|
|
|
|||
|
|
@ -0,0 +1,58 @@
|
|||
package com.tangem.features.details.ui
|
||||
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Modifier
|
||||
import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig
|
||||
import com.tangem.core.ui.components.bottomsheets.sheet.TangemBottomSheet
|
||||
import com.tangem.core.ui.components.inputrow.InputRowChecked
|
||||
import com.tangem.core.ui.components.inputrow.inner.DividerContainer
|
||||
import com.tangem.core.ui.decorations.roundedShapeItemDecoration
|
||||
import com.tangem.core.ui.extensions.resourceReference
|
||||
import com.tangem.core.ui.res.TangemTheme
|
||||
import com.tangem.features.details.entity.SelectContactSupportTypeBS
|
||||
import com.tangem.features.details.impl.R
|
||||
|
||||
@Composable
|
||||
internal fun SelectContactSupportTypeBottomSheet(config: TangemBottomSheetConfig) {
|
||||
TangemBottomSheet<SelectContactSupportTypeBS>(
|
||||
config = config,
|
||||
titleText = resourceReference(R.string.common_contact_support),
|
||||
containerColor = TangemTheme.colors.background.tertiary,
|
||||
content = { Content(it) },
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun Content(content: SelectContactSupportTypeBS) {
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.padding(
|
||||
start = TangemTheme.dimens.spacing16,
|
||||
end = TangemTheme.dimens.spacing16,
|
||||
bottom = TangemTheme.dimens.spacing16,
|
||||
),
|
||||
) {
|
||||
SelectContactSupportTypeBS.Option.entries.forEachIndexed { index, type ->
|
||||
DividerContainer(
|
||||
modifier = Modifier
|
||||
.roundedShapeItemDecoration(
|
||||
currentIndex = index,
|
||||
lastIndex = SelectContactSupportTypeBS.Option.entries.lastIndex,
|
||||
addDefaultPadding = false,
|
||||
)
|
||||
.background(TangemTheme.colors.background.action)
|
||||
.clickable { content.onOptionClick(type) },
|
||||
showDivider = index != SelectContactSupportTypeBS.Option.entries.lastIndex,
|
||||
) {
|
||||
InputRowChecked(
|
||||
text = type.text,
|
||||
checked = false,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -27,11 +27,9 @@ internal class ItemsBuilder @Inject constructor(
|
|||
fun buildAll(
|
||||
isWalletConnectAvailable: Boolean,
|
||||
isAddressBookAvailable: Boolean,
|
||||
isSupportChatAvailable: Boolean,
|
||||
hasAnyMobileWallet: Boolean,
|
||||
userWalletId: UserWalletId,
|
||||
onSupportEmailClick: () -> Unit,
|
||||
onSupportChatClick: () -> Unit,
|
||||
onSupportClick: () -> Unit,
|
||||
onBuyClick: () -> Unit,
|
||||
): ImmutableList<DetailsItemUM> = buildList {
|
||||
if (isAddressBookAvailable) {
|
||||
|
|
@ -50,11 +48,7 @@ internal class ItemsBuilder @Inject constructor(
|
|||
|
||||
buildShopBlock(onBuyClick).let(::add)
|
||||
buildSettingsBlock().let(::add)
|
||||
buildSupportBlock(
|
||||
onSupportEmailClick = onSupportEmailClick,
|
||||
onSupportChatClick = onSupportChatClick,
|
||||
isSupportChatAvailable = isSupportChatAvailable,
|
||||
).let(::add)
|
||||
buildSupportBlock(onSupportClick = onSupportClick).let(::add)
|
||||
}.toImmutableList()
|
||||
|
||||
fun addTangemPayItem(items: ImmutableList<DetailsItemUM>, onClick: () -> Unit): ImmutableList<DetailsItemUM> {
|
||||
|
|
@ -148,33 +142,18 @@ internal class ItemsBuilder @Inject constructor(
|
|||
}.toImmutableList(),
|
||||
)
|
||||
|
||||
private fun buildSupportBlock(
|
||||
onSupportEmailClick: () -> Unit,
|
||||
onSupportChatClick: () -> Unit,
|
||||
isSupportChatAvailable: Boolean,
|
||||
): DetailsItemUM = DetailsItemUM.Basic(
|
||||
private fun buildSupportBlock(onSupportClick: () -> Unit): DetailsItemUM = DetailsItemUM.Basic(
|
||||
id = "support",
|
||||
items = buildList {
|
||||
DetailsItemUM.Basic.Item(
|
||||
id = "support_email",
|
||||
id = "contact_support",
|
||||
block = BlockUM(
|
||||
text = resourceReference(R.string.common_contact_support),
|
||||
iconRes = R.drawable.ic_comment_24,
|
||||
onClick = onSupportEmailClick,
|
||||
onClick = onSupportClick,
|
||||
),
|
||||
).let(::add)
|
||||
|
||||
if (isSupportChatAvailable) {
|
||||
DetailsItemUM.Basic.Item(
|
||||
id = "support_chat",
|
||||
block = BlockUM(
|
||||
text = resourceReference(R.string.details_row_title_contact_to_support_chat),
|
||||
iconRes = R.drawable.ic_chat_24,
|
||||
onClick = onSupportChatClick,
|
||||
),
|
||||
).let(::add)
|
||||
}
|
||||
|
||||
DetailsItemUM.Basic.Item(
|
||||
id = "disclaimer",
|
||||
block = BlockUM(
|
||||
|
|
|
|||
|
|
@ -34,7 +34,7 @@ internal class DetailsModelFeedbackTest : DetailsModelTestBase() {
|
|||
// Act
|
||||
val model = createModel(this)
|
||||
advanceUntilIdle()
|
||||
onEmailSlot.captured.invoke()
|
||||
onSupportSlot.captured.invoke()
|
||||
advanceUntilIdle()
|
||||
|
||||
// Assert
|
||||
|
|
@ -54,7 +54,7 @@ internal class DetailsModelFeedbackTest : DetailsModelTestBase() {
|
|||
|
||||
val model = createModel(this)
|
||||
advanceUntilIdle()
|
||||
onEmailSlot.captured.invoke()
|
||||
onSupportSlot.captured.invoke()
|
||||
advanceUntilIdle()
|
||||
|
||||
verify { analyticsEventHandler.send(any<Basic.ButtonSupport>()) }
|
||||
|
|
@ -73,7 +73,7 @@ internal class DetailsModelFeedbackTest : DetailsModelTestBase() {
|
|||
|
||||
val model = createModel(this)
|
||||
advanceUntilIdle()
|
||||
onEmailSlot.captured.invoke()
|
||||
onSupportSlot.captured.invoke()
|
||||
advanceUntilIdle()
|
||||
|
||||
val bsConfig = model.state.value.selectFeedbackEmailTypeBSConfig
|
||||
|
|
@ -93,7 +93,7 @@ internal class DetailsModelFeedbackTest : DetailsModelTestBase() {
|
|||
|
||||
val model = createModel(this)
|
||||
advanceUntilIdle()
|
||||
onEmailSlot.captured.invoke()
|
||||
onSupportSlot.captured.invoke()
|
||||
advanceUntilIdle()
|
||||
|
||||
coVerify(exactly = 0) { sendFeedbackEmailUseCase(any()) }
|
||||
|
|
@ -183,7 +183,7 @@ internal class DetailsModelFeedbackTest : DetailsModelTestBase() {
|
|||
|
||||
currentModel = createModel(this)
|
||||
advanceUntilIdle()
|
||||
onEmailSlot.captured.invoke()
|
||||
onSupportSlot.captured.invoke()
|
||||
advanceUntilIdle()
|
||||
|
||||
return currentModel.state.value.selectFeedbackEmailTypeBSConfig.content as SelectEmailFeedbackTypeBS
|
||||
|
|
|
|||
|
|
@ -65,24 +65,6 @@ internal class DetailsModelInitTest : DetailsModelTestBase() {
|
|||
assertThat(abSlot.captured).isFalse()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `GIVEN usedesk enabled WHEN init THEN buildAll receives isSupportChatAvailable true`() = runTest {
|
||||
every { feedbackFeatureToggles.isUsedeskEnabled } returns true
|
||||
|
||||
createModel(this).also { advanceUntilIdle() }.onDestroy()
|
||||
|
||||
assertThat(chatSlot.captured).isTrue()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `GIVEN usedesk disabled WHEN init THEN buildAll receives isSupportChatAvailable false`() = runTest {
|
||||
every { feedbackFeatureToggles.isUsedeskEnabled } returns false
|
||||
|
||||
createModel(this).also { advanceUntilIdle() }.onDestroy()
|
||||
|
||||
assertThat(chatSlot.captured).isFalse()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `GIVEN a hot wallet present WHEN init THEN buildAll receives hasAnyMobileWallet true`() = runTest {
|
||||
every { getWalletsUseCase.invokeSync() } returns listOf(hotWallet(wallet1))
|
||||
|
|
|
|||
|
|
@ -5,7 +5,9 @@ import arrow.core.right
|
|||
import com.tangem.common.routing.AppRoute
|
||||
import com.tangem.core.analytics.models.Basic
|
||||
import com.tangem.domain.wallets.usecase.GenerateBuyTangemCardLinkUseCase
|
||||
import com.tangem.features.details.entity.SelectContactSupportTypeBS
|
||||
import io.mockk.coEvery
|
||||
import io.mockk.coVerify
|
||||
import io.mockk.every
|
||||
import io.mockk.verify
|
||||
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||
|
|
@ -17,17 +19,19 @@ import org.junit.jupiter.api.Test
|
|||
internal class DetailsModelNavigationTest : DetailsModelTestBase() {
|
||||
|
||||
@Test
|
||||
fun `GIVEN selected wallet and meta WHEN support chat clicked THEN router pushes Usedesk`() = runTest {
|
||||
fun `GIVEN usedesk enabled WHEN chat option selected THEN router pushes Usedesk`() = runTest {
|
||||
// Arrange
|
||||
val wallet = hotWallet(wallet1)
|
||||
val meta = metaInfo(wallet1)
|
||||
every { feedbackFeatureToggles.isUsedeskEnabled } returns true
|
||||
every { getSelectedWalletSyncUseCase() } returns wallet.right()
|
||||
coEvery { getWalletMetaInfoUseCase(wallet1) } returns meta.right()
|
||||
|
||||
// Act
|
||||
val model = createModel(this)
|
||||
advanceUntilIdle()
|
||||
onChatSlot.captured.invoke()
|
||||
onSupportSlot.captured.invoke()
|
||||
selectContactSupportOption(model, SelectContactSupportTypeBS.Option.Chat)
|
||||
advanceUntilIdle()
|
||||
|
||||
// Assert
|
||||
|
|
@ -36,20 +40,51 @@ internal class DetailsModelNavigationTest : DetailsModelTestBase() {
|
|||
}
|
||||
|
||||
@Test
|
||||
fun `GIVEN meta info missing WHEN support chat clicked THEN no navigation`() = runTest {
|
||||
fun `GIVEN meta info missing WHEN chat option selected THEN no navigation`() = runTest {
|
||||
val wallet = hotWallet(wallet1)
|
||||
every { feedbackFeatureToggles.isUsedeskEnabled } returns true
|
||||
every { getSelectedWalletSyncUseCase() } returns wallet.right()
|
||||
coEvery { getWalletMetaInfoUseCase(wallet1) } returns Throwable().left()
|
||||
|
||||
val model = createModel(this)
|
||||
advanceUntilIdle()
|
||||
onChatSlot.captured.invoke()
|
||||
onSupportSlot.captured.invoke()
|
||||
selectContactSupportOption(model, SelectContactSupportTypeBS.Option.Chat)
|
||||
advanceUntilIdle()
|
||||
|
||||
verify(exactly = 0) { router.push(route = any(), onComplete = any()) }
|
||||
model.onDestroy()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `GIVEN usedesk enabled WHEN mail option selected THEN sends email and does not open Usedesk`() = runTest {
|
||||
// Arrange
|
||||
val wallet = hotWallet(wallet1)
|
||||
val meta = metaInfo(wallet1)
|
||||
every { feedbackFeatureToggles.isUsedeskEnabled } returns true
|
||||
every { getWalletsUseCase.invokeSync() } returns listOf(wallet)
|
||||
every { getSelectedWalletSyncUseCase() } returns wallet.right()
|
||||
coEvery { getWalletMetaInfoUseCase(wallet1) } returns meta.right()
|
||||
every { getTangemPayCustomerIdUseCase(wallet1) } returns customerId.right()
|
||||
|
||||
// Act
|
||||
val model = createModel(this)
|
||||
advanceUntilIdle()
|
||||
onSupportSlot.captured.invoke()
|
||||
selectContactSupportOption(model, SelectContactSupportTypeBS.Option.Mail)
|
||||
advanceUntilIdle()
|
||||
|
||||
// Assert
|
||||
coVerify { sendFeedbackEmailUseCase(any()) }
|
||||
verify(exactly = 0) { router.push(route = AppRoute.Usedesk(meta), onComplete = any()) }
|
||||
model.onDestroy()
|
||||
}
|
||||
|
||||
private fun selectContactSupportOption(model: DetailsModel, option: SelectContactSupportTypeBS.Option) {
|
||||
val content = model.state.value.selectContactSupportTypeBSConfig.content as SelectContactSupportTypeBS
|
||||
content.onOptionClick(option)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `GIVEN buy link WHEN buy clicked THEN opens url and sends analytics`() = runTest {
|
||||
coEvery {
|
||||
|
|
|
|||
|
|
@ -70,11 +70,9 @@ internal abstract class DetailsModelTestBase {
|
|||
// Captured from itemsBuilder.buildAll(...) so the feature buttons can be driven.
|
||||
protected val wcSlot = slot<Boolean>()
|
||||
protected val abSlot = slot<Boolean>()
|
||||
protected val chatSlot = slot<Boolean>()
|
||||
protected val mobileSlot = slot<Boolean>()
|
||||
protected val walletIdSlot = slot<UserWalletId>()
|
||||
protected val onEmailSlot = slot<() -> Unit>()
|
||||
protected val onChatSlot = slot<() -> Unit>()
|
||||
protected val onSupportSlot = slot<() -> Unit>()
|
||||
protected val onBuySlot = slot<() -> Unit>()
|
||||
protected val onTangemPaySlot = slot<() -> Unit>()
|
||||
|
||||
|
|
@ -96,11 +94,9 @@ internal abstract class DetailsModelTestBase {
|
|||
itemsBuilder.buildAll(
|
||||
isWalletConnectAvailable = capture(wcSlot),
|
||||
isAddressBookAvailable = capture(abSlot),
|
||||
isSupportChatAvailable = capture(chatSlot),
|
||||
hasAnyMobileWallet = capture(mobileSlot),
|
||||
userWalletId = capture(walletIdSlot),
|
||||
onSupportEmailClick = capture(onEmailSlot),
|
||||
onSupportChatClick = capture(onChatSlot),
|
||||
onSupportClick = capture(onSupportSlot),
|
||||
onBuyClick = capture(onBuySlot),
|
||||
)
|
||||
} returns persistentListOf()
|
||||
|
|
@ -136,7 +132,7 @@ internal abstract class DetailsModelTestBase {
|
|||
|
||||
protected fun stubBuildAllReturns(list: ImmutableList<DetailsItemUM>) {
|
||||
every {
|
||||
itemsBuilder.buildAll(any(), any(), any(), any(), any(), any(), any(), any())
|
||||
itemsBuilder.buildAll(any(), any(), any(), any(), any(), any())
|
||||
} returns list
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -192,7 +192,6 @@ internal class ItemsBuilderTest {
|
|||
val result = buildAll(
|
||||
isWalletConnectAvailable = false,
|
||||
isAddressBookAvailable = false,
|
||||
isSupportChatAvailable = false,
|
||||
hasAnyMobileWallet = false,
|
||||
)
|
||||
|
||||
|
|
@ -209,7 +208,7 @@ internal class ItemsBuilderTest {
|
|||
assertThat(shop.items.map { it.id }).containsExactly("buy_tangem_wallet")
|
||||
|
||||
val support = result.single { it.id == "support" } as DetailsItemUM.Basic
|
||||
assertThat(support.items.map { it.id }).containsExactly("support_email", "disclaimer").inOrder()
|
||||
assertThat(support.items.map { it.id }).containsExactly("contact_support", "disclaimer").inOrder()
|
||||
}
|
||||
|
||||
@Test
|
||||
|
|
@ -254,16 +253,13 @@ internal class ItemsBuilderTest {
|
|||
private fun buildAll(
|
||||
isWalletConnectAvailable: Boolean = false,
|
||||
isAddressBookAvailable: Boolean = false,
|
||||
isSupportChatAvailable: Boolean = false,
|
||||
hasAnyMobileWallet: Boolean = false,
|
||||
): ImmutableList<DetailsItemUM> = itemsBuilder.buildAll(
|
||||
isWalletConnectAvailable = isWalletConnectAvailable,
|
||||
isAddressBookAvailable = isAddressBookAvailable,
|
||||
isSupportChatAvailable = isSupportChatAvailable,
|
||||
hasAnyMobileWallet = hasAnyMobileWallet,
|
||||
userWalletId = USER_WALLET_ID,
|
||||
onSupportEmailClick = {},
|
||||
onSupportChatClick = {},
|
||||
onSupportClick = {},
|
||||
onBuyClick = {},
|
||||
)
|
||||
|
||||
|
|
|
|||
|
|
@ -6,7 +6,8 @@ import com.tangem.core.ui.decompose.ComposableContentComponent
|
|||
interface UsedeskComponent : ComposableContentComponent {
|
||||
|
||||
data class Params(
|
||||
val feedback: String? = null, // TODO [REDACTED_TASK_KEY]
|
||||
/** User wallet id (hex string) sent to Usedesk as the client email to identify the user. */
|
||||
val userWalletId: String? = null,
|
||||
)
|
||||
|
||||
interface Factory : ComponentFactory<Params, UsedeskComponent>
|
||||
|
|
|
|||
|
|
@ -20,6 +20,13 @@ dependencies {
|
|||
implementation(projects.common.routing)
|
||||
implementation(projects.features.usedesk.api)
|
||||
|
||||
/** Core */
|
||||
implementation(projects.core.datasource)
|
||||
implementation(projects.core.analytics)
|
||||
|
||||
/** Domain */
|
||||
implementation(projects.domain.feedback)
|
||||
|
||||
/** DI */
|
||||
implementation(deps.hilt.android)
|
||||
kapt(deps.hilt.kapt)
|
||||
|
|
@ -29,8 +36,8 @@ dependencies {
|
|||
implementation(deps.compose.ui)
|
||||
|
||||
/** Usedesk */
|
||||
implementation(deps.usedesk.chat.sdk)
|
||||
implementation(deps.usedesk.chat.gui)
|
||||
implementation(tangemDeps.usedesk.chat.sdk)
|
||||
implementation(tangemDeps.usedesk.chat.gui)
|
||||
|
||||
|
||||
}
|
||||
|
|
@ -1,11 +1,16 @@
|
|||
package com.tangem.feature.usedesk
|
||||
|
||||
import android.view.View
|
||||
import android.view.inputmethod.InputMethodManager
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.DisposableEffect
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.saveable.rememberSaveable
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.viewinterop.AndroidView
|
||||
import androidx.core.content.FileProvider
|
||||
import androidx.fragment.app.FragmentActivity
|
||||
import androidx.fragment.app.FragmentContainerView
|
||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||
|
|
@ -16,7 +21,7 @@ import com.tangem.feature.usedesk.model.UsedeskModel
|
|||
import dagger.assisted.Assisted
|
||||
import dagger.assisted.AssistedFactory
|
||||
import dagger.assisted.AssistedInject
|
||||
import ru.usedesk.chat_gui.chat.UsedeskChatScreen
|
||||
import com.tangem.usedesk.chat_gui.chat.UsedeskChatScreen
|
||||
|
||||
internal class DefaultUsedeskComponent @AssistedInject constructor(
|
||||
@Assisted appComponentContext: AppComponentContext,
|
||||
|
|
@ -25,9 +30,11 @@ internal class DefaultUsedeskComponent @AssistedInject constructor(
|
|||
|
||||
private val model: UsedeskModel = getOrCreateModel(params)
|
||||
|
||||
@Suppress("NestedScopeFunctions")
|
||||
@Composable
|
||||
override fun Content(modifier: Modifier) {
|
||||
val state by model.state.collectAsStateWithLifecycle()
|
||||
val configuration = state.usedeskChatConfiguration
|
||||
|
||||
Box(
|
||||
modifier = Modifier
|
||||
|
|
@ -35,21 +42,65 @@ internal class DefaultUsedeskComponent @AssistedInject constructor(
|
|||
.systemBarsPadding()
|
||||
.imePadding(),
|
||||
) {
|
||||
AndroidView(
|
||||
factory = { context ->
|
||||
FragmentContainerView(context).apply {
|
||||
id = View.generateViewId()
|
||||
// Show the chat only once the configuration is ready (clientId is loaded asynchronously).
|
||||
if (configuration != null) {
|
||||
val activity = LocalContext.current as? FragmentActivity ?: return@Box
|
||||
val containerId = rememberSaveable { View.generateViewId() }
|
||||
|
||||
val fragment = UsedeskChatScreen.newInstance(
|
||||
state.usedeskChatConfiguration,
|
||||
)
|
||||
(context as FragmentActivity).supportFragmentManager.beginTransaction()
|
||||
.replace(id, fragment)
|
||||
.commit()
|
||||
AndroidView(
|
||||
factory = { context ->
|
||||
FragmentContainerView(context).apply {
|
||||
id = containerId
|
||||
|
||||
val fragment = UsedeskChatScreen.newInstance(
|
||||
usedeskChatConfiguration = configuration,
|
||||
allowedFileExtensions = ALLOWED_FILE_EXTENSIONS,
|
||||
cameraEnabled = false,
|
||||
).apply {
|
||||
onChatLoaded = { model.onChatLoaded() }
|
||||
onChatLoadError = { model.onChatLoadError() }
|
||||
onAttachLogs = { onReady ->
|
||||
model.provideLogsFile { file ->
|
||||
val uri = file?.let { logsFile ->
|
||||
FileProvider.getUriForFile(
|
||||
context,
|
||||
"${context.packageName}.provider",
|
||||
logsFile,
|
||||
)
|
||||
}
|
||||
onReady(uri)
|
||||
}
|
||||
}
|
||||
}
|
||||
activity.supportFragmentManager.beginTransaction()
|
||||
.replace(containerId, fragment)
|
||||
.commit()
|
||||
}
|
||||
},
|
||||
modifier = Modifier.fillMaxSize(),
|
||||
)
|
||||
|
||||
DisposableEffect(Unit) {
|
||||
onDispose {
|
||||
// When leaving the chat, hide the keyboard via the (still alive) activity
|
||||
// window and remove the hosted fragment. Otherwise the input connection to
|
||||
// the destroyed EditText leaks: the keyboard stays open and crashes the app
|
||||
// when it is dismissed later.
|
||||
activity.getSystemService(InputMethodManager::class.java)
|
||||
?.hideSoftInputFromWindow(activity.window.decorView.windowToken, 0)
|
||||
|
||||
val fragmentManager = activity.supportFragmentManager
|
||||
if (!fragmentManager.isStateSaved) {
|
||||
fragmentManager.executePendingTransactions()
|
||||
fragmentManager.findFragmentById(containerId)?.let { fragment ->
|
||||
fragmentManager.beginTransaction()
|
||||
.remove(fragment)
|
||||
.commit()
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
modifier = Modifier.fillMaxSize(),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -57,4 +108,14 @@ internal class DefaultUsedeskComponent @AssistedInject constructor(
|
|||
interface Factory : UsedeskComponent.Factory {
|
||||
override fun create(context: AppComponentContext, params: UsedeskComponent.Params): DefaultUsedeskComponent
|
||||
}
|
||||
|
||||
private companion object {
|
||||
// Allowed attachment types: zip, images (jpg/jpeg/png/gif/webp/heic),
|
||||
// videos (mp4/mov/webm). jpeg is added as a synonym for jpg.
|
||||
val ALLOWED_FILE_EXTENSIONS = listOf(
|
||||
"zip",
|
||||
"jpg", "jpeg", "png", "gif", "webp", "heic",
|
||||
"mp4", "mov", "webm",
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,19 @@
|
|||
package com.tangem.feature.usedesk.analytics
|
||||
|
||||
import com.tangem.core.analytics.models.AnalyticsEvent
|
||||
import com.tangem.core.analytics.models.AnalyticsParam
|
||||
|
||||
internal sealed class UsedeskAnalyticsEvents(
|
||||
event: String,
|
||||
params: Map<String, String> = emptyMap(),
|
||||
) : AnalyticsEvent(category = "Support", event = event, params = params) {
|
||||
|
||||
class ChatScreenOpened(source: AnalyticsParam.ScreensSources) : UsedeskAnalyticsEvents(
|
||||
event = "Chat Screen Opened",
|
||||
params = mapOf(AnalyticsParam.SOURCE to source.value),
|
||||
)
|
||||
|
||||
class ChatScreenError : UsedeskAnalyticsEvents(event = "Chat Screen Error")
|
||||
|
||||
class ChatScreenClosed : UsedeskAnalyticsEvents(event = "Chat Screen Closed")
|
||||
}
|
||||
|
|
@ -1,35 +1,109 @@
|
|||
package com.tangem.feature.usedesk.model
|
||||
|
||||
import androidx.compose.runtime.Stable
|
||||
import com.tangem.core.analytics.api.AnalyticsEventHandler
|
||||
import com.tangem.core.analytics.models.AnalyticsParam
|
||||
import com.tangem.core.decompose.di.ModelScoped
|
||||
import com.tangem.core.decompose.model.Model
|
||||
import com.tangem.core.decompose.model.ParamsContainer
|
||||
import com.tangem.datasource.local.preferences.AppPreferencesStore
|
||||
import com.tangem.datasource.local.preferences.PreferencesKeys
|
||||
import com.tangem.domain.feedback.repository.FeedbackRepository
|
||||
import com.tangem.feature.usedesk.analytics.UsedeskAnalyticsEvents
|
||||
import com.tangem.feature.usedesk.api.UsedeskComponent
|
||||
import com.tangem.usedesk.chat_sdk.entity.UsedeskChatConfiguration
|
||||
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
|
||||
import com.tangem.utils.coroutines.runSuspendCatching
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import ru.usedesk.chat_sdk.entity.UsedeskChatConfiguration
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
import java.io.File
|
||||
import java.util.UUID
|
||||
import javax.inject.Inject
|
||||
|
||||
@Stable
|
||||
@ModelScoped
|
||||
internal class UsedeskModel @Inject constructor(
|
||||
override val dispatchers: CoroutineDispatcherProvider,
|
||||
private val appPreferencesStore: AppPreferencesStore,
|
||||
private val analyticsEventHandler: AnalyticsEventHandler,
|
||||
private val feedbackRepository: FeedbackRepository,
|
||||
paramsContainer: ParamsContainer,
|
||||
) : Model() {
|
||||
|
||||
private val _state = MutableStateFlow(getInitialState())
|
||||
private val params = paramsContainer.require<UsedeskComponent.Params>()
|
||||
|
||||
private val _state = MutableStateFlow(UsedeskState(usedeskChatConfiguration = null))
|
||||
|
||||
val state: StateFlow<UsedeskState> = _state
|
||||
|
||||
private fun getInitialState(): UsedeskState {
|
||||
return UsedeskState(
|
||||
UsedeskChatConfiguration(
|
||||
companyId = COMPANY_ID,
|
||||
channelId = CHANNEL_ID,
|
||||
),
|
||||
// The chat screen opened/error event is sent only once.
|
||||
private var isScreenLoadEventSent = false
|
||||
|
||||
init {
|
||||
modelScope.launch {
|
||||
val clientId = getOrCreateClientId()
|
||||
_state.value = UsedeskState(
|
||||
UsedeskChatConfiguration(
|
||||
urlChat = URL_CHAT,
|
||||
urlChatApi = URL_CHAT_API,
|
||||
companyId = COMPANY_ID,
|
||||
channelId = CHANNEL_ID,
|
||||
clientId = clientId,
|
||||
clientEmail = params.userWalletId,
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
fun onChatLoaded() {
|
||||
if (isScreenLoadEventSent) return
|
||||
isScreenLoadEventSent = true
|
||||
analyticsEventHandler.send(
|
||||
UsedeskAnalyticsEvents.ChatScreenOpened(source = AnalyticsParam.ScreensSources.Settings),
|
||||
)
|
||||
}
|
||||
|
||||
fun onChatLoadError() {
|
||||
if (isScreenLoadEventSent) return
|
||||
isScreenLoadEventSent = true
|
||||
analyticsEventHandler.send(UsedeskAnalyticsEvents.ChatScreenError())
|
||||
}
|
||||
|
||||
/**
|
||||
* Collects the app logs into a zip archive (the same one attached to the support email)
|
||||
* and returns it via [onReady] on the main thread, or null if there are no logs.
|
||||
*/
|
||||
fun provideLogsFile(onReady: (File?) -> Unit) {
|
||||
modelScope.launch {
|
||||
val file = runSuspendCatching { feedbackRepository.getZipLogFile() }.getOrNull()
|
||||
withContext(dispatchers.main) { onReady(file) }
|
||||
}
|
||||
}
|
||||
|
||||
override fun onDestroy() {
|
||||
analyticsEventHandler.send(UsedeskAnalyticsEvents.ChatScreenClosed())
|
||||
super.onDestroy()
|
||||
}
|
||||
|
||||
private suspend fun getOrCreateClientId(): String {
|
||||
var clientId = ""
|
||||
appPreferencesStore.editData { preferences ->
|
||||
val existing = preferences[PreferencesKeys.USEDESK_CLIENT_ID_KEY]
|
||||
clientId = if (existing.isNullOrBlank()) {
|
||||
UUID.randomUUID().toString().also { preferences[PreferencesKeys.USEDESK_CLIENT_ID_KEY] = it }
|
||||
} else {
|
||||
existing
|
||||
}
|
||||
}
|
||||
return clientId
|
||||
}
|
||||
|
||||
private companion object {
|
||||
const val COMPANY_ID = "170509"
|
||||
const val CHANNEL_ID = "65637"
|
||||
const val URL_CHAT = "https://pubsub.tangem.org"
|
||||
const val URL_CHAT_API = "https://ud.tangem.org"
|
||||
const val COMPANY_ID = "2"
|
||||
const val CHANNEL_ID = "54"
|
||||
}
|
||||
}
|
||||
|
|
@ -1,9 +1,10 @@
|
|||
package com.tangem.feature.usedesk.model
|
||||
|
||||
import androidx.compose.runtime.Stable
|
||||
import ru.usedesk.chat_sdk.entity.UsedeskChatConfiguration
|
||||
import com.tangem.usedesk.chat_sdk.entity.UsedeskChatConfiguration
|
||||
|
||||
@Stable
|
||||
internal data class UsedeskState(
|
||||
val usedeskChatConfiguration: UsedeskChatConfiguration,
|
||||
// null while the clientId (UUID from AppPreferencesStore) is being prepared asynchronously.
|
||||
val usedeskChatConfiguration: UsedeskChatConfiguration?,
|
||||
)
|
||||
|
|
@ -96,7 +96,6 @@ decompose = "3.3.0"
|
|||
room = "2.7.2"
|
||||
markdown = "0.7.2"
|
||||
markdownComposeView = "0.5.4"
|
||||
usedesk = "4.4.0"
|
||||
sumsub = "1.38.0"
|
||||
haze = "1.7.2"
|
||||
kotlinpoet = "1.18.1"
|
||||
|
|
@ -306,8 +305,6 @@ room-compiler = { module = "androidx.room:room-compiler", version.ref = "room" }
|
|||
room-ktx = { module = "androidx.room:room-ktx", version.ref = "room" }
|
||||
markdown = { module = "org.jetbrains:markdown", version.ref = "markdown" }
|
||||
markdown-composeview = { module = "com.github.jeziellago:compose-markdown", version.ref = "markdownComposeView" }
|
||||
usedesk-chat-sdk = { module = "com.github.Usedesk.Android_SDK:chat-sdk", version.ref = "usedesk" }
|
||||
usedesk-chat-gui = { module = "com.github.Usedesk.Android_SDK:chat-gui", version.ref = "usedesk" }
|
||||
sumsub-sdk = { module = "com.sumsub.sns:idensic-mobile-sdk", version.ref = "sumsub" }
|
||||
haze = { module = "dev.chrisbanes.haze:haze", version.ref = "haze" }
|
||||
haze-materials = { module = "dev.chrisbanes.haze:haze-materials", version.ref = "haze" }
|
||||
|
|
|
|||
|
|
@ -13,8 +13,8 @@ tangemVico = "tangem-master-21"
|
|||
#tangemVico = "0.0.1" # Keep it! - used for local builds ^
|
||||
tangemHotSdk = "develop-550"
|
||||
#tangemHotSdk = "0.0.1" # Keep it! - used for local builds ^
|
||||
|
||||
|
||||
tangemUsedeskSdk = "main-9"
|
||||
#tangemUsedeskSdk = "0.0.1" # Keep it! - used for local builds ^
|
||||
|
||||
[libraries]
|
||||
blockchain = { module = "com.tangem:blockchain", version.ref = "tangemBlockchainSdk" }
|
||||
|
|
@ -23,6 +23,9 @@ card-core = { module = "com.tangem.tangem-sdk-kotlin:core", version.ref = "tange
|
|||
hot-core = { module = "com.tangem.tangem-hot-sdk-kotlin:core", version.ref = "tangemHotSdk" }
|
||||
hot-android = { module = "com.tangem.tangem-hot-sdk-kotlin:android", version.ref = "tangemHotSdk" }
|
||||
|
||||
usedesk-chat-sdk = { module = "com.tangem.usedesk:chat-sdk", version.ref = "tangemUsedeskSdk" }
|
||||
usedesk-chat-gui = { module = "com.tangem.usedesk:chat-gui", version.ref = "tangemUsedeskSdk" }
|
||||
|
||||
vico-compose = { group = "com.tangem.vico", name = "compose", version.ref = "tangemVico" }
|
||||
vico-compose-m3 = { group = "com.tangem.vico", name = "compose-m3", version.ref = "tangemVico" }
|
||||
vico-core = { group = "com.tangem.vico", name = "core", version.ref = "tangemVico" }
|
||||
|
|
|
|||
|
|
@ -42,6 +42,7 @@ dependencyResolutionManagement {
|
|||
includeGroupAndSubgroups("com.tangem.tangem-sdk-kotlin")
|
||||
includeGroupAndSubgroups("com.tangem.tangem-hot-sdk-kotlin")
|
||||
includeGroupAndSubgroups("com.tangem.vico")
|
||||
includeGroupAndSubgroups("com.tangem.usedesk")
|
||||
includeModule("com.tangem", "blstlib")
|
||||
includeModule("com.tangem", "blockchain")
|
||||
includeModule("com.tangem", "wallet-core-proto")
|
||||
|
|
@ -129,6 +130,15 @@ dependencyResolutionManagement {
|
|||
includeGroupAndSubgroups("org.web3j")
|
||||
}
|
||||
}
|
||||
maven {
|
||||
// setting any repository from tangem project allows maven search all packages in the project
|
||||
url = uri("https://maven.pkg.github.com/tangem/ud-android-sdk")
|
||||
credentials {
|
||||
username = properties.getProperty("gpr.user") ?: System.getenv("GITHUB_ACTOR")
|
||||
password = properties.getProperty("gpr.key") ?: System.getenv("GITHUB_TOKEN")
|
||||
}
|
||||
content { includeGroupAndSubgroups("com.tangem.usedesk") }
|
||||
}
|
||||
maven("https://jitpack.io")
|
||||
maven("https://maven.sumsub.com/repository/maven-public/")
|
||||
}
|
||||
|
|
@ -162,6 +172,30 @@ if (properties.getProperty("blockchainSdk.local").toBoolean()) {
|
|||
}
|
||||
}
|
||||
|
||||
// Optional local composite build for the Usedesk SDK fork.
|
||||
// Enable it from local.properties (which is git-ignored, so it never reaches CI/develop):
|
||||
//
|
||||
// usedesk.local=true
|
||||
// usedesk.path=../ud-android-sdk # optional, this is the default
|
||||
//
|
||||
// When enabled, com.tangem.usedesk:* is resolved from local sources instead of the
|
||||
// published Maven artifact (tangemUsedeskSdk in gradle/tangem_dependencies.toml).
|
||||
if (properties.getProperty("usedesk.local").toBoolean()) {
|
||||
val usedeskPath = properties.getProperty("usedesk.path") ?: "../ud-android-sdk"
|
||||
println("Usedesk SDK: using local composite build from '$usedeskPath'")
|
||||
includeBuild(usedeskPath) {
|
||||
dependencySubstitution {
|
||||
substitute(module("com.tangem.usedesk:common-sdk")).using(project(":common-sdk"))
|
||||
substitute(module("com.tangem.usedesk:common-gui")).using(project(":common-gui"))
|
||||
substitute(module("com.tangem.usedesk:chat-sdk")).using(project(":chat-sdk"))
|
||||
substitute(module("com.tangem.usedesk:chat-gui")).using(project(":chat-gui"))
|
||||
substitute(module("com.tangem.usedesk:knowledgebase-sdk")).using(project(":knowledgebase-sdk"))
|
||||
substitute(module("com.tangem.usedesk:knowledgebase-gui")).using(project(":knowledgebase-gui"))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
enableFeaturePreview("TYPESAFE_PROJECT_ACCESSORS")
|
||||
|
||||
include(":app")
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue