diff --git a/apps/flipcash/features/login/src/main/kotlin/com/flipcash/app/login/internal/accounts/AccountRow.kt b/apps/flipcash/features/login/src/main/kotlin/com/flipcash/app/login/internal/accounts/AccountRow.kt
index f795d4ca3f..0081716f65 100644
--- a/apps/flipcash/features/login/src/main/kotlin/com/flipcash/app/login/internal/accounts/AccountRow.kt
+++ b/apps/flipcash/features/login/src/main/kotlin/com/flipcash/app/login/internal/accounts/AccountRow.kt
@@ -2,6 +2,7 @@ package com.flipcash.app.login.internal.accounts
import androidx.compose.foundation.ExperimentalFoundationApi
import androidx.compose.foundation.Image
+import androidx.compose.foundation.background
import androidx.compose.foundation.combinedClickable
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
@@ -10,6 +11,7 @@ import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
+import androidx.compose.foundation.shape.CircleShape
import androidx.compose.material.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
@@ -18,6 +20,7 @@ import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.dp
+import com.flipcash.app.core.ui.FlagWithFiat
import com.flipcash.app.core.ui.shimmer
import com.flipcash.features.login.R
import com.getcode.theme.CodeTheme
@@ -40,7 +43,8 @@ internal fun AccountRow(
onClick = onClick,
onLongClick = onLongClick,
)
- .padding(vertical = CodeTheme.dimens.grid.x3),
+ // iOS pads each row 20pt on every side; the list's contentPadding supplies the sides.
+ .padding(vertical = CodeTheme.dimens.grid.x4),
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.spacedBy(CodeTheme.dimens.grid.x3),
) {
@@ -60,56 +64,87 @@ internal fun AccountRow(
modifier = Modifier.weight(1f),
verticalArrangement = Arrangement.spacedBy(CodeTheme.dimens.grid.x1),
) {
- Text(
- text = account.name,
- // textLarge is the primary line on the app's other list rows (Blocklist).
- style = CodeTheme.typography.textLarge,
- color = CodeTheme.colors.textMain,
- maxLines = 1,
- overflow = TextOverflow.Ellipsis,
- )
+ // iOS sets the name and the balance on one 16pt line (appTextMedium), bottom-aligned
+ // 10pt apart, with 5pt under the line on top of the column's spacing.
+ Row(
+ modifier = Modifier.padding(bottom = CodeTheme.dimens.grid.x1),
+ verticalAlignment = Alignment.Bottom,
+ horizontalArrangement = Arrangement.spacedBy(CodeTheme.dimens.grid.x2),
+ ) {
+ Text(
+ // Without a balance to push to the end, the Not Found badge follows the name
+ // directly, as on iOS.
+ modifier = Modifier.weight(1f, fill = !account.notFound),
+ text = account.name,
+ style = CodeTheme.typography.textMedium,
+ color = CodeTheme.colors.textMain,
+ maxLines = 1,
+ overflow = TextOverflow.Ellipsis,
+ )
+
+ // Order matters: a resolved balance wins, then the backend's own "no such
+ // account", then our inability to ask, and only then is the fetch still in
+ // flight. An account whose balance we could not fetch must not be reported as not
+ // found — the two say very different things to someone checking their own wallet.
+ when {
+ // iOS's AmountText(flagSize: .small): a 15pt flag beside the balance.
+ account.balance != null -> FlagWithFiat(
+ fiat = account.balance,
+ iconSize = CodeTheme.dimens.grid.x3,
+ )
+
+ account.notFound -> NotFoundBadge()
+
+ account.balanceUnavailable -> Text(
+ text = stringResource(R.string.subtitle_balanceUnavailable),
+ style = CodeTheme.typography.caption,
+ color = CodeTheme.colors.textSecondary,
+ )
+
+ // The skeleton the discovery list uses, sized to a short balance on the
+ // textMedium line. It carries no semantics: TalkBack announces the balance
+ // when it lands.
+ else -> Box(
+ Modifier
+ .size(width = 60.dp, height = 14.dp)
+ .shimmer()
+ )
+ }
+ }
Text(
text = stringResource(R.string.subtitle_accountCreated, relativeCreationDate),
- style = CodeTheme.typography.textSmall,
+ style = CodeTheme.typography.caption,
color = CodeTheme.colors.textSecondary,
)
+ // The full key on one line, as iOS shows it; the middle gives way only if it can't fit.
Text(
text = account.ownerAddress,
- style = CodeTheme.typography.textSmall,
+ style = CodeTheme.typography.caption,
color = CodeTheme.colors.textSecondary,
- )
- }
-
- // Order matters: a resolved balance wins, then the backend's own "no such account", then
- // our inability to ask, and only then is the fetch still in flight. An account whose balance we could not fetch must not be reported as
- // not found — the two say very different things to someone checking their own wallet.
- when {
- account.balance != null -> Text(
- text = account.balance.formatted(),
- style = CodeTheme.typography.textMedium,
- color = CodeTheme.colors.textMain,
- )
-
- account.notFound -> Text(
- text = stringResource(R.string.subtitle_accountNotFound),
- style = CodeTheme.typography.textSmall,
- color = CodeTheme.colors.textSecondary,
- )
-
- account.balanceUnavailable -> Text(
- text = stringResource(R.string.subtitle_balanceUnavailable),
- style = CodeTheme.typography.textSmall,
- color = CodeTheme.colors.textSecondary,
- )
-
- // The skeleton the discovery list uses, sized to a short balance on the textMedium
- // line. It carries no semantics: TalkBack announces the balance when it lands.
- else -> Box(
- Modifier
- .size(width = 60.dp, height = 14.dp)
- .shimmer()
+ maxLines = 1,
+ overflow = TextOverflow.MiddleEllipsis,
)
}
}
}
+/** iOS's `Badge(decoration: .circle(.textError))`: a 6pt dot in textError beside appTextSmall. */
+@Composable
+private fun NotFoundBadge(modifier: Modifier = Modifier) {
+ Row(
+ modifier = modifier,
+ verticalAlignment = Alignment.CenterVertically,
+ horizontalArrangement = Arrangement.spacedBy(CodeTheme.dimens.grid.x2),
+ ) {
+ Box(
+ Modifier
+ .size(6.dp)
+ .background(CodeTheme.colors.errorText, CircleShape)
+ )
+ Text(
+ text = stringResource(R.string.subtitle_accountNotFound),
+ style = CodeTheme.typography.textSmall,
+ color = CodeTheme.colors.textSecondary,
+ )
+ }
+}
diff --git a/apps/flipcash/features/login/src/main/kotlin/com/flipcash/app/login/internal/accounts/AccountSelectionContent.kt b/apps/flipcash/features/login/src/main/kotlin/com/flipcash/app/login/internal/accounts/AccountSelectionContent.kt
index d5c6bbb857..9e1f38ad7f 100644
--- a/apps/flipcash/features/login/src/main/kotlin/com/flipcash/app/login/internal/accounts/AccountSelectionContent.kt
+++ b/apps/flipcash/features/login/src/main/kotlin/com/flipcash/app/login/internal/accounts/AccountSelectionContent.kt
@@ -8,14 +8,16 @@ import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.lazy.LazyColumn
-import androidx.compose.foundation.lazy.items
+import androidx.compose.foundation.lazy.itemsIndexed
import androidx.compose.material.Text
+import androidx.compose.material3.HorizontalDivider
import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.style.TextAlign
+import androidx.compose.ui.unit.dp
import com.flipcash.features.login.R
import com.getcode.theme.CodeTheme
import com.getcode.ui.theme.ButtonState
@@ -64,7 +66,7 @@ internal fun AccountSelectionContent(
modifier = Modifier.fillMaxSize(),
contentPadding = PaddingValues(horizontal = CodeTheme.dimens.inset),
) {
- items(state.accounts, key = { it.id }) { account ->
+ itemsIndexed(state.accounts, key = { _, account -> account.id }) { index, account ->
AccountRow(
account = account,
isCurrent = account.entropy == state.currentEntropy,
@@ -74,6 +76,14 @@ internal fun AccountSelectionContent(
onClick = { onSelect(account.entropy) },
onLongClick = { onRemove(account.entropy) },
)
+ // iOS separates the rows with a hairline in rowSeparator, the same white
+ // at 10% as divider.
+ if (index < state.accounts.lastIndex) {
+ HorizontalDivider(
+ color = CodeTheme.colors.divider,
+ thickness = 0.5.dp,
+ )
+ }
}
}
}
diff --git a/apps/flipcash/features/login/src/main/kotlin/com/flipcash/app/login/internal/accounts/AccountSelectionViewModel.kt b/apps/flipcash/features/login/src/main/kotlin/com/flipcash/app/login/internal/accounts/AccountSelectionViewModel.kt
index da0153d4dd..eeda5020af 100644
--- a/apps/flipcash/features/login/src/main/kotlin/com/flipcash/app/login/internal/accounts/AccountSelectionViewModel.kt
+++ b/apps/flipcash/features/login/src/main/kotlin/com/flipcash/app/login/internal/accounts/AccountSelectionViewModel.kt
@@ -239,7 +239,7 @@ class AccountSelectionViewModel @Inject constructor(
mnemonicName = mnemonic?.let { displayName(it) }.orEmpty(),
username = profile?.username,
displayName = profile?.displayName,
- ownerAddress = owner?.let { truncateAddress(it) }.orEmpty(),
+ ownerAddress = owner.orEmpty(),
creationDate = creationDate,
// A record we cannot derive from is one we can never fetch a balance for.
balanceUnavailable = cluster == null,
@@ -341,10 +341,6 @@ class AccountSelectionViewModel @Inject constructor(
word.lowercase().replaceFirstChar { it.titlecase() }
}
- fun truncateAddress(address: String): String =
- if (address.length <= 8) address
- else "${address.take(4)}...${address.takeLast(4)}"
-
private val updateStateForEvent: (Event) -> ((State) -> State) = { event ->
when (event) {
is Event.OnAccountsLoaded -> { state ->
diff --git a/apps/flipcash/features/login/src/test/kotlin/com/flipcash/app/login/internal/accounts/AccountSelectionViewModelTest.kt b/apps/flipcash/features/login/src/test/kotlin/com/flipcash/app/login/internal/accounts/AccountSelectionViewModelTest.kt
index 6e036de4c9..4f2af226cb 100644
--- a/apps/flipcash/features/login/src/test/kotlin/com/flipcash/app/login/internal/accounts/AccountSelectionViewModelTest.kt
+++ b/apps/flipcash/features/login/src/test/kotlin/com/flipcash/app/login/internal/accounts/AccountSelectionViewModelTest.kt
@@ -24,15 +24,4 @@ class AccountSelectionViewModelTest {
val name = AccountSelectionViewModel.displayName(phrase("Apple", "Elder"))
assertEquals("Apple ... Elder", name)
}
-
- @Test
- fun `truncates an owner address in the middle`() {
- val truncated = AccountSelectionViewModel.truncateAddress("ABCDEFGHIJKLMNOPQRSTUVWXYZ")
- assertEquals("ABCD...WXYZ", truncated)
- }
-
- @Test
- fun `leaves a short address alone`() {
- assertEquals("ABCD", AccountSelectionViewModel.truncateAddress("ABCD"))
- }
}
diff --git a/maestro/blockstore_account_list.yaml b/maestro/blockstore_account_list.yaml
index a0bb3e07b0..2baa4c3537 100644
--- a/maestro/blockstore_account_list.yaml
+++ b/maestro/blockstore_account_list.yaml
@@ -22,7 +22,7 @@ tags:
visible:
id: account_selection_screen
timeout: 15000
-- assertVisible: "Select an Account"
+- assertVisible: "Select Account"
# The row itself. The display name comes from the seed, which the flow does not know, so the
# creation subtitle is what identifies a row generically.
- assertVisible: "Created .*"
diff --git a/maestro/blockstore_switcher.yaml b/maestro/blockstore_switcher.yaml
index 934ec80165..7866538eba 100644
--- a/maestro/blockstore_switcher.yaml
+++ b/maestro/blockstore_switcher.yaml
@@ -17,7 +17,7 @@ tags:
visible:
id: account_selection_screen
timeout: 15000
-- assertVisible: "Select an Account"
+- assertVisible: "Select Account"
- assertVisible: "Created .*"
# From inside the app there is no onboarding flow to fall through to, so the footer is hidden —
# which makes the back control the only way out.
diff --git a/ui/resources/src/main/res/values/strings-localized.xml b/ui/resources/src/main/res/values/strings-localized.xml
index 3b58ba6e5d..9e122ff4a4 100644
--- a/ui/resources/src/main/res/values/strings-localized.xml
+++ b/ui/resources/src/main/res/values/strings-localized.xml
@@ -375,7 +375,7 @@
Require Touch ID
Results
Returned
- Select an Account
+ Select Account
Select a Country
Select a Currency
Sent