diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 2de49eab54..097bcd4c84 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -273,6 +273,8 @@ dependencies { implementation(projects.features.welcome.impl) implementation(projects.features.createWalletSelection.api) implementation(projects.features.createWalletSelection.impl) + implementation(projects.features.createWalletStart.api) + implementation(projects.features.createWalletStart.impl) implementation(projects.features.home.api) implementation(projects.features.home.impl) implementation(projects.features.account.api) 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 e2ce3a2b4a..c858ffedc0 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 @@ -13,6 +13,7 @@ import com.tangem.features.account.AccountCreateEditComponent import com.tangem.features.account.AccountDetailsComponent import com.tangem.features.account.ArchivedAccountListComponent import com.tangem.features.createwalletselection.CreateWalletSelectionComponent +import com.tangem.features.createwalletstart.CreateWalletStartComponent import com.tangem.features.details.component.DetailsComponent import com.tangem.features.disclaimer.api.components.DisclaimerComponent import com.tangem.features.home.api.HomeComponent @@ -99,6 +100,7 @@ internal class ChildFactory @Inject constructor( private val usedeskComponentFactory: UsedeskComponent.Factory, private val chooseManagedTokensComponentFactory: ChooseManagedTokensComponent.Factory, private val createWalletSelectionComponentFactory: CreateWalletSelectionComponent.Factory, + private val createWalletStartComponentFactory: CreateWalletStartComponent.Factory, private val createMobileWalletComponentFactory: CreateMobileWalletComponent.Factory, private val upgradeWalletComponentFactory: UpgradeWalletComponent.Factory, private val addExistingWalletComponentFactory: AddExistingWalletComponent.Factory, @@ -476,6 +478,19 @@ internal class ChildFactory @Inject constructor( componentFactory = chooseManagedTokensComponentFactory, ) } + is AppRoute.CreateWalletStart -> { + val mode = when (route.mode) { + AppRoute.CreateWalletStart.Mode.ColdWallet -> CreateWalletStartComponent.Mode.ColdWallet + AppRoute.CreateWalletStart.Mode.HotWallet -> CreateWalletStartComponent.Mode.HotWallet + } + createComponentChild( + context = context, + params = CreateWalletStartComponent.Params( + mode = mode, + ), + componentFactory = createWalletStartComponentFactory, + ) + } is AppRoute.CreateWalletSelection -> { createComponentChild( context = context, 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 d5506408c3..cdba5c18f3 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 @@ -311,6 +311,16 @@ sealed class AppRoute(val path: String) : Route { @Serializable object CreateWalletSelection : AppRoute(path = "/create_wallet_selection") + @Serializable + data class CreateWalletStart( + val mode: Mode, + ) : AppRoute(path = "/create_wallet_start") { + enum class Mode { + ColdWallet, + HotWallet, + } + } + @Serializable object CreateMobileWallet : AppRoute(path = "/create_mobile_wallet") diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/SystemBarsUtils.kt b/core/ui/src/main/java/com/tangem/core/ui/components/SystemBarsUtils.kt index 0e40554581..494129175b 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/components/SystemBarsUtils.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/components/SystemBarsUtils.kt @@ -1,12 +1,54 @@ package com.tangem.core.ui.components import androidx.compose.runtime.Composable +import androidx.compose.runtime.CompositionLocalProvider import androidx.compose.runtime.DisposableEffect import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.SideEffect +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableIntStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.runtime.staticCompositionLocalOf +import com.google.accompanist.systemuicontroller.SystemUiController import com.google.accompanist.systemuicontroller.rememberSystemUiController import com.tangem.core.ui.res.LocalIsInDarkTheme +val LocalSystemBarsIconsController = staticCompositionLocalOf { + error("No SystemBarsIconsController provided") +} + +class SystemBarsIconsController(private val systemUiController: SystemUiController) { + private var count by mutableIntStateOf(0) + + fun setIcons(darkIcons: Boolean, isNavigationBarContrastEnforced: Boolean) { + if (count == 0) { + systemUiController.systemBarsDarkContentEnabled = darkIcons + systemUiController.isNavigationBarContrastEnforced = isNavigationBarContrastEnforced + } + count++ + } + + fun restoreIcons(isDarkTheme: Boolean) { + count-- + if (count == 0) { + systemUiController.systemBarsDarkContentEnabled = !isDarkTheme + systemUiController.isNavigationBarContrastEnforced = false + } + } +} + +@Composable +fun ProvideSystemBarsIconsController(content: @Composable () -> Unit) { + val systemUiController = rememberSystemUiController() + val controller = remember(systemUiController) { SystemBarsIconsController(systemUiController) } + + CompositionLocalProvider( + LocalSystemBarsIconsController provides controller, + content = content, + ) +} + /** * Provides the ability to set a scrim for 3-button navigation * @@ -43,19 +85,16 @@ fun NavigationBar3ButtonsScrim() { */ @Composable fun SystemBarsIconsDisposable(darkIcons: Boolean, isNavigationBarContrastEnforced: Boolean = false) { - val systemUiController = rememberSystemUiController() + val controller = LocalSystemBarsIconsController.current + val isDarkTheme = LocalIsInDarkTheme.current SideEffect { - systemUiController.systemBarsDarkContentEnabled = darkIcons - systemUiController.isNavigationBarContrastEnforced = isNavigationBarContrastEnforced + controller.setIcons(darkIcons, isNavigationBarContrastEnforced) } - val isDarkTheme = LocalIsInDarkTheme.current - DisposableEffect(isDarkTheme) { onDispose { - systemUiController.systemBarsDarkContentEnabled = !isDarkTheme - systemUiController.isNavigationBarContrastEnforced = false + controller.restoreIcons(isDarkTheme) } } } \ No newline at end of file diff --git a/core/ui/src/main/java/com/tangem/core/ui/res/TangemTheme.kt b/core/ui/src/main/java/com/tangem/core/ui/res/TangemTheme.kt index 85baa41281..01765f772d 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/res/TangemTheme.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/res/TangemTheme.kt @@ -13,6 +13,8 @@ import androidx.compose.ui.graphics.Color import androidx.compose.ui.platform.LocalView import com.google.accompanist.systemuicontroller.rememberSystemUiController import com.tangem.core.ui.UiDependencies +import com.tangem.core.ui.components.LocalSystemBarsIconsController +import com.tangem.core.ui.components.SystemBarsIconsController import com.tangem.core.ui.components.TangemShimmer import com.tangem.core.ui.components.text.BladeAnimation import com.tangem.core.ui.components.text.rememberBladeAnimation @@ -65,6 +67,8 @@ fun TangemTheme( val themeColors = if (isDark) darkThemeColors() else lightThemeColors() val rememberedColors = remember { themeColors } .also { it.update(themeColors) } + val systemUiController = rememberSystemUiController() + val systemBarsIconsController = remember(systemUiController) { SystemBarsIconsController(systemUiController) } val shapes = remember { TangemShapes(dimens) } @@ -103,6 +107,7 @@ fun TangemTheme( LocalEventMessageHandler provides eventMessageHandler, LocalWindowSize provides windowSize, LocalBladeAnimation provides rememberBladeAnimation(), + LocalSystemBarsIconsController provides systemBarsIconsController, ) { CompositionLocalProvider( LocalTangemShimmer provides TangemShimmer, @@ -119,6 +124,14 @@ fun TangemTheme( } } +@Composable +fun ForceDarkTheme(content: @Composable () -> Unit) { + CompositionLocalProvider( + LocalTangemColors provides darkThemeColors(), + content = content, + ) +} + object TangemTheme { val colors: TangemColors @Composable diff --git a/core/ui/src/main/res/drawable-hdpi/img_hardware_wallet.webp b/core/ui/src/main/res/drawable-hdpi/img_hardware_wallet.webp new file mode 100644 index 0000000000..471779539c Binary files /dev/null and b/core/ui/src/main/res/drawable-hdpi/img_hardware_wallet.webp differ diff --git a/core/ui/src/main/res/drawable-hdpi/img_mobile_wallet.webp b/core/ui/src/main/res/drawable-hdpi/img_mobile_wallet.webp new file mode 100644 index 0000000000..74316b4df5 Binary files /dev/null and b/core/ui/src/main/res/drawable-hdpi/img_mobile_wallet.webp differ diff --git a/core/ui/src/main/res/drawable-mdpi/img_hardware_wallet.webp b/core/ui/src/main/res/drawable-mdpi/img_hardware_wallet.webp new file mode 100644 index 0000000000..20fe7241fc Binary files /dev/null and b/core/ui/src/main/res/drawable-mdpi/img_hardware_wallet.webp differ diff --git a/core/ui/src/main/res/drawable-mdpi/img_mobile_wallet.webp b/core/ui/src/main/res/drawable-mdpi/img_mobile_wallet.webp new file mode 100644 index 0000000000..7c2cbfe1fe Binary files /dev/null and b/core/ui/src/main/res/drawable-mdpi/img_mobile_wallet.webp differ diff --git a/core/ui/src/main/res/drawable-xhdpi/img_hardware_wallet.webp b/core/ui/src/main/res/drawable-xhdpi/img_hardware_wallet.webp new file mode 100644 index 0000000000..7aadd5dc10 Binary files /dev/null and b/core/ui/src/main/res/drawable-xhdpi/img_hardware_wallet.webp differ diff --git a/core/ui/src/main/res/drawable-xhdpi/img_mobile_wallet.webp b/core/ui/src/main/res/drawable-xhdpi/img_mobile_wallet.webp new file mode 100644 index 0000000000..b42de3dea2 Binary files /dev/null and b/core/ui/src/main/res/drawable-xhdpi/img_mobile_wallet.webp differ diff --git a/core/ui/src/main/res/drawable-xxhdpi/img_hardware_wallet.webp b/core/ui/src/main/res/drawable-xxhdpi/img_hardware_wallet.webp new file mode 100644 index 0000000000..c9e3b37299 Binary files /dev/null and b/core/ui/src/main/res/drawable-xxhdpi/img_hardware_wallet.webp differ diff --git a/core/ui/src/main/res/drawable-xxhdpi/img_mobile_wallet.webp b/core/ui/src/main/res/drawable-xxhdpi/img_mobile_wallet.webp new file mode 100644 index 0000000000..d0dc780332 Binary files /dev/null and b/core/ui/src/main/res/drawable-xxhdpi/img_mobile_wallet.webp differ diff --git a/core/ui/src/main/res/drawable-xxxhdpi/img_hardware_wallet.webp b/core/ui/src/main/res/drawable-xxxhdpi/img_hardware_wallet.webp new file mode 100644 index 0000000000..2626432e01 Binary files /dev/null and b/core/ui/src/main/res/drawable-xxxhdpi/img_hardware_wallet.webp differ diff --git a/core/ui/src/main/res/drawable-xxxhdpi/img_mobile_wallet.webp b/core/ui/src/main/res/drawable-xxxhdpi/img_mobile_wallet.webp new file mode 100644 index 0000000000..4b9a5bd3c6 Binary files /dev/null and b/core/ui/src/main/res/drawable-xxxhdpi/img_mobile_wallet.webp differ diff --git a/core/ui/src/main/res/drawable/ic_chevron_right_18x24.xml b/core/ui/src/main/res/drawable/ic_chevron_right_18x24.xml new file mode 100644 index 0000000000..b3e1a0461d --- /dev/null +++ b/core/ui/src/main/res/drawable/ic_chevron_right_18x24.xml @@ -0,0 +1,9 @@ + + + diff --git a/core/ui/src/main/res/drawable/ic_flash_16.xml b/core/ui/src/main/res/drawable/ic_flash_16.xml new file mode 100644 index 0000000000..ae87edfc41 --- /dev/null +++ b/core/ui/src/main/res/drawable/ic_flash_16.xml @@ -0,0 +1,9 @@ + + + diff --git a/core/ui/src/main/res/drawable/ic_shield_check_16.xml b/core/ui/src/main/res/drawable/ic_shield_check_16.xml new file mode 100644 index 0000000000..3b36f1acf0 --- /dev/null +++ b/core/ui/src/main/res/drawable/ic_shield_check_16.xml @@ -0,0 +1,10 @@ + + + diff --git a/core/ui/src/main/res/drawable/ic_sparkles_16.xml b/core/ui/src/main/res/drawable/ic_sparkles_16.xml new file mode 100644 index 0000000000..8add70e924 --- /dev/null +++ b/core/ui/src/main/res/drawable/ic_sparkles_16.xml @@ -0,0 +1,12 @@ + + + + diff --git a/core/ui/src/main/res/drawable/ic_stack_fill_new_16.xml b/core/ui/src/main/res/drawable/ic_stack_fill_new_16.xml new file mode 100644 index 0000000000..e5518cf3fd --- /dev/null +++ b/core/ui/src/main/res/drawable/ic_stack_fill_new_16.xml @@ -0,0 +1,15 @@ + + + + + diff --git a/features/create-wallet-start/api/.gitignore b/features/create-wallet-start/api/.gitignore new file mode 100644 index 0000000000..796b96d1c4 --- /dev/null +++ b/features/create-wallet-start/api/.gitignore @@ -0,0 +1 @@ +/build diff --git a/features/create-wallet-start/api/build.gradle.kts b/features/create-wallet-start/api/build.gradle.kts new file mode 100644 index 0000000000..8c60a57f79 --- /dev/null +++ b/features/create-wallet-start/api/build.gradle.kts @@ -0,0 +1,21 @@ +plugins { + alias(deps.plugins.android.library) + alias(deps.plugins.kotlin.android) + id("configuration") +} + +android { + namespace = "com.tangem.features.createwalletstart.api" +} + +dependencies { + /* Project - Domain */ + implementation(projects.domain.models) + + /* Project - Core */ + implementation(projects.core.decompose) + implementation(projects.core.ui) + + /* Compose */ + implementation(deps.compose.runtime) +} \ No newline at end of file diff --git a/features/create-wallet-start/api/src/main/kotlin/com/tangem/features/createwalletstart/CreateWalletStartComponent.kt b/features/create-wallet-start/api/src/main/kotlin/com/tangem/features/createwalletstart/CreateWalletStartComponent.kt new file mode 100644 index 0000000000..120293bd9a --- /dev/null +++ b/features/create-wallet-start/api/src/main/kotlin/com/tangem/features/createwalletstart/CreateWalletStartComponent.kt @@ -0,0 +1,18 @@ +package com.tangem.features.createwalletstart + +import com.tangem.core.decompose.factory.ComponentFactory +import com.tangem.core.ui.decompose.ComposableContentComponent + +interface CreateWalletStartComponent : ComposableContentComponent { + + data class Params( + val mode: Mode, + ) + + enum class Mode { + ColdWallet, + HotWallet, + } + + interface Factory : ComponentFactory +} \ No newline at end of file diff --git a/features/create-wallet-start/impl/.gitignore b/features/create-wallet-start/impl/.gitignore new file mode 100644 index 0000000000..796b96d1c4 --- /dev/null +++ b/features/create-wallet-start/impl/.gitignore @@ -0,0 +1 @@ +/build diff --git a/features/create-wallet-start/impl/build.gradle.kts b/features/create-wallet-start/impl/build.gradle.kts new file mode 100644 index 0000000000..6909f9d54c --- /dev/null +++ b/features/create-wallet-start/impl/build.gradle.kts @@ -0,0 +1,72 @@ +plugins { + alias(deps.plugins.android.library) + alias(deps.plugins.kotlin.android) + alias(deps.plugins.kotlin.kapt) + alias(deps.plugins.kotlin.serialization) + alias(deps.plugins.hilt.android) + id("configuration") +} + +android { + namespace = "com.tangem.features.createwalletstart.impl" +} + +dependencies { + /** Api */ + implementation(projects.features.createWalletStart.api) + + /** Project - Domain */ + implementation(projects.domain.card) + implementation(projects.domain.settings) + implementation(projects.domain.wallets) + implementation(projects.domain.models) + + /** Core modules */ + implementation(projects.core.configToggles) + implementation(projects.core.analytics) + implementation(projects.core.analytics.models) + implementation(projects.core.utils) + implementation(projects.core.ui) + implementation(projects.core.res) + implementation(projects.core.decompose) + implementation(projects.core.navigation) + implementation(projects.core.datasource) + + /** Common */ + implementation(projects.common.ui) + implementation(projects.common.routing) + + /** Tangem libraries */ + implementation(projects.libs.tangemSdkApi) + implementation(tangemDeps.card.core) + implementation(tangemDeps.card.android) { + exclude(module = "joda-time") + } + + /** AndroidX libraries */ + implementation(deps.androidx.core.ktx) + implementation(deps.lifecycle.runtime.ktx) + + /** Compose libraries */ + implementation(deps.compose.material3) + implementation(deps.compose.animation) + implementation(deps.compose.foundation) + implementation(deps.compose.ui) + implementation(deps.compose.ui.tooling) + implementation(deps.compose.coil) + implementation(deps.lottie.compose) + implementation(deps.decompose.ext.compose) + implementation(deps.androidx.activity.compose) + implementation(deps.androidx.datastore) + + /** Other libraries */ + implementation(deps.arrow.core) + implementation(deps.kotlin.immutable.collections) + implementation(deps.kotlin.serialization) + implementation(deps.timber) + implementation(deps.firebase.crashlytics) + + /** DI */ + implementation(deps.hilt.android) + kapt(deps.hilt.kapt) +} \ No newline at end of file diff --git a/features/create-wallet-start/impl/src/main/kotlin/com/tangem/features/createwalletstart/CreateWalletStartModel.kt b/features/create-wallet-start/impl/src/main/kotlin/com/tangem/features/createwalletstart/CreateWalletStartModel.kt new file mode 100644 index 0000000000..6441bbb2e2 --- /dev/null +++ b/features/create-wallet-start/impl/src/main/kotlin/com/tangem/features/createwalletstart/CreateWalletStartModel.kt @@ -0,0 +1,243 @@ +package com.tangem.features.createwalletstart + +import com.tangem.common.core.TangemError +import com.tangem.common.core.TangemSdkError +import com.tangem.common.routing.AppRoute +import com.tangem.common.routing.AppRouter +import com.tangem.core.analytics.api.AnalyticsEventHandler +import com.tangem.core.analytics.models.AnalyticsParam +import com.tangem.core.analytics.models.Basic.SignedIn +import com.tangem.core.analytics.models.Basic.SignedIn.SignInType +import com.tangem.core.decompose.di.GlobalUiMessageSender +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.core.decompose.ui.UiMessageSender +import com.tangem.core.navigation.url.UrlOpener +import com.tangem.core.ui.R +import com.tangem.core.ui.extensions.resourceReference +import com.tangem.core.ui.message.DialogMessage +import com.tangem.domain.card.ScanCardProcessor +import com.tangem.domain.card.analytics.ParamCardCurrencyConverter +import com.tangem.domain.card.common.util.cardTypesResolver +import com.tangem.domain.card.repository.CardSdkConfigRepository +import com.tangem.domain.common.wallets.UserWalletsListRepository +import com.tangem.domain.common.wallets.error.SaveWalletError +import com.tangem.domain.models.scan.ScanResponse +import com.tangem.domain.settings.repositories.SettingsRepository +import com.tangem.domain.wallets.builder.ColdUserWalletBuilder +import com.tangem.domain.wallets.usecase.GenerateBuyTangemCardLinkUseCase +import com.tangem.domain.wallets.usecase.SaveWalletUseCase +import com.tangem.features.createwalletstart.entity.CreateWalletStartUM +import com.tangem.utils.coroutines.CoroutineDispatcherProvider +import kotlinx.collections.immutable.persistentListOf +import kotlinx.coroutines.delay +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.update +import kotlinx.coroutines.launch +import timber.log.Timber +import javax.inject.Inject + +private const val HIDE_PROGRESS_DELAY = 400L + +@Suppress("LongParameterList") +@ModelScoped +internal class CreateWalletStartModel @Inject constructor( + paramsContainer: ParamsContainer, + override val dispatchers: CoroutineDispatcherProvider, + private val router: Router, + private val scanCardProcessor: ScanCardProcessor, + private val cardSdkConfigRepository: CardSdkConfigRepository, + private val settingsRepository: SettingsRepository, + private val analyticsEventHandler: AnalyticsEventHandler, + private val appRouter: AppRouter, + private val coldUserWalletBuilderFactory: ColdUserWalletBuilder.Factory, + private val saveWalletUseCase: SaveWalletUseCase, + private val userWalletsListRepository: UserWalletsListRepository, + @GlobalUiMessageSender private val uiMessageSender: UiMessageSender, + private val generateBuyTangemCardLinkUseCase: GenerateBuyTangemCardLinkUseCase, + private val urlOpener: UrlOpener, +) : Model() { + + private val params = paramsContainer.require() + + internal val uiState: StateFlow + field = MutableStateFlow( + when (params.mode) { + CreateWalletStartComponent.Mode.ColdWallet -> CreateWalletStartUM( + title = resourceReference(R.string.common_tangem_wallet), + description = resourceReference(R.string.welcome_create_wallet_hardware_description), + featureItems = persistentListOf( + CreateWalletStartUM.FeatureItem( + iconResId = R.drawable.ic_shield_check_16, + text = resourceReference(R.string.welcome_create_wallet_feature_class), + ), + CreateWalletStartUM.FeatureItem( + iconResId = R.drawable.ic_flash_16, + text = resourceReference(R.string.welcome_create_wallet_feature_delivery), + ), + CreateWalletStartUM.FeatureItem( + iconResId = R.drawable.ic_sparkles_16, + text = resourceReference(R.string.welcome_create_wallet_feature_use), + ), + ), + imageResId = R.drawable.img_hardware_wallet, + showScanSecondaryButton = true, + onPrimaryButtonClick = ::onBuyClick, + primaryButtonText = resourceReference(R.string.details_buy_wallet), + otherMethodTitle = resourceReference(R.string.welcome_create_wallet_mobile_title), + otherMethodDescription = resourceReference(R.string.welcome_create_wallet_mobile_description), + otherMethodClick = ::onStartWithMobileWalletClick, + onBackClick = { router.pop() }, + onScanClick = ::onScanClick, + isScanInProgress = false, + ) + CreateWalletStartComponent.Mode.HotWallet -> CreateWalletStartUM( + title = resourceReference(R.string.hw_mobile_wallet), + description = resourceReference(R.string.welcome_create_wallet_mobile_description_full), + featureItems = persistentListOf( + CreateWalletStartUM.FeatureItem( + iconResId = R.drawable.ic_shield_check_16, + text = resourceReference(R.string.welcome_create_wallet_feature_seamless), + ), + CreateWalletStartUM.FeatureItem( + iconResId = R.drawable.ic_flash_16, + text = resourceReference(R.string.welcome_create_wallet_feature_one_tap), + ), + CreateWalletStartUM.FeatureItem( + iconResId = R.drawable.ic_stack_fill_new_16, + text = resourceReference(R.string.welcome_create_wallet_feature_assets), + ), + ), + imageResId = R.drawable.img_mobile_wallet, + showScanSecondaryButton = false, + onPrimaryButtonClick = ::onStartWithMobileWalletClick, + primaryButtonText = resourceReference(R.string.welcome_create_wallet_mobile_title), + otherMethodTitle = resourceReference(R.string.welcome_create_wallet_use_hardware_title), + otherMethodDescription = resourceReference(R.string.welcome_create_wallet_use_hardware_description), + otherMethodClick = ::onBuyClick, + onBackClick = { router.pop() }, + onScanClick = ::onScanClick, + isScanInProgress = false, + ) + }, + ) + + private fun onScanClick() { + scanCard() + } + + private fun onStartWithMobileWalletClick() { + router.push(AppRoute.CreateMobileWallet) + } + + private fun onBuyClick() { + modelScope.launch { + generateBuyTangemCardLinkUseCase.invoke().let { urlOpener.openUrl(it) } + } + } + + private fun scanCard() { + modelScope.launch { + setLoading(true) + + val shouldSaveAccessCodes = settingsRepository.shouldSaveAccessCodes() + cardSdkConfigRepository.setAccessCodeRequestPolicy( + isBiometricsRequestPolicy = shouldSaveAccessCodes, + ) + + val analyticsSource = AnalyticsParam.ScreensSources.Intro + + scanCardProcessor.scan( + analyticsSource = analyticsSource, + onProgressStateChange = { showProgress -> + if (!showProgress) { + delay(HIDE_PROGRESS_DELAY) + setLoading(false) + } else { + setLoading(true) + } + }, + onFailure = { error -> + handleScanError(error) + delay(HIDE_PROGRESS_DELAY) + setLoading(false) + }, + onSuccess = { scanResponse -> + proceedWithScanResponse(scanResponse) + }, + ) + } + } + + private suspend fun proceedWithScanResponse(scanResponse: ScanResponse) { + val userWallet = coldUserWalletBuilderFactory.create(scanResponse = scanResponse).build() + + if (userWallet == null) { + Timber.e("User wallet not created") + setLoading(false) + return + } + + saveWalletUseCase(userWallet = userWallet).fold( + ifLeft = { + delay(HIDE_PROGRESS_DELAY) + setLoading(false) + when (it) { + is SaveWalletError.DataError -> Timber.e(it.toString(), "Unable to save user wallet") + is SaveWalletError.WalletAlreadySaved -> { + userWalletsListRepository.unlock( + userWalletId = userWallet.walletId, + unlockMethod = UserWalletsListRepository.UnlockMethod.Scan(scanResponse), + ).onRight { + appRouter.replaceAll(AppRoute.Wallet) + } + } + } + }, + ifRight = { + setLoading(false) + sendSignedInCardAnalyticsEvent(scanResponse) + appRouter.replaceAll(AppRoute.Wallet) + }, + ) + } + + private suspend fun sendSignedInCardAnalyticsEvent(scanResponse: ScanResponse) { + val currency = ParamCardCurrencyConverter().convert(value = scanResponse.cardTypesResolver) + if (currency != null) { + analyticsEventHandler.send( + SignedIn( + currency = currency, + batch = scanResponse.card.batchId, + signInType = SignInType.Card, + walletsCount = userWalletsListRepository.userWalletsSync().size.toString(), + hasBackup = scanResponse.card.backupStatus?.isActive, + ), + ) + } + } + + private fun setLoading(isLoading: Boolean) { + uiState.update { it.copy(isScanInProgress = isLoading) } + } + + private fun handleScanError(error: TangemError) { + when (error) { + is TangemSdkError.NfcFeatureIsUnavailable -> handleNfcFeatureUnavailable() + is TangemSdkError -> Timber.e(error, "Scan error occurred") + else -> Timber.e(error, "Error happened") + } + } + + private fun handleNfcFeatureUnavailable() { + uiMessageSender.send( + message = DialogMessage( + message = resourceReference(R.string.nfc_error_unavailable), + title = resourceReference(id = R.string.common_error), + ), + ) + } +} \ No newline at end of file diff --git a/features/create-wallet-start/impl/src/main/kotlin/com/tangem/features/createwalletstart/DefaultCreateWalletStartComponent.kt b/features/create-wallet-start/impl/src/main/kotlin/com/tangem/features/createwalletstart/DefaultCreateWalletStartComponent.kt new file mode 100644 index 0000000000..598669a656 --- /dev/null +++ b/features/create-wallet-start/impl/src/main/kotlin/com/tangem/features/createwalletstart/DefaultCreateWalletStartComponent.kt @@ -0,0 +1,42 @@ +package com.tangem.features.createwalletstart + +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.core.ui.components.SystemBarsIconsDisposable +import com.tangem.core.ui.res.ForceDarkTheme +import com.tangem.features.createwalletstart.ui.CreateWalletStartContent +import dagger.assisted.Assisted +import dagger.assisted.AssistedFactory +import dagger.assisted.AssistedInject + +internal class DefaultCreateWalletStartComponent @AssistedInject constructor( + @Assisted private val context: AppComponentContext, + @Assisted private val params: CreateWalletStartComponent.Params, +) : CreateWalletStartComponent, AppComponentContext by context { + + private val model: CreateWalletStartModel = getOrCreateModel(params) + + @Composable + override fun Content(modifier: Modifier) { + val state by model.uiState.collectAsStateWithLifecycle() + SystemBarsIconsDisposable(darkIcons = false) + ForceDarkTheme { + CreateWalletStartContent( + state = state, + modifier = modifier, + ) + } + } + + @AssistedFactory + interface Factory : CreateWalletStartComponent.Factory { + override fun create( + context: AppComponentContext, + params: CreateWalletStartComponent.Params, + ): DefaultCreateWalletStartComponent + } +} \ No newline at end of file diff --git a/features/create-wallet-start/impl/src/main/kotlin/com/tangem/features/createwalletstart/di/CreateWalletStartModule.kt b/features/create-wallet-start/impl/src/main/kotlin/com/tangem/features/createwalletstart/di/CreateWalletStartModule.kt new file mode 100644 index 0000000000..c534d77b9a --- /dev/null +++ b/features/create-wallet-start/impl/src/main/kotlin/com/tangem/features/createwalletstart/di/CreateWalletStartModule.kt @@ -0,0 +1,33 @@ +package com.tangem.features.createwalletstart.di + +import com.tangem.core.decompose.model.Model +import com.tangem.features.createwalletstart.CreateWalletStartComponent +import com.tangem.features.createwalletstart.CreateWalletStartModel +import com.tangem.features.createwalletstart.DefaultCreateWalletStartComponent +import dagger.Binds +import dagger.Module +import dagger.hilt.InstallIn +import dagger.hilt.components.SingletonComponent +import dagger.multibindings.ClassKey +import dagger.multibindings.IntoMap +import javax.inject.Singleton + +@Module +@InstallIn(SingletonComponent::class) +internal object CreateWalletStartModule + +@Module +@InstallIn(SingletonComponent::class) +internal interface CreateWalletStartModuleBinds { + + @Binds + @Singleton + fun bindCreateWalletStartComponentFactory( + impl: DefaultCreateWalletStartComponent.Factory, + ): CreateWalletStartComponent.Factory + + @Binds + @IntoMap + @ClassKey(CreateWalletStartModel::class) + fun bindCreateWalletStartModel(model: CreateWalletStartModel): Model +} \ No newline at end of file diff --git a/features/create-wallet-start/impl/src/main/kotlin/com/tangem/features/createwalletstart/entity/CreateWalletStartUM.kt b/features/create-wallet-start/impl/src/main/kotlin/com/tangem/features/createwalletstart/entity/CreateWalletStartUM.kt new file mode 100644 index 0000000000..58f58d2bc0 --- /dev/null +++ b/features/create-wallet-start/impl/src/main/kotlin/com/tangem/features/createwalletstart/entity/CreateWalletStartUM.kt @@ -0,0 +1,25 @@ +package com.tangem.features.createwalletstart.entity + +import com.tangem.core.ui.extensions.TextReference +import kotlinx.collections.immutable.ImmutableList + +internal data class CreateWalletStartUM( + val title: TextReference, + val description: TextReference, + val featureItems: ImmutableList, + val imageResId: Int, + val isScanInProgress: Boolean, + val showScanSecondaryButton: Boolean, + val primaryButtonText: TextReference, + val onPrimaryButtonClick: () -> Unit, + val otherMethodDescription: TextReference, + val otherMethodTitle: TextReference, + val otherMethodClick: () -> Unit, + val onScanClick: () -> Unit, + val onBackClick: () -> Unit, +) { + data class FeatureItem( + val iconResId: Int, + val text: TextReference, + ) +} \ No newline at end of file diff --git a/features/create-wallet-start/impl/src/main/kotlin/com/tangem/features/createwalletstart/ui/CreateWalletStartContent.kt b/features/create-wallet-start/impl/src/main/kotlin/com/tangem/features/createwalletstart/ui/CreateWalletStartContent.kt new file mode 100644 index 0000000000..9c6e3fedf3 --- /dev/null +++ b/features/create-wallet-start/impl/src/main/kotlin/com/tangem/features/createwalletstart/ui/CreateWalletStartContent.kt @@ -0,0 +1,486 @@ +package com.tangem.features.createwalletstart.ui + +import android.annotation.SuppressLint +import android.content.res.Configuration +import androidx.annotation.DrawableRes +import androidx.compose.foundation.* +import androidx.compose.foundation.interaction.MutableInteractionSource +import androidx.compose.foundation.layout.* +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.material3.* +import androidx.compose.runtime.Composable +import androidx.compose.runtime.remember +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.scale +import androidx.compose.ui.geometry.Offset +import androidx.compose.ui.graphics.Brush +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.PathEffect +import androidx.compose.ui.graphics.StrokeCap +import androidx.compose.ui.layout.ContentScale +import androidx.compose.ui.layout.Layout +import androidx.compose.ui.platform.LocalDensity +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.text.style.TextAlign +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 androidx.compose.ui.unit.dp +import com.tangem.core.ui.components.PrimaryButton +import com.tangem.core.ui.components.SecondaryButtonIconEnd +import com.tangem.core.ui.components.bottomFade +import com.tangem.core.ui.extensions.TextReference +import com.tangem.core.ui.extensions.resolveReference +import com.tangem.core.ui.extensions.resourceReference +import com.tangem.core.ui.extensions.stringResourceSafe +import com.tangem.core.ui.res.TangemColorPalette +import com.tangem.core.ui.res.TangemTheme +import com.tangem.core.ui.res.TangemThemePreview +import com.tangem.features.createwalletstart.entity.CreateWalletStartUM +import com.tangem.features.createwalletstart.impl.R +import kotlinx.collections.immutable.persistentListOf +import kotlin.math.max + +@Suppress("LongMethod", "MagicNumber") +@OptIn(ExperimentalMaterial3Api::class, ExperimentalLayoutApi::class) +@Composable +internal fun CreateWalletStartContent(state: CreateWalletStartUM, modifier: Modifier = Modifier) { + Column( + modifier = modifier + .background( + brush = Brush.verticalGradient( + listOf( + TangemColorPalette.Dark6, + TangemColorPalette.Black, + ), + ), + ) + .fillMaxSize() + .systemBarsPadding(), + horizontalAlignment = Alignment.CenterHorizontally, + ) { + TopAppBar( + colors = TopAppBarDefaults.topAppBarColors( + containerColor = Color.Transparent, + ), + navigationIcon = { + IconButton(onClick = state.onBackClick) { + Icon( + painter = painterResource(R.drawable.ic_back_24), + tint = TangemTheme.colors.icon.primary1, + contentDescription = null, + ) + } + }, + title = { }, + ) + Box( + modifier = Modifier + .weight(1f) + .bottomFade(height = 24.dp), + ) { + AdaptiveScrollableContent( + topContent = { + Text( + modifier = Modifier + .fillMaxWidth() + .padding( + start = 32.dp, + top = 16.dp, + end = 32.dp, + ), + text = state.title.resolveReference(), + style = TangemTheme.typography.h2, + color = TangemTheme.colors.text.primary1, + textAlign = TextAlign.Center, + ) + Text( + modifier = Modifier + .fillMaxWidth() + .padding( + start = 32.dp, + top = 8.dp, + end = 32.dp, + ), + text = state.description.resolveReference(), + style = TangemTheme.typography.body2, + color = TangemTheme.colors.text.primary1, + textAlign = TextAlign.Center, + ) + FlowRow( + modifier = Modifier + .fillMaxWidth() + .padding( + start = 24.dp, + top = 16.dp, + end = 24.dp, + ), + horizontalArrangement = Arrangement.Center, + verticalArrangement = Arrangement.spacedBy(8.dp), + ) { + state.featureItems.forEach { + FeatureItem( + iconResId = it.iconResId, + text = it.text, + ) + } + } + }, + imageContent = { + Image( + modifier = Modifier + .fillMaxWidth() + .wrapContentHeight() + .padding( + vertical = 12.dp, + horizontal = 16.dp, + ), + painter = painterResource(id = state.imageResId), + contentDescription = null, + contentScale = ContentScale.Fit, + ) + }, + bottomContent = { + if (state.showScanSecondaryButton) { + SecondaryButtonIconEnd( + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = 16.dp), + text = stringResourceSafe(R.string.welcome_unlock_card), + onClick = state.onScanClick, + showProgress = state.isScanInProgress, + iconResId = R.drawable.ic_tangem_24, + ) + } + PrimaryButton( + modifier = Modifier + .fillMaxWidth() + .padding( + start = 16.dp, + top = 8.dp, + end = 16.dp, + ), + text = state.primaryButtonText.resolveReference(), + onClick = state.onPrimaryButtonClick, + ) + Row( + modifier = Modifier + .padding( + start = 16.dp, + top = 24.dp, + end = 16.dp, + ), + horizontalArrangement = Arrangement.spacedBy(16.dp), + verticalAlignment = Alignment.CenterVertically, + ) { + DashedGradientLine( + modifier = Modifier + .weight(1f) + .height(16.dp), + ) + Text( + text = stringResourceSafe(R.string.welcome_create_wallet_other_method), + style = TangemTheme.typography.caption1, + color = TangemTheme.colors.text.secondary, + textAlign = TextAlign.Center, + ) + DashedGradientLine( + modifier = Modifier + .weight(1f) + .height(16.dp) + .scale(scaleX = -1f, scaleY = 1f), + ) + } + Text( + modifier = Modifier + .fillMaxWidth() + .padding( + start = 16.dp, + top = 16.dp, + end = 16.dp, + ), + text = state.otherMethodDescription.resolveReference(), + style = TangemTheme.typography.subtitle2, + color = TangemTheme.colors.text.tertiary, + textAlign = TextAlign.Center, + ) + Row( + modifier = Modifier + .wrapContentWidth() + .clickable { state.otherMethodClick() } + .padding( + horizontal = 16.dp, + vertical = 12.dp, + ), + horizontalArrangement = Arrangement.Center, + ) { + Text( + text = state.otherMethodTitle.resolveReference(), + style = TangemTheme.typography.subtitle1, + color = TangemTheme.colors.text.primary1, + textAlign = TextAlign.Center, + ) + Icon( + painter = painterResource(id = R.drawable.ic_chevron_right_18x24), + tint = TangemTheme.colors.icon.primary1, + contentDescription = null, + ) + } + }, + minImageHeight = 160.dp, + ) + } + if (!state.showScanSecondaryButton) { + FlowRow( + modifier = Modifier + .wrapContentWidth() + .padding( + start = 16.dp, + top = 24.dp, + end = 16.dp, + bottom = 8.dp, + ), + horizontalArrangement = Arrangement.Center, + ) { + Text( + text = stringResourceSafe(R.string.welcome_create_wallet_already_have), + style = TangemTheme.typography.caption1, + color = TangemTheme.colors.text.secondary, + ) + Spacer(modifier = Modifier.size(4.dp)) + Row( + modifier = Modifier + .clickable( + interactionSource = remember { MutableInteractionSource() }, + indication = null, + ) { state.onScanClick() }, + verticalAlignment = Alignment.CenterVertically, + ) { + Text( + text = stringResourceSafe(R.string.wallet_create_scan_title), + style = TangemTheme.typography.caption1, + color = TangemTheme.colors.text.primary1, + ) + Spacer(modifier = Modifier.size(2.dp)) + Icon( + modifier = Modifier.size(16.dp), + painter = painterResource(id = R.drawable.ic_tangem_24), + tint = TangemTheme.colors.icon.primary1, + contentDescription = null, + ) + } + } + } + Spacer(modifier = Modifier.size(16.dp)) + } +} + +@SuppressLint("UnusedBoxWithConstraintsScope") +@Composable +private fun AdaptiveScrollableContent( + minImageHeight: Dp, + modifier: Modifier = Modifier, + topContent: @Composable () -> Unit, + imageContent: @Composable () -> Unit, + bottomContent: @Composable () -> Unit, +) { + BoxWithConstraints( + modifier = modifier.fillMaxSize(), + ) { + val density = LocalDensity.current + val viewportHeight = maxHeight + val minImageHeightPx = with(density) { minImageHeight.roundToPx() } + val viewportHeightPx = with(density) { viewportHeight.roundToPx() } + Layout( + modifier = Modifier + .fillMaxWidth() + .verticalScroll(rememberScrollState()), + content = { + Column( + modifier = Modifier.fillMaxWidth(), + horizontalAlignment = Alignment.CenterHorizontally, + ) { + topContent() + } + Box( + modifier = Modifier.fillMaxWidth(), + contentAlignment = Alignment.Center, + ) { + imageContent() + } + Column( + modifier = Modifier.fillMaxWidth(), + horizontalAlignment = Alignment.CenterHorizontally, + ) { + bottomContent() + } + }, + ) { measurables, constraints -> + val topPlaceable = measurables[0].measure( + constraints.copy(minHeight = 0, maxHeight = androidx.compose.ui.unit.Constraints.Infinity), + ) + val bottomPlaceable = measurables[2].measure( + constraints.copy(minHeight = 0, maxHeight = androidx.compose.ui.unit.Constraints.Infinity), + ) + val imageIntrinsicHeight = measurables[1].maxIntrinsicHeight(constraints.maxWidth) + val availableHeightForImage = max(0, viewportHeightPx - topPlaceable.height - bottomPlaceable.height) + val targetImageHeight = when { + imageIntrinsicHeight < minImageHeightPx -> minImageHeightPx + imageIntrinsicHeight > availableHeightForImage -> max(minImageHeightPx, availableHeightForImage) + else -> imageIntrinsicHeight + } + val imagePlaceable = measurables[1].measure( + constraints.copy( + minHeight = targetImageHeight, + maxHeight = targetImageHeight, + ), + ) + val totalContentHeight = topPlaceable.height + imagePlaceable.height + bottomPlaceable.height + layout(constraints.maxWidth, totalContentHeight) { + var yOffset = 0 + topPlaceable.placeRelative(0, yOffset) + yOffset += topPlaceable.height + imagePlaceable.placeRelative(0, yOffset) + yOffset += imagePlaceable.height + bottomPlaceable.placeRelative(0, yOffset) + } + } + } +} + +@Composable +private fun DashedGradientLine(modifier: Modifier = Modifier) { + val density = LocalDensity.current + + val strokeColor = TangemTheme.colors.stroke.primary + + Canvas(modifier = modifier) { + val strokePx = with(density) { 4.dp.toPx() } + val dashPx = with(density) { 4.dp.toPx() } + val gapPx = with(density) { 8.dp.toPx() } + + val width = size.width + val centerY = size.height / 2 + + val brush = Brush.linearGradient( + colors = listOf(strokeColor.copy(alpha = 0f), strokeColor), + start = Offset(0f, 0f), + end = Offset(width, 0f), + ) + + val pathEffect = PathEffect.dashPathEffect(floatArrayOf(dashPx, gapPx), 0f) + + drawLine( + brush = brush, + start = Offset(0f, centerY), + end = Offset(width, centerY), + strokeWidth = strokePx, + pathEffect = pathEffect, + cap = StrokeCap.Round, + ) + } +} + +@Composable +private fun FeatureItem(@DrawableRes iconResId: Int, text: TextReference) { + Row( + modifier = Modifier + .wrapContentWidth() + .padding(horizontal = 8.dp), + horizontalArrangement = Arrangement.spacedBy(6.dp), + ) { + Icon( + modifier = Modifier.size(16.dp), + painter = painterResource(iconResId), + tint = TangemTheme.colors.icon.accent, + contentDescription = null, + ) + Text( + text = text.resolveReference(), + style = TangemTheme.typography.caption1, + color = TangemTheme.colors.text.secondary, + ) + } +} + +private class CreateWalletStartStateProvider : CollectionPreviewParameterProvider( + collection = listOf( + CreateWalletStartUM( + title = resourceReference(R.string.common_tangem_wallet), + description = resourceReference(R.string.welcome_create_wallet_hardware_description), + featureItems = persistentListOf( + CreateWalletStartUM.FeatureItem( + iconResId = R.drawable.ic_shield_check_16, + text = resourceReference(R.string.welcome_create_wallet_feature_class), + ), + CreateWalletStartUM.FeatureItem( + iconResId = R.drawable.ic_flash_16, + text = resourceReference(R.string.welcome_create_wallet_feature_delivery), + ), + CreateWalletStartUM.FeatureItem( + iconResId = R.drawable.ic_sparkles_16, + text = resourceReference(R.string.welcome_create_wallet_feature_use), + ), + ), + imageResId = R.drawable.img_hardware_wallet, + showScanSecondaryButton = true, + onPrimaryButtonClick = { }, + primaryButtonText = resourceReference(R.string.details_buy_wallet), + otherMethodTitle = resourceReference(R.string.welcome_create_wallet_mobile_title), + otherMethodDescription = resourceReference( + R.string.welcome_create_wallet_mobile_description, + ), + otherMethodClick = { }, + onBackClick = { }, + onScanClick = { }, + isScanInProgress = false, + ), + CreateWalletStartUM( + title = resourceReference(R.string.hw_mobile_wallet), + description = resourceReference(R.string.welcome_create_wallet_mobile_description_full), + featureItems = persistentListOf( + CreateWalletStartUM.FeatureItem( + iconResId = R.drawable.ic_shield_check_16, + text = resourceReference(R.string.welcome_create_wallet_feature_seamless), + ), + CreateWalletStartUM.FeatureItem( + iconResId = R.drawable.ic_flash_16, + text = resourceReference(R.string.welcome_create_wallet_feature_one_tap), + ), + CreateWalletStartUM.FeatureItem( + iconResId = R.drawable.ic_stack_fill_new_16, + text = resourceReference(R.string.welcome_create_wallet_feature_assets), + ), + ), + imageResId = R.drawable.img_mobile_wallet, + showScanSecondaryButton = false, + onPrimaryButtonClick = { }, + primaryButtonText = resourceReference(R.string.welcome_create_wallet_mobile_title), + otherMethodTitle = resourceReference(R.string.welcome_create_wallet_use_hardware_title), + otherMethodDescription = resourceReference( + R.string.welcome_create_wallet_use_hardware_description, + ), + otherMethodClick = { }, + onBackClick = { }, + onScanClick = { }, + isScanInProgress = false, + ), + ), +) + +@Preview(showBackground = true, widthDp = 360, heightDp = 480, uiMode = Configuration.UI_MODE_NIGHT_YES) +@Preview(showBackground = true, widthDp = 360, heightDp = 560, uiMode = Configuration.UI_MODE_NIGHT_YES) +@Preview(showBackground = true, widthDp = 360, heightDp = 720, uiMode = Configuration.UI_MODE_NIGHT_YES) +@Preview(showBackground = true, widthDp = 360, heightDp = 840, uiMode = Configuration.UI_MODE_NIGHT_YES) +@Composable +private fun PreviewCreateWalletStartContent( + @PreviewParameter(CreateWalletStartStateProvider::class) param: CreateWalletStartUM, +) { + TangemThemePreview { + CreateWalletStartContent( + state = param, + ) + } +} \ No newline at end of file diff --git a/settings.gradle.kts b/settings.gradle.kts index ac0557883f..d69405d8eb 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -279,6 +279,9 @@ include(":features:tangempay:onboarding:impl") include(":features:create-wallet-selection:api") include(":features:create-wallet-selection:impl") +include(":features:create-wallet-start:api") +include(":features:create-wallet-start:impl") + include(":features:welcome:api") include(":features:welcome:impl")