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
10 changes: 10 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,16 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

### Added

- Profile screen (cu-15): `ProfileViewModel` maps the signed-in
`AuthRepository.currentUser` into the identity block (name, email,
Google provider, avatar monogram); the P&L summary and game-activity
breakdown render as placeholders until the stats layer ships. Includes
the Loading skeleton, the Error retry card, the settings sheet
(`ModalBottomSheet`), deep links to KYC and the House Wallet, and Sign
Out, which tears down the session and clears the back stack to Login.
- Shared `initialsOf` (avatar monogram) and `GameKey.iconRes` (game
glyph) helpers, extracted from the login, lobby and history screens so
the profile screen reuses them instead of duplicating.
- Self-contained HMAC-SHA256 implementation under `cpp_engine/include/Sha256.h`,
following FIPS 180-4 (SHA-256) and RFC 2104 (HMAC). Header-only, no external
dependencies.
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
package com.plainstudio.stackcasino.feature.profile

import androidx.compose.ui.test.assertCountEquals
import androidx.compose.ui.test.assertIsDisplayed
import androidx.compose.ui.test.junit4.createComposeRule
import androidx.compose.ui.test.onAllNodesWithText
import androidx.compose.ui.test.onNodeWithText
import androidx.compose.ui.test.performClick
import androidx.compose.ui.test.performScrollTo
import androidx.test.ext.junit.runners.AndroidJUnit4
import com.plainstudio.stackcasino.ui.theme.StackcasinoTheme
import org.junit.Assert.assertTrue
import org.junit.Rule
import org.junit.Test
import org.junit.runner.RunWith

@RunWith(AndroidJUnit4::class)
class ProfileScreenTest {
@get:Rule
val composeRule = createComposeRule()

@Test
fun success_renders_identity_stats_and_account_actions() {
composeRule.setContent {
StackcasinoTheme {
ProfileScreenContent(
state = ProfileUiState.Success(previewProfileData()),
onOpenSettings = {},
onOpenKyc = {},
onOpenHouseWallet = {},
onSignOut = {},
onRetry = {},
)
}
}

composeRule.onNodeWithText("John Doe").assertIsDisplayed()
composeRule.onNodeWithText("john.doe@gmail.com").assertIsDisplayed()
composeRule.onNodeWithText("Net P&L (all-time)").performScrollTo().assertIsDisplayed()
composeRule.onNodeWithText("Verify Identity").performScrollTo().assertIsDisplayed()
composeRule.onNodeWithText("Sign Out").performScrollTo().assertIsDisplayed()
}

@Test
fun sign_out_row_invokes_the_callback() {
var signedOut = false
composeRule.setContent {
StackcasinoTheme {
ProfileScreenContent(
state = ProfileUiState.Success(previewProfileData()),
onOpenSettings = {},
onOpenKyc = {},
onOpenHouseWallet = {},
onSignOut = { signedOut = true },
onRetry = {},
)
}
}

composeRule.onNodeWithText("Sign Out").performScrollTo().performClick()

assertTrue(signedOut)
}

@Test
fun error_shows_the_retry_card_and_no_identity() {
composeRule.setContent {
StackcasinoTheme {
ProfileScreenContent(
state = ProfileUiState.Error("Couldn't load your profile."),
onOpenSettings = {},
onOpenKyc = {},
onOpenHouseWallet = {},
onSignOut = {},
onRetry = {},
)
}
}

composeRule.onNodeWithText("Couldn't load profile").assertIsDisplayed()
composeRule.onNodeWithText("RETRY").assertIsDisplayed()
composeRule.onAllNodesWithText("John Doe").assertCountEquals(0)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,7 @@ import com.plainstudio.stackcasino.ui.theme.SurfaceOutline
import com.plainstudio.stackcasino.ui.theme.TextHigh
import com.plainstudio.stackcasino.ui.theme.TextLow
import com.plainstudio.stackcasino.ui.theme.TextMedium
import com.plainstudio.stackcasino.util.initialsOf

/**
* Login screen reproducing the cu-02 mockup (default / returning /
Expand Down Expand Up @@ -554,20 +555,6 @@ private fun ErrorBanner(
// Helpers
// ---------------------------------------------------------------------------

/**
* Splits the display name on whitespace and joins the first letter of
* the first two tokens for the avatar tile (e.g. "John Doe" -> "JD").
* Falls back to the first character when only a single word is present.
*/
private fun initialsOf(name: String): String {
val tokens = name.split(' ').filter { it.isNotBlank() }
return when (tokens.size) {
0 -> ""
1 -> tokens[0].first().uppercase()
else -> "${tokens[0].first().uppercase()}${tokens[1].first().uppercase()}"
}
}

/**
* Walks the ContextWrapper chain until it lands on an Activity.
* Required because Credential Manager's getCredential expects an
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -42,14 +42,14 @@ import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import com.plainstudio.stackcasino.R
import com.plainstudio.stackcasino.model.GameKey
import com.plainstudio.stackcasino.model.RoundOutcome
import com.plainstudio.stackcasino.ui.components.EmptyState
import com.plainstudio.stackcasino.ui.components.FilterChip
import com.plainstudio.stackcasino.ui.components.FilterChipRow
import com.plainstudio.stackcasino.ui.components.StackCard
import com.plainstudio.stackcasino.ui.components.gridBackground
import com.plainstudio.stackcasino.ui.iconRes
import com.plainstudio.stackcasino.ui.theme.AccentViolet
import com.plainstudio.stackcasino.ui.theme.SemanticDanger
import com.plainstudio.stackcasino.ui.theme.SemanticOk
Expand Down Expand Up @@ -526,15 +526,6 @@ private fun GameKey.label(): String =
GameKey.Coinflip -> "Coinflip"
}

private fun GameKey.iconRes(): Int =
when (this) {
GameKey.Roulette -> R.drawable.ic_game_roulette
GameKey.Blackjack -> R.drawable.ic_game_blackjack
GameKey.Crash -> R.drawable.ic_game_crash
GameKey.Mines -> R.drawable.ic_game_mines
GameKey.Coinflip -> R.drawable.ic_game_coinflip
}

// ---------------------------------------------------------------------------
// Tokens
// ---------------------------------------------------------------------------
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import com.plainstudio.stackcasino.model.GameKey
import com.plainstudio.stackcasino.ui.iconRes
import com.plainstudio.stackcasino.ui.theme.AccentViolet
import com.plainstudio.stackcasino.ui.theme.SurfaceOutline
import com.plainstudio.stackcasino.ui.theme.SurfaceRaised
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ import com.plainstudio.stackcasino.ui.theme.SurfaceOutline
import com.plainstudio.stackcasino.ui.theme.SurfaceRaised
import com.plainstudio.stackcasino.ui.theme.TextHigh
import com.plainstudio.stackcasino.ui.theme.TextMedium
import com.plainstudio.stackcasino.util.initialsOf

/**
* Lobby top bar: avatar tile, greeting + display name, and the
Expand Down Expand Up @@ -123,20 +124,6 @@ private fun NotificationsButton(
}
}

/**
* Splits the display name on whitespace and joins the first letter of
* the first two tokens for the avatar tile (e.g. "John Doe" -> "JD").
* Falls back to the first character when only a single word is present.
*/
private fun initialsOf(name: String): String {
val tokens = name.split(' ').filter { it.isNotBlank() }
return when (tokens.size) {
0 -> ""
1 -> tokens[0].first().uppercase()
else -> "${tokens[0].first().uppercase()}${tokens[1].first().uppercase()}"
}
}

private val HeaderTopPadding = 24.dp
private val HeaderBottomPadding = 16.dp
private val AvatarSize = 40.dp
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import com.plainstudio.stackcasino.model.RoundOutcome
import com.plainstudio.stackcasino.ui.components.StackCard
import com.plainstudio.stackcasino.ui.iconRes
import com.plainstudio.stackcasino.ui.theme.AccentViolet
import com.plainstudio.stackcasino.ui.theme.SemanticDanger
import com.plainstudio.stackcasino.ui.theme.SemanticOk
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -241,15 +241,6 @@ private fun GameKey.toRoute(): Route =
GameKey.Coinflip -> Route.Coinflip
}

internal fun GameKey.iconRes(): Int =
when (this) {
GameKey.Roulette -> R.drawable.ic_game_roulette
GameKey.Blackjack -> R.drawable.ic_game_blackjack
GameKey.Crash -> R.drawable.ic_game_crash
GameKey.Mines -> R.drawable.ic_game_mines
GameKey.Coinflip -> R.drawable.ic_game_coinflip
}

// ---------------------------------------------------------------------------
// Tokens shared across the lobby section files via the `internal` modifier.
// ---------------------------------------------------------------------------
Expand Down
Loading
Loading