From 24be421dda0d6cfb4fca5f7ff9c44c018a480c65 Mon Sep 17 00:00:00 2001 From: Sam Newby Date: Thu, 30 Apr 2026 09:41:37 -0400 Subject: [PATCH 1/5] Update deposit tokens and logos --- .../feature/shared/TransferTokenDetails.kt | 57 ++++++++----------- .../deposit/qrcode/DydxTurnkeyQRCodeView.kt | 50 +++++++++++++--- .../qrcode/DydxTurnkeyQRCodeViewModel.kt | 20 ++----- .../QRCode/dydxTurnkeyQRCodeViewBuilder.swift | 3 +- .../_v4/Transfer/TransferTokenDetails.swift | 53 ++++++++--------- .../QRCode/dydxTurnkeyQRCodeView.swift | 31 +++++++--- 6 files changed, 124 insertions(+), 90 deletions(-) diff --git a/android/v4/feature/shared/src/main/java/exchange/dydx/trading/feature/shared/TransferTokenDetails.kt b/android/v4/feature/shared/src/main/java/exchange/dydx/trading/feature/shared/TransferTokenDetails.kt index 5975c28e..2e8ce3cb 100644 --- a/android/v4/feature/shared/src/main/java/exchange/dydx/trading/feature/shared/TransferTokenDetails.kt +++ b/android/v4/feature/shared/src/main/java/exchange/dydx/trading/feature/shared/TransferTokenDetails.kt @@ -104,17 +104,17 @@ class TransferTokenDetails @Inject constructor( enum class TransferChain { Ethereum, Optimism, Arbitrum, Base, Polygon, Solana, Avalanche; - val supportedDepositTokenString: String + val supportedDepositTokens: List get() = when (this) { - Ethereum -> "ETH, USDC" - Optimism -> "ETH, USDC" - Arbitrum -> "ETH, USDC" - Base -> "ETH, USDC" - Polygon -> "POL, USDC" - Solana -> "USDC" - Avalanche -> "USDC" + Ethereum, Optimism, Arbitrum, Base -> listOf(TransferToken.ETH, TransferToken.USDC) + Polygon -> listOf(TransferToken.POL, TransferToken.USDC) + Solana -> listOf(TransferToken.USDC) + Avalanche -> listOf(TransferToken.USDC) } + val supportedDepositTokenString: String + get() = supportedDepositTokens.joinToString(", ") { it.name } + fun depositFeesString(localizer: LocalizerProtocol): String { return when (this) { Ethereum -> localizer.localizeWithParams( @@ -127,18 +127,9 @@ enum class TransferChain { } fun depositWarningString(localizer: LocalizerProtocol, remoteFlags: RemoteFlags): String { - val tokens = when (this) { - Ethereum, Optimism, Arbitrum, Base -> "ETH " + localizer.localize(path = "APP.GENERAL.OR") + " USDC" - Polygon -> "POL " + localizer.localize(path = "APP.GENERAL.OR") + " USDC" - Solana -> "USDC" - Avalanche -> " USDC" - } + val orSeparator = " " + localizer.localize(path = "APP.GENERAL.OR") + " " + val tokens = supportedDepositTokens.joinToString(orSeparator) { it.name } - val minSlowVal = if (this == TransferChain.Ethereum) { - remoteFlags.getParamStoreValue("eth_min_slow", "-") - } else { - remoteFlags.getParamStoreValue("default_min_slow", "-") - } val minFastVal = if (this == TransferChain.Ethereum) { remoteFlags.getParamStoreValue("eth_min_fast", "-") } else { @@ -151,11 +142,11 @@ enum class TransferChain { } return localizer.localizeWithParams( - path = "APP.TURNKEY_ONBOARD.DEPOSIT_NETWORK_WARNING", + path = "APP.TURNKEY_ONBOARD.DEPOSIT_LOSS_OF_FUNDS_WARNING", params = mapOf( "ASSETS" to tokens, "NETWORK" to name, - "MIN_DEPOSIT" to minSlowVal, + "LOSS_OF_FUNDS" to localizer.localize(path = "APP.TURNKEY_ONBOARD.LOSS_OF_FUNDS"), "MIN_INSTANT_DEPOSIT" to minFastVal, "MAX_DEPOSIT" to maxVal, ), @@ -183,7 +174,18 @@ enum class TransferChain { } enum class TransferToken { - ETH, USDC, POL, SOL, AVAX + ETH, USDC, POL, SOL, AVAX; + + fun logoUrl(deploymentUri: String): String { + val logoName = when (this) { + ETH -> "eth.png" + USDC -> "usdc.png" + POL -> "pol.png" + SOL -> "sol.png" + AVAX -> "avax.png" + } + return "$deploymentUri/currencies/$logoName" + } } data class TransferTokenInfo( @@ -207,16 +209,7 @@ data class TransferTokenInfo( return "$deploymentUri/chains/$logoName" } - fun tokenLogoUrl(deploymentUri: String): String { - val logoName = when (token) { - TransferToken.ETH -> "eth.png" - TransferToken.USDC -> "usdc.png" - TransferToken.POL -> "pol.png" - TransferToken.SOL -> "sol.png" - TransferToken.AVAX -> "avax.png" - } - return "$deploymentUri/currencies/$logoName" - } + fun tokenLogoUrl(deploymentUri: String): String = token.logoUrl(deploymentUri) val decimals: Int get() = when (token) { diff --git a/android/v4/feature/transfer/src/main/java/exchange/dydx/trading/feature/transfer/deposit/qrcode/DydxTurnkeyQRCodeView.kt b/android/v4/feature/transfer/src/main/java/exchange/dydx/trading/feature/transfer/deposit/qrcode/DydxTurnkeyQRCodeView.kt index 61f4fe3a..3cfa8f93 100644 --- a/android/v4/feature/transfer/src/main/java/exchange/dydx/trading/feature/transfer/deposit/qrcode/DydxTurnkeyQRCodeView.kt +++ b/android/v4/feature/transfer/src/main/java/exchange/dydx/trading/feature/transfer/deposit/qrcode/DydxTurnkeyQRCodeView.kt @@ -4,6 +4,7 @@ import android.R.attr.bottom import android.R.attr.shape import android.R.attr.text import androidx.compose.foundation.Image +import androidx.compose.foundation.background import androidx.compose.foundation.border import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box @@ -16,6 +17,7 @@ import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.width +import androidx.compose.foundation.shape.CircleShape import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material.Text import androidx.compose.runtime.Composable @@ -38,6 +40,7 @@ import androidx.compose.ui.text.buildAnnotatedString import androidx.compose.ui.text.withStyle import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp +import androidx.compose.ui.zIndex import androidx.hilt.navigation.compose.hiltViewModel import androidx.lifecycle.compose.collectAsStateWithLifecycle import androidx.lifecycle.viewmodel.compose.viewModel @@ -77,7 +80,8 @@ object DydxTurnkeyQRCodeView : DydxComponent { val subtitle: String? = null, val footer: String? = null, val address: String? = null, - val chainIconUrl: String? = null, + val supportedAssetsLabel: String? = null, + val supportedAssetIconUrls: List = emptyList(), val onCopyAction: (() -> Unit)? = null, val copied: Boolean = true ) { @@ -87,7 +91,11 @@ object DydxTurnkeyQRCodeView : DydxComponent { subtitle = "Scan the QR code to deposit USDC", footer = "Powered by dYdX Turnkey", address = "0x1234567890abcdef1234567890abcdef12345678", - chainIconUrl = "https://example.com/icon.png", + supportedAssetsLabel = "Supported assets", + supportedAssetIconUrls = listOf( + "https://v4.testnet.dydx.exchange/currencies/eth.png", + "https://v4.testnet.dydx.exchange/currencies/usdc.png", + ), ) } } @@ -155,11 +163,39 @@ object DydxTurnkeyQRCodeView : DydxComponent { Box( modifier = Modifier.weight(1f), ) { - PlatformRoundImage( - icon = state.chainIconUrl, - size = 36.dp, - modifier = Modifier.padding(24.dp), - ) + Column( + modifier = Modifier.padding(16.dp), + verticalArrangement = Arrangement.spacedBy(8.dp), + ) { + if (!state.supportedAssetsLabel.isNullOrEmpty()) { + Text( + text = state.supportedAssetsLabel, + style = TextStyle.dydxDefault + .themeFont(fontSize = ThemeFont.FontSize.small) + .themeColor(ThemeColor.SemanticColor.text_tertiary), + ) + } + Row( + horizontalArrangement = Arrangement.spacedBy((-10).dp), + ) { + state.supportedAssetIconUrls.forEachIndexed { index, iconUrl -> + Box( + modifier = Modifier + .zIndex((state.supportedAssetIconUrls.size - index).toFloat()) + .background( + color = ThemeColor.SemanticColor.layer_2.color, + shape = CircleShape, + ) + .padding(2.dp), + ) { + PlatformRoundImage( + icon = iconUrl, + size = 28.dp, + ) + } + } + } + } } Box( diff --git a/android/v4/feature/transfer/src/main/java/exchange/dydx/trading/feature/transfer/deposit/qrcode/DydxTurnkeyQRCodeViewModel.kt b/android/v4/feature/transfer/src/main/java/exchange/dydx/trading/feature/transfer/deposit/qrcode/DydxTurnkeyQRCodeViewModel.kt index 5089fedb..1e998d37 100644 --- a/android/v4/feature/transfer/src/main/java/exchange/dydx/trading/feature/transfer/deposit/qrcode/DydxTurnkeyQRCodeViewModel.kt +++ b/android/v4/feature/transfer/src/main/java/exchange/dydx/trading/feature/transfer/deposit/qrcode/DydxTurnkeyQRCodeViewModel.kt @@ -57,28 +57,16 @@ class DydxTurnkeyQRCodeViewModel @Inject constructor( TransferChain.Avalanche -> addresses.avalancheAddress else -> null } - val minSlowVal = if (chain == TransferChain.Ethereum) { - remoteFlags.getParamStoreValue("eth_min_slow", "-") - } else { - remoteFlags.getParamStoreValue("default_min_slow", "-") - } - val minFastVal = if (chain == TransferChain.Ethereum) { - remoteFlags.getParamStoreValue("eth_min_fast", "-") - } else { - remoteFlags.getParamStoreValue("default_min_fast", "-") - } - val maxVal = if (chain == TransferChain.Ethereum) { - remoteFlags.getParamStoreValue("eth_max", "-") - } else { - remoteFlags.getParamStoreValue("default_max", "-") - } return DydxTurnkeyQRCodeView.ViewState( localizer = localizer, backAction = { router.navigateBack() }, address = address, - chainIconUrl = chain.chainLogoUrl(abacusStateManager.deploymentUri), + supportedAssetsLabel = localizer.localize(path = "APP.TURNKEY_ONBOARD.SUPPORTED_ASSETS"), + supportedAssetIconUrls = chain.supportedDepositTokens.map { + it.logoUrl(abacusStateManager.deploymentUri) + }, subtitle = localizer.localizeWithParams( path = "APP.DEPOSIT_MODAL.TURNKEY_DEPOSIT_SUBTITLE", params = mapOf( diff --git a/ios/dydx/dydxPresenters/dydxPresenters/_v4/Transfer/Deposit/QRCode/dydxTurnkeyQRCodeViewBuilder.swift b/ios/dydx/dydxPresenters/dydxPresenters/_v4/Transfer/Deposit/QRCode/dydxTurnkeyQRCodeViewBuilder.swift index c58d158a..76f695d0 100644 --- a/ios/dydx/dydxPresenters/dydxPresenters/_v4/Transfer/Deposit/QRCode/dydxTurnkeyQRCodeViewBuilder.swift +++ b/ios/dydx/dydxPresenters/dydxPresenters/_v4/Transfer/Deposit/QRCode/dydxTurnkeyQRCodeViewBuilder.swift @@ -76,7 +76,8 @@ private class dydxTurnkeyQRCodeViewPresenter: HostedViewPresenter Void)? @Published public var copied: Bool = false @@ -27,7 +28,11 @@ public class dydxTurnkeyQRCodeViewModel: PlatformViewModel { vm.subtitle = "Subtitle" vm.footer = "Footer" vm.address = "0xdeadbeef" - vm.chainIcon = URL(string: "https://v4.testnet.dydx.exchange/chains/ethereum.png") + vm.supportedAssetsLabel = "Supported assets" + vm.supportedAssetIcons = [ + URL(string: "https://v4.testnet.dydx.exchange/currencies/usdc.png")!, + URL(string: "https://v4.testnet.dydx.exchange/currencies/eth.png")! + ] return vm } @@ -82,13 +87,23 @@ public class dydxTurnkeyQRCodeViewModel: PlatformViewModel { private func createQRCodeSection(style: ThemeStyle) -> some View { HStack { - HStack { - PlatformIconViewModel(type: .url(url: self.chainIcon), clip: .circle(background: .transparent, spacing: 0), size: CGSize(width: 36, height: 36)) - .createView(parentStyle: style) - .padding(16) - .topAligned() - .leftAligned() + VStack(alignment: .leading, spacing: 8) { + if let label = self.supportedAssetsLabel { + Text(label) + .themeColor(foreground: .textTertiary) + .themeFont(fontSize: .small) + } + HStack(spacing: -10) { + ForEach(Array(self.supportedAssetIcons.enumerated()), id: \.offset) { index, iconUrl in + PlatformIconViewModel(type: .url(url: iconUrl), clip: .circle(background: .layer2, spacing: 4), size: CGSize(width: 28, height: 28)) + .createView(parentStyle: style) + .zIndex(Double(self.supportedAssetIcons.count - index)) + } + } } + .padding(16) + .topAligned() + .leftAligned() .frame(maxWidth: .infinity) HStack { From 36fcfb538afc8b5039756de993d18728ae53afb6 Mon Sep 17 00:00:00 2001 From: Sam Newby Date: Thu, 30 Apr 2026 11:26:11 -0400 Subject: [PATCH 2/5] Update rewards --- .../feature/profile/DydxProfileView.kt | 19 +- .../components/DydxProfileRewardsView.kt | 228 ++++++------------ .../components/DydxProfileRewardsViewModel.kt | 110 +++++---- .../dydxProfileRewardsViewPresenter.swift | 38 +-- .../dydxProfileRewardsViewModel.swift | 134 +++++++--- .../_v4/Profile/dydxProfileView.swift | 11 +- 6 files changed, 258 insertions(+), 282 deletions(-) diff --git a/android/v4/feature/profile/src/main/java/exchange/dydx/trading/feature/profile/DydxProfileView.kt b/android/v4/feature/profile/src/main/java/exchange/dydx/trading/feature/profile/DydxProfileView.kt index 017b2ac9..ff108811 100644 --- a/android/v4/feature/profile/src/main/java/exchange/dydx/trading/feature/profile/DydxProfileView.kt +++ b/android/v4/feature/profile/src/main/java/exchange/dydx/trading/feature/profile/DydxProfileView.kt @@ -2,13 +2,10 @@ package exchange.dydx.trading.feature.profile import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box -import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.fillMaxSize -import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.layout.width import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier @@ -93,18 +90,10 @@ object DydxProfileView : DydxComponent { DydxProfileBalancesView.Content(Modifier.padding(horizontal = ThemeShapes.HorizontalPadding)) } item(key = "fees") { - Row( - Modifier - .fillMaxWidth() - .padding( - horizontal = ThemeShapes.HorizontalPadding, - ), - horizontalArrangement = Arrangement.SpaceBetween, - ) { - DydxProfileFeesView.Content(modifier = Modifier.weight(1f)) - Spacer(modifier = Modifier.width(16.dp)) - DydxProfileRewardsView.Content(modifier = Modifier.weight(1f)) - } + DydxProfileFeesView.Content(Modifier.padding(horizontal = ThemeShapes.HorizontalPadding)) + } + item(key = "rewards") { + DydxProfileRewardsView.Content(Modifier.padding(horizontal = ThemeShapes.HorizontalPadding)) } item(key = "history") { DydxProfileHistoryView.Content(Modifier.padding(horizontal = ThemeShapes.HorizontalPadding)) diff --git a/android/v4/feature/profile/src/main/java/exchange/dydx/trading/feature/profile/components/DydxProfileRewardsView.kt b/android/v4/feature/profile/src/main/java/exchange/dydx/trading/feature/profile/components/DydxProfileRewardsView.kt index 2faa4225..6f912a01 100644 --- a/android/v4/feature/profile/src/main/java/exchange/dydx/trading/feature/profile/components/DydxProfileRewardsView.kt +++ b/android/v4/feature/profile/src/main/java/exchange/dydx/trading/feature/profile/components/DydxProfileRewardsView.kt @@ -1,19 +1,24 @@ package exchange.dydx.trading.feature.profile.components import androidx.compose.foundation.background +import androidx.compose.foundation.border import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.width import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material.Icon import androidx.compose.material.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.platform.LocalUriHandler import androidx.compose.ui.res.painterResource import androidx.compose.ui.text.TextStyle import androidx.compose.ui.tooling.preview.Preview @@ -21,8 +26,6 @@ import androidx.compose.ui.unit.dp import androidx.hilt.navigation.compose.hiltViewModel import androidx.lifecycle.compose.collectAsStateWithLifecycle import exchange.dydx.abacus.protocols.LocalizerProtocol -import exchange.dydx.platformui.components.dividers.PlatformDivider -import exchange.dydx.platformui.components.icons.PlatformImage import exchange.dydx.platformui.designSystem.theme.ThemeColor import exchange.dydx.platformui.designSystem.theme.ThemeFont import exchange.dydx.platformui.designSystem.theme.ThemeShapes @@ -44,21 +47,24 @@ fun Preview_DydxProfileRewardsView() { } object DydxProfileRewardsView : DydxComponent { + private const val LIQUIDATION_REBATES_URL = "https://dydx.trade/DYDX" + data class ViewState( val localizer: LocalizerProtocol, - val summary: DydxRewardsSummaryState?, - val nativeTokenLogoUrl: String? = null, - val onTapAction: (() -> Unit)? = null, + val title: String, + val activeBadgeText: String, + val bodyText: String, + val countdownLabel: String, + val countdownText: String, ) { companion object { val preview = ViewState( localizer = MockLocalizer(), - summary = DydxRewardsSummaryState( - titleText = "Rewards", - rewards7DaysText = "0.01", - range7DaysText = "Jan 1 to Jan 7", - rewardsAllTimeText = "1.0", - ), + title = "Liquidation Rebates", + activeBadgeText = "Active", + bodyText = "With the Liquidation Rebate Program, eligible traders liquidated on non-BTC perpetual pairs have the opportunity to earn DYDX rebates on a portion of their losses. Reminder to claim previous loss rebates and check eligibility here.", + countdownLabel = "Month April Countdown", + countdownText = "0d 9h 51m 46s", ) } } @@ -68,181 +74,93 @@ object DydxProfileRewardsView : DydxComponent { val viewModel: DydxProfileRewardsViewModel = hiltViewModel() val state = viewModel.state.collectAsStateWithLifecycle(initialValue = null).value - Content(modifier, state, false) + Content(modifier, state) } @Composable - fun Content(modifier: Modifier, state: ViewState?, detailed: Boolean = false) { + fun Content(modifier: Modifier, state: ViewState?, @Suppress("UNUSED_PARAMETER") detailed: Boolean = false) { if (state == null) { return } + val uriHandler = LocalUriHandler.current + Column( modifier = modifier - .clickable { state.onTapAction?.invoke() } - .background( - color = ThemeColor.SemanticColor.layer_3.color, - shape = RoundedCornerShape(14.dp), - ), + .fillMaxWidth() + .clip(RoundedCornerShape(14.dp)) + .background(color = ThemeColor.SemanticColor.layer_3.color) + .clickable { uriHandler.openUri(LIQUIDATION_REBATES_URL) }, ) { - CreateHeader( - modifier = Modifier.padding(horizontal = ThemeShapes.HorizontalPadding), - state = state, - detailed = detailed, - ) - - PlatformDivider() - - if (detailed) { - Row( - modifier = Modifier.fillMaxWidth(), - horizontalArrangement = Arrangement.SpaceEvenly, - ) { - Column( - modifier = Modifier.weight(1f), - verticalArrangement = Arrangement.spacedBy(8.dp), - ) { - CreateLastWeekContent( - modifier = Modifier - .padding(horizontal = ThemeShapes.HorizontalPadding) - .padding(vertical = 16.dp), - state = state, - detailed = detailed, - ) - } - Column( - modifier = Modifier.weight(1f), - verticalArrangement = Arrangement.spacedBy(8.dp), - ) { - CreateAllTimeContent( - modifier = Modifier - .padding(horizontal = ThemeShapes.HorizontalPadding) - .padding(vertical = 16.dp), - state = state, - ) - } - } - } else { - CreateLastWeekContent( - modifier = Modifier - .padding(horizontal = ThemeShapes.HorizontalPadding) - .padding(vertical = 16.dp), - state = state, - detailed = detailed, + Row( + modifier = Modifier + .padding(horizontal = ThemeShapes.HorizontalPadding) + .padding(vertical = 12.dp), + verticalAlignment = Alignment.CenterVertically, + ) { + Text( + text = state.title, + style = TextStyle.dydxDefault + .themeFont(fontSize = ThemeFont.FontSize.small) + .themeColor(foreground = ThemeColor.SemanticColor.text_secondary), + modifier = Modifier.weight(1f), ) - CreateAllTimeContent( + + Text( + text = state.activeBadgeText, + style = TextStyle.dydxDefault + .themeFont(fontSize = ThemeFont.FontSize.mini) + .themeColor(foreground = ThemeColor.SemanticColor.color_green), modifier = Modifier - .padding(horizontal = ThemeShapes.HorizontalPadding) - .padding(vertical = 16.dp), - state = state, + .border( + width = 1.dp, + color = ThemeColor.SemanticColor.color_green.color, + shape = RoundedCornerShape(4.dp), + ) + .padding(horizontal = 6.dp, vertical = 4.dp), ) } - } - } - - @Composable - private fun CreateHeader(modifier: Modifier, state: ViewState, detailed: Boolean = false) { - Row( - modifier = modifier.padding(vertical = 12.dp), - verticalAlignment = Alignment.CenterVertically, - horizontalArrangement = Arrangement.Center, - ) { - Text( - text = state.summary?.titleText ?: "", - style = TextStyle.dydxDefault - .themeFont(fontSize = ThemeFont.FontSize.small), - modifier = Modifier.weight(1f), - ) - - if (!detailed) { - Column(modifier = Modifier.align(Alignment.CenterVertically)) { - Icon( - painter = painterResource(id = R.drawable.chevron_right), - contentDescription = "", - modifier = Modifier.size(16.dp), - tint = ThemeColor.SemanticColor.text_secondary.color, - ) - } - } - } - } - @Composable - private fun CreateLastWeekContent(modifier: Modifier, state: ViewState, detailed: Boolean) { - Row( - modifier = modifier.fillMaxWidth(), - horizontalArrangement = Arrangement.SpaceEvenly, - ) { Column( - modifier = Modifier.weight(1f), - verticalArrangement = Arrangement.spacedBy(8.dp), + modifier = Modifier + .padding(horizontal = ThemeShapes.HorizontalPadding) + .padding(bottom = 16.dp), + verticalArrangement = Arrangement.spacedBy(12.dp), ) { Text( - text = state.localizer.localize("APP.GENERAL.TIME_STRINGS.THIS_WEEK"), + text = state.bodyText, style = TextStyle.dydxDefault - .themeFont(fontSize = ThemeFont.FontSize.small), + .themeFont(fontSize = ThemeFont.FontSize.small) + .themeColor(foreground = ThemeColor.SemanticColor.text_tertiary), ) Row( + modifier = Modifier + .clip(RoundedCornerShape(20.dp)) + .background(ThemeColor.SemanticColor.layer_4.color) + .padding(horizontal = 12.dp, vertical = 8.dp), verticalAlignment = Alignment.CenterVertically, - horizontalArrangement = Arrangement.spacedBy(4.dp), ) { - Text( - text = state.summary?.rewards7DaysText ?: "-", - style = TextStyle.dydxDefault - .themeFont(fontSize = ThemeFont.FontSize.medium) - .themeColor(ThemeColor.SemanticColor.text_primary), + Icon( + painter = painterResource(id = R.drawable.icon_clock), + contentDescription = null, + modifier = Modifier.size(14.dp), + tint = ThemeColor.SemanticColor.color_purple.color, ) - if (state.nativeTokenLogoUrl != null && state.summary?.rewards7DaysText != null) { - PlatformImage( - icon = state.nativeTokenLogoUrl, - modifier = Modifier.size(24.dp), - ) - } - } - - if (detailed) { + Spacer(modifier = Modifier.width(8.dp)) Text( - text = state.summary?.range7DaysText ?: "", + text = state.countdownLabel, style = TextStyle.dydxDefault - .themeFont(fontSize = ThemeFont.FontSize.small), + .themeFont(fontSize = ThemeFont.FontSize.small) + .themeColor(foreground = ThemeColor.SemanticColor.color_purple), ) - } - } - } - } - - @Composable - private fun CreateAllTimeContent(modifier: Modifier, state: ViewState) { - Row( - modifier = modifier.fillMaxWidth(), - horizontalArrangement = Arrangement.SpaceEvenly, - ) { - Column( - modifier = Modifier.weight(1f), - verticalArrangement = Arrangement.spacedBy(8.dp), - ) { - Text( - text = state.localizer.localize("APP.GENERAL.TIME_STRINGS.ALL_TIME"), - style = TextStyle.dydxDefault - .themeFont(fontSize = ThemeFont.FontSize.small), - ) - Row( - verticalAlignment = Alignment.CenterVertically, - horizontalArrangement = Arrangement.spacedBy(4.dp), - ) { + Spacer(modifier = Modifier.width(6.dp)) Text( - text = state.summary?.rewardsAllTimeText ?: "-", + text = state.countdownText, style = TextStyle.dydxDefault - .themeFont(fontSize = ThemeFont.FontSize.medium) - .themeColor(ThemeColor.SemanticColor.text_primary), + .themeFont(fontSize = ThemeFont.FontSize.small) + .themeColor(foreground = ThemeColor.SemanticColor.text_primary), ) - if (state.nativeTokenLogoUrl != null && state.summary?.rewardsAllTimeText != null) { - PlatformImage( - icon = state.nativeTokenLogoUrl, - modifier = Modifier.size(24.dp), - ) - } } } } diff --git a/android/v4/feature/profile/src/main/java/exchange/dydx/trading/feature/profile/components/DydxProfileRewardsViewModel.kt b/android/v4/feature/profile/src/main/java/exchange/dydx/trading/feature/profile/components/DydxProfileRewardsViewModel.kt index 80b3fdcf..557f047a 100644 --- a/android/v4/feature/profile/src/main/java/exchange/dydx/trading/feature/profile/components/DydxProfileRewardsViewModel.kt +++ b/android/v4/feature/profile/src/main/java/exchange/dydx/trading/feature/profile/components/DydxProfileRewardsViewModel.kt @@ -2,71 +2,79 @@ package exchange.dydx.trading.feature.profile.components import androidx.lifecycle.ViewModel import dagger.hilt.android.lifecycle.HiltViewModel -import exchange.dydx.abacus.output.account.Account import exchange.dydx.abacus.protocols.LocalizerProtocol -import exchange.dydx.abacus.state.manager.HistoricalTradingRewardsPeriod -import exchange.dydx.dydxstatemanager.AbacusStateManagerProtocol -import exchange.dydx.dydxstatemanager.nativeTokenLogoUrl +import exchange.dydx.dydxstatemanager.localizeWithParams import exchange.dydx.trading.common.DydxViewModel -import exchange.dydx.trading.common.formatter.DydxFormatter -import exchange.dydx.trading.common.navigation.DydxRouter -import exchange.dydx.trading.common.navigation.ProfileRoutes import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.distinctUntilChanged -import kotlinx.coroutines.flow.map -import java.time.Instant +import kotlinx.coroutines.flow.flow +import kotlinx.coroutines.delay +import java.time.Duration +import java.time.ZoneOffset +import java.time.ZonedDateTime +import java.time.format.TextStyle +import java.util.Locale import javax.inject.Inject @HiltViewModel class DydxProfileRewardsViewModel @Inject constructor( val localizer: LocalizerProtocol, - private val abacusStateManager: AbacusStateManagerProtocol, - private val formatter: DydxFormatter, - private val router: DydxRouter, ) : ViewModel(), DydxViewModel { - init { - abacusStateManager.setHistoricalTradingRewardsPeriod(HistoricalTradingRewardsPeriod.WEEKLY) - } - - val state: Flow = abacusStateManager.state.account - .map { - createViewState(it) + val state: Flow = flow { + while (true) { + val now = ZonedDateTime.now(ZoneOffset.UTC) + emit( + DydxProfileRewardsView.ViewState( + localizer = localizer, + title = localizer.localize("APP.GENERAL.LIQUIDATION_REBATES"), + activeBadgeText = localizer.localize("APP.GENERAL.ACTIVE"), + bodyText = buildBodyText(), + countdownLabel = localizer.localizeWithParams( + path = "APP.REWARDS_SURGE_APRIL_2025.MONTH_COUNTDOWN", + params = mapOf("MONTH" to currentMonthName(now)), + ), + countdownText = formatCountdown(remainingUntilNextMonthUtc(now)), + ), + ) + delay(1000L) } - .distinctUntilChanged() + }.distinctUntilChanged() - private fun createViewState(account: Account?): DydxProfileRewardsView.ViewState { - val rewards = account?.tradingRewards - val total = rewards?.total - val thisWeek = rewards?.filledHistory?.get("WEEKLY")?.lastOrNull() - val thisWeekAmount = thisWeek?.amount - val thisWeekStart = thisWeek?.startedAtInMilliseconds?.toLong()?.let { - Instant.ofEpochMilli(it) - } - val thisWeekStartText = thisWeekStart?.let { formatter.utcDate(it) } - val thisWeekEnd = thisWeek?.endedAtInMilliseconds?.toLong()?.let { - Instant.ofEpochMilli(it) - } - val thisWeekEndText = thisWeekEnd?.let { formatter.utcDate(it) } - return DydxProfileRewardsView.ViewState( - localizer = localizer, - summary = DydxRewardsSummaryState( - titleText = localizer.localize("APP.GENERAL.TRADING_REWARDS"), - rewards7DaysText = formatter.raw(thisWeekAmount, 6), - range7DaysText = thisWeekStartText?.let { start -> - thisWeekEndText?.let { end -> - "$start - $end" - } ?: "$start - " - } ?: "", - rewardsAllTimeText = formatter.raw(total, 6), + private fun buildBodyText(): String { + val body = localizer.localize("APP.REWARDS_SURGE_APRIL_2025.LIQUIDATION_REBATES_BODY") + val subBody = localizer.localizeWithParams( + path = "APP.REWARDS_SURGE_APRIL_2025.LIQUIDATION_REBATES_SUB_BODY", + params = mapOf( + "LOSS_REBATES_LINK" to localizer.localize("APP.REWARDS_SURGE_APRIL_2025.LOSS_REBATES"), + "CHECK_ELIGIBILITY_LINK" to localizer.localize("APP.GENERAL.HERE"), ), - nativeTokenLogoUrl = abacusStateManager.nativeTokenLogoUrl, - onTapAction = { - router.navigateTo( - route = ProfileRoutes.rewards, - presentation = DydxRouter.Presentation.Push, - ) - }, ) + return "$body $subBody" + } + + private fun currentMonthName(now: ZonedDateTime): String { + return now.month.getDisplayName(TextStyle.FULL, Locale.ENGLISH) + } + + private fun remainingUntilNextMonthUtc(now: ZonedDateTime): Duration { + val nextMonthStart = now + .plusMonths(1) + .withDayOfMonth(1) + .withHour(0) + .withMinute(0) + .withSecond(0) + .withNano(0) + val duration = Duration.between(now, nextMonthStart) + return if (duration.isNegative) Duration.ZERO else duration + } + + private fun formatCountdown(duration: Duration): String { + val totalSeconds = duration.seconds + val days = totalSeconds / 86_400 + val hours = (totalSeconds % 86_400) / 3_600 + val minutes = (totalSeconds % 3_600) / 60 + val seconds = totalSeconds % 60 + return "${days}d ${hours}h ${minutes}m ${seconds}s" } } diff --git a/ios/dydx/dydxPresenters/dydxPresenters/_v4/Profile/Components/dydxProfileRewardsViewPresenter.swift b/ios/dydx/dydxPresenters/dydxPresenters/_v4/Profile/Components/dydxProfileRewardsViewPresenter.swift index de6f51a1..624a84fd 100644 --- a/ios/dydx/dydxPresenters/dydxPresenters/_v4/Profile/Components/dydxProfileRewardsViewPresenter.swift +++ b/ios/dydx/dydxPresenters/dydxPresenters/_v4/Profile/Components/dydxProfileRewardsViewPresenter.swift @@ -7,48 +7,30 @@ import Utilities import dydxViews -import PlatformParticles -import RoutingKit import ParticlesKit import PlatformUI -import Abacus -import dydxStateManager -import dydxFormatter -import Combine public protocol dydxProfileRewardsViewPresenterProtocol: HostedViewPresenterProtocol { var viewModel: dydxProfileRewardsViewModel? { get } } public class dydxProfileRewardsViewPresenter: HostedViewPresenter, dydxProfileRewardsViewPresenterProtocol { + private static let liquidationRebatesUrl = "https://dydx.trade/DYDX" + override init() { super.init() viewModel = dydxProfileRewardsViewModel() - - viewModel?.tapAction = { - Router.shared?.navigate(to: RoutingRequest(path: "/profile/trading-rewards"), animated: true, completion: nil) + viewModel?.tapAction = { [weak self] in + self?.openLiquidationRebatesUrl() } - - AbacusStateManager.shared.setHistoricalTradingRewardPeriod(period: HistoricalTradingRewardsPeriod.weekly) } - public override func start() { - super.start() - - AbacusStateManager.shared.state.account - .sink { [weak self] account in - if let amount = account?.tradingRewards?.total?.doubleValue { - self?.viewModel?.allTimeRewardsAmount = dydxFormatter.shared.raw(number: NSNumber(value: amount), digits: 4) - } else { - self?.viewModel?.allTimeRewardsAmount = nil - } - if let amount = account?.tradingRewards?.filledHistory?["WEEKLY"]?.first?.amount { - self?.viewModel?.last7DaysRewardsAmount = dydxFormatter.shared.raw(number: NSNumber(value: amount), digits: 4) - } else { - self?.viewModel?.last7DaysRewardsAmount = nil - } - } - .store(in: &subscriptions) + private func openLiquidationRebatesUrl() { + guard let url = URL(string: Self.liquidationRebatesUrl), + URLHandler.shared?.canOpenURL(url) ?? false else { + return + } + URLHandler.shared?.open(url, completionHandler: nil) } } diff --git a/ios/dydx/dydxViews/dydxViews/_v4/Profile/Components/dydxProfileRewardsViewModel.swift b/ios/dydx/dydxViews/dydxViews/_v4/Profile/Components/dydxProfileRewardsViewModel.swift index ee5298c3..13647a38 100644 --- a/ios/dydx/dydxViews/dydxViews/_v4/Profile/Components/dydxProfileRewardsViewModel.swift +++ b/ios/dydx/dydxViews/dydxViews/_v4/Profile/Components/dydxProfileRewardsViewModel.swift @@ -9,50 +9,130 @@ import SwiftUI import PlatformUI import Utilities +import Combine public class dydxProfileRewardsViewModel: dydxTitledCardViewModel { - @Published public var last7DaysRewardsAmount: String? - @Published public var allTimeRewardsAmount: String? + @Published public var countdownText: String = "-" + + private var timerCancellable: AnyCancellable? public init() { - super.init(title: DataLocalizer.shared?.localize(path: "APP.GENERAL.TRADING_REWARDS", params: nil) ?? "") + super.init(title: DataLocalizer.localize(path: "APP.GENERAL.LIQUIDATION_REBATES"), + verticalContentPadding: 16, + horizontalContentPadding: 16) + updateCountdown() + timerCancellable = Timer.publish(every: 1.0, on: .main, in: .common) + .autoconnect() + .sink { [weak self] _ in + self?.updateCountdown() + } } - override func createContentView(parentStyle: ThemeStyle = ThemeStyle.defaultStyle, styleKey: String? = nil) -> AnyView? { - HStack(spacing: 0) { - VStack(alignment: .leading, spacing: 10) { - titleValueStack(title: DataLocalizer.shared?.localize(path: "APP.GENERAL.TIME_STRINGS.THIS_WEEK", params: nil) ?? "", value: last7DaysRewardsAmount) - if let allTimeRewardsAmount = allTimeRewardsAmount { - titleValueStack(title: DataLocalizer.shared?.localize(path: "APP.GENERAL.TIME_STRINGS.ALL_TIME", params: nil) ?? "", value: allTimeRewardsAmount) - } - } - Spacer() + deinit { + timerCancellable?.cancel() + } + + private func updateCountdown() { + var calendar = Calendar(identifier: .gregorian) + calendar.timeZone = TimeZone(identifier: "UTC") ?? .current + + let now = Date() + let nowComponents = calendar.dateComponents([.year, .month], from: now) + guard let year = nowComponents.year, let month = nowComponents.month else { + countdownText = "-" + return } - .wrappedInAnyView() + + var nextMonthComponents = DateComponents() + nextMonthComponents.year = month == 12 ? year + 1 : year + nextMonthComponents.month = month == 12 ? 1 : month + 1 + nextMonthComponents.day = 1 + nextMonthComponents.hour = 0 + nextMonthComponents.minute = 0 + nextMonthComponents.second = 0 + + guard let nextMonth = calendar.date(from: nextMonthComponents) else { + countdownText = "-" + return + } + + let interval = max(0, Int(nextMonth.timeIntervalSince(now))) + let days = interval / 86400 + let hours = (interval % 86400) / 3600 + let minutes = (interval % 3600) / 60 + let seconds = interval % 60 + + countdownText = String(format: "%dd %dh %dm %ds", days, hours, minutes, seconds) } - private func titleValueStack(title: String, value: String?) -> some View { - VStack(alignment: .leading, spacing: 4) { - Text(title) - .themeColor(foreground: .textSecondary) - .themeFont(fontType: .base, fontSize: .smaller) - HStack(spacing: 6) { - Text(value ?? "-") - .themeColor(foreground: .textPrimary) - .themeFont(fontType: .base, fontSize: .small) - .lineLimit(1) - .minimumScaleFactor(0.5) - PlatformIconViewModel(type: .asset(name: "icon_dydx", bundle: .dydxView), clip: .noClip, size: .init(width: 24, height: 24), templateColor: nil) + override func createTitleAccessoryView(parentStyle: ThemeStyle = ThemeStyle.defaultStyle, styleKey: String? = nil) -> AnyView? { + Text(DataLocalizer.localize(path: "APP.GENERAL.ACTIVE")) + .themeColor(foreground: .colorGreen) + .themeFont(fontType: .base, fontSize: .smaller) + .padding(.horizontal, 6) + .padding(.vertical, 4) + .border(borderWidth: 1, cornerRadius: 4, borderColor: ThemeColor.SemanticColor.colorGreen.color) + .wrappedInAnyView() + } + + override func createContentView(parentStyle: ThemeStyle = ThemeStyle.defaultStyle, styleKey: String? = nil) -> AnyView? { + VStack(alignment: .leading, spacing: 12) { + Text(Self.localizedBody) + .themeFont(fontType: .base, fontSize: .small) + .themeColor(foreground: .textTertiary) + .fixedSize(horizontal: false, vertical: true) + + HStack(spacing: 8) { + PlatformIconViewModel(type: .system(name: "clock"), + size: CGSize(width: 14, height: 14), + templateColor: .colorPurple) .createView() + Text(Self.localizedCountdownLabel) + .themeFont(fontType: .base, fontSize: .small) + .themeColor(foreground: .colorPurple) + Text(countdownText) + .themeFont(fontType: .base, fontSize: .small) + .themeColor(foreground: .textPrimary) } + .padding(.horizontal, 12) + .padding(.vertical, 8) + .themeColor(background: .layer4) + .cornerRadius(20) } + .frame(maxWidth: .infinity, alignment: .leading) + .wrappedInAnyView() + } + + private static var localizedBody: String { + let body = DataLocalizer.localize(path: "APP.REWARDS_SURGE_APRIL_2025.LIQUIDATION_REBATES_BODY") + let subBody = DataLocalizer.localize( + path: "APP.REWARDS_SURGE_APRIL_2025.LIQUIDATION_REBATES_SUB_BODY", + params: [ + "LOSS_REBATES_LINK": DataLocalizer.localize(path: "APP.REWARDS_SURGE_APRIL_2025.LOSS_REBATES"), + "CHECK_ELIGIBILITY_LINK": DataLocalizer.localize(path: "APP.GENERAL.HERE"), + ] + ) + return "\(body) \(subBody)" + } + private static var localizedCountdownLabel: String { + var calendar = Calendar(identifier: .gregorian) + calendar.timeZone = TimeZone(identifier: "UTC") ?? .current + let formatter = DateFormatter() + formatter.calendar = calendar + formatter.timeZone = calendar.timeZone + formatter.locale = Locale(identifier: "en_US") + formatter.dateFormat = "MMMM" + let monthName = formatter.string(from: Date()) + return DataLocalizer.localize( + path: "APP.REWARDS_SURGE_APRIL_2025.MONTH_COUNTDOWN", + params: ["MONTH": monthName] + ) } public static var previewValue: dydxProfileRewardsViewModel { let vm = dydxProfileRewardsViewModel() - vm.last7DaysRewardsAmount = "20.00" - vm.allTimeRewardsAmount = "30.00" + vm.countdownText = "0d 9h 51m 46s" return vm } diff --git a/ios/dydx/dydxViews/dydxViews/_v4/Profile/dydxProfileView.swift b/ios/dydx/dydxViews/dydxViews/_v4/Profile/dydxProfileView.swift index da678bc9..8e1df029 100644 --- a/ios/dydx/dydxViews/dydxViews/_v4/Profile/dydxProfileView.swift +++ b/ios/dydx/dydxViews/dydxViews/_v4/Profile/dydxProfileView.swift @@ -59,12 +59,11 @@ public class dydxProfileViewModel: PlatformViewModel { self.balances? .createView(parentStyle: style) - HStack(spacing: 14) { - self.fees? - .createView(parentStyle: style) - self.rewards? - .createView(parentStyle: style) - } + self.fees? + .createView(parentStyle: style) + + self.rewards? + .createView(parentStyle: style) self.history? .createView(parentStyle: style) From 560a3ab2832b0194fb9d3cb2f50d78e14301dce1 Mon Sep 17 00:00:00 2001 From: Sam Newby Date: Thu, 30 Apr 2026 11:33:34 -0400 Subject: [PATCH 3/5] Bump app version --- android/app/build.gradle | 2 +- ios/dydxV4/dydxV4.xcodeproj/project.pbxproj | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/android/app/build.gradle b/android/app/build.gradle index cd357563..e505a3c1 100644 --- a/android/app/build.gradle +++ b/android/app/build.gradle @@ -26,7 +26,7 @@ android { minSdkVersion parent.minSdkVersion targetSdkVersion parent.targetSdkVersion versionCode 10000 - versionName "1.13.2" + versionName "1.13.3" testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" diff --git a/ios/dydxV4/dydxV4.xcodeproj/project.pbxproj b/ios/dydxV4/dydxV4.xcodeproj/project.pbxproj index 0e3464fc..3d67b533 100644 --- a/ios/dydxV4/dydxV4.xcodeproj/project.pbxproj +++ b/ios/dydxV4/dydxV4.xcodeproj/project.pbxproj @@ -2923,7 +2923,7 @@ "$(inherited)", "@executable_path/Frameworks", ); - MARKETING_VERSION = 1.13.4; + MARKETING_VERSION = 1.13.5; PRODUCT_NAME = "$(TARGET_NAME)"; SWIFT_ACTIVE_COMPILATION_CONDITIONS = "$(inherited) _iOS"; TARGETED_DEVICE_FAMILY = 1; @@ -2946,7 +2946,7 @@ "$(inherited)", "@executable_path/Frameworks", ); - MARKETING_VERSION = 1.13.4; + MARKETING_VERSION = 1.13.5; PRODUCT_NAME = "$(TARGET_NAME)"; PROVISIONING_PROFILE_SPECIFIER = ""; SWIFT_ACTIVE_COMPILATION_CONDITIONS = "$(inherited) _iOS"; From c0e66e14a260ce1269941335f7bd72fea52df0d6 Mon Sep 17 00:00:00 2001 From: Sam Newby Date: Thu, 30 Apr 2026 11:54:27 -0400 Subject: [PATCH 4/5] Fix linting issues --- .../feature/profile/components/DydxProfileRewardsViewModel.kt | 2 +- .../_v4/Profile/Components/dydxProfileRewardsViewModel.swift | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/android/v4/feature/profile/src/main/java/exchange/dydx/trading/feature/profile/components/DydxProfileRewardsViewModel.kt b/android/v4/feature/profile/src/main/java/exchange/dydx/trading/feature/profile/components/DydxProfileRewardsViewModel.kt index 557f047a..f60e2bb6 100644 --- a/android/v4/feature/profile/src/main/java/exchange/dydx/trading/feature/profile/components/DydxProfileRewardsViewModel.kt +++ b/android/v4/feature/profile/src/main/java/exchange/dydx/trading/feature/profile/components/DydxProfileRewardsViewModel.kt @@ -5,10 +5,10 @@ import dagger.hilt.android.lifecycle.HiltViewModel import exchange.dydx.abacus.protocols.LocalizerProtocol import exchange.dydx.dydxstatemanager.localizeWithParams import exchange.dydx.trading.common.DydxViewModel +import kotlinx.coroutines.delay import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.distinctUntilChanged import kotlinx.coroutines.flow.flow -import kotlinx.coroutines.delay import java.time.Duration import java.time.ZoneOffset import java.time.ZonedDateTime diff --git a/ios/dydx/dydxViews/dydxViews/_v4/Profile/Components/dydxProfileRewardsViewModel.swift b/ios/dydx/dydxViews/dydxViews/_v4/Profile/Components/dydxProfileRewardsViewModel.swift index 13647a38..93dcc1f4 100644 --- a/ios/dydx/dydxViews/dydxViews/_v4/Profile/Components/dydxProfileRewardsViewModel.swift +++ b/ios/dydx/dydxViews/dydxViews/_v4/Profile/Components/dydxProfileRewardsViewModel.swift @@ -109,7 +109,7 @@ public class dydxProfileRewardsViewModel: dydxTitledCardViewModel { path: "APP.REWARDS_SURGE_APRIL_2025.LIQUIDATION_REBATES_SUB_BODY", params: [ "LOSS_REBATES_LINK": DataLocalizer.localize(path: "APP.REWARDS_SURGE_APRIL_2025.LOSS_REBATES"), - "CHECK_ELIGIBILITY_LINK": DataLocalizer.localize(path: "APP.GENERAL.HERE"), + "CHECK_ELIGIBILITY_LINK": DataLocalizer.localize(path: "APP.GENERAL.HERE") ] ) return "\(body) \(subBody)" From 61816da8752697abe11de5910c4231b394e885fc Mon Sep 17 00:00:00 2001 From: Sam Newby Date: Mon, 4 May 2026 13:18:23 -0400 Subject: [PATCH 5/5] Update mock text to may --- .../profile/components/DydxProfileRewardsView.kt | 10 +++------- .../profile/components/DydxProfileRewardsViewModel.kt | 9 +++++++++ 2 files changed, 12 insertions(+), 7 deletions(-) diff --git a/android/v4/feature/profile/src/main/java/exchange/dydx/trading/feature/profile/components/DydxProfileRewardsView.kt b/android/v4/feature/profile/src/main/java/exchange/dydx/trading/feature/profile/components/DydxProfileRewardsView.kt index 6f912a01..feaf83a1 100644 --- a/android/v4/feature/profile/src/main/java/exchange/dydx/trading/feature/profile/components/DydxProfileRewardsView.kt +++ b/android/v4/feature/profile/src/main/java/exchange/dydx/trading/feature/profile/components/DydxProfileRewardsView.kt @@ -18,7 +18,6 @@ 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.platform.LocalUriHandler import androidx.compose.ui.res.painterResource import androidx.compose.ui.text.TextStyle import androidx.compose.ui.tooling.preview.Preview @@ -47,8 +46,6 @@ fun Preview_DydxProfileRewardsView() { } object DydxProfileRewardsView : DydxComponent { - private const val LIQUIDATION_REBATES_URL = "https://dydx.trade/DYDX" - data class ViewState( val localizer: LocalizerProtocol, val title: String, @@ -56,6 +53,7 @@ object DydxProfileRewardsView : DydxComponent { val bodyText: String, val countdownLabel: String, val countdownText: String, + val onTapAction: (() -> Unit)? = null, ) { companion object { val preview = ViewState( @@ -63,7 +61,7 @@ object DydxProfileRewardsView : DydxComponent { title = "Liquidation Rebates", activeBadgeText = "Active", bodyText = "With the Liquidation Rebate Program, eligible traders liquidated on non-BTC perpetual pairs have the opportunity to earn DYDX rebates on a portion of their losses. Reminder to claim previous loss rebates and check eligibility here.", - countdownLabel = "Month April Countdown", + countdownLabel = "Month May Countdown", countdownText = "0d 9h 51m 46s", ) } @@ -83,14 +81,12 @@ object DydxProfileRewardsView : DydxComponent { return } - val uriHandler = LocalUriHandler.current - Column( modifier = modifier .fillMaxWidth() .clip(RoundedCornerShape(14.dp)) .background(color = ThemeColor.SemanticColor.layer_3.color) - .clickable { uriHandler.openUri(LIQUIDATION_REBATES_URL) }, + .clickable(enabled = state.onTapAction != null) { state.onTapAction?.invoke() }, ) { Row( modifier = Modifier diff --git a/android/v4/feature/profile/src/main/java/exchange/dydx/trading/feature/profile/components/DydxProfileRewardsViewModel.kt b/android/v4/feature/profile/src/main/java/exchange/dydx/trading/feature/profile/components/DydxProfileRewardsViewModel.kt index f60e2bb6..d72fe50e 100644 --- a/android/v4/feature/profile/src/main/java/exchange/dydx/trading/feature/profile/components/DydxProfileRewardsViewModel.kt +++ b/android/v4/feature/profile/src/main/java/exchange/dydx/trading/feature/profile/components/DydxProfileRewardsViewModel.kt @@ -5,6 +5,7 @@ import dagger.hilt.android.lifecycle.HiltViewModel import exchange.dydx.abacus.protocols.LocalizerProtocol import exchange.dydx.dydxstatemanager.localizeWithParams import exchange.dydx.trading.common.DydxViewModel +import exchange.dydx.trading.common.navigation.DydxRouter import kotlinx.coroutines.delay import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.distinctUntilChanged @@ -19,6 +20,7 @@ import javax.inject.Inject @HiltViewModel class DydxProfileRewardsViewModel @Inject constructor( val localizer: LocalizerProtocol, + private val router: DydxRouter, ) : ViewModel(), DydxViewModel { val state: Flow = flow { @@ -35,6 +37,9 @@ class DydxProfileRewardsViewModel @Inject constructor( params = mapOf("MONTH" to currentMonthName(now)), ), countdownText = formatCountdown(remainingUntilNextMonthUtc(now)), + onTapAction = { + router.navigateTo(LIQUIDATION_REBATES_URL) + }, ), ) delay(1000L) @@ -77,4 +82,8 @@ class DydxProfileRewardsViewModel @Inject constructor( val seconds = totalSeconds % 60 return "${days}d ${hours}h ${minutes}m ${seconds}s" } + + private companion object { + const val LIQUIDATION_REBATES_URL = "https://dydx.trade/DYDX" + } }