From e95243ab79a34ac7a756b89402dba45721918b25 Mon Sep 17 00:00:00 2001 From: Tangem Date: Thu, 25 Jun 2026 20:02:05 +0500 Subject: [PATCH] Updated on 2026-08-14 --- .../DefaultUserWalletsListRepository.kt | 2 + .../tangem/tap/routing/utils/ChildFactory.kt | 6 +- .../configs/feature_toggles_config.json | 2 +- .../FeatureTogglesNamingConventionTest.kt | 1 - .../local/preferences/PreferencesKeys.kt | 2 + .../feedback/DefaultFeedbackFeatureToggles.kt | 2 +- .../domain/wallets/analytics/Settings.kt | 2 + .../preview/PreviewDetailsComponent.kt | 5 +- .../features/details/entity/DetailsUM.kt | 1 + .../entity/SelectContactSupportTypeBS.kt | 16 ++++ .../features/details/model/DetailsModel.kt | 49 +++++++++- .../features/details/ui/DetailsScreen.kt | 1 + .../ui/SelectContactSupportTypeBottomSheet.kt | 58 ++++++++++++ .../features/details/utils/ItemsBuilder.kt | 31 +----- .../details/model/DetailsModelFeedbackTest.kt | 10 +- .../details/model/DetailsModelInitTest.kt | 18 ---- .../model/DetailsModelNavigationTest.kt | 43 ++++++++- .../details/model/DetailsModelTestBase.kt | 10 +- .../details/utils/ItemsBuilderTest.kt | 8 +- .../feature/usedesk/api/UsedeskComponent.kt | 3 +- features/usedesk/impl/build.gradle.kts | 11 ++- .../usedesk/DefaultUsedeskComponent.kt | 89 +++++++++++++++--- .../analytics/UsedeskAnalyticsEvents.kt | 19 ++++ .../feature/usedesk/model/UsedeskModel.kt | 94 +++++++++++++++++-- .../feature/usedesk/model/UsedeskState.kt | 5 +- gradle/dependencies.toml | 3 - gradle/tangem_dependencies.toml | 7 +- settings.gradle.kts | 36 ++++++- 28 files changed, 422 insertions(+), 112 deletions(-) create mode 100644 features/details/impl/src/main/kotlin/com/tangem/features/details/entity/SelectContactSupportTypeBS.kt create mode 100644 features/details/impl/src/main/kotlin/com/tangem/features/details/ui/SelectContactSupportTypeBottomSheet.kt create mode 100644 features/usedesk/impl/src/main/java/com/tangem/feature/usedesk/analytics/UsedeskAnalyticsEvents.kt diff --git a/app/src/main/java/com/tangem/tap/domain/userWalletList/repository/DefaultUserWalletsListRepository.kt b/app/src/main/java/com/tangem/tap/domain/userWalletList/repository/DefaultUserWalletsListRepository.kt index 5c6d10635a..4f02866bda 100644 --- a/app/src/main/java/com/tangem/tap/domain/userWalletList/repository/DefaultUserWalletsListRepository.kt +++ b/app/src/main/java/com/tangem/tap/domain/userWalletList/repository/DefaultUserWalletsListRepository.kt @@ -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) } } } \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/routing/utils/ChildFactory.kt b/app/src/main/java/com/tangem/tap/routing/utils/ChildFactory.kt index 4ef8ee038e..559a3bef90 100644 --- a/app/src/main/java/com/tangem/tap/routing/utils/ChildFactory.kt +++ b/app/src/main/java/com/tangem/tap/routing/utils/ChildFactory.kt @@ -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, ) } diff --git a/core/config-toggles/src/main/assets/configs/feature_toggles_config.json b/core/config-toggles/src/main/assets/configs/feature_toggles_config.json index a6add9266b..f5eb00d4a8 100644 --- a/core/config-toggles/src/main/assets/configs/feature_toggles_config.json +++ b/core/config-toggles/src/main/assets/configs/feature_toggles_config.json @@ -16,7 +16,7 @@ "version": "5.39" }, { - "name": "USEDESK_ENABLED", + "name": "TWI_485_USEDESK_ENABLED", "version": "undefined" }, { diff --git a/core/config-toggles/src/test/kotlin/com/tangem/core/configtoggle/feature/FeatureTogglesNamingConventionTest.kt b/core/config-toggles/src/test/kotlin/com/tangem/core/configtoggle/feature/FeatureTogglesNamingConventionTest.kt index b4d72d4288..7eea6b890e 100644 --- a/core/config-toggles/src/test/kotlin/com/tangem/core/configtoggle/feature/FeatureTogglesNamingConventionTest.kt +++ b/core/config-toggles/src/test/kotlin/com/tangem/core/configtoggle/feature/FeatureTogglesNamingConventionTest.kt @@ -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", diff --git a/core/datasource/src/main/java/com/tangem/datasource/local/preferences/PreferencesKeys.kt b/core/datasource/src/main/java/com/tangem/datasource/local/preferences/PreferencesKeys.kt index 96ea2c114d..32568b5de7 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/local/preferences/PreferencesKeys.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/local/preferences/PreferencesKeys.kt @@ -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") } diff --git a/data/feedback/src/main/java/com/tangem/data/feedback/DefaultFeedbackFeatureToggles.kt b/data/feedback/src/main/java/com/tangem/data/feedback/DefaultFeedbackFeatureToggles.kt index a58c715e2d..e4a87e8503 100644 --- a/data/feedback/src/main/java/com/tangem/data/feedback/DefaultFeedbackFeatureToggles.kt +++ b/data/feedback/src/main/java/com/tangem/data/feedback/DefaultFeedbackFeatureToggles.kt @@ -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) } \ No newline at end of file diff --git a/domain/wallets/src/main/java/com/tangem/domain/wallets/analytics/Settings.kt b/domain/wallets/src/main/java/com/tangem/domain/wallets/analytics/Settings.kt index 7f8f5b9916..28ea914763 100644 --- a/domain/wallets/src/main/java/com/tangem/domain/wallets/analytics/Settings.kt +++ b/domain/wallets/src/main/java/com/tangem/domain/wallets/analytics/Settings.kt @@ -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( diff --git a/features/details/impl/src/main/kotlin/com/tangem/features/details/component/preview/PreviewDetailsComponent.kt b/features/details/impl/src/main/kotlin/com/tangem/features/details/component/preview/PreviewDetailsComponent.kt index 6b1b9e2655..8ca3706279 100644 --- a/features/details/impl/src/main/kotlin/com/tangem/features/details/component/preview/PreviewDetailsComponent.kt +++ b/features/details/impl/src/main/kotlin/com/tangem/features/details/component/preview/PreviewDetailsComponent.kt @@ -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 */ }, ) diff --git a/features/details/impl/src/main/kotlin/com/tangem/features/details/entity/DetailsUM.kt b/features/details/impl/src/main/kotlin/com/tangem/features/details/entity/DetailsUM.kt index b7c390e3bd..317dff7122 100644 --- a/features/details/impl/src/main/kotlin/com/tangem/features/details/entity/DetailsUM.kt +++ b/features/details/impl/src/main/kotlin/com/tangem/features/details/entity/DetailsUM.kt @@ -7,5 +7,6 @@ internal data class DetailsUM( val items: ImmutableList, val footer: DetailsFooterUM, val selectFeedbackEmailTypeBSConfig: TangemBottomSheetConfig, + val selectContactSupportTypeBSConfig: TangemBottomSheetConfig, val popBack: () -> Unit, ) \ No newline at end of file diff --git a/features/details/impl/src/main/kotlin/com/tangem/features/details/entity/SelectContactSupportTypeBS.kt b/features/details/impl/src/main/kotlin/com/tangem/features/details/entity/SelectContactSupportTypeBS.kt new file mode 100644 index 0000000000..f563e8d546 --- /dev/null +++ b/features/details/impl/src/main/kotlin/com/tangem/features/details/entity/SelectContactSupportTypeBS.kt @@ -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)), + } +} \ No newline at end of file diff --git a/features/details/impl/src/main/kotlin/com/tangem/features/details/model/DetailsModel.kt b/features/details/impl/src/main/kotlin/com/tangem/features/details/model/DetailsModel.kt index e1538a81b3..6e8d03041f 100644 --- a/features/details/impl/src/main/kotlin/com/tangem/features/details/model/DetailsModel.kt +++ b/features/details/impl/src/main/kotlin/com/tangem/features/details/model/DetailsModel.kt @@ -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> val state: MutableStateFlow @@ -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") diff --git a/features/details/impl/src/main/kotlin/com/tangem/features/details/ui/DetailsScreen.kt b/features/details/impl/src/main/kotlin/com/tangem/features/details/ui/DetailsScreen.kt index 48d51c0835..ada9cb0574 100644 --- a/features/details/impl/src/main/kotlin/com/tangem/features/details/ui/DetailsScreen.kt +++ b/features/details/impl/src/main/kotlin/com/tangem/features/details/ui/DetailsScreen.kt @@ -76,6 +76,7 @@ internal fun DetailsScreen( } SelectFeedbackEmailTypeBottomSheet(state.selectFeedbackEmailTypeBSConfig) + SelectContactSupportTypeBottomSheet(state.selectContactSupportTypeBSConfig) } @Composable diff --git a/features/details/impl/src/main/kotlin/com/tangem/features/details/ui/SelectContactSupportTypeBottomSheet.kt b/features/details/impl/src/main/kotlin/com/tangem/features/details/ui/SelectContactSupportTypeBottomSheet.kt new file mode 100644 index 0000000000..9a94c95173 --- /dev/null +++ b/features/details/impl/src/main/kotlin/com/tangem/features/details/ui/SelectContactSupportTypeBottomSheet.kt @@ -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( + 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, + ) + } + } + } +} \ No newline at end of file diff --git a/features/details/impl/src/main/kotlin/com/tangem/features/details/utils/ItemsBuilder.kt b/features/details/impl/src/main/kotlin/com/tangem/features/details/utils/ItemsBuilder.kt index 99d34d298d..856b5f8dab 100644 --- a/features/details/impl/src/main/kotlin/com/tangem/features/details/utils/ItemsBuilder.kt +++ b/features/details/impl/src/main/kotlin/com/tangem/features/details/utils/ItemsBuilder.kt @@ -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 = 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, onClick: () -> Unit): ImmutableList { @@ -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( diff --git a/features/details/impl/src/test/kotlin/com/tangem/features/details/model/DetailsModelFeedbackTest.kt b/features/details/impl/src/test/kotlin/com/tangem/features/details/model/DetailsModelFeedbackTest.kt index bf6f2a1888..73478e1f2e 100644 --- a/features/details/impl/src/test/kotlin/com/tangem/features/details/model/DetailsModelFeedbackTest.kt +++ b/features/details/impl/src/test/kotlin/com/tangem/features/details/model/DetailsModelFeedbackTest.kt @@ -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()) } @@ -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 diff --git a/features/details/impl/src/test/kotlin/com/tangem/features/details/model/DetailsModelInitTest.kt b/features/details/impl/src/test/kotlin/com/tangem/features/details/model/DetailsModelInitTest.kt index 42af4c4f40..c770841dd7 100644 --- a/features/details/impl/src/test/kotlin/com/tangem/features/details/model/DetailsModelInitTest.kt +++ b/features/details/impl/src/test/kotlin/com/tangem/features/details/model/DetailsModelInitTest.kt @@ -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)) diff --git a/features/details/impl/src/test/kotlin/com/tangem/features/details/model/DetailsModelNavigationTest.kt b/features/details/impl/src/test/kotlin/com/tangem/features/details/model/DetailsModelNavigationTest.kt index 3ec4b6770b..2a9374dd58 100644 --- a/features/details/impl/src/test/kotlin/com/tangem/features/details/model/DetailsModelNavigationTest.kt +++ b/features/details/impl/src/test/kotlin/com/tangem/features/details/model/DetailsModelNavigationTest.kt @@ -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 { diff --git a/features/details/impl/src/test/kotlin/com/tangem/features/details/model/DetailsModelTestBase.kt b/features/details/impl/src/test/kotlin/com/tangem/features/details/model/DetailsModelTestBase.kt index 6c48d43c14..d40b5d6c0a 100644 --- a/features/details/impl/src/test/kotlin/com/tangem/features/details/model/DetailsModelTestBase.kt +++ b/features/details/impl/src/test/kotlin/com/tangem/features/details/model/DetailsModelTestBase.kt @@ -70,11 +70,9 @@ internal abstract class DetailsModelTestBase { // Captured from itemsBuilder.buildAll(...) so the feature buttons can be driven. protected val wcSlot = slot() protected val abSlot = slot() - protected val chatSlot = slot() protected val mobileSlot = slot() protected val walletIdSlot = slot() - 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) { every { - itemsBuilder.buildAll(any(), any(), any(), any(), any(), any(), any(), any()) + itemsBuilder.buildAll(any(), any(), any(), any(), any(), any()) } returns list } diff --git a/features/details/impl/src/test/kotlin/com/tangem/features/details/utils/ItemsBuilderTest.kt b/features/details/impl/src/test/kotlin/com/tangem/features/details/utils/ItemsBuilderTest.kt index 6f4c22d644..5c7e721d20 100644 --- a/features/details/impl/src/test/kotlin/com/tangem/features/details/utils/ItemsBuilderTest.kt +++ b/features/details/impl/src/test/kotlin/com/tangem/features/details/utils/ItemsBuilderTest.kt @@ -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 = itemsBuilder.buildAll( isWalletConnectAvailable = isWalletConnectAvailable, isAddressBookAvailable = isAddressBookAvailable, - isSupportChatAvailable = isSupportChatAvailable, hasAnyMobileWallet = hasAnyMobileWallet, userWalletId = USER_WALLET_ID, - onSupportEmailClick = {}, - onSupportChatClick = {}, + onSupportClick = {}, onBuyClick = {}, ) diff --git a/features/usedesk/api/src/main/java/com/tangem/feature/usedesk/api/UsedeskComponent.kt b/features/usedesk/api/src/main/java/com/tangem/feature/usedesk/api/UsedeskComponent.kt index 7542b8adcd..aa2cd6922c 100644 --- a/features/usedesk/api/src/main/java/com/tangem/feature/usedesk/api/UsedeskComponent.kt +++ b/features/usedesk/api/src/main/java/com/tangem/feature/usedesk/api/UsedeskComponent.kt @@ -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 diff --git a/features/usedesk/impl/build.gradle.kts b/features/usedesk/impl/build.gradle.kts index 8bba93f420..1a0b339e56 100644 --- a/features/usedesk/impl/build.gradle.kts +++ b/features/usedesk/impl/build.gradle.kts @@ -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) } \ No newline at end of file diff --git a/features/usedesk/impl/src/main/java/com/tangem/feature/usedesk/DefaultUsedeskComponent.kt b/features/usedesk/impl/src/main/java/com/tangem/feature/usedesk/DefaultUsedeskComponent.kt index 114b564964..8fe2ae5a87 100644 --- a/features/usedesk/impl/src/main/java/com/tangem/feature/usedesk/DefaultUsedeskComponent.kt +++ b/features/usedesk/impl/src/main/java/com/tangem/feature/usedesk/DefaultUsedeskComponent.kt @@ -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", + ) + } } \ No newline at end of file diff --git a/features/usedesk/impl/src/main/java/com/tangem/feature/usedesk/analytics/UsedeskAnalyticsEvents.kt b/features/usedesk/impl/src/main/java/com/tangem/feature/usedesk/analytics/UsedeskAnalyticsEvents.kt new file mode 100644 index 0000000000..e6041c9d19 --- /dev/null +++ b/features/usedesk/impl/src/main/java/com/tangem/feature/usedesk/analytics/UsedeskAnalyticsEvents.kt @@ -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 = 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") +} \ No newline at end of file diff --git a/features/usedesk/impl/src/main/java/com/tangem/feature/usedesk/model/UsedeskModel.kt b/features/usedesk/impl/src/main/java/com/tangem/feature/usedesk/model/UsedeskModel.kt index 47139c815e..a0056b0721 100644 --- a/features/usedesk/impl/src/main/java/com/tangem/feature/usedesk/model/UsedeskModel.kt +++ b/features/usedesk/impl/src/main/java/com/tangem/feature/usedesk/model/UsedeskModel.kt @@ -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() + + private val _state = MutableStateFlow(UsedeskState(usedeskChatConfiguration = null)) val state: StateFlow = _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" } } \ No newline at end of file diff --git a/features/usedesk/impl/src/main/java/com/tangem/feature/usedesk/model/UsedeskState.kt b/features/usedesk/impl/src/main/java/com/tangem/feature/usedesk/model/UsedeskState.kt index 26098b62e5..1a20bab10f 100644 --- a/features/usedesk/impl/src/main/java/com/tangem/feature/usedesk/model/UsedeskState.kt +++ b/features/usedesk/impl/src/main/java/com/tangem/feature/usedesk/model/UsedeskState.kt @@ -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?, ) \ No newline at end of file diff --git a/gradle/dependencies.toml b/gradle/dependencies.toml index 79e299a9ae..c4f8dd88fc 100644 --- a/gradle/dependencies.toml +++ b/gradle/dependencies.toml @@ -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" } diff --git a/gradle/tangem_dependencies.toml b/gradle/tangem_dependencies.toml index f5f4f1626b..8b7ac52f4f 100644 --- a/gradle/tangem_dependencies.toml +++ b/gradle/tangem_dependencies.toml @@ -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" } diff --git a/settings.gradle.kts b/settings.gradle.kts index b1fabd1412..e3a06c17ba 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -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") @@ -480,4 +514,4 @@ include(":data:yield-supply") include(":data:news") include(":data:earn") include(":data:search") -// endregion Data modules \ No newline at end of file +// endregion Data modules