diff --git a/settings.gradle b/settings.gradle index 64453d31..ff6a5d65 100644 --- a/settings.gradle +++ b/settings.gradle @@ -72,3 +72,4 @@ include ':v4:integration:web3' include ':v4:integration:analytics' include ':v4:integration:statsig' include ':v4:integration:fcm' +include ':v4:feature:vault' diff --git a/v4/app/src/main/assets/features.json b/v4/app/src/main/assets/features.json index ada153c3..dff4c9b4 100644 --- a/v4/app/src/main/assets/features.json +++ b/v4/app/src/main/assets/features.json @@ -52,6 +52,26 @@ } ] } + }, + { + "title":{ + "text":"Vault" + }, + "field":{ + "field":"vault_enabled", + "optional":true, + "type" : "text", + "options" : [ + { + "text": "yes", + "value" : "1" + }, + { + "text": "no", + "value" : "0" + } + ] + } } ] } diff --git a/v4/common/src/main/java/exchange/dydx/trading/common/featureflags/DydxFeatureFlags.kt b/v4/common/src/main/java/exchange/dydx/trading/common/featureflags/DydxFeatureFlags.kt index 22b3c51d..71d1d307 100644 --- a/v4/common/src/main/java/exchange/dydx/trading/common/featureflags/DydxFeatureFlags.kt +++ b/v4/common/src/main/java/exchange/dydx/trading/common/featureflags/DydxFeatureFlags.kt @@ -7,6 +7,7 @@ enum class DydxFeatureFlag { deployment_url, force_mainnet, abacus_static_typing, + vault_enabled, } class DydxFeatureFlags @Inject constructor( diff --git a/v4/common/src/main/java/exchange/dydx/trading/common/navigation/DydxRoutes.kt b/v4/common/src/main/java/exchange/dydx/trading/common/navigation/DydxRoutes.kt index 9b9e3149..e500a9ab 100644 --- a/v4/common/src/main/java/exchange/dydx/trading/common/navigation/DydxRoutes.kt +++ b/v4/common/src/main/java/exchange/dydx/trading/common/navigation/DydxRoutes.kt @@ -41,6 +41,7 @@ object ProfileRoutes { const val debug_enable = "action/debug/enable" const val report_issue = "settings/report_issue" const val notifications = "settings/notifications" + const val alerts = "settings/alerts" } object NewsAlertsRoutes { @@ -67,3 +68,7 @@ object TransferRoutes { const val transfer_search = "transfer/search" const val transfer_status = "transfer/status" } + +object VaultRoutes { + const val main = "vault" +} diff --git a/v4/core/build.gradle b/v4/core/build.gradle index 24715615..fb995d71 100644 --- a/v4/core/build.gradle +++ b/v4/core/build.gradle @@ -58,6 +58,7 @@ dependencies { implementation project(':v4:feature:newsalerts') implementation project(':v4:feature:workers') implementation project(':v4:feature:transfer') + implementation project(':v4:feature:vault') /* Local Dependencies */ implementation "androidx.core:core:$androidKtxVersion" diff --git a/v4/core/src/main/java/exchange/dydx/trading/core/DydxNavGraph.kt b/v4/core/src/main/java/exchange/dydx/trading/core/DydxNavGraph.kt index 805f2908..ca743a58 100644 --- a/v4/core/src/main/java/exchange/dydx/trading/core/DydxNavGraph.kt +++ b/v4/core/src/main/java/exchange/dydx/trading/core/DydxNavGraph.kt @@ -17,6 +17,7 @@ import exchange.dydx.trading.feature.profile.profileGraph import exchange.dydx.trading.feature.trade.tradeGraph import exchange.dydx.trading.feature.transfer.transferGraph import exchange.dydx.utilities.utils.Logging +import exchange.dydx.vault.vaultGraph private const val TAG = "DydxNavGraph" @@ -81,6 +82,11 @@ fun DydxNavGraph( appRouter = appRouter, logger = logger, ) + + vaultGraph( + appRouter = appRouter, + logger = logger, + ) } } diff --git a/v4/feature/newsalerts/src/main/java/exchange/dydx/trading/feature/newsalerts/alerts/DydxAlertsViewModel.kt b/v4/feature/newsalerts/src/main/java/exchange/dydx/trading/feature/newsalerts/alerts/DydxAlertsViewModel.kt index 00409363..39bed8a1 100644 --- a/v4/feature/newsalerts/src/main/java/exchange/dydx/trading/feature/newsalerts/alerts/DydxAlertsViewModel.kt +++ b/v4/feature/newsalerts/src/main/java/exchange/dydx/trading/feature/newsalerts/alerts/DydxAlertsViewModel.kt @@ -3,9 +3,7 @@ package exchange.dydx.trading.feature.newsalerts.alerts import androidx.lifecycle.ViewModel import dagger.hilt.android.lifecycle.HiltViewModel import exchange.dydx.abacus.protocols.LocalizerProtocol -import exchange.dydx.dydxstatemanager.AbacusStateManagerProtocol import exchange.dydx.trading.common.DydxViewModel -import exchange.dydx.trading.common.formatter.DydxFormatter import exchange.dydx.trading.feature.newsalerts.alerts.alertprovider.DydxAlertsProvider import exchange.dydx.trading.feature.newsalerts.alerts.alertprovider.DydxAlertsProviderItemProtocol import kotlinx.coroutines.flow.Flow @@ -16,8 +14,6 @@ import javax.inject.Inject @HiltViewModel class DydxAlertsViewModel @Inject constructor( private val localizer: LocalizerProtocol, - private val abacusStateManager: AbacusStateManagerProtocol, - private val formatter: DydxFormatter, private val alertsProvider: DydxAlertsProvider, ) : ViewModel(), DydxViewModel { diff --git a/v4/feature/profile/build.gradle b/v4/feature/profile/build.gradle index 41a2710c..ea53095c 100644 --- a/v4/feature/profile/build.gradle +++ b/v4/feature/profile/build.gradle @@ -49,6 +49,7 @@ dependencies { implementation project(path: ':v4:platformUI') implementation project(path: ':v4:feature:shared') implementation project(path: ':v4:feature:portfolio') + implementation project(path: ':v4:feature:newsalerts') /* Local Dependencies */ diff --git a/v4/feature/profile/src/main/java/exchange/dydx/trading/feature/profile/DydxProfileRouter.kt b/v4/feature/profile/src/main/java/exchange/dydx/trading/feature/profile/DydxProfileRouter.kt index 3743e61d..70d39651 100644 --- a/v4/feature/profile/src/main/java/exchange/dydx/trading/feature/profile/DydxProfileRouter.kt +++ b/v4/feature/profile/src/main/java/exchange/dydx/trading/feature/profile/DydxProfileRouter.kt @@ -7,6 +7,7 @@ import exchange.dydx.trading.common.navigation.ProfileRoutes import exchange.dydx.trading.common.navigation.ProfileRoutes.debug_enable import exchange.dydx.trading.common.navigation.dydxComposable import exchange.dydx.trading.feature.profile.actions.debugenabled.DydxDebugEnableView +import exchange.dydx.trading.feature.profile.alerts.DydxAlertsContainerView import exchange.dydx.trading.feature.profile.color.DydxDirectionColorPreferenceView import exchange.dydx.trading.feature.profile.debug.DydxDebugView import exchange.dydx.trading.feature.profile.featureflags.DydxFeatureFlagsView @@ -190,4 +191,12 @@ fun NavGraphBuilder.profileGraph( ) { navBackStackEntry -> DydxGasTokenView.Content(Modifier) } + + dydxComposable( + router = appRouter, + route = ProfileRoutes.alerts, + deepLinks = appRouter.deeplinks(ProfileRoutes.alerts), + ) { navBackStackEntry -> + DydxAlertsContainerView.Content(Modifier) + } } diff --git a/v4/feature/profile/src/main/java/exchange/dydx/trading/feature/profile/DydxProfileView.kt b/v4/feature/profile/src/main/java/exchange/dydx/trading/feature/profile/DydxProfileView.kt index 77c90949..90bd6f5b 100644 --- a/v4/feature/profile/src/main/java/exchange/dydx/trading/feature/profile/DydxProfileView.kt +++ b/v4/feature/profile/src/main/java/exchange/dydx/trading/feature/profile/DydxProfileView.kt @@ -24,6 +24,7 @@ import exchange.dydx.platformui.designSystem.theme.themeColor import exchange.dydx.platformui.theme.DydxThemedPreviewSurface import exchange.dydx.platformui.theme.MockLocalizer import exchange.dydx.trading.common.component.DydxComponent +import exchange.dydx.trading.feature.profile.components.DydxProfileAlertsView import exchange.dydx.trading.feature.profile.components.DydxProfileBalancesView import exchange.dydx.trading.feature.profile.components.DydxProfileButtonsView import exchange.dydx.trading.feature.profile.components.DydxProfileFeesView @@ -43,10 +44,12 @@ fun Preview_DydxProfileView() { object DydxProfileView : DydxComponent { data class ViewState( val localizer: LocalizerProtocol, + val hasAlerts: Boolean, ) { companion object { val preview = ViewState( localizer = MockLocalizer(), + hasAlerts = true, ) } } @@ -81,6 +84,11 @@ object DydxProfileView : DydxComponent { item(key = "buttons") { DydxProfileButtonsView.Content(Modifier.padding(horizontal = ThemeShapes.HorizontalPadding)) } + if (state?.hasAlerts == true) { + item(key = "vault") { + DydxProfileAlertsView.Content(Modifier.padding(horizontal = ThemeShapes.HorizontalPadding)) + } + } item(key = "balances") { DydxProfileBalancesView.Content(Modifier.padding(horizontal = ThemeShapes.HorizontalPadding)) } diff --git a/v4/feature/profile/src/main/java/exchange/dydx/trading/feature/profile/DydxProfileViewModel.kt b/v4/feature/profile/src/main/java/exchange/dydx/trading/feature/profile/DydxProfileViewModel.kt index efe979fd..8615f8b0 100644 --- a/v4/feature/profile/src/main/java/exchange/dydx/trading/feature/profile/DydxProfileViewModel.kt +++ b/v4/feature/profile/src/main/java/exchange/dydx/trading/feature/profile/DydxProfileViewModel.kt @@ -4,6 +4,8 @@ import androidx.lifecycle.ViewModel import dagger.hilt.android.lifecycle.HiltViewModel import exchange.dydx.abacus.protocols.LocalizerProtocol import exchange.dydx.trading.common.DydxViewModel +import exchange.dydx.trading.common.featureflags.DydxFeatureFlag +import exchange.dydx.trading.common.featureflags.DydxFeatureFlags import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.flowOf import javax.inject.Inject @@ -11,6 +13,7 @@ import javax.inject.Inject @HiltViewModel class DydxProfileViewModel @Inject constructor( val localizer: LocalizerProtocol, + val featureFlags: DydxFeatureFlags, ) : ViewModel(), DydxViewModel { val state: Flow = flowOf(createViewState()) @@ -18,6 +21,7 @@ class DydxProfileViewModel @Inject constructor( private fun createViewState(): DydxProfileView.ViewState { return DydxProfileView.ViewState( localizer = localizer, + hasAlerts = featureFlags.isFeatureEnabled(DydxFeatureFlag.vault_enabled, default = false), ) } } diff --git a/v4/feature/profile/src/main/java/exchange/dydx/trading/feature/profile/alerts/DydxAlertsContainerView.kt b/v4/feature/profile/src/main/java/exchange/dydx/trading/feature/profile/alerts/DydxAlertsContainerView.kt new file mode 100644 index 00000000..06a44a30 --- /dev/null +++ b/v4/feature/profile/src/main/java/exchange/dydx/trading/feature/profile/alerts/DydxAlertsContainerView.kt @@ -0,0 +1,68 @@ +package exchange.dydx.trading.feature.profile.alerts + +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.tooling.preview.Preview +import androidx.hilt.navigation.compose.hiltViewModel +import exchange.dydx.abacus.protocols.LocalizerProtocol +import exchange.dydx.platformui.compose.collectAsStateWithLifecycle +import exchange.dydx.platformui.designSystem.theme.ThemeColor +import exchange.dydx.platformui.designSystem.theme.themeColor +import exchange.dydx.platformui.theme.DydxThemedPreviewSurface +import exchange.dydx.platformui.theme.MockLocalizer +import exchange.dydx.trading.common.component.DydxComponent +import exchange.dydx.trading.feature.newsalerts.alerts.DydxAlertsView +import exchange.dydx.trading.feature.shared.views.HeaderView + +@Preview +@Composable +fun Preview_DydxAlertsContainerView() { + DydxThemedPreviewSurface { + DydxAlertsContainerView.Content(Modifier, DydxAlertsContainerView.ViewState.preview) + } +} + +object DydxAlertsContainerView : DydxComponent { + data class ViewState( + val localizer: LocalizerProtocol, + val backButtionAction: () -> Unit = {}, + ) { + companion object { + val preview = ViewState( + localizer = MockLocalizer(), + ) + } + } + + @Composable + override fun Content(modifier: Modifier) { + val viewModel: DydxAlertsContainerViewModel = hiltViewModel() + + val state = viewModel.state.collectAsStateWithLifecycle(initialValue = null).value + Content(modifier, state) + } + + @Composable + fun Content(modifier: Modifier, state: ViewState?) { + if (state == null) { + return + } + + Column( + modifier = modifier + .fillMaxSize() + .themeColor(ThemeColor.SemanticColor.layer_2), + ) { + HeaderView( + title = state.localizer.localize("APP.GENERAL.ALERTS"), + modifier = Modifier.fillMaxWidth(), + backAction = state.backButtionAction, + ) + + DydxAlertsView.Content(modifier = Modifier) + } + } +} diff --git a/v4/feature/profile/src/main/java/exchange/dydx/trading/feature/profile/alerts/DydxAlertsContainerViewModel.kt b/v4/feature/profile/src/main/java/exchange/dydx/trading/feature/profile/alerts/DydxAlertsContainerViewModel.kt new file mode 100644 index 00000000..5cb62263 --- /dev/null +++ b/v4/feature/profile/src/main/java/exchange/dydx/trading/feature/profile/alerts/DydxAlertsContainerViewModel.kt @@ -0,0 +1,28 @@ +package exchange.dydx.trading.feature.profile.alerts + +import androidx.lifecycle.ViewModel +import dagger.hilt.android.lifecycle.HiltViewModel +import exchange.dydx.abacus.protocols.LocalizerProtocol +import exchange.dydx.trading.common.DydxViewModel +import exchange.dydx.trading.common.navigation.DydxRouter +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.flowOf +import javax.inject.Inject + +@HiltViewModel +class DydxAlertsContainerViewModel @Inject constructor( + private val localizer: LocalizerProtocol, + private val router: DydxRouter, +) : ViewModel(), DydxViewModel { + + val state: Flow = flowOf(createViewState()) + + private fun createViewState(): DydxAlertsContainerView.ViewState { + return DydxAlertsContainerView.ViewState( + localizer = localizer, + backButtionAction = { + router.navigateBack() + }, + ) + } +} diff --git a/v4/feature/profile/src/main/java/exchange/dydx/trading/feature/profile/components/DydxProfileAlertsView.kt b/v4/feature/profile/src/main/java/exchange/dydx/trading/feature/profile/components/DydxProfileAlertsView.kt new file mode 100644 index 00000000..c5f16dae --- /dev/null +++ b/v4/feature/profile/src/main/java/exchange/dydx/trading/feature/profile/components/DydxProfileAlertsView.kt @@ -0,0 +1,112 @@ +package exchange.dydx.trading.feature.profile.components + +import androidx.compose.foundation.background +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.shape.CircleShape +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.text.TextStyle +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import androidx.hilt.navigation.compose.hiltViewModel +import exchange.dydx.abacus.protocols.LocalizerProtocol +import exchange.dydx.platformui.components.icons.PlatformImage +import exchange.dydx.platformui.compose.collectAsStateWithLifecycle +import exchange.dydx.platformui.designSystem.theme.ThemeColor +import exchange.dydx.platformui.designSystem.theme.ThemeFont +import exchange.dydx.platformui.designSystem.theme.color +import exchange.dydx.platformui.designSystem.theme.dydxDefault +import exchange.dydx.platformui.designSystem.theme.themeColor +import exchange.dydx.platformui.designSystem.theme.themeFont +import exchange.dydx.platformui.theme.DydxThemedPreviewSurface +import exchange.dydx.platformui.theme.MockLocalizer +import exchange.dydx.trading.common.component.DydxComponent +import exchange.dydx.trading.feature.shared.R + +@Preview +@Composable +fun Preview_DydxProfileAlertsView() { + DydxThemedPreviewSurface { + DydxProfileAlertsView.Content(Modifier, DydxProfileAlertsView.ViewState.preview) + } +} + +object DydxProfileAlertsView : DydxComponent { + data class ViewState( + val localizer: LocalizerProtocol, + val tapAction: () -> Unit = {}, + val hasAlerts: Boolean = true, + ) { + companion object { + val preview = ViewState( + localizer = MockLocalizer(), + ) + } + } + + @Composable + override fun Content(modifier: Modifier) { + val viewModel: DydxProfileAlertsViewModel = hiltViewModel() + + val state = viewModel.state.collectAsStateWithLifecycle(initialValue = null).value + Content(modifier, state) + } + + @Composable + fun Content(modifier: Modifier, state: ViewState?) { + if (state == null) { + return + } + Column( + modifier = modifier + .clickable(onClick = state.tapAction) + .background( + color = ThemeColor.SemanticColor.layer_3.color, + shape = RoundedCornerShape(14.dp), + ), + ) { + Row( + modifier = Modifier.padding(vertical = 26.dp, horizontal = 24.dp), + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(16.dp), + ) { + PlatformImage( + modifier = Modifier + .size(26.dp), + icon = R.drawable.ic_tap_alerts, + ) + Text( + text = state.localizer.localize("APP.GENERAL.ALERTS"), + style = TextStyle.dydxDefault + .themeColor(ThemeColor.SemanticColor.text_primary) + .themeFont( + fontSize = ThemeFont.FontSize.base, + ), + ) + + Spacer(modifier = Modifier.weight(1f)) + + if (state.hasAlerts) { + Box( + modifier = Modifier + .size(12.dp) + .background( + color = ThemeColor.SemanticColor.color_purple.color, + shape = CircleShape, + ), + ) + } + } + } + } +} diff --git a/v4/feature/profile/src/main/java/exchange/dydx/trading/feature/profile/components/DydxProfileAlertsViewModel.kt b/v4/feature/profile/src/main/java/exchange/dydx/trading/feature/profile/components/DydxProfileAlertsViewModel.kt new file mode 100644 index 00000000..816a2737 --- /dev/null +++ b/v4/feature/profile/src/main/java/exchange/dydx/trading/feature/profile/components/DydxProfileAlertsViewModel.kt @@ -0,0 +1,44 @@ +package exchange.dydx.trading.feature.profile.components + +import androidx.lifecycle.ViewModel +import dagger.hilt.android.lifecycle.HiltViewModel +import exchange.dydx.abacus.protocols.LocalizerProtocol +import exchange.dydx.trading.common.DydxViewModel +import exchange.dydx.trading.common.navigation.DydxRouter +import exchange.dydx.trading.common.navigation.ProfileRoutes +import exchange.dydx.trading.feature.newsalerts.alerts.alertprovider.DydxAlertsProvider +import exchange.dydx.trading.feature.newsalerts.alerts.alertprovider.DydxAlertsProviderItemProtocol +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.distinctUntilChanged +import kotlinx.coroutines.flow.map +import javax.inject.Inject + +@HiltViewModel +class DydxProfileAlertsViewModel @Inject constructor( + private val localizer: LocalizerProtocol, + private val alertsProvider: DydxAlertsProvider, + private val router: DydxRouter, +) : ViewModel(), DydxViewModel { + + val state: Flow = + alertsProvider.items + .map { + createViewState(it) + } + .distinctUntilChanged() + + private fun createViewState( + items: List + ): DydxProfileAlertsView.ViewState { + return DydxProfileAlertsView.ViewState( + localizer = localizer, + hasAlerts = items.isNotEmpty(), + tapAction = { + router.navigateTo( + route = ProfileRoutes.alerts, + presentation = DydxRouter.Presentation.Push, + ) + }, + ) + } +} diff --git a/v4/feature/profile/src/main/java/exchange/dydx/trading/feature/profile/history/DydxHistoryView.kt b/v4/feature/profile/src/main/java/exchange/dydx/trading/feature/profile/history/DydxHistoryView.kt index e3984df4..5a3474eb 100644 --- a/v4/feature/profile/src/main/java/exchange/dydx/trading/feature/profile/history/DydxHistoryView.kt +++ b/v4/feature/profile/src/main/java/exchange/dydx/trading/feature/profile/history/DydxHistoryView.kt @@ -93,12 +93,12 @@ object DydxHistoryView : DydxComponent { ) { HeaderView( title = state.localizer.localize("APP.GENERAL.HISTORY"), - modifier = modifier.fillMaxWidth(), + modifier = Modifier.fillMaxWidth(), backAction = state.backButtionAction, ) SelectionBar.Content( - modifier = modifier.fillMaxWidth(), + modifier = Modifier.fillMaxWidth(), state = state.selectionBarViewState, ) diff --git a/v4/feature/shared/src/main/java/exchange/dydx/trading/feature/shared/bottombar/DydxBottomBarModel.kt b/v4/feature/shared/src/main/java/exchange/dydx/trading/feature/shared/bottombar/DydxBottomBarModel.kt index ffdb06ec..6d7a2743 100644 --- a/v4/feature/shared/src/main/java/exchange/dydx/trading/feature/shared/bottombar/DydxBottomBarModel.kt +++ b/v4/feature/shared/src/main/java/exchange/dydx/trading/feature/shared/bottombar/DydxBottomBarModel.kt @@ -5,11 +5,14 @@ import dagger.hilt.android.lifecycle.HiltViewModel import exchange.dydx.abacus.protocols.LocalizerProtocol import exchange.dydx.dydxstatemanager.AbacusStateManagerProtocol import exchange.dydx.trading.common.DydxViewModel +import exchange.dydx.trading.common.featureflags.DydxFeatureFlag +import exchange.dydx.trading.common.featureflags.DydxFeatureFlags import exchange.dydx.trading.common.navigation.DydxRouter import exchange.dydx.trading.common.navigation.MarketRoutes import exchange.dydx.trading.common.navigation.NewsAlertsRoutes import exchange.dydx.trading.common.navigation.PortfolioRoutes import exchange.dydx.trading.common.navigation.ProfileRoutes +import exchange.dydx.trading.common.navigation.VaultRoutes import exchange.dydx.trading.feature.shared.R import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.MutableStateFlow @@ -20,6 +23,7 @@ class DydxBottomBarModel @Inject constructor( private val localizer: LocalizerProtocol, private val abacusStateManager: AbacusStateManagerProtocol, private val router: DydxRouter, + private val featureFlags: DydxFeatureFlags, ) : ViewModel(), DydxViewModel { val state: Flow = MutableStateFlow(createViewState()) @@ -29,7 +33,11 @@ class DydxBottomBarModel @Inject constructor( portfolioItem(router), marketItem(router), centerButton(router), - newsAlertsItem(router), + if (featureFlags.isFeatureEnabled(DydxFeatureFlag.vault_enabled, default = false)) { + vaultItem(router) + } else { + newsAlertsItem(router) + }, profileItem(router), ) return DydxBottomBar.ViewState( @@ -89,4 +97,14 @@ class DydxBottomBarModel @Inject constructor( router.tabTo(ProfileRoutes.main) }, ) + + private fun vaultItem(router: DydxRouter) = BottomBarItem( + route = NewsAlertsRoutes.main, + label = "APP.GENERAL.EARN_SHORT", + icon = R.drawable.ic_tab_vault, + selected = router.routeIsInBackStack(VaultRoutes.main), + onTapAction = { + router.tabTo(VaultRoutes.main) + }, + ) } diff --git a/v4/feature/shared/src/main/res/drawable/ic_tab_vault.xml b/v4/feature/shared/src/main/res/drawable/ic_tab_vault.xml new file mode 100644 index 00000000..e33e447d --- /dev/null +++ b/v4/feature/shared/src/main/res/drawable/ic_tab_vault.xml @@ -0,0 +1,17 @@ + + + + + + + diff --git a/v4/feature/vault/.gitignore b/v4/feature/vault/.gitignore new file mode 100644 index 00000000..42afabfd --- /dev/null +++ b/v4/feature/vault/.gitignore @@ -0,0 +1 @@ +/build \ No newline at end of file diff --git a/v4/feature/vault/build.gradle b/v4/feature/vault/build.gradle new file mode 100644 index 00000000..0fa63d58 --- /dev/null +++ b/v4/feature/vault/build.gradle @@ -0,0 +1,114 @@ +plugins { + id 'com.android.library' + id 'org.jetbrains.kotlin.android' + id 'kotlin-kapt' + id 'dagger.hilt.android.plugin' +} + +android { + compileSdk parent.compileSdkVersion + + defaultConfig { + minSdkVersion parent.minSdkVersion + targetSdkVersion parent.targetSdkVersion + + testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" + consumerProguardFiles "consumer-rules.pro" + } + + buildTypes { + release { + minifyEnabled false + proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro' + } + } + compileOptions { + sourceCompatibility JavaVersion.VERSION_17 + targetCompatibility JavaVersion.VERSION_17 + } + kotlinOptions { + jvmTarget = '17' + } + + buildFeatures { + compose true + } + composeOptions { + kotlinCompilerExtensionVersion "$composeCompilerVersion" + } + namespace 'exchange.dydx.trading.feature.vault' +} + +dependencies { + /* Module Dependencies */ + + implementation project(':v4:integration:chart') + implementation project(':v4:common') + implementation project(path: ':v4:integration:dydxStateManager') + implementation project(path: ':v4:platformUI') + implementation project(path: ':v4:feature:shared') + implementation project(':v4:utilities') + + /* Local Dependencies */ + + api "exchange.dydx.abacus:v4-abacus-jvm:$abacusVersion" + + + api "androidx.compose.foundation:foundation:$composeVersion" + api "androidx.compose.runtime:runtime:$composeVersion" + implementation "androidx.compose.ui:ui-graphics:$composeVersion" + + implementation "org.jetbrains.kotlinx:kotlinx-serialization-json:$kotlinxSerializationVersion" + implementation "org.jetbrains.kotlinx:kotlinx-serialization-core:$kotlinxSerializationVersion" + + // Add non-standard deps above. Deps added below this line may be periodically overwritten + /* Standard Dependencies */ + + api "org.jetbrains.kotlinx:kotlinx-coroutines-core:$coroutinesVersion" + + + kapt "com.google.dagger:hilt-compiler:$hiltVersion" + + // Hilt + api "com.google.dagger:dagger:$hiltVersion" + implementation "com.google.dagger:hilt-core:$hiltVersion" + implementation "com.google.dagger:hilt-android:$hiltVersion" + implementation "androidx.hilt:hilt-navigation-compose:$hiltAndroidXVersion" + + // Compose + api "androidx.compose.runtime:runtime:$composeVersion" + + api "androidx.compose.foundation:foundation-layout:$composeVersion" + api "androidx.compose.ui:ui:$composeVersion" + implementation "androidx.compose.ui:ui-text:$composeVersion" + implementation "androidx.compose.ui:ui-unit:$composeVersion" + implementation "androidx.compose.material:material:$composeVersion" + implementation "androidx.compose.material3:material3:$material3Version" + + implementation("io.coil-kt:coil-compose:$coilVersion") + implementation("io.coil-kt:coil-svg:$coilVersion") + + //Lifecycle + api "androidx.lifecycle:lifecycle-viewmodel:$archLifecycleVersion" + api "androidx.lifecycle:lifecycle-viewmodel-savedstate:$archLifecycleVersion" + implementation "androidx.lifecycle:lifecycle-common:$archLifecycleVersion" + implementation "androidx.lifecycle:lifecycle-viewmodel-compose:$archLifecycleVersion" + implementation "androidx.lifecycle:lifecycle-viewmodel-ktx:$archLifecycleVersion" + + //Logging + implementation "com.jakewharton.timber:timber:$timberVersion" + + // Compose Tooling + implementation "androidx.compose.ui:ui-tooling-preview:$composeVersion" + debugImplementation "androidx.compose.ui:ui-tooling:$composeVersion" + debugRuntimeOnly "androidx.compose.ui:ui-test-manifest:$composeVersion" + debugImplementation "androidx.customview:customview:$customviewVersion" + debugImplementation "androidx.customview:customview-poolingcontainer:$customviewPoolingVersion" + + // Testing + testImplementation "junit:junit:$junitVersion" +// androidTestImplementation "androidx.test.espresso:espresso-core:$espressoVersion" + androidTestImplementation "androidx.compose.ui:ui-test-junit4:$composeVersion" + + implementation("tz.co.asoft:kollections-interoperable:$kollectionsVersion") +} \ No newline at end of file diff --git a/v4/feature/vault/consumer-rules.pro b/v4/feature/vault/consumer-rules.pro new file mode 100644 index 00000000..e69de29b diff --git a/v4/feature/vault/proguard-rules.pro b/v4/feature/vault/proguard-rules.pro new file mode 100644 index 00000000..481bb434 --- /dev/null +++ b/v4/feature/vault/proguard-rules.pro @@ -0,0 +1,21 @@ +# Add project specific ProGuard rules here. +# You can control the set of applied configuration files using the +# proguardFiles setting in build.gradle. +# +# For more details, see +# http://developer.android.com/guide/developing/tools/proguard.html + +# If your project uses WebView with JS, uncomment the following +# and specify the fully qualified class name to the JavaScript interface +# class: +#-keepclassmembers class fqcn.of.javascript.interface.for.webview { +# public *; +#} + +# Uncomment this to preserve the line number information for +# debugging stack traces. +#-keepattributes SourceFile,LineNumberTable + +# If you keep the line number information, uncomment this to +# hide the original source file name. +#-renamesourcefileattribute SourceFile \ No newline at end of file diff --git a/v4/feature/vault/src/androidTest/java/exchange/dydx/vault/ExampleInstrumentedTest.kt b/v4/feature/vault/src/androidTest/java/exchange/dydx/vault/ExampleInstrumentedTest.kt new file mode 100644 index 00000000..2556d6c1 --- /dev/null +++ b/v4/feature/vault/src/androidTest/java/exchange/dydx/vault/ExampleInstrumentedTest.kt @@ -0,0 +1,22 @@ +package exchange.dydx.vault + +import androidx.test.ext.junit.runners.AndroidJUnit4 +import androidx.test.platform.app.InstrumentationRegistry +import org.junit.Assert.assertEquals +import org.junit.Test +import org.junit.runner.RunWith + +/** + * Instrumented test, which will execute on an Android device. + * + * See [testing documentation](http://d.android.com/tools/testing). + */ +@RunWith(AndroidJUnit4::class) +class ExampleInstrumentedTest { + @Test + fun useAppContext() { + // Context of the app under test. + val appContext = InstrumentationRegistry.getInstrumentation().targetContext + assertEquals("exchange.dydx.trading.feature.vault.test", appContext.packageName) + } +} diff --git a/v4/feature/vault/src/main/AndroidManifest.xml b/v4/feature/vault/src/main/AndroidManifest.xml new file mode 100644 index 00000000..a5918e68 --- /dev/null +++ b/v4/feature/vault/src/main/AndroidManifest.xml @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/v4/feature/vault/src/main/java/exchange/dydx/vault/DydxVaultRouter.kt b/v4/feature/vault/src/main/java/exchange/dydx/vault/DydxVaultRouter.kt new file mode 100644 index 00000000..cba2405c --- /dev/null +++ b/v4/feature/vault/src/main/java/exchange/dydx/vault/DydxVaultRouter.kt @@ -0,0 +1,24 @@ +package exchange.dydx.vault + +import androidx.compose.ui.Modifier +import androidx.navigation.NavGraphBuilder +import exchange.dydx.trading.common.navigation.DydxRouter +import exchange.dydx.trading.common.navigation.VaultRoutes +import exchange.dydx.trading.common.navigation.dydxComposable +import exchange.dydx.trading.feature.shared.bottombar.DydxBottomBarScaffold +import exchange.dydx.utilities.utils.Logging + +fun NavGraphBuilder.vaultGraph( + appRouter: DydxRouter, + logger: Logging, +) { + dydxComposable( + router = appRouter, + route = VaultRoutes.main, + deepLinks = appRouter.deeplinks(VaultRoutes.main), + ) { navBackStackEntry -> + DydxBottomBarScaffold(Modifier) { + DydxVaultView.Content(Modifier) + } + } +} diff --git a/v4/feature/vault/src/main/java/exchange/dydx/vault/DydxVaultView.kt b/v4/feature/vault/src/main/java/exchange/dydx/vault/DydxVaultView.kt new file mode 100644 index 00000000..e888a04e --- /dev/null +++ b/v4/feature/vault/src/main/java/exchange/dydx/vault/DydxVaultView.kt @@ -0,0 +1,50 @@ +package exchange.dydx.vault + +import androidx.compose.material.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.tooling.preview.Preview +import androidx.hilt.navigation.compose.hiltViewModel +import exchange.dydx.abacus.protocols.LocalizerProtocol +import exchange.dydx.platformui.compose.collectAsStateWithLifecycle +import exchange.dydx.platformui.theme.DydxThemedPreviewSurface +import exchange.dydx.platformui.theme.MockLocalizer +import exchange.dydx.trading.common.component.DydxComponent + +@Preview +@Composable +fun Preview_DydxVaultView() { + DydxThemedPreviewSurface { + DydxVaultView.Content(Modifier, DydxVaultView.ViewState.preview) + } +} + +object DydxVaultView : DydxComponent { + data class ViewState( + val localizer: LocalizerProtocol, + val text: String?, + ) { + companion object { + val preview = ViewState( + localizer = MockLocalizer(), + text = "1.0M", + ) + } + } + + @Composable + override fun Content(modifier: Modifier) { + val viewModel: DydxVaultViewModel = hiltViewModel() + + val state = viewModel.state.collectAsStateWithLifecycle(initialValue = null).value + Content(modifier, state) + } + + @Composable + fun Content(modifier: Modifier, state: ViewState?) { + if (state == null) { + return + } + Text(text = state?.text ?: "") + } +} diff --git a/v4/feature/vault/src/main/java/exchange/dydx/vault/DydxVaultViewModel.kt b/v4/feature/vault/src/main/java/exchange/dydx/vault/DydxVaultViewModel.kt new file mode 100644 index 00000000..65d2d5c7 --- /dev/null +++ b/v4/feature/vault/src/main/java/exchange/dydx/vault/DydxVaultViewModel.kt @@ -0,0 +1,35 @@ +package exchange.dydx.vault + +import androidx.lifecycle.ViewModel +import dagger.hilt.android.lifecycle.HiltViewModel +import exchange.dydx.abacus.output.PerpetualMarketSummary +import exchange.dydx.abacus.protocols.LocalizerProtocol +import exchange.dydx.dydxstatemanager.AbacusStateManagerProtocol +import exchange.dydx.trading.common.DydxViewModel +import exchange.dydx.trading.common.formatter.DydxFormatter +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.distinctUntilChanged +import kotlinx.coroutines.flow.map +import javax.inject.Inject + +@HiltViewModel +class DydxVaultViewModel @Inject constructor( + private val localizer: LocalizerProtocol, + private val abacusStateManager: AbacusStateManagerProtocol, + private val formatter: DydxFormatter, +) : ViewModel(), DydxViewModel { + + val state: Flow = abacusStateManager.state.marketSummary + .map { + createViewState(it) + } + .distinctUntilChanged() + + private fun createViewState(marketSummary: PerpetualMarketSummary?): DydxVaultView.ViewState { + val volume = formatter.dollarVolume(marketSummary?.volume24HUSDC) + return DydxVaultView.ViewState( + localizer = localizer, + text = volume, + ) + } +} diff --git a/v4/feature/vault/src/test/java/exchange/dydx/vault/ExampleUnitTest.kt b/v4/feature/vault/src/test/java/exchange/dydx/vault/ExampleUnitTest.kt new file mode 100644 index 00000000..e2f14f9c --- /dev/null +++ b/v4/feature/vault/src/test/java/exchange/dydx/vault/ExampleUnitTest.kt @@ -0,0 +1,16 @@ +package exchange.dydx.vault + +import org.junit.Assert.assertEquals +import org.junit.Test + +/** + * Example local unit test, which will execute on the development machine (host). + * + * See [testing documentation](http://d.android.com/tools/testing). + */ +class ExampleUnitTest { + @Test + fun addition_isCorrect() { + assertEquals(4, 2 + 2) + } +}