Skip to content

Commit d5fa092

Browse files
authored
Merge pull request #395 from code-payments/feat/currency-selection-in-balance
feat: add currency selection in balance screen
2 parents 9d77e47 + 921e130 commit d5fa092

26 files changed

+302
-126
lines changed
Lines changed: 16 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,32 +1,39 @@
11
package com.getcode.model
22

3+
import com.getcode.network.repository.BetaOptions
4+
35
sealed interface Feature {
46
val enabled: Boolean
57
val available: Boolean
68
}
79

810
data class BuyModuleFeature(
9-
override val enabled: Boolean = false,
11+
override val enabled: Boolean = BetaOptions.Defaults.buyModuleEnabled,
1012
override val available: Boolean = false, // server driven availability
1113
): Feature
1214

1315
data class TipCardFeature(
14-
override val enabled: Boolean = false,
15-
override val available: Boolean = true, // always enabled
16+
override val enabled: Boolean = BetaOptions.Defaults.tipsEnabled,
17+
override val available: Boolean = true, // always available
1618
): Feature
1719

1820
data class TipChatFeature(
19-
override val enabled: Boolean = false,
20-
override val available: Boolean = true, // always enabled
21+
override val enabled: Boolean = BetaOptions.Defaults.tipsChatEnabled,
22+
override val available: Boolean = true, // always available
2123
): Feature
2224

2325
data class TipChatCashFeature(
24-
override val enabled: Boolean = false,
25-
override val available: Boolean = true, // always enabled
26+
override val enabled: Boolean = BetaOptions.Defaults.tipsChatCashEnabled,
27+
override val available: Boolean = true, // always available
2628
): Feature
2729

2830

2931
data class RequestKinFeature(
30-
override val enabled: Boolean = false,
31-
override val available: Boolean = true, // always enabled
32+
override val enabled: Boolean = BetaOptions.Defaults.giveRequestsEnabled,
33+
override val available: Boolean = true, // always available
34+
): Feature
35+
36+
data class BalanceCurrencyFeature(
37+
override val enabled: Boolean = BetaOptions.Defaults.balanceCurrencySelectionEnabled,
38+
override val available: Boolean = true, // always available
3239
): Feature

api/src/main/java/com/getcode/model/PrefBool.kt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,4 +38,5 @@ sealed class PrefsBool(val value: String) {
3838
data object TIPS_ENABLED : PrefsBool("tips_enabled"), BetaFlag
3939
data object TIPS_CHAT_ENABLED: PrefsBool("tips_chat_enabled"), BetaFlag
4040
data object TIPS_CHAT_CASH_ENABLED: PrefsBool("tips_chat_cash_enabled"), BetaFlag
41+
data object BALANCE_CURRENCY_SELECTION_ENABLED: PrefsBool("balance_currency_enabled"), BetaFlag
4142
}

api/src/main/java/com/getcode/model/PrefString.kt

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,5 +14,6 @@ enum class PrefsString(val value: String) {
1414
KEY_DATA_CONTAINER_ID("data_container_id"),
1515
KEY_CURRENCY_SELECTED("currency_selected"),//keep
1616
KEY_CURRENCIES_RECENT("currencies_recent"),//keep
17-
KEY_TIP_ACCOUNT("tip_account")
17+
KEY_TIP_ACCOUNT("tip_account"),
18+
KEY_BALANCE_CURRENCY_SELECTED("balance_currency_selected"),//keep
1819
}

api/src/main/java/com/getcode/network/BalanceController.kt

Lines changed: 71 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,25 @@
11
package com.getcode.network
22

33
import android.content.Context
4+
import android.icu.text.NumberFormat
5+
import android.icu.text.NumberFormat.INTEGERSTYLE
46
import com.getcode.manager.SessionManager
57
import com.getcode.model.Currency
68
import com.getcode.model.CurrencyCode
9+
import com.getcode.model.KinAmount
10+
import com.getcode.model.PrefsString
711
import com.getcode.model.Rate
812
import com.getcode.network.client.TransactionReceiver
913
import com.getcode.network.exchange.Exchange
1014
import com.getcode.network.repository.AccountRepository
1115
import com.getcode.network.repository.BalanceRepository
16+
import com.getcode.network.repository.PrefRepository
1217
import com.getcode.network.repository.TransactionRepository
1318
import com.getcode.solana.organizer.Organizer
1419
import com.getcode.solana.organizer.Tray
1520
import com.getcode.utils.FormatUtils
1621
import com.getcode.utils.network.NetworkConnectivityListener
22+
import com.getcode.utils.startupLog
1723
import dagger.hilt.android.qualifiers.ApplicationContext
1824
import io.reactivex.rxjava3.core.Completable
1925
import kotlinx.coroutines.CoroutineScope
@@ -22,59 +28,85 @@ import kotlinx.coroutines.flow.Flow
2228
import kotlinx.coroutines.flow.MutableStateFlow
2329
import kotlinx.coroutines.flow.SharedFlow
2430
import kotlinx.coroutines.flow.SharingStarted
31+
import kotlinx.coroutines.flow.StateFlow
2532
import kotlinx.coroutines.flow.combine
2633
import kotlinx.coroutines.flow.distinctUntilChanged
34+
import kotlinx.coroutines.flow.filterNotNull
35+
import kotlinx.coroutines.flow.flatMapLatest
36+
import kotlinx.coroutines.flow.flowOf
2737
import kotlinx.coroutines.flow.flowOn
2838
import kotlinx.coroutines.flow.launchIn
2939
import kotlinx.coroutines.flow.map
3040
import kotlinx.coroutines.flow.mapNotNull
3141
import kotlinx.coroutines.flow.onEach
3242
import kotlinx.coroutines.flow.stateIn
43+
import kotlinx.coroutines.launch
3344
import timber.log.Timber
3445
import java.util.Locale
3546
import java.util.concurrent.TimeUnit
3647
import javax.inject.Inject
48+
import kotlin.math.roundToInt
3749

3850
data class BalanceDisplay(
39-
val flag: Int? = null,
4051
val marketValue: Double = 0.0,
4152
val formattedValue: String = "",
53+
val currency: Currency? = null,
4254

4355
)
44-
class BalanceController @Inject constructor(
56+
open class BalanceController @Inject constructor(
4557
exchange: Exchange,
4658
networkObserver: NetworkConnectivityListener,
47-
getCurrency: suspend (rates: Map<CurrencyCode, Rate>) -> Currency,
59+
getCurrency: suspend (rates: Map<CurrencyCode, Rate>, selected: String?) -> Currency,
4860
@ApplicationContext
4961
private val context: Context,
5062
private val balanceRepository: BalanceRepository,
5163
private val transactionRepository: TransactionRepository,
5264
private val accountRepository: AccountRepository,
5365
private val privacyMigration: PrivacyMigration,
5466
private val transactionReceiver: TransactionReceiver,
55-
val getDefaultCountry: () -> String,
56-
val suffix: () -> String,
57-
): CoroutineScope by CoroutineScope(Dispatchers.IO) {
58-
67+
prefs: PrefRepository,
68+
getDefaultCurrency: () -> CurrencyCode?,
69+
getCurrencyFromCode: (CurrencyCode?) -> Currency?,
70+
val suffix: (Currency?) -> String,
71+
) {
72+
private val scope = CoroutineScope(Dispatchers.IO)
5973
fun observeRawBalance(): Flow<Double> = balanceRepository.balanceFlow
6074

6175
val rawBalance: Double
6276
get() = balanceRepository.balanceFlow.value
6377

78+
private val preferredCurrency: StateFlow<Currency?> =
79+
prefs.observeOrDefault(
80+
PrefsString.KEY_BALANCE_CURRENCY_SELECTED,
81+
getDefaultCurrency()?.name.orEmpty()
82+
)
83+
.mapNotNull { CurrencyCode.tryValueOf(it) }
84+
.map { getCurrencyFromCode(it) }
85+
.stateIn(scope, SharingStarted.Eagerly, getCurrencyFromCode(getDefaultCurrency()))
86+
6487
private val _balanceDisplay = MutableStateFlow<BalanceDisplay?>(null)
65-
val formattedBalance: SharedFlow<BalanceDisplay?>
88+
89+
val formattedBalance: StateFlow<BalanceDisplay?>
6690
get() = _balanceDisplay
67-
.stateIn(this, SharingStarted.Eagerly, BalanceDisplay())
91+
.stateIn(scope, SharingStarted.Eagerly, BalanceDisplay())
6892

6993
init {
7094
combine(
7195
exchange.observeRates()
7296
.distinctUntilChanged()
7397
.flowOn(Dispatchers.IO)
74-
.map { getCurrency(it) }
98+
.flatMapLatest {
99+
combine(
100+
flowOf(it),
101+
preferredCurrency
102+
) { a, b -> a to b }
103+
}
104+
.map { (rates, preferred) ->
105+
getCurrency(rates, preferred?.code)
106+
}
75107
.onEach {
76108
val display = _balanceDisplay.value ?: BalanceDisplay()
77-
_balanceDisplay.value = display.copy(flag = it.resId)
109+
_balanceDisplay.value = display.copy(currency = it)
78110
}
79111
.mapNotNull { currency -> CurrencyCode.tryValueOf(currency.code) }
80112
.mapNotNull {
@@ -90,7 +122,7 @@ class BalanceController @Inject constructor(
90122
}.distinctUntilChanged().onEach { (marketValue, amountText) ->
91123
val display = _balanceDisplay.value ?: BalanceDisplay()
92124
_balanceDisplay.value = display.copy(marketValue = marketValue, formattedValue = amountText)
93-
}.launchIn(this)
125+
}.launchIn(scope)
94126
}
95127

96128
fun setTray(organizer: Organizer, tray: Tray) {
@@ -99,6 +131,7 @@ class BalanceController @Inject constructor(
99131
}
100132

101133
fun fetchBalance(): Completable {
134+
startupLog("fetchBalance")
102135
if (SessionManager.isAuthenticated() != true) {
103136
Timber.d("FetchBalance - Not authenticated")
104137
return Completable.complete()
@@ -111,7 +144,7 @@ class BalanceController @Inject constructor(
111144
val organizer = SessionManager.getOrganizer() ?:
112145
return@flatMapCompletable Completable.error(IllegalStateException("Missing Organizer"))
113146

114-
organizer.setAccountInfo(infos)
147+
scope.launch { organizer.setAccountInfo(infos) }
115148
balanceRepository.setBalance(organizer.availableBalance.toKinValueDouble())
116149
transactionReceiver.receiveFromIncomingCompletable(organizer)
117150
}
@@ -154,7 +187,6 @@ class BalanceController @Inject constructor(
154187

155188

156189
suspend fun fetchBalanceSuspend() {
157-
Timber.d("fetchBalance")
158190
if (SessionManager.isAuthenticated() != true) {
159191
Timber.d("FetchBalance - Not authenticated")
160192
return
@@ -165,7 +197,6 @@ class BalanceController @Inject constructor(
165197
val accountInfo = accountRepository.getTokenAccountInfos(owner).blockingGet()
166198
val organizer = SessionManager.getOrganizer() ?: throw IllegalStateException("Missing Organizer")
167199

168-
Timber.d("updating balance and organizer")
169200
organizer.setAccountInfo(accountInfo)
170201
balanceRepository.setBalance(organizer.availableBalance.toKinValueDouble())
171202
transactionReceiver.receiveFromIncoming(organizer)
@@ -193,18 +224,35 @@ class BalanceController @Inject constructor(
193224
}
194225

195226
private fun refreshBalance(balance: Double, rate: Double): Pair<Double, String> {
227+
val preferredCurrency = preferredCurrency.value
196228
val fiatValue = FormatUtils.getFiatValue(balance, rate)
197-
val locale = Locale(
198-
Locale.getDefault().language,
199-
getDefaultCountry()
200-
)
201-
val fiatValueFormatted = FormatUtils.formatCurrency(fiatValue, locale)
229+
230+
val prefix = formatPrefix(preferredCurrency).takeIf { it != preferredCurrency?.code }.orEmpty()
202231
val amountText = StringBuilder().apply {
203-
append(fiatValueFormatted)
204-
append(" ")
205-
append(suffix())
232+
append(prefix)
233+
append(formatAmount(fiatValue, preferredCurrency))
234+
val suffix = suffix(preferredCurrency)
235+
if (suffix.isNotEmpty()) {
236+
append(" ")
237+
append(suffix)
238+
}
206239
}.toString()
207240

208241
return fiatValue to amountText
209242
}
243+
244+
private fun formatPrefix(selectedCurrency: Currency?): String {
245+
if (selectedCurrency == null) return ""
246+
return if (!isKin(selectedCurrency)) selectedCurrency.symbol else ""
247+
}
248+
249+
private fun isKin(selectedCurrency: Currency): Boolean = selectedCurrency.code == CurrencyCode.KIN.name
250+
251+
private fun formatAmount(amount: Double, currency: Currency?): String {
252+
return if (amount % 1 == 0.0 || currency?.code == CurrencyCode.KIN.name) {
253+
String.format("%,.0f", amount)
254+
} else {
255+
String.format("%,.2f", amount)
256+
}
257+
}
210258
}

api/src/main/java/com/getcode/network/client/TransactionReceiver.kt

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import com.getcode.solana.organizer.GiftCardAccount
1212
import com.getcode.solana.organizer.Organizer
1313
import com.getcode.solana.organizer.Tray
1414
import com.getcode.utils.ErrorUtils
15+
import com.getcode.utils.startupLog
1516
import dagger.hilt.android.qualifiers.ApplicationContext
1617
import io.reactivex.rxjava3.core.Completable
1718
import timber.log.Timber
@@ -121,6 +122,7 @@ class TransactionReceiver @Inject constructor(
121122
}
122123

123124
fun receiveFromIncoming(amount: Kin, organizer: Organizer): Completable {
125+
startupLog("receiveFromIncoming $amount")
124126
return transactionRepository.receiveFromIncoming(
125127
context, amount, organizer
126128
).map {

api/src/main/java/com/getcode/network/repository/AccountRepository.kt

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import com.getcode.ed25519.Ed25519.KeyPair
99
import com.getcode.model.*
1010
import com.getcode.network.api.AccountApi
1111
import com.getcode.solana.keys.PublicKey
12+
import com.getcode.utils.startupLog
1213
import com.google.firebase.messaging.Constants.ScionAnalytics.MessageType
1314
import com.google.protobuf.GeneratedMessageLite
1415
import dagger.hilt.android.qualifiers.ApplicationContext
@@ -83,6 +84,7 @@ class AccountRepository @Inject constructor(
8384
.flatMap { response ->
8485
when (response.result) {
8586
AccountService.GetTokenAccountInfosResponse.Result.OK -> {
87+
Timber.d("token account infos fetched")
8688
val container = mutableMapOf<PublicKey, AccountInfo>()
8789

8890
for ((base58, info) in response.tokenAccountInfosMap) {
@@ -100,6 +102,7 @@ class AccountRepository @Inject constructor(
100102

101103
container[account] = accountInfo
102104
}
105+
Timber.d("token account infos handled")
103106
Single.just(container)
104107
}
105108
AccountService.GetTokenAccountInfosResponse.Result.NOT_FOUND -> {

api/src/main/java/com/getcode/network/repository/BetaFlagsRepository.kt

Lines changed: 18 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
package com.getcode.network.repository
22

33
import com.getcode.model.PrefsBool
4-
import com.getcode.utils.combine
54
import kotlinx.coroutines.flow.Flow
65
import kotlinx.coroutines.flow.combine
76
import javax.inject.Inject
@@ -19,6 +18,7 @@ data class BetaOptions(
1918
val tipsEnabled: Boolean,
2019
val tipsChatEnabled: Boolean,
2120
val tipsChatCashEnabled: Boolean,
21+
val balanceCurrencySelectionEnabled: Boolean,
2222
) {
2323
companion object {
2424
// Default states for various beta flags in app.
@@ -34,7 +34,8 @@ data class BetaOptions(
3434
chatUnsubEnabled = false,
3535
tipsEnabled = false,
3636
tipsChatEnabled = false,
37-
tipsChatCashEnabled = false
37+
tipsChatCashEnabled = false,
38+
balanceCurrencySelectionEnabled = false
3839
)
3940
}
4041
}
@@ -66,21 +67,23 @@ class BetaFlagsRepository @Inject constructor(
6667
observeBetaFlag(PrefsBool.TIPS_ENABLED, default = defaults.tipsEnabled),
6768
observeBetaFlag(PrefsBool.TIPS_CHAT_ENABLED, default = defaults.tipsChatEnabled),
6869
observeBetaFlag(PrefsBool.TIPS_CHAT_CASH_ENABLED, default = defaults.tipsChatCashEnabled),
70+
observeBetaFlag(PrefsBool.BALANCE_CURRENCY_SELECTION_ENABLED, defaults.balanceCurrencySelectionEnabled),
6971
observeBetaFlag(PrefsBool.DISPLAY_ERRORS, default = defaults.displayErrors),
70-
) { network, buckets, vibez, times, giveRequests, buyKin, relationship, chatUnsub, tips, tipsChat, tipsChatCash, errors ->
72+
) {
7173
BetaOptions(
72-
showNetworkDropOff = network,
73-
canViewBuckets = buckets,
74-
tickOnScan = vibez,
75-
debugScanTimesEnabled = times,
76-
giveRequestsEnabled = giveRequests,
77-
buyModuleEnabled = buyKin,
78-
establishCodeRelationship = relationship,
79-
chatUnsubEnabled = chatUnsub,
80-
tipsEnabled = tips,
81-
tipsChatEnabled = tipsChat,
82-
tipsChatCashEnabled = tipsChatCash,
83-
displayErrors = errors
74+
showNetworkDropOff = it[0],
75+
canViewBuckets = it[1],
76+
tickOnScan = it[2],
77+
debugScanTimesEnabled = it[3],
78+
giveRequestsEnabled = it[4],
79+
buyModuleEnabled = it[5],
80+
establishCodeRelationship = it[6],
81+
chatUnsubEnabled = it[7],
82+
tipsEnabled = it[8],
83+
tipsChatEnabled = it[9],
84+
tipsChatCashEnabled = it[10],
85+
balanceCurrencySelectionEnabled = it[11],
86+
displayErrors = it[12],
8487
)
8588
}
8689
}

api/src/main/java/com/getcode/network/repository/FeatureRepository.kt

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
package com.getcode.network.repository
22

3+
import com.getcode.model.BalanceCurrencyFeature
34
import com.getcode.model.BuyModuleFeature
45
import com.getcode.model.PrefsBool
56
import com.getcode.model.RequestKinFeature
@@ -28,4 +29,6 @@ class FeatureRepository @Inject constructor(
2829
val tipChatCash = betaFlags.observe().map { TipChatCashFeature(it.tipsChatCashEnabled) }
2930

3031
val requestKin = betaFlags.observe().map { RequestKinFeature(it.giveRequestsEnabled) }
32+
33+
val balanceCurrencySelection = betaFlags.observe().map { BalanceCurrencyFeature(it.balanceCurrencySelectionEnabled) }
3134
}

0 commit comments

Comments
 (0)