Skip to content

Commit 2bc5816

Browse files
authored
Merge pull request #441 from urnetwork/balance-codes-screen
balance codes screen
2 parents b88961a + 69dbd02 commit 2bc5816

26 files changed

Lines changed: 509 additions & 3 deletions

File tree

app/app/src/main/java/com/bringyour/network/ui/MainNavHost.kt

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -97,6 +97,7 @@ import com.solana.mobilewalletadapter.clientlib.ActivityResultSender
9797
import com.bringyour.network.R
9898
import com.bringyour.network.TAG
9999
import com.bringyour.network.ui.api_error.ApiErrorScreen
100+
import com.bringyour.network.ui.balance_codes.BalanceCodesScreen
100101
import com.bringyour.network.ui.blocked_regions.BlockedRegionsScreen
101102
import com.bringyour.network.ui.components.overlays.OverlayMode
102103
import com.bringyour.network.ui.introduction.IntroductionInitial
@@ -786,6 +787,17 @@ fun MainNavContent(
786787
)
787788
}
788789

790+
composable<Route.BalanceCodes>(
791+
enterTransition = NavigationAnimations.enterTransition(),
792+
exitTransition = NavigationAnimations.exitTransition(),
793+
popEnterTransition = NavigationAnimations.popEnterTransition(),
794+
popExitTransition = NavigationAnimations.popExitTransition()
795+
) {
796+
BalanceCodesScreen(
797+
navController = navController,
798+
)
799+
}
800+
789801
composable<Route.Wallets>(
790802
enterTransition = NavigationAnimations.enterTransition(),
791803
exitTransition = NavigationAnimations.exitTransition(),

app/app/src/main/java/com/bringyour/network/ui/MainNavViewModel.kt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -98,6 +98,7 @@ sealed class Route {
9898
@Serializable data class Wallet(val id: String) : Route()
9999
@Serializable data class Payout(val id: String) : Route()
100100
@Serializable object BlockedRegions: Route()
101+
@Serializable object BalanceCodes: Route()
101102
}
102103

103104

Lines changed: 261 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,261 @@
1+
package com.bringyour.network.ui.balance_codes
2+
3+
import android.widget.Toast
4+
import androidx.compose.foundation.layout.Arrangement
5+
import androidx.compose.foundation.layout.Box
6+
import androidx.compose.foundation.layout.Row
7+
import androidx.compose.foundation.layout.fillMaxSize
8+
import androidx.compose.foundation.layout.fillMaxWidth
9+
import androidx.compose.foundation.layout.padding
10+
import androidx.compose.foundation.lazy.LazyColumn
11+
import androidx.compose.foundation.lazy.items
12+
import androidx.compose.foundation.lazy.rememberLazyListState
13+
import androidx.compose.material.icons.Icons
14+
import androidx.compose.material.icons.automirrored.filled.KeyboardArrowLeft
15+
import androidx.compose.material3.CenterAlignedTopAppBar
16+
import androidx.compose.material3.ExperimentalMaterial3Api
17+
import androidx.compose.material3.HorizontalDivider
18+
import androidx.compose.material3.Icon
19+
import androidx.compose.material3.IconButton
20+
import androidx.compose.material3.MaterialTheme
21+
import androidx.compose.material3.Scaffold
22+
import androidx.compose.material3.Text
23+
import androidx.compose.material3.TopAppBarDefaults
24+
import androidx.compose.material3.pulltorefresh.PullToRefreshBox
25+
import androidx.compose.material3.rememberModalBottomSheetState
26+
import androidx.compose.runtime.Composable
27+
import androidx.compose.runtime.collectAsState
28+
import androidx.compose.runtime.getValue
29+
import androidx.compose.runtime.rememberCoroutineScope
30+
import androidx.compose.ui.Alignment
31+
import androidx.compose.ui.Modifier
32+
import androidx.compose.ui.platform.LocalContext
33+
import androidx.compose.ui.res.painterResource
34+
import androidx.compose.ui.res.stringResource
35+
import androidx.compose.ui.unit.dp
36+
import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel
37+
import androidx.navigation.NavController
38+
import com.bringyour.network.R
39+
import com.bringyour.network.ui.components.redeemTransferBalanceCode.RedeemTransferBalanceCodeSheet
40+
import com.bringyour.network.ui.theme.Black
41+
import com.bringyour.network.ui.theme.TextMuted
42+
import com.bringyour.network.ui.theme.TopBarTitleTextStyle
43+
import com.bringyour.sdk.RedeemedBalanceCode
44+
45+
@OptIn(ExperimentalMaterial3Api::class)
46+
@Composable
47+
fun BalanceCodesScreen(
48+
navController: NavController,
49+
viewModel: BalanceCodesViewModel = hiltViewModel<BalanceCodesViewModel>()
50+
) {
51+
52+
val sheetState = rememberModalBottomSheetState(
53+
skipPartiallyExpanded = true
54+
)
55+
val scope = rememberCoroutineScope()
56+
57+
val balanceCodes by viewModel.balanceCodes.collectAsState()
58+
59+
val context = LocalContext.current
60+
61+
val errorEvents = viewModel.errorEvents.collectAsState(initial = null)
62+
errorEvents.value?.let { message ->
63+
Toast.makeText(context, message, Toast.LENGTH_SHORT).show()
64+
}
65+
66+
Scaffold(
67+
topBar = {
68+
CenterAlignedTopAppBar(
69+
title = {
70+
Text(
71+
stringResource(id = R.string.balance_codes_title),
72+
style = TopBarTitleTextStyle
73+
)
74+
},
75+
navigationIcon = {
76+
IconButton(onClick = { navController.popBackStack() }) {
77+
Icon(Icons.AutoMirrored.Filled.KeyboardArrowLeft, contentDescription = "Back")
78+
}
79+
},
80+
colors = TopAppBarDefaults.topAppBarColors(
81+
containerColor = Black
82+
),
83+
actions = {
84+
IconButton(onClick = {
85+
viewModel.setDisplayBottomSheet(true)
86+
}) {
87+
Icon(
88+
painter = painterResource(id = R.drawable.plus_icon),
89+
contentDescription = stringResource(id = R.string.redeem_balance_code)
90+
)
91+
}
92+
},
93+
)
94+
},
95+
containerColor = Black
96+
) { padding ->
97+
98+
Box(
99+
modifier = Modifier
100+
.padding(padding)
101+
.fillMaxSize()
102+
) {
103+
104+
BalanceCodesTable(
105+
balanceCodes = balanceCodes,
106+
onRefresh = {
107+
viewModel.fetchNetworkBalanceCodes()
108+
},
109+
isRefreshing = viewModel.isFetchingBalanceCodes.collectAsState().value
110+
)
111+
112+
}
113+
114+
if (viewModel.displayBottonSheet.collectAsState().value) {
115+
RedeemTransferBalanceCodeSheet(
116+
sheetState = sheetState,
117+
setIsPresenting = {
118+
viewModel.setDisplayBottomSheet(it)
119+
},
120+
onSuccess = {
121+
viewModel.fetchNetworkBalanceCodes()
122+
}
123+
)
124+
}
125+
126+
}
127+
128+
}
129+
130+
@Composable
131+
private fun BalanceCodesTable(
132+
balanceCodes: List<RedeemedBalanceCode>,
133+
onRefresh: () -> Unit,
134+
isRefreshing: Boolean
135+
) {
136+
137+
val listState = rememberLazyListState()
138+
139+
PullToRefreshBox(
140+
onRefresh = onRefresh,
141+
isRefreshing = isRefreshing
142+
) {
143+
144+
if (balanceCodes.isEmpty()) {
145+
146+
LazyColumn(
147+
modifier = Modifier.fillMaxSize(),
148+
horizontalAlignment = Alignment.CenterHorizontally,
149+
verticalArrangement = Arrangement.Center
150+
) {
151+
item {
152+
Text(
153+
stringResource(id = R.string.no_redeemed_balance_codes_found),
154+
style = MaterialTheme.typography.bodyLarge,
155+
color = TextMuted
156+
)
157+
}
158+
}
159+
160+
} else {
161+
162+
LazyColumn(
163+
modifier = Modifier
164+
.fillMaxSize(),
165+
state = listState
166+
) {
167+
item {
168+
Row(
169+
modifier = Modifier
170+
.fillMaxWidth()
171+
.padding(16.dp),
172+
) {
173+
Text(
174+
stringResource(id = R.string.balance_code),
175+
modifier = Modifier.weight(1f),
176+
color = TextMuted,
177+
style = MaterialTheme.typography.labelMedium
178+
)
179+
180+
Text(
181+
stringResource(id = R.string.data),
182+
modifier = Modifier.weight(1f),
183+
color = TextMuted,
184+
style = MaterialTheme.typography.labelMedium
185+
)
186+
187+
Text(
188+
stringResource(id = R.string.redeem_time),
189+
modifier = Modifier.weight(1f),
190+
color = TextMuted,
191+
style = MaterialTheme.typography.labelMedium
192+
)
193+
194+
Text(
195+
stringResource(id = R.string.expiry_time),
196+
modifier = Modifier.weight(1f),
197+
color = TextMuted,
198+
style = MaterialTheme.typography.labelMedium
199+
)
200+
}
201+
}
202+
203+
items(balanceCodes, key = {it.balanceCodeId.idStr}) { balanceCode ->
204+
BalanceCodeListItem(
205+
balanceCode = balanceCode,
206+
)
207+
HorizontalDivider()
208+
}
209+
}
210+
}
211+
}
212+
}
213+
214+
@Composable
215+
fun BalanceCodeListItem(
216+
balanceCode: RedeemedBalanceCode,
217+
) {
218+
219+
val maskSecret: (String) -> String = { secret ->
220+
if (secret.length <= 6) secret
221+
else "${secret.take(3)}...${secret.takeLast(3)}"
222+
}
223+
224+
val formatBytes: (Long) -> String = { bytes ->
225+
val oneTiB = 1024L * 1024 * 1024 * 1024
226+
val oneGiB = 1024L * 1024 * 1024
227+
when {
228+
bytes >= oneTiB -> String.format("%.2f TiB", bytes.toDouble() / oneTiB)
229+
else -> String.format("%.2f GiB", bytes.toDouble() / oneGiB)
230+
}
231+
}
232+
233+
Row(
234+
modifier = Modifier
235+
.fillMaxWidth()
236+
.padding(16.dp),
237+
) {
238+
239+
Text(
240+
maskSecret(balanceCode.secret),
241+
modifier = Modifier.weight(1f)
242+
)
243+
244+
Text(
245+
"+${formatBytes(balanceCode.balanceByteCount)}",
246+
modifier = Modifier.weight(1f)
247+
)
248+
249+
Text(
250+
balanceCode.redeemTime.format("Jan 2, 2006"),
251+
modifier = Modifier.weight(1f)
252+
)
253+
254+
Text(
255+
balanceCode.endTime.format("Jan 2, 2006"),
256+
modifier = Modifier.weight(1f)
257+
)
258+
259+
}
260+
261+
}
Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,90 @@
1+
package com.bringyour.network.ui.balance_codes
2+
3+
import androidx.lifecycle.ViewModel
4+
import androidx.lifecycle.viewModelScope
5+
import com.bringyour.network.DeviceManager
6+
import com.bringyour.sdk.BlockedLocation
7+
import com.bringyour.sdk.RedeemedBalanceCode
8+
import dagger.hilt.android.lifecycle.HiltViewModel
9+
import kotlinx.coroutines.channels.Channel
10+
import kotlinx.coroutines.flow.MutableStateFlow
11+
import kotlinx.coroutines.flow.StateFlow
12+
import kotlinx.coroutines.flow.asStateFlow
13+
import kotlinx.coroutines.flow.receiveAsFlow
14+
import kotlinx.coroutines.launch
15+
import javax.inject.Inject
16+
17+
@HiltViewModel
18+
class BalanceCodesViewModel @Inject constructor(
19+
private val deviceManager: DeviceManager,
20+
): ViewModel() {
21+
22+
private val _isFetchingBalanceCodes = MutableStateFlow<Boolean>(false)
23+
val isFetchingBalanceCodes: StateFlow<Boolean> = _isFetchingBalanceCodes.asStateFlow()
24+
25+
private val _balanceCodes = MutableStateFlow<List<RedeemedBalanceCode>>(emptyList())
26+
val balanceCodes: StateFlow<List<RedeemedBalanceCode>> = _balanceCodes.asStateFlow()
27+
28+
private val _errorEvents = Channel<String>(Channel.BUFFERED)
29+
val errorEvents = _errorEvents.receiveAsFlow()
30+
31+
private val _displayBottomSheet = MutableStateFlow<Boolean>(false)
32+
val displayBottonSheet: StateFlow<Boolean> = _displayBottomSheet.asStateFlow()
33+
34+
val setDisplayBottomSheet: (Boolean) -> Unit = {
35+
_displayBottomSheet.value = it
36+
}
37+
38+
val fetchNetworkBalanceCodes: () -> Unit = {
39+
40+
if (!_isFetchingBalanceCodes.value) {
41+
42+
_isFetchingBalanceCodes.value = true
43+
44+
deviceManager.device?.api?.getNetworkRedeemedBalanceCodes { result, err ->
45+
46+
viewModelScope.launch {
47+
if (err != null) {
48+
_errorEvents.send("Error fetching network balance codes")
49+
_isFetchingBalanceCodes.value = false
50+
return@launch
51+
}
52+
53+
if (result.error != null) {
54+
_errorEvents.send("Error fetching network balance codes: ${result.error.message}")
55+
_isFetchingBalanceCodes.value = false
56+
return@launch
57+
}
58+
59+
if (result.balanceCodes != null) {
60+
61+
val balanceCodes = mutableListOf<RedeemedBalanceCode>()
62+
val n = result.balanceCodes.len()
63+
64+
for (i in 0 until n) {
65+
val balanceCode = result.balanceCodes.get(i)
66+
balanceCodes.add(balanceCode)
67+
}
68+
69+
_balanceCodes.value = balanceCodes
70+
}
71+
_isFetchingBalanceCodes.value = false
72+
73+
}
74+
75+
} ?: {
76+
viewModelScope.launch {
77+
_errorEvents.send("API not found")
78+
_isFetchingBalanceCodes.value = false
79+
}
80+
}
81+
82+
}
83+
84+
}
85+
86+
init {
87+
fetchNetworkBalanceCodes()
88+
}
89+
90+
}

0 commit comments

Comments
 (0)