diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index dcb214db57..eb8e94ac35 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -313,6 +313,16 @@ + + + + + + + + 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 f7c46d1d64..ec6982db3a 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 @@ -43,6 +43,7 @@ import com.tangem.features.tangempay.components.TangemPayHotWalletOnboardingComp import com.tangem.features.tangempay.components.TangemPayOnboardingComponent import com.tangem.features.tangempay.components.TangemPayOnboardingComponent.Params.* import com.tangem.features.tokendetails.TokenDetailsComponent +import com.tangem.features.virtualaccount.onboarding.component.VirtualAccountOnboardingComponent import com.tangem.features.wallet.WalletEntryComponent import com.tangem.features.walletconnect.components.WalletConnectEntryComponent import com.tangem.features.yield.supply.api.YieldSupplyEntryComponent @@ -114,6 +115,7 @@ internal class ChildFactory @Inject constructor( private val tangemPayDetailsContainerComponentFactory: TangemPayDetailsContainerComponent.Factory, private val tangemPayOnboardingComponentFactory: TangemPayOnboardingComponent.Factory, private val tangemPayWalletOnboardingComponentFactory: TangemPayHotWalletOnboardingComponent.Factory, + private val virtualAccountOnboardingComponentFactory: VirtualAccountOnboardingComponent.Factory, private val kycComponentFactory: KycComponent.Factory, private val surveyComponentFactory: SurveyComponent.Factory, private val yieldSupplyEntryComponentFactory: YieldSupplyEntryComponent.Factory, @@ -703,6 +705,23 @@ internal class ChildFactory @Inject constructor( componentFactory = tangemPayWalletOnboardingComponentFactory, ) } + is AppRoute.VirtualAccountOnboarding -> { + createComponentChild( + context = context, + params = when (val mode = route.mode) { + is AppRoute.VirtualAccountOnboarding.Mode.Deeplink -> + VirtualAccountOnboardingComponent.Params.Deeplink( + userWalletId = mode.userWalletId, + deeplink = mode.deeplink, + ) + is AppRoute.VirtualAccountOnboarding.Mode.FromMain -> + VirtualAccountOnboardingComponent.Params.FromMain(userWalletId = mode.userWalletId) + is AppRoute.VirtualAccountOnboarding.Mode.FromDetailsScreen -> + VirtualAccountOnboardingComponent.Params.FromDetailsScreen(userWalletId = mode.userWalletId) + }, + componentFactory = virtualAccountOnboardingComponentFactory, + ) + } is AppRoute.Kyc -> { createComponentChild( context = context, diff --git a/app/src/main/java/com/tangem/tap/routing/utils/DeepLinkFactory.kt b/app/src/main/java/com/tangem/tap/routing/utils/DeepLinkFactory.kt index 378e230a37..c30d377259 100644 --- a/app/src/main/java/com/tangem/tap/routing/utils/DeepLinkFactory.kt +++ b/app/src/main/java/com/tangem/tap/routing/utils/DeepLinkFactory.kt @@ -21,6 +21,7 @@ import com.tangem.features.send.api.deeplink.SellRedirectDeepLinkHandler import com.tangem.features.staking.api.deeplink.StakingDeepLinkHandler import com.tangem.features.survey.deeplink.SurveyDeepLinkHandler import com.tangem.features.tangempay.deeplink.OnboardVisaDeepLinkHandler +import com.tangem.features.virtualaccount.onboarding.deeplink.OnboardVirtualAccountsDeepLinkHandler import com.tangem.features.tangempay.deeplink.TangemPayMainDeepLinkHandler import com.tangem.features.tokendetails.deeplink.TokenDetailsDeepLinkHandler import com.tangem.features.wallet.deeplink.PromoDeeplinkHandler @@ -57,6 +58,7 @@ internal class DeepLinkFactory @Inject constructor( private val swapDeepLink: SwapDeepLinkHandler.Factory, private val promoDeepLink: PromoDeeplinkHandler.Factory, private val onboardVisaDeepLink: OnboardVisaDeepLinkHandler.Factory, + private val onboardVirtualAccountsDeepLink: OnboardVirtualAccountsDeepLinkHandler.Factory, private val marketsTokenExchangesDeepLink: MarketsTokenExchangesDeepLinkHandler.Factory, private val tangemPayMainDeepLink: TangemPayMainDeepLinkHandler.Factory, private val newsDetailsDeepLink: NewsDetailsDeepLinkHandler.Factory, @@ -173,6 +175,7 @@ internal class DeepLinkFactory @Inject constructor( DeepLinkRoute.WalletConnect.host -> walletConnectDeepLink.create(deeplinkUri) DeepLinkRoute.Promo.host -> promoDeepLink.create(coroutineScope, queryParams) DeepLinkRoute.OnboardVisa.host -> onboardVisaDeepLink.create(deeplinkUri) + DeepLinkRoute.OnboardVirtualAccounts.host -> onboardVirtualAccountsDeepLink.create(deeplinkUri) DeepLinkRoute.News.host -> newsDeepLink.create(queryParams) DeepLinkRoute.Earn.host -> earnDeepLink.create(queryParams) DeepLinkRoute.Yield.host -> yieldDeepLink.create(coroutineScope, queryParams) diff --git a/app/src/test/kotlin/com/tangem/tap/routing/utils/DeepLinkFactoryTest.kt b/app/src/test/kotlin/com/tangem/tap/routing/utils/DeepLinkFactoryTest.kt index c6cfb8973c..c6744e1b77 100644 --- a/app/src/test/kotlin/com/tangem/tap/routing/utils/DeepLinkFactoryTest.kt +++ b/app/src/test/kotlin/com/tangem/tap/routing/utils/DeepLinkFactoryTest.kt @@ -19,6 +19,7 @@ import com.tangem.features.onramp.deeplink.SwapDeepLinkHandler import com.tangem.features.send.api.deeplink.SellRedirectDeepLinkHandler import com.tangem.features.staking.api.deeplink.StakingDeepLinkHandler import com.tangem.features.tangempay.deeplink.OnboardVisaDeepLinkHandler +import com.tangem.features.virtualaccount.onboarding.deeplink.OnboardVirtualAccountsDeepLinkHandler import com.tangem.features.tangempay.deeplink.TangemPayMainDeepLinkHandler import com.tangem.features.tokendetails.deeplink.TokenDetailsDeepLinkHandler import com.tangem.features.wallet.deeplink.PromoDeeplinkHandler @@ -84,6 +85,10 @@ class DeepLinkFactoryTest { every { create(any()) } returns mockk() } + private val onboardVirtualAccountsDeepLink = mockk(relaxed = true) { + every { create(any()) } returns mockk() + } + private val tangemPayMainDeepLink = mockk(relaxed = true) { every { create(any(), any()) } returns mockk() } @@ -140,6 +145,7 @@ class DeepLinkFactoryTest { swapDeepLink = swapDeepLinkFactory, promoDeepLink = promoDeepLinkFactory, onboardVisaDeepLink = onboardVisaDeepLink, + onboardVirtualAccountsDeepLink = onboardVirtualAccountsDeepLink, tangemPayMainDeepLink = tangemPayMainDeepLink, newsDetailsDeepLink = newsDeeplink, newsDeepLink = newsDeepLinkFactory, diff --git a/common/routing/src/main/kotlin/com/tangem/common/routing/AppRoute.kt b/common/routing/src/main/kotlin/com/tangem/common/routing/AppRoute.kt index cd1e1253a0..9536a684a0 100644 --- a/common/routing/src/main/kotlin/com/tangem/common/routing/AppRoute.kt +++ b/common/routing/src/main/kotlin/com/tangem/common/routing/AppRoute.kt @@ -509,6 +509,24 @@ sealed class AppRoute(val path: String) : Route { @Serializable data class Kyc(val userWalletId: UserWalletId) : AppRoute(path = "/kyc") + @Serializable + data class VirtualAccountOnboarding( + val mode: Mode, + ) : AppRoute(path = "/virtual_account_onboarding/$mode") { + + @Serializable + sealed class Mode { + @Serializable + data class Deeplink(val userWalletId: UserWalletId, val deeplink: String) : Mode() + + @Serializable + data class FromMain(val userWalletId: UserWalletId) : Mode() + + @Serializable + data class FromDetailsScreen(val userWalletId: UserWalletId) : Mode() + } + } + @Serializable data class Survey(val token: String, val displayId: String? = null) : AppRoute(path = "/survey") diff --git a/common/routing/src/main/kotlin/com/tangem/common/routing/DeepLinkRoute.kt b/common/routing/src/main/kotlin/com/tangem/common/routing/DeepLinkRoute.kt index e2e31a626c..9abdcf8b09 100644 --- a/common/routing/src/main/kotlin/com/tangem/common/routing/DeepLinkRoute.kt +++ b/common/routing/src/main/kotlin/com/tangem/common/routing/DeepLinkRoute.kt @@ -68,6 +68,10 @@ sealed class DeepLinkRoute { override val host: String = "onboard-visa" } + data object OnboardVirtualAccounts : DeepLinkRoute() { + override val host: String = "onboard-virtual-account" + } + data object PayApp : DeepLinkRoute() { override val host: String = "tangem.com" } diff --git a/core/datasource/src/main/java/com/tangem/datasource/api/pay/models/response/CustomerMeResponse.kt b/core/datasource/src/main/java/com/tangem/datasource/api/pay/models/response/CustomerMeResponse.kt index da654f8d16..17de6c4cd0 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/api/pay/models/response/CustomerMeResponse.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/api/pay/models/response/CustomerMeResponse.kt @@ -35,7 +35,17 @@ data class CustomerMeResponse( @Json(name = "display_name") val displayName: String?, @Json(name = "actual_card_limit") val actualCardLimit: CardLimit?, @Json(name = "admin_card_limit") val adminCardLimit: CardLimit?, + @Json(name = "product_specification_data_type") val specificationDataType: SpecificationDataType, ) { + @JsonClass(generateAdapter = false) + enum class SpecificationDataType { + @Json(name = "ACCOUNT") + ACCOUNT, + + @Json(name = "CARD") + CARD, + } + @JsonClass(generateAdapter = false) enum class Status { @Json(name = "NEW") diff --git a/features/virtual-accounts/onboarding/api/build.gradle.kts b/features/virtual-accounts/onboarding/api/build.gradle.kts index bd895bec0a..a409f095d3 100644 --- a/features/virtual-accounts/onboarding/api/build.gradle.kts +++ b/features/virtual-accounts/onboarding/api/build.gradle.kts @@ -9,4 +9,13 @@ android { } dependencies { + /** Core */ + implementation(projects.core.decompose) + implementation(projects.core.ui) + + /** Domain */ + implementation(projects.domain.models) + + /** Compose */ + implementation(deps.compose.runtime) } \ No newline at end of file diff --git a/features/virtual-accounts/onboarding/api/src/main/kotlin/com/tangem/features/virtualaccount/onboarding/component/VirtualAccountOnboardingComponent.kt b/features/virtual-accounts/onboarding/api/src/main/kotlin/com/tangem/features/virtualaccount/onboarding/component/VirtualAccountOnboardingComponent.kt new file mode 100644 index 0000000000..5aac13f9ca --- /dev/null +++ b/features/virtual-accounts/onboarding/api/src/main/kotlin/com/tangem/features/virtualaccount/onboarding/component/VirtualAccountOnboardingComponent.kt @@ -0,0 +1,21 @@ +package com.tangem.features.virtualaccount.onboarding.component + +import com.tangem.core.decompose.factory.ComponentFactory +import com.tangem.core.ui.decompose.ComposableContentComponent +import com.tangem.domain.models.wallet.UserWalletId + +interface VirtualAccountOnboardingComponent : ComposableContentComponent { + + sealed class Params { + + abstract val userWalletId: UserWalletId + + data class Deeplink(override val userWalletId: UserWalletId, val deeplink: String) : Params() + + data class FromMain(override val userWalletId: UserWalletId) : Params() + + data class FromDetailsScreen(override val userWalletId: UserWalletId) : Params() + } + + interface Factory : ComponentFactory +} \ No newline at end of file diff --git a/features/virtual-accounts/onboarding/api/src/main/kotlin/com/tangem/features/virtualaccount/onboarding/deeplink/OnboardVirtualAccountsDeepLinkHandler.kt b/features/virtual-accounts/onboarding/api/src/main/kotlin/com/tangem/features/virtualaccount/onboarding/deeplink/OnboardVirtualAccountsDeepLinkHandler.kt new file mode 100644 index 0000000000..62350c86f4 --- /dev/null +++ b/features/virtual-accounts/onboarding/api/src/main/kotlin/com/tangem/features/virtualaccount/onboarding/deeplink/OnboardVirtualAccountsDeepLinkHandler.kt @@ -0,0 +1,10 @@ +package com.tangem.features.virtualaccount.onboarding.deeplink + +import android.net.Uri + +interface OnboardVirtualAccountsDeepLinkHandler { + + interface Factory { + fun create(uri: Uri): OnboardVirtualAccountsDeepLinkHandler + } +} \ No newline at end of file diff --git a/features/virtual-accounts/onboarding/impl/build.gradle.kts b/features/virtual-accounts/onboarding/impl/build.gradle.kts index b187abb29a..8ea2dc7f4d 100644 --- a/features/virtual-accounts/onboarding/impl/build.gradle.kts +++ b/features/virtual-accounts/onboarding/impl/build.gradle.kts @@ -11,11 +11,23 @@ android { } dependencies { + /** Core */ + implementation(projects.core.decompose) + implementation(projects.core.error) + implementation(projects.core.ui) + implementation(projects.core.utils) + + /** Common */ + implementation(projects.common.routing) + implementation(projects.common.ui) + /** Api */ implementation(projects.features.virtualAccounts.onboarding.api) - /** Core modules */ - implementation(projects.core.configToggles) + /** Domain */ + implementation(projects.domain.common) + implementation(projects.domain.models) + implementation(projects.domain.visa) /** Compose */ implementation(deps.compose.foundation) @@ -27,4 +39,7 @@ dependencies { /** DI */ implementation(deps.hilt.android) kapt(deps.hilt.kapt) + + /** Other */ + implementation(deps.arrow.core) } \ No newline at end of file diff --git a/features/virtual-accounts/onboarding/impl/src/main/kotlin/com/tangem/features/virtualaccount/onboarding/component/DefaultVirtualAccountOnboardingComponent.kt b/features/virtual-accounts/onboarding/impl/src/main/kotlin/com/tangem/features/virtualaccount/onboarding/component/DefaultVirtualAccountOnboardingComponent.kt new file mode 100644 index 0000000000..253935756f --- /dev/null +++ b/features/virtual-accounts/onboarding/impl/src/main/kotlin/com/tangem/features/virtualaccount/onboarding/component/DefaultVirtualAccountOnboardingComponent.kt @@ -0,0 +1,35 @@ +package com.tangem.features.virtualaccount.onboarding.component + +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.ui.Modifier +import androidx.lifecycle.compose.collectAsStateWithLifecycle +import com.tangem.core.decompose.context.AppComponentContext +import com.tangem.core.decompose.model.getOrCreateModel +import com.tangem.features.virtualaccount.onboarding.model.VirtualAccountOnboardingModel +import com.tangem.features.virtualaccount.onboarding.ui.VirtualAccountOnboardingScreen +import dagger.assisted.Assisted +import dagger.assisted.AssistedFactory +import dagger.assisted.AssistedInject + +internal class DefaultVirtualAccountOnboardingComponent @AssistedInject constructor( + @Assisted appComponentContext: AppComponentContext, + @Assisted params: VirtualAccountOnboardingComponent.Params, +) : VirtualAccountOnboardingComponent, AppComponentContext by appComponentContext { + + private val model: VirtualAccountOnboardingModel = getOrCreateModel(params) + + @Composable + override fun Content(modifier: Modifier) { + val state by model.uiState.collectAsStateWithLifecycle() + VirtualAccountOnboardingScreen(modifier = modifier, state = state) + } + + @AssistedFactory + interface Factory : VirtualAccountOnboardingComponent.Factory { + override fun create( + context: AppComponentContext, + params: VirtualAccountOnboardingComponent.Params, + ): DefaultVirtualAccountOnboardingComponent + } +} \ No newline at end of file diff --git a/features/virtual-accounts/onboarding/impl/src/main/kotlin/com/tangem/features/virtualaccount/onboarding/deeplink/DefaultOnboardVirtualAccountsDeepLinkHandler.kt b/features/virtual-accounts/onboarding/impl/src/main/kotlin/com/tangem/features/virtualaccount/onboarding/deeplink/DefaultOnboardVirtualAccountsDeepLinkHandler.kt new file mode 100644 index 0000000000..87dcb0729b --- /dev/null +++ b/features/virtual-accounts/onboarding/impl/src/main/kotlin/com/tangem/features/virtualaccount/onboarding/deeplink/DefaultOnboardVirtualAccountsDeepLinkHandler.kt @@ -0,0 +1,35 @@ +package com.tangem.features.virtualaccount.onboarding.deeplink + +import android.net.Uri +import com.tangem.common.routing.AppRoute +import com.tangem.common.routing.AppRouter +import com.tangem.domain.common.wallets.UserWalletsListRepository +import com.tangem.utils.logging.TangemLogger +import dagger.assisted.Assisted +import dagger.assisted.AssistedFactory +import dagger.assisted.AssistedInject + +internal class DefaultOnboardVirtualAccountsDeepLinkHandler @AssistedInject constructor( + @Assisted uri: Uri, + appRouter: AppRouter, + userWalletsListRepository: UserWalletsListRepository, +) : OnboardVirtualAccountsDeepLinkHandler { + + init { + val userWalletId = userWalletsListRepository.selectedUserWallet.value?.walletId + if (userWalletId == null) { + TangemLogger.e("Can not open virtual account onboarding deeplink: no selected wallet") + } else { + val mode = AppRoute.VirtualAccountOnboarding.Mode.Deeplink( + userWalletId = userWalletId, + deeplink = uri.toString(), + ) + appRouter.push(AppRoute.VirtualAccountOnboarding(mode)) + } + } + + @AssistedFactory + interface Factory : OnboardVirtualAccountsDeepLinkHandler.Factory { + override fun create(uri: Uri): DefaultOnboardVirtualAccountsDeepLinkHandler + } +} \ No newline at end of file diff --git a/features/virtual-accounts/onboarding/impl/src/main/kotlin/com/tangem/features/virtualaccount/onboarding/di/VirtualAccountOnboardingFeatureModule.kt b/features/virtual-accounts/onboarding/impl/src/main/kotlin/com/tangem/features/virtualaccount/onboarding/di/VirtualAccountOnboardingFeatureModule.kt new file mode 100644 index 0000000000..3d1c743658 --- /dev/null +++ b/features/virtual-accounts/onboarding/impl/src/main/kotlin/com/tangem/features/virtualaccount/onboarding/di/VirtualAccountOnboardingFeatureModule.kt @@ -0,0 +1,25 @@ +package com.tangem.features.virtualaccount.onboarding.di + +import com.tangem.features.virtualaccount.onboarding.component.DefaultVirtualAccountOnboardingComponent +import com.tangem.features.virtualaccount.onboarding.component.VirtualAccountOnboardingComponent +import com.tangem.features.virtualaccount.onboarding.deeplink.DefaultOnboardVirtualAccountsDeepLinkHandler +import com.tangem.features.virtualaccount.onboarding.deeplink.OnboardVirtualAccountsDeepLinkHandler +import dagger.Binds +import dagger.Module +import dagger.hilt.InstallIn +import dagger.hilt.components.SingletonComponent +import javax.inject.Singleton + +@Module +@InstallIn(SingletonComponent::class) +internal interface VirtualAccountOnboardingFeatureModule { + + @Binds + fun bindFactory(impl: DefaultVirtualAccountOnboardingComponent.Factory): VirtualAccountOnboardingComponent.Factory + + @Binds + @Singleton + fun bindOnboardVirtualAccountsDeepLinkHandlerFactory( + impl: DefaultOnboardVirtualAccountsDeepLinkHandler.Factory, + ): OnboardVirtualAccountsDeepLinkHandler.Factory +} \ No newline at end of file diff --git a/features/virtual-accounts/onboarding/impl/src/main/kotlin/com/tangem/features/virtualaccount/onboarding/di/VirtualAccountOnboardingModelsModule.kt b/features/virtual-accounts/onboarding/impl/src/main/kotlin/com/tangem/features/virtualaccount/onboarding/di/VirtualAccountOnboardingModelsModule.kt new file mode 100644 index 0000000000..e14040c3ea --- /dev/null +++ b/features/virtual-accounts/onboarding/impl/src/main/kotlin/com/tangem/features/virtualaccount/onboarding/di/VirtualAccountOnboardingModelsModule.kt @@ -0,0 +1,20 @@ +package com.tangem.features.virtualaccount.onboarding.di + +import com.tangem.core.decompose.di.ModelComponent +import com.tangem.core.decompose.model.Model +import com.tangem.features.virtualaccount.onboarding.model.VirtualAccountOnboardingModel +import dagger.Binds +import dagger.Module +import dagger.hilt.InstallIn +import dagger.multibindings.ClassKey +import dagger.multibindings.IntoMap + +@Module +@InstallIn(ModelComponent::class) +internal interface VirtualAccountOnboardingModelsModule { + + @Binds + @IntoMap + @ClassKey(VirtualAccountOnboardingModel::class) + fun bindVirtualAccountOnboardingModel(model: VirtualAccountOnboardingModel): Model +} \ No newline at end of file diff --git a/features/virtual-accounts/onboarding/impl/src/main/kotlin/com/tangem/features/virtualaccount/onboarding/model/VirtualAccountOnboardingModel.kt b/features/virtual-accounts/onboarding/impl/src/main/kotlin/com/tangem/features/virtualaccount/onboarding/model/VirtualAccountOnboardingModel.kt new file mode 100644 index 0000000000..9c30c6911e --- /dev/null +++ b/features/virtual-accounts/onboarding/impl/src/main/kotlin/com/tangem/features/virtualaccount/onboarding/model/VirtualAccountOnboardingModel.kt @@ -0,0 +1,96 @@ +package com.tangem.features.virtualaccount.onboarding.model + +import androidx.compose.runtime.Stable +import com.tangem.core.decompose.di.ModelScoped +import com.tangem.core.decompose.model.Model +import com.tangem.core.decompose.model.ParamsContainer +import com.tangem.core.decompose.navigation.Router +import com.tangem.domain.pay.repository.OnboardingRepository +import com.tangem.features.virtualaccount.onboarding.component.VirtualAccountOnboardingComponent +import com.tangem.features.virtualaccount.onboarding.ui.VirtualAccountOnboardingUM +import com.tangem.utils.coroutines.CoroutineDispatcherProvider +import kotlinx.coroutines.delay +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.update +import kotlinx.coroutines.launch +import javax.inject.Inject + +@Stable +@ModelScoped +internal class VirtualAccountOnboardingModel @Inject constructor( + paramsContainer: ParamsContainer, + override val dispatchers: CoroutineDispatcherProvider, + private val router: Router, + private val onboardingRepository: OnboardingRepository, +) : Model() { + + private val params = paramsContainer.require() + + val uiState: StateFlow + field = MutableStateFlow(VirtualAccountOnboardingUM.Loading(onBack = ::back)) + + init { + when (params) { + is VirtualAccountOnboardingComponent.Params.Deeplink -> validateDeeplinkAndShow(params.deeplink) + is VirtualAccountOnboardingComponent.Params.FromMain, + is VirtualAccountOnboardingComponent.Params.FromDetailsScreen, + -> showOnboarding() + } + } + + private fun validateDeeplinkAndShow(deeplink: String) { + modelScope.launch { + onboardingRepository.validateDeeplink(deeplink) + .onRight { isValid -> if (isValid) showOnboarding() else back() } + .onLeft { back() } + } + } + + private fun showOnboarding() { + uiState.update { + VirtualAccountOnboardingUM.Content( + onBack = ::back, + isLoading = false, + onGetCardClick = ::onGetCardClick, + onTermsClick = ::onTermsClick, + onPrivacyClick = ::onPrivacyClick, + ) + } + } + + private fun onTermsClick() { + // TODO([REDACTED_TASK_KEY]): open the provider Terms of Use link. + } + + private fun onPrivacyClick() { + // TODO([REDACTED_TASK_KEY]): open the provider Privacy Policy link. + } + + private fun onGetCardClick() { + modelScope.launch { + setLoading(isLoading = true) + delay(STUB_GET_CARD_DELAY_MS) + // TODO: create order and sign challenge [REDACTED_JIRA] + setLoading(isLoading = false) + } + } + + private fun setLoading(isLoading: Boolean) { + uiState.update { state -> + when (state) { + is VirtualAccountOnboardingUM.Content -> state.copy(isLoading = isLoading) + is VirtualAccountOnboardingUM.Loading -> state + } + } + } + + private fun back() { + router.pop() + } + + private companion object { + // TODO([REDACTED_TASK_KEY]): remove the stub delay once create-order + sign-challenge is implemented. + const val STUB_GET_CARD_DELAY_MS = 3000L + } +} \ No newline at end of file diff --git a/features/virtual-accounts/onboarding/impl/src/main/kotlin/com/tangem/features/virtualaccount/onboarding/ui/VirtualAccountOnboardingScreen.kt b/features/virtual-accounts/onboarding/impl/src/main/kotlin/com/tangem/features/virtualaccount/onboarding/ui/VirtualAccountOnboardingScreen.kt new file mode 100644 index 0000000000..537d302e29 --- /dev/null +++ b/features/virtual-accounts/onboarding/impl/src/main/kotlin/com/tangem/features/virtualaccount/onboarding/ui/VirtualAccountOnboardingScreen.kt @@ -0,0 +1,213 @@ +package com.tangem.features.virtualaccount.onboarding.ui + +import android.content.res.Configuration +import androidx.compose.foundation.Image +import androidx.compose.foundation.background +import androidx.compose.foundation.border +import androidx.compose.foundation.layout.* +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.CircularProgressIndicator +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.graphics.Brush +import androidx.compose.ui.layout.ContentScale +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.text.LinkAnnotation +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.text.withLink +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.tooling.preview.PreviewParameter +import androidx.compose.ui.tooling.preview.datasource.CollectionPreviewParameterProvider +import androidx.compose.ui.unit.dp +import com.tangem.core.ui.ds.image.TangemIconUM +import com.tangem.core.ui.ds2.button.Close +import com.tangem.core.ui.ds2.button.TangemButton +import com.tangem.core.ui.extensions.annotatedReference +import com.tangem.core.ui.extensions.appendColored +import com.tangem.core.ui.extensions.resolveAnnotatedReference +import com.tangem.core.ui.extensions.stringReference +import com.tangem.core.ui.res.TangemTheme +import com.tangem.core.ui.res.TangemThemePreviewRedesign +import com.tangem.features.virtualaccount.onboarding.impl.R + +private const val GRADIENT_TRANSPARENT_STOP = 0.45f +private const val GRADIENT_OPAQUE_STOP = 0.72f + +private const val TERMS_LINK_TAG = "VA_TERMS" +private const val PRIVACY_LINK_TAG = "VA_PRIVACY" + +@Composable +internal fun VirtualAccountOnboardingScreen(state: VirtualAccountOnboardingUM, modifier: Modifier = Modifier) { + Box( + modifier = modifier + .fillMaxSize() + .background(TangemTheme.colors3.bg.primary), + ) { + Image( + painter = painterResource(id = R.drawable.bg_virtual_account_onboarding), + contentDescription = null, + contentScale = ContentScale.Crop, + alignment = Alignment.TopCenter, + modifier = Modifier.fillMaxSize(), + ) + Box( + modifier = Modifier + .fillMaxSize() + .background( + Brush.verticalGradient( + colorStops = arrayOf( + 0f to TangemTheme.colors3.bg.primary.copy(alpha = 0f), + GRADIENT_TRANSPARENT_STOP to TangemTheme.colors3.bg.primary.copy(alpha = 0f), + GRADIENT_OPAQUE_STOP to TangemTheme.colors3.bg.primary, + 1f to TangemTheme.colors3.bg.primary, + ), + ), + ), + ) + + when (state) { + is VirtualAccountOnboardingUM.Loading -> Loading(modifier = Modifier.fillMaxSize()) + is VirtualAccountOnboardingUM.Content -> Content(state = state) + } + + TangemButton.Close( + modifier = Modifier + .align(Alignment.TopEnd) + .statusBarsPadding() + .padding(top = 4.dp, end = 16.dp), + onClick = state.onBack, + ) + } +} + +@Composable +private fun Loading(modifier: Modifier = Modifier) { + Box(modifier = modifier, contentAlignment = Alignment.Center) { + CircularProgressIndicator(color = TangemTheme.colors3.icon.primary) + } +} + +@Composable +private fun Content(state: VirtualAccountOnboardingUM.Content, modifier: Modifier = Modifier) { + Column( + modifier = modifier + .fillMaxSize() + .systemBarsPadding(), + ) { + Spacer(modifier = Modifier.weight(1f)) + + Column( + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = 24.dp), + verticalArrangement = Arrangement.spacedBy(12.dp), + ) { + Text( + text = "Send USD from your bank. Receive USDC", + style = TangemTheme.typography3.heading.medium, + color = TangemTheme.colors3.text.primary, + ) + Text( + text = "A dedicated account with US banking details — no deposit or maintenance fees", + style = TangemTheme.typography3.subheading.medium, + color = TangemTheme.colors3.text.secondary, + ) + } + + TermsCard( + modifier = Modifier.padding(top = 24.dp, start = 8.dp, end = 8.dp), + state = state, + ) + } +} + +@Composable +private fun TermsCard(state: VirtualAccountOnboardingUM.Content, modifier: Modifier = Modifier) { + val shape = RoundedCornerShape(topStart = 24.dp, topEnd = 24.dp, bottomStart = 28.dp, bottomEnd = 28.dp) + Column( + modifier = modifier + .fillMaxWidth() + .clip(shape) + .background(TangemTheme.colors3.bg.opaque.primary) + .border(width = 1.dp, color = TangemTheme.colors3.border.secondary, shape = shape), + ) { + Text( + modifier = Modifier.padding(top = 12.dp, start = 16.dp, end = 16.dp), + text = buildTermsAndPolicy( + onTermsClick = state.onTermsClick, + onPrivacyClick = state.onPrivacyClick, + ).resolveAnnotatedReference(), + style = TangemTheme.typography3.caption.medium, + color = TangemTheme.colors3.text.secondary, + textAlign = TextAlign.Center, + ) + TangemButton( + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = 8.dp, vertical = 12.dp), + variant = TangemButton.Variant.Primary, + size = TangemButton.Size.X12, + text = stringReference("Open account"), + iconEnd = TangemIconUM.Icon(R.drawable.ic_tangem_24), + isLoading = state.isLoading, + onClick = state.onGetCardClick, + ) + } +} + +@Composable +private fun buildTermsAndPolicy(onTermsClick: () -> Unit, onPrivacyClick: () -> Unit) = annotatedReference { + val linkColor = TangemTheme.colors3.text.primary + append("By using service, you agree with provider ") + withLink( + link = LinkAnnotation.Clickable( + tag = TERMS_LINK_TAG, + linkInteractionListener = { onTermsClick() }, + ), + block = { appendColored(text = "Terms of Use", color = linkColor) }, + ) + append(" and ") + withLink( + link = LinkAnnotation.Clickable( + tag = PRIVACY_LINK_TAG, + linkInteractionListener = { onPrivacyClick() }, + ), + block = { appendColored(text = "Privacy Policy", color = linkColor) }, + ) +} + +@Preview(showBackground = true) +@Preview(showBackground = true, uiMode = Configuration.UI_MODE_NIGHT_YES) +@Composable +private fun VirtualAccountOnboardingScreenPreview( + @PreviewParameter(VirtualAccountOnboardingStateProvider::class) + state: VirtualAccountOnboardingUM, +) { + TangemThemePreviewRedesign { + VirtualAccountOnboardingScreen(state = state, modifier = Modifier.fillMaxSize()) + } +} + +private class VirtualAccountOnboardingStateProvider : + CollectionPreviewParameterProvider( + listOf( + VirtualAccountOnboardingUM.Loading(onBack = {}), + VirtualAccountOnboardingUM.Content( + onBack = {}, + isLoading = false, + onGetCardClick = {}, + onTermsClick = {}, + onPrivacyClick = {}, + ), + VirtualAccountOnboardingUM.Content( + onBack = {}, + isLoading = true, + onGetCardClick = {}, + onTermsClick = {}, + onPrivacyClick = {}, + ), + ), + ) \ No newline at end of file diff --git a/features/virtual-accounts/onboarding/impl/src/main/kotlin/com/tangem/features/virtualaccount/onboarding/ui/VirtualAccountOnboardingUM.kt b/features/virtual-accounts/onboarding/impl/src/main/kotlin/com/tangem/features/virtualaccount/onboarding/ui/VirtualAccountOnboardingUM.kt new file mode 100644 index 0000000000..c5ab5b0a33 --- /dev/null +++ b/features/virtual-accounts/onboarding/impl/src/main/kotlin/com/tangem/features/virtualaccount/onboarding/ui/VirtualAccountOnboardingUM.kt @@ -0,0 +1,22 @@ +package com.tangem.features.virtualaccount.onboarding.ui + +import androidx.compose.runtime.Immutable + +/** + * UI model for the Virtual Account onboarding screen. + */ +@Immutable +internal sealed class VirtualAccountOnboardingUM { + + abstract val onBack: () -> Unit + + data class Loading(override val onBack: () -> Unit) : VirtualAccountOnboardingUM() + + data class Content( + override val onBack: () -> Unit, + val isLoading: Boolean, + val onGetCardClick: () -> Unit, + val onTermsClick: () -> Unit, + val onPrivacyClick: () -> Unit, + ) : VirtualAccountOnboardingUM() +} \ No newline at end of file diff --git a/features/virtual-accounts/onboarding/impl/src/main/res/drawable/bg_virtual_account_onboarding.webp b/features/virtual-accounts/onboarding/impl/src/main/res/drawable/bg_virtual_account_onboarding.webp new file mode 100644 index 0000000000..40030c0fd9 Binary files /dev/null and b/features/virtual-accounts/onboarding/impl/src/main/res/drawable/bg_virtual_account_onboarding.webp differ