Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions apps/flipcash/features/userflags/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -8,5 +8,6 @@ android {

dependencies {
implementation(project(":apps:flipcash:shared:userflags"))
implementation(project(":apps:flipcash:shared:region-selection:core"))
implementation(project(":libs:messaging"))
}
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ import com.flipcash.app.core.data.Loadable
import com.getcode.navigation.scrim.LocalScrimController
import com.flipcash.app.userflags.internal.components.EditableRow
import com.flipcash.app.userflags.internal.components.ReadOnlyRow
import com.flipcash.app.userflags.internal.components.ReadOnlyTextRow
import com.getcode.theme.CodeTheme
import com.getcode.ui.components.text.SectionHeader
import com.getcode.ui.core.verticalScrollStateGradient
Expand Down Expand Up @@ -82,6 +83,13 @@ private fun UserFlagsEditor(
) { entry ->
ReadOnlyRow(entry)
}
items(
items = state.readOnlyTextEntries,
key = { it.label },
contentType = { "read_only_text_row" },
) { entry ->
ReadOnlyTextRow(entry)
}

// Editable section
item(contentType = "section_header") { SectionHeader("Overridable") }
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,11 @@ import com.flipcash.app.userflags.ResolvedFlag
import com.flipcash.app.userflags.ResolvedUserFlags
import com.flipcash.app.userflags.UserFlagsCoordinator
import com.flipcash.app.userflags.internal.components.FieldEditorSheet
import com.flipcash.app.currency.PreferredCurrencyController
import com.flipcash.features.userflags.R
import com.flipcash.libs.coroutines.DispatcherProvider
import com.getcode.opencode.model.financial.CurrencyCode
import com.getcode.opencode.model.financial.Fiat
import com.getcode.view.BaseViewModel
import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.flow.filterIsInstance
Expand All @@ -21,6 +24,7 @@ import javax.inject.Inject
@HiltViewModel
internal class UserFlagsViewModel @Inject constructor(
flagsCoordinator: UserFlagsCoordinator,
preferredCurrencyController: PreferredCurrencyController,
dispatchers: DispatcherProvider,
) : BaseViewModel<UserFlagsViewModel.State, UserFlagsViewModel.Event>(
initialState = State(),
Expand All @@ -29,6 +33,7 @@ internal class UserFlagsViewModel @Inject constructor(
) {
internal data class State(
val flags: Loadable<ResolvedUserFlags> = Loadable.Loading(),
val preferredCurrency: String? = null,
) {
val readOnlyEntries: List<ReadOnlyEntry>
get() = when (flags) {
Expand All @@ -54,6 +59,30 @@ internal class UserFlagsViewModel @Inject constructor(
else -> emptyList()
}

val readOnlyTextEntries: List<ReadOnlyTextEntry>
get() = when (flags) {
is Loadable.Loaded -> {
val code = preferredCurrency?.let { CurrencyCode.tryValueOf(it) } ?: CurrencyCode.USD
val presets = flags.data.tipPresets.effectiveValue
.firstOrNull { it.region.equals(code.name, ignoreCase = true) }
fun format(amount: Double) = Fiat(fiat = amount, currencyCode = code).formatted()
listOf(
ReadOnlyTextEntry(
R.string.label_flag_tipPresets,
if (presets == null) "None"
else listOf(
"Min ${format(presets.minimum)}",
"Low ${format(presets.low)}",
"Med ${format(presets.medium)}",
"High ${format(presets.high)}",
).joinToString(separator = "\n")
),
)
}

else -> emptyList()
}

val editableEntries: List<EditableEntry<*>>
get() = when (flags) {
is Loadable.Loaded -> flags.data.editableEntries()
Expand All @@ -63,6 +92,7 @@ internal class UserFlagsViewModel @Inject constructor(

internal sealed interface Event {
data class FlagsUpdated(val flags: Loadable<ResolvedUserFlags>) : Event
data class PreferredCurrencyUpdated(val currencyCode: String) : Event
data class UpdateFlag<Stored, Domain>(
val field: Field<Stored, Domain>,
val value: Domain,
Expand All @@ -81,6 +111,10 @@ internal class UserFlagsViewModel @Inject constructor(
.onEach { dispatchEvent(Event.FlagsUpdated(Loadable.Loaded(it))) }
.launchIn(viewModelScope)

preferredCurrencyController.observePreferredCurrency()
.onEach { dispatchEvent(Event.PreferredCurrencyUpdated(it)) }
.launchIn(viewModelScope)

eventFlow
.filterIsInstance<Event.UpdateFlag<*, *>>()
.onEach { event ->
Expand All @@ -105,6 +139,10 @@ internal class UserFlagsViewModel @Inject constructor(
state.copy(flags = event.flags)
}

is Event.PreferredCurrencyUpdated -> { state ->
state.copy(preferredCurrency = event.currencyCode)
}

is Event.UpdateFlag<*, *> -> { state -> state }
is Event.ResetFlag<*> -> { state -> state }
is Event.Reset -> { state -> state }
Expand All @@ -115,6 +153,8 @@ internal class UserFlagsViewModel @Inject constructor(

internal data class ReadOnlyEntry(@get:StringRes val label: Int, val value: Boolean)

internal data class ReadOnlyTextEntry(@get:StringRes val label: Int, val value: String)

internal data class EditableEntry<Domain>(
val field: Field<*, Domain>,
val resolved: ResolvedFlag<Domain>,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
package com.flipcash.app.userflags.internal.components

import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.padding
import androidx.compose.material3.HorizontalDivider
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
import com.flipcash.app.userflags.internal.ReadOnlyTextEntry
import com.getcode.theme.CodeTheme

@Composable
internal fun ReadOnlyTextRow(
entry: ReadOnlyTextEntry,
modifier: Modifier = Modifier
) {
Column(modifier = modifier) {
Column(
modifier = Modifier.padding(
horizontal = CodeTheme.dimens.grid.x3,
vertical = CodeTheme.dimens.grid.x3,
),
) {
Text(
text = stringResource(entry.label),
style = CodeTheme.typography.textLarge,
color = CodeTheme.colors.textMain,
)
Text(
text = entry.value,
style = CodeTheme.typography.textSmall,
color = CodeTheme.colors.textSecondary,
)
}

HorizontalDivider(
modifier = Modifier.padding(horizontal = CodeTheme.dimens.inset),
color = CodeTheme.colors.divider,
thickness = 0.5.dp,
)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package com.flipcash.app.userflags
import com.flipcash.app.userflags.UserFlagsCoordinator.Overrides
import com.flipcash.services.internal.model.thirdparty.OnRampProvider
import com.flipcash.services.internal.model.thirdparty.UsdcLiquidtyPool
import com.flipcash.services.models.TipPresets
import com.flipcash.services.models.UserFlags
import com.getcode.opencode.model.financial.Fiat
import kotlin.time.Duration
Expand Down Expand Up @@ -34,6 +35,7 @@ data class ResolvedUserFlags(
val enablePhoneNumberSend: ResolvedFlag<Boolean>,
val minimumHolderAmountForLeaderboard: ResolvedFlag<Fiat>,
val requireCoinbaseEmailVerification: ResolvedFlag<Boolean>,
val tipPresets: ResolvedFlag<List<TipPresets>>,
)

internal fun UserFlags.resolve(overrides: Overrides): ResolvedUserFlags = ResolvedUserFlags(
Expand All @@ -51,4 +53,5 @@ internal fun UserFlags.resolve(overrides: Overrides): ResolvedUserFlags = Resolv
enablePhoneNumberSend = ResolvedFlag(enablePhoneNumberSend, FieldOverride.None),
minimumHolderAmountForLeaderboard = ResolvedFlag(minimumHolderValue, overrides.minimumHolderAmountForLeaderboard),
requireCoinbaseEmailVerification = ResolvedFlag(requireCoinbaseEmailVerification, overrides.requireCoinbaseEmailVerification),
tipPresets = ResolvedFlag(tipPresets, FieldOverride.None),
)
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import com.flipcash.libs.coroutines.DispatcherProvider
import com.flipcash.services.internal.model.thirdparty.OnRampProvider
import com.flipcash.services.internal.model.thirdparty.OnRampType
import com.flipcash.services.internal.model.thirdparty.UsdcLiquidtyPool
import com.flipcash.services.models.TipPresets
import com.flipcash.services.models.UserFlags
import com.flipcash.services.user.UserManager
import com.getcode.opencode.model.financial.CurrencyCode
Expand Down Expand Up @@ -179,6 +180,7 @@ private data class CachedFlags(
val enablePhoneNumberSend: Boolean,
val minimumHolderValue: CachedFiat,
val requireCoinbaseEmailVerification: Boolean,
val tipPresets: List<CachedTipPresets> = emptyList(),
) {
fun toDomain(): UserFlags = UserFlags(
isStaff = isStaff,
Expand All @@ -197,6 +199,7 @@ private data class CachedFlags(
enablePhoneNumberSend = enablePhoneNumberSend,
minimumHolderValue = minimumHolderValue.toDomain(),
requireCoinbaseEmailVerification = requireCoinbaseEmailVerification,
tipPresets = tipPresets.map { it.toDomain() },
)

companion object {
Expand All @@ -219,6 +222,34 @@ private data class CachedFlags(
enablePhoneNumberSend = flags.enablePhoneNumberSend,
minimumHolderValue = CachedFiat.fromDomain(flags.minimumHolderValue),
requireCoinbaseEmailVerification = flags.requireCoinbaseEmailVerification,
tipPresets = flags.tipPresets.map { CachedTipPresets.fromDomain(it) },
)
}
}

@Serializable
private data class CachedTipPresets(
val region: String,
val minimum: Double,
val low: Double,
val medium: Double,
val high: Double,
) {
fun toDomain(): TipPresets = TipPresets(
region = region,
minimum = minimum,
low = low,
medium = medium,
high = high,
)

companion object {
fun fromDomain(presets: TipPresets): CachedTipPresets = CachedTipPresets(
region = presets.region,
minimum = presets.minimum,
low = presets.low,
medium = presets.medium,
high = presets.high,
)
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,4 +21,5 @@
<string name="label_flag_requireCoinbaseEmailVerification">Require Coinbase Email Verification</string>
<string name="label_flag_minHolderAmount">Minimum Holder Amount for Leaderboard</string>
<string name="hint_flag_minHolderAmount">Enter amount</string>
<string name="label_flag_tipPresets">Tip Presets</string>
</resources>
Original file line number Diff line number Diff line change
Expand Up @@ -166,4 +166,16 @@ message UserFlags {

// Whether email verification is required for Coinbase purchase flows
bool require_coinbase_email_verification = 14;

// Tip presets for all currencies
repeated TipPresets tip_presets = 15;
}

message TipPresets {
common.v1.Region region = 1 [(validate.rules).message.required = true];

double minimum = 2;
double low = 3;
double medium = 4;
double high = 5;
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package com.flipcash.services.internal.domain

import com.codeinc.flipcash.gen.account.v1.FlipcashAccountService
import com.flipcash.services.internal.domain.mapper.Mapper
import com.flipcash.services.models.TipPresets
import com.flipcash.services.models.UserFlags
import com.flipcash.services.internal.model.thirdparty.OnRampProvider
import com.flipcash.services.internal.model.thirdparty.OnRampType
Expand Down Expand Up @@ -29,10 +30,19 @@ internal class UserFlagsMapper @Inject constructor():
enablePhoneNumberSend = from.enablePhoneNumberSend,
minimumHolderValue = Fiat(quarks = from.minimumHolderValue),
requireCoinbaseEmailVerification = from.requireCoinbaseEmailVerification,
tipPresets = from.tipPresetsList.map { it.toDomain() },
)
}
}

private fun FlipcashAccountService.TipPresets.toDomain(): TipPresets = TipPresets(
region = region.value,
minimum = minimum,
low = low,
medium = medium,
high = high,
)

private fun FlipcashAccountService.UserFlags.OnRampProvider.toDomain(): OnRampProvider {
return when (this) {
FlipcashAccountService.UserFlags.OnRampProvider.COINBASE_VIRTUAL -> OnRampProvider.Coinbase(OnRampType.Virtual)
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
package com.flipcash.services.models

/**
* Suggested tip amounts for a given currency/region, sourced from server-side [UserFlags].
*
* [region] is a lowercase 3-4 character region/currency code (matching `common.v1.Region.value`).
* The four amounts are expressed in the region's currency units.
*/
data class TipPresets(
val region: String,
val minimum: Double,
val low: Double,
val medium: Double,
val high: Double,
)
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ data class UserFlags(
val enablePhoneNumberSend: Boolean,
val minimumHolderValue: Fiat,
val requireCoinbaseEmailVerification: Boolean,
val tipPresets: List<TipPresets>,
) {
companion object {
val Default = UserFlags(
Expand All @@ -37,6 +38,7 @@ data class UserFlags(
enablePhoneNumberSend = false,
minimumHolderValue = Fiat.Zero,
requireCoinbaseEmailVerification = false,
tipPresets = emptyList(),
)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -148,4 +148,36 @@ class UserFlagsMapperTest {

assertEquals(42, result.minimumVersion)
}

@Test
fun `maps tip presets`() {
val proto = FlipcashAccountService.UserFlags.newBuilder()
.addTipPresets(
FlipcashAccountService.TipPresets.newBuilder()
.setRegion(com.codeinc.flipcash.gen.common.v1.Common.Region.newBuilder().setValue("usd"))
.setMinimum(1.0)
.setLow(2.0)
.setMedium(5.0)
.setHigh(10.0)
).build()

val result = mapper.map(proto)

assertEquals(1, result.tipPresets.size)
val presets = result.tipPresets.first()
assertEquals("usd", presets.region)
assertEquals(1.0, presets.minimum)
assertEquals(2.0, presets.low)
assertEquals(5.0, presets.medium)
assertEquals(10.0, presets.high)
}

@Test
fun `maps empty tip presets by default`() {
val proto = userFlags { }

val result = mapper.map(proto)

assertTrue(result.tipPresets.isEmpty())
}
}
Loading