Updated on 2026-08-14
This commit is contained in:
parent
e326873ac1
commit
16b8957165
10 changed files with 120 additions and 7 deletions
|
|
@ -14,6 +14,7 @@ dependencies {
|
||||||
api(projects.core.pagination)
|
api(projects.core.pagination)
|
||||||
implementation(projects.core.utils)
|
implementation(projects.core.utils)
|
||||||
implementation(projects.core.error)
|
implementation(projects.core.error)
|
||||||
|
implementation(projects.core.analytics.models)
|
||||||
|
|
||||||
/** Project - Domain */
|
/** Project - Domain */
|
||||||
api(projects.domain.models)
|
api(projects.domain.models)
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,50 @@
|
||||||
|
package com.tangem.domain.tangempay
|
||||||
|
|
||||||
|
import com.tangem.core.analytics.models.AnalyticsEvent
|
||||||
|
|
||||||
|
sealed class TangemPayAnalyticsEvents(
|
||||||
|
categoryName: String,
|
||||||
|
event: String,
|
||||||
|
params: Map<String, String> = emptyMap(),
|
||||||
|
) : AnalyticsEvent(category = categoryName, event = event, params = params) {
|
||||||
|
|
||||||
|
data object ActivationScreenOpened : TangemPayAnalyticsEvents(
|
||||||
|
categoryName = "Visa Onboarding",
|
||||||
|
event = "Activation Screen Opened",
|
||||||
|
)
|
||||||
|
|
||||||
|
data object ViewTermsClicked : TangemPayAnalyticsEvents(
|
||||||
|
categoryName = "Visa Onboarding",
|
||||||
|
event = "Button - Visa View Terms",
|
||||||
|
)
|
||||||
|
|
||||||
|
data object GetCardClicked : TangemPayAnalyticsEvents(
|
||||||
|
categoryName = "Visa Onboarding",
|
||||||
|
event = "Button - Visa Get Card",
|
||||||
|
)
|
||||||
|
|
||||||
|
data object KycFlowOpened : TangemPayAnalyticsEvents(
|
||||||
|
categoryName = "Visa Onboarding",
|
||||||
|
event = "Visa KYC Flow Opened",
|
||||||
|
)
|
||||||
|
|
||||||
|
data object IssuingBannerDisplayed : TangemPayAnalyticsEvents(
|
||||||
|
categoryName = "Visa Onboarding",
|
||||||
|
event = "Visa Issuing Banner Displayed",
|
||||||
|
)
|
||||||
|
|
||||||
|
data object MainScreenOpened : TangemPayAnalyticsEvents(
|
||||||
|
categoryName = "Visa Screen",
|
||||||
|
event = "Visa Main Screen Opened",
|
||||||
|
)
|
||||||
|
|
||||||
|
data object ReceiveFundsClicked : TangemPayAnalyticsEvents(
|
||||||
|
categoryName = "Visa Screen",
|
||||||
|
event = "Button - Visa Receive",
|
||||||
|
)
|
||||||
|
|
||||||
|
data object SwapClicked : TangemPayAnalyticsEvents(
|
||||||
|
categoryName = "Visa Screen",
|
||||||
|
event = "Button - Visa Swap",
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
@ -1,11 +1,13 @@
|
||||||
package com.tangem.features.kyc
|
package com.tangem.features.kyc
|
||||||
|
|
||||||
import androidx.compose.runtime.Stable
|
import androidx.compose.runtime.Stable
|
||||||
|
import com.tangem.core.analytics.api.AnalyticsEventHandler
|
||||||
import com.tangem.core.decompose.di.ModelScoped
|
import com.tangem.core.decompose.di.ModelScoped
|
||||||
import com.tangem.core.decompose.model.Model
|
import com.tangem.core.decompose.model.Model
|
||||||
import com.tangem.core.decompose.model.ParamsContainer
|
import com.tangem.core.decompose.model.ParamsContainer
|
||||||
import com.tangem.domain.pay.KycStartInfo
|
import com.tangem.domain.pay.KycStartInfo
|
||||||
import com.tangem.domain.pay.repository.KycRepository
|
import com.tangem.domain.pay.repository.KycRepository
|
||||||
|
import com.tangem.domain.tangempay.TangemPayAnalyticsEvents
|
||||||
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
|
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
|
||||||
import kotlinx.coroutines.flow.MutableStateFlow
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
import kotlinx.coroutines.flow.asStateFlow
|
import kotlinx.coroutines.flow.asStateFlow
|
||||||
|
|
@ -17,6 +19,7 @@ import javax.inject.Inject
|
||||||
@ModelScoped
|
@ModelScoped
|
||||||
class DefaultKycModel @Inject constructor(
|
class DefaultKycModel @Inject constructor(
|
||||||
paramsContainer: ParamsContainer,
|
paramsContainer: ParamsContainer,
|
||||||
|
analytics: AnalyticsEventHandler,
|
||||||
override val dispatchers: CoroutineDispatcherProvider,
|
override val dispatchers: CoroutineDispatcherProvider,
|
||||||
private val kycRepository: KycRepository,
|
private val kycRepository: KycRepository,
|
||||||
) : Model() {
|
) : Model() {
|
||||||
|
|
@ -27,6 +30,7 @@ class DefaultKycModel @Inject constructor(
|
||||||
val uiState = _uiState.asStateFlow()
|
val uiState = _uiState.asStateFlow()
|
||||||
|
|
||||||
init {
|
init {
|
||||||
|
analytics.send(TangemPayAnalyticsEvents.KycFlowOpened)
|
||||||
modelScope.launch {
|
modelScope.launch {
|
||||||
try {
|
try {
|
||||||
kycRepository.getKycStartInfo(params.userWalletId).getOrNull()?.let { _uiState.emit(it) }
|
kycRepository.getKycStartInfo(params.userWalletId).getOrNull()?.let { _uiState.emit(it) }
|
||||||
|
|
|
||||||
|
|
@ -13,12 +13,13 @@ android {
|
||||||
|
|
||||||
dependencies {
|
dependencies {
|
||||||
/** Core */
|
/** Core */
|
||||||
|
implementation(projects.core.analytics)
|
||||||
|
implementation(projects.core.analytics.models)
|
||||||
implementation(projects.core.configToggles)
|
implementation(projects.core.configToggles)
|
||||||
implementation(projects.core.decompose)
|
implementation(projects.core.decompose)
|
||||||
implementation(projects.core.error)
|
implementation(projects.core.error)
|
||||||
implementation(projects.core.navigation)
|
implementation(projects.core.navigation)
|
||||||
implementation(projects.core.ui)
|
implementation(projects.core.ui)
|
||||||
implementation(projects.core.analytics.models)
|
|
||||||
|
|
||||||
/** Common */
|
/** Common */
|
||||||
implementation(projects.common.ui)
|
implementation(projects.common.ui)
|
||||||
|
|
@ -26,8 +27,8 @@ dependencies {
|
||||||
|
|
||||||
/** Features api */
|
/** Features api */
|
||||||
implementation(projects.features.tangempay.details.api)
|
implementation(projects.features.tangempay.details.api)
|
||||||
implementation(projects.features.txhistory.api)
|
|
||||||
implementation(projects.features.tokenRecieve.api)
|
implementation(projects.features.tokenRecieve.api)
|
||||||
|
implementation(projects.features.txhistory.api)
|
||||||
|
|
||||||
/** Domain */
|
/** Domain */
|
||||||
implementation(projects.domain.balanceHiding)
|
implementation(projects.domain.balanceHiding)
|
||||||
|
|
|
||||||
|
|
@ -5,6 +5,7 @@ import com.arkivanov.decompose.router.slot.SlotNavigation
|
||||||
import com.arkivanov.decompose.router.slot.activate
|
import com.arkivanov.decompose.router.slot.activate
|
||||||
import com.arkivanov.decompose.router.slot.dismiss
|
import com.arkivanov.decompose.router.slot.dismiss
|
||||||
import com.tangem.common.routing.AppRoute
|
import com.tangem.common.routing.AppRoute
|
||||||
|
import com.tangem.core.analytics.api.AnalyticsEventHandler
|
||||||
import com.tangem.core.analytics.models.AnalyticsParam
|
import com.tangem.core.analytics.models.AnalyticsParam
|
||||||
import com.tangem.core.decompose.di.ModelScoped
|
import com.tangem.core.decompose.di.ModelScoped
|
||||||
import com.tangem.core.decompose.model.Model
|
import com.tangem.core.decompose.model.Model
|
||||||
|
|
@ -22,6 +23,7 @@ import com.tangem.domain.pay.TangemPaySwapDataFactory
|
||||||
import com.tangem.domain.pay.model.TangemPayCardBalance
|
import com.tangem.domain.pay.model.TangemPayCardBalance
|
||||||
import com.tangem.domain.pay.repository.CustomerOrderRepository
|
import com.tangem.domain.pay.repository.CustomerOrderRepository
|
||||||
import com.tangem.domain.pay.repository.TangemPayCardDetailsRepository
|
import com.tangem.domain.pay.repository.TangemPayCardDetailsRepository
|
||||||
|
import com.tangem.domain.tangempay.TangemPayAnalyticsEvents
|
||||||
import com.tangem.domain.visa.model.TangemPayCardFrozenState
|
import com.tangem.domain.visa.model.TangemPayCardFrozenState
|
||||||
import com.tangem.domain.visa.model.TangemPayTxHistoryItem
|
import com.tangem.domain.visa.model.TangemPayTxHistoryItem
|
||||||
import com.tangem.features.tangempay.TangemPayConstants
|
import com.tangem.features.tangempay.TangemPayConstants
|
||||||
|
|
@ -59,6 +61,7 @@ import javax.inject.Inject
|
||||||
internal class TangemPayDetailsModel @Inject constructor(
|
internal class TangemPayDetailsModel @Inject constructor(
|
||||||
paramsContainer: ParamsContainer,
|
paramsContainer: ParamsContainer,
|
||||||
override val dispatchers: CoroutineDispatcherProvider,
|
override val dispatchers: CoroutineDispatcherProvider,
|
||||||
|
private val analytics: AnalyticsEventHandler,
|
||||||
private val router: Router,
|
private val router: Router,
|
||||||
private val urlOpener: UrlOpener,
|
private val urlOpener: UrlOpener,
|
||||||
private val cardDetailsRepository: TangemPayCardDetailsRepository,
|
private val cardDetailsRepository: TangemPayCardDetailsRepository,
|
||||||
|
|
@ -328,6 +331,7 @@ internal class TangemPayDetailsModel @Inject constructor(
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onClickSwap(data: TangemPayTopUpData) {
|
override fun onClickSwap(data: TangemPayTopUpData) {
|
||||||
|
analytics.send(TangemPayAnalyticsEvents.SwapClicked)
|
||||||
bottomSheetNavigation.dismiss()
|
bottomSheetNavigation.dismiss()
|
||||||
router.push(
|
router.push(
|
||||||
AppRoute.Swap(
|
AppRoute.Swap(
|
||||||
|
|
@ -346,6 +350,7 @@ internal class TangemPayDetailsModel @Inject constructor(
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onClickReceive(data: TangemPayTopUpData) {
|
override fun onClickReceive(data: TangemPayTopUpData) {
|
||||||
|
analytics.send(TangemPayAnalyticsEvents.ReceiveFundsClicked)
|
||||||
bottomSheetNavigation.dismiss()
|
bottomSheetNavigation.dismiss()
|
||||||
val config = TokenReceiveConfig(
|
val config = TokenReceiveConfig(
|
||||||
shouldShowWarning = false,
|
shouldShowWarning = false,
|
||||||
|
|
|
||||||
|
|
@ -13,12 +13,11 @@ android {
|
||||||
|
|
||||||
dependencies {
|
dependencies {
|
||||||
/** Core */
|
/** Core */
|
||||||
|
implementation(projects.core.analytics)
|
||||||
implementation(projects.core.decompose)
|
implementation(projects.core.decompose)
|
||||||
implementation(projects.core.ui)
|
|
||||||
implementation(projects.core.error)
|
implementation(projects.core.error)
|
||||||
implementation(projects.core.navigation)
|
implementation(projects.core.navigation)
|
||||||
implementation(deps.arrow.core)
|
implementation(projects.core.ui)
|
||||||
implementation(deps.arrow.core)
|
|
||||||
|
|
||||||
/** Common */
|
/** Common */
|
||||||
implementation(projects.common.routing)
|
implementation(projects.common.routing)
|
||||||
|
|
@ -47,4 +46,5 @@ dependencies {
|
||||||
|
|
||||||
/** Other */
|
/** Other */
|
||||||
implementation(deps.timber)
|
implementation(deps.timber)
|
||||||
|
implementation(deps.arrow.core)
|
||||||
}
|
}
|
||||||
|
|
@ -2,6 +2,7 @@ package com.tangem.features.tangempay.model
|
||||||
|
|
||||||
import androidx.compose.runtime.Stable
|
import androidx.compose.runtime.Stable
|
||||||
import com.tangem.common.routing.AppRoute
|
import com.tangem.common.routing.AppRoute
|
||||||
|
import com.tangem.core.analytics.api.AnalyticsEventHandler
|
||||||
import com.tangem.core.decompose.di.ModelScoped
|
import com.tangem.core.decompose.di.ModelScoped
|
||||||
import com.tangem.core.decompose.model.Model
|
import com.tangem.core.decompose.model.Model
|
||||||
import com.tangem.core.decompose.model.ParamsContainer
|
import com.tangem.core.decompose.model.ParamsContainer
|
||||||
|
|
@ -10,6 +11,7 @@ import com.tangem.core.navigation.url.UrlOpener
|
||||||
import com.tangem.data.pay.util.TangemPayWalletsManager
|
import com.tangem.data.pay.util.TangemPayWalletsManager
|
||||||
import com.tangem.domain.pay.repository.OnboardingRepository
|
import com.tangem.domain.pay.repository.OnboardingRepository
|
||||||
import com.tangem.domain.pay.usecase.ProduceTangemPayInitialDataUseCase
|
import com.tangem.domain.pay.usecase.ProduceTangemPayInitialDataUseCase
|
||||||
|
import com.tangem.domain.tangempay.TangemPayAnalyticsEvents
|
||||||
import com.tangem.features.tangempay.TangemPayConstants
|
import com.tangem.features.tangempay.TangemPayConstants
|
||||||
import com.tangem.features.tangempay.components.TangemPayOnboardingComponent
|
import com.tangem.features.tangempay.components.TangemPayOnboardingComponent
|
||||||
import com.tangem.features.tangempay.model.transformers.TangemPayOnboardingButtonLoadingTransformer
|
import com.tangem.features.tangempay.model.transformers.TangemPayOnboardingButtonLoadingTransformer
|
||||||
|
|
@ -28,8 +30,9 @@ import com.tangem.utils.transformer.update as transformerUpdate
|
||||||
@Suppress("LongParameterList")
|
@Suppress("LongParameterList")
|
||||||
internal class TangemPayOnboardingModel @Inject constructor(
|
internal class TangemPayOnboardingModel @Inject constructor(
|
||||||
paramsContainer: ParamsContainer,
|
paramsContainer: ParamsContainer,
|
||||||
private val router: Router,
|
|
||||||
override val dispatchers: CoroutineDispatcherProvider,
|
override val dispatchers: CoroutineDispatcherProvider,
|
||||||
|
private val analytics: AnalyticsEventHandler,
|
||||||
|
private val router: Router,
|
||||||
private val repository: OnboardingRepository,
|
private val repository: OnboardingRepository,
|
||||||
private val produceInitialDataUseCase: ProduceTangemPayInitialDataUseCase,
|
private val produceInitialDataUseCase: ProduceTangemPayInitialDataUseCase,
|
||||||
private val urlOpener: UrlOpener,
|
private val urlOpener: UrlOpener,
|
||||||
|
|
@ -41,6 +44,7 @@ internal class TangemPayOnboardingModel @Inject constructor(
|
||||||
field = MutableStateFlow(getInitialState())
|
field = MutableStateFlow(getInitialState())
|
||||||
|
|
||||||
init {
|
init {
|
||||||
|
analytics.send(TangemPayAnalyticsEvents.ActivationScreenOpened)
|
||||||
modelScope.launch {
|
modelScope.launch {
|
||||||
when (params) {
|
when (params) {
|
||||||
is TangemPayOnboardingComponent.Params.ContinueOnboarding -> {
|
is TangemPayOnboardingComponent.Params.ContinueOnboarding -> {
|
||||||
|
|
@ -89,10 +93,12 @@ internal class TangemPayOnboardingModel @Inject constructor(
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun onTermsClick() {
|
private fun onTermsClick() {
|
||||||
|
analytics.send(TangemPayAnalyticsEvents.ViewTermsClicked)
|
||||||
urlOpener.openUrl(TangemPayConstants.TERMS_AND_LIMITS_LINK)
|
urlOpener.openUrl(TangemPayConstants.TERMS_AND_LIMITS_LINK)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun onGetCardClick() {
|
private fun onGetCardClick() {
|
||||||
|
analytics.send(TangemPayAnalyticsEvents.GetCardClicked)
|
||||||
uiState.transformerUpdate(TangemPayOnboardingButtonLoadingTransformer(isLoading = true))
|
uiState.transformerUpdate(TangemPayOnboardingButtonLoadingTransformer(isLoading = true))
|
||||||
modelScope.launch {
|
modelScope.launch {
|
||||||
// TODO implement selector
|
// TODO implement selector
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,38 @@
|
||||||
|
package com.tangem.feature.wallet.presentation.wallet.analytics.utils
|
||||||
|
|
||||||
|
import com.tangem.core.analytics.api.AnalyticsEventHandler
|
||||||
|
import com.tangem.core.decompose.di.ModelScoped
|
||||||
|
import com.tangem.domain.pay.model.MainScreenCustomerInfo
|
||||||
|
import com.tangem.domain.pay.model.OrderStatus
|
||||||
|
import com.tangem.domain.tangempay.TangemPayAnalyticsEvents
|
||||||
|
import com.tangem.feature.wallet.presentation.wallet.utils.ScreenLifecycleProvider
|
||||||
|
import javax.inject.Inject
|
||||||
|
|
||||||
|
@ModelScoped
|
||||||
|
internal class WalletTangemPayAnalyticsEventSender @Inject constructor(
|
||||||
|
private val analyticsEventHandler: AnalyticsEventHandler,
|
||||||
|
private val screenLifecycleProvider: ScreenLifecycleProvider,
|
||||||
|
) {
|
||||||
|
|
||||||
|
private val sentEvents = mutableSetOf<TangemPayAnalyticsEvents>()
|
||||||
|
|
||||||
|
fun send(customerInfo: MainScreenCustomerInfo) {
|
||||||
|
if (screenLifecycleProvider.isBackgroundState.value) return
|
||||||
|
|
||||||
|
val cardInfo = customerInfo.info.cardInfo
|
||||||
|
val productInstance = customerInfo.info.productInstance
|
||||||
|
|
||||||
|
// TODO: TangemPay refactor analytics
|
||||||
|
// when statement copied from TangemPayUpdateInfoStateTransformer. Be careful when editing
|
||||||
|
val event = when {
|
||||||
|
customerInfo.orderStatus == OrderStatus.CANCELED -> return // ignore cancelled state on analytics
|
||||||
|
!customerInfo.info.isKycApproved -> return // ignore kyc not approved state on analytics
|
||||||
|
cardInfo != null && productInstance != null -> TangemPayAnalyticsEvents.MainScreenOpened
|
||||||
|
else -> TangemPayAnalyticsEvents.IssuingBannerDisplayed
|
||||||
|
}
|
||||||
|
|
||||||
|
if (sentEvents.add(event)) {
|
||||||
|
analyticsEventHandler.send(event)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -46,6 +46,8 @@ internal class TangemPayUpdateInfoStateTransformer(
|
||||||
private fun createInitialState(): TangemPayState {
|
private fun createInitialState(): TangemPayState {
|
||||||
val cardInfo = value?.info?.cardInfo
|
val cardInfo = value?.info?.cardInfo
|
||||||
val productInstance = value?.info?.productInstance
|
val productInstance = value?.info?.productInstance
|
||||||
|
|
||||||
|
// when statement copied to WalletTangemPayAnalyticsEventSender. Be careful when editing.
|
||||||
return when {
|
return when {
|
||||||
value == null -> TangemPayState.Empty
|
value == null -> TangemPayState.Empty
|
||||||
value.orderStatus == OrderStatus.CANCELED -> createCancelledState(onIssuingFailed)
|
value.orderStatus == OrderStatus.CANCELED -> createCancelledState(onIssuingFailed)
|
||||||
|
|
|
||||||
|
|
@ -9,6 +9,7 @@ import com.tangem.domain.visa.model.TangemPayCardFrozenState
|
||||||
import com.tangem.feature.wallet.child.wallet.model.TangemPayMainInfoManager
|
import com.tangem.feature.wallet.child.wallet.model.TangemPayMainInfoManager
|
||||||
import com.tangem.feature.wallet.child.wallet.model.intents.WalletClickIntents
|
import com.tangem.feature.wallet.child.wallet.model.intents.WalletClickIntents
|
||||||
import com.tangem.feature.wallet.presentation.router.InnerWalletRouter
|
import com.tangem.feature.wallet.presentation.router.InnerWalletRouter
|
||||||
|
import com.tangem.feature.wallet.presentation.wallet.analytics.utils.WalletTangemPayAnalyticsEventSender
|
||||||
import com.tangem.feature.wallet.presentation.wallet.state.WalletStateController
|
import com.tangem.feature.wallet.presentation.wallet.state.WalletStateController
|
||||||
import com.tangem.feature.wallet.presentation.wallet.state.transformers.TangemPayHiddenStateTransformer
|
import com.tangem.feature.wallet.presentation.wallet.state.transformers.TangemPayHiddenStateTransformer
|
||||||
import com.tangem.feature.wallet.presentation.wallet.state.transformers.TangemPayRefreshNeededStateTransformer
|
import com.tangem.feature.wallet.presentation.wallet.state.transformers.TangemPayRefreshNeededStateTransformer
|
||||||
|
|
@ -21,6 +22,7 @@ import kotlinx.coroutines.CoroutineScope
|
||||||
import kotlinx.coroutines.flow.*
|
import kotlinx.coroutines.flow.*
|
||||||
import timber.log.Timber
|
import timber.log.Timber
|
||||||
|
|
||||||
|
@Suppress("LongParameterList")
|
||||||
internal class TangemPayMainSubscriber @AssistedInject constructor(
|
internal class TangemPayMainSubscriber @AssistedInject constructor(
|
||||||
@Assisted private val userWallet: UserWallet,
|
@Assisted private val userWallet: UserWallet,
|
||||||
private val stateController: WalletStateController,
|
private val stateController: WalletStateController,
|
||||||
|
|
@ -28,6 +30,7 @@ internal class TangemPayMainSubscriber @AssistedInject constructor(
|
||||||
private val innerWalletRouter: InnerWalletRouter,
|
private val innerWalletRouter: InnerWalletRouter,
|
||||||
private val cardDetailsRepository: TangemPayCardDetailsRepository,
|
private val cardDetailsRepository: TangemPayCardDetailsRepository,
|
||||||
private val tangemPayMainInfoManager: TangemPayMainInfoManager,
|
private val tangemPayMainInfoManager: TangemPayMainInfoManager,
|
||||||
|
private val analytics: WalletTangemPayAnalyticsEventSender,
|
||||||
) : WalletSubscriber() {
|
) : WalletSubscriber() {
|
||||||
|
|
||||||
override fun create(coroutineScope: CoroutineScope): Flow<*> {
|
override fun create(coroutineScope: CoroutineScope): Flow<*> {
|
||||||
|
|
@ -63,7 +66,10 @@ internal class TangemPayMainSubscriber @AssistedInject constructor(
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}.onRight { data -> updateTangemPay(data, userWalletId) }
|
}.onRight { data ->
|
||||||
|
updateTangemPay(data, userWalletId)
|
||||||
|
analytics.send(customerInfo = data)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue