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
6 changes: 6 additions & 0 deletions app/src/main/java/com/plainstudio/stackcasino/MainActivity.kt
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,12 @@ import com.plainstudio.stackcasino.feature.splash.SplashViewModel
import com.plainstudio.stackcasino.ui.theme.StackcasinoTheme
import dagger.hilt.android.AndroidEntryPoint

/**
* Single-activity host. Installs the AndroidX system splash, keeps it on
* screen while [SplashViewModel] resolves the start destination from the
* cached auth state, then hands off to [StackApp] once the decision is
* ready.
*/
@AndroidEntryPoint
class MainActivity : ComponentActivity() {
private val splashViewModel: SplashViewModel by viewModels()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,5 +3,10 @@ package com.plainstudio.stackcasino
import android.app.Application
import dagger.hilt.android.HiltAndroidApp

/**
* Application entry point. Annotated with [HiltAndroidApp] so Hilt
* generates the application-level dependency container that every
* other component (activities, ViewModels, modules) is built on top of.
*/
@HiltAndroidApp
class StackCasinoApp : Application()
Original file line number Diff line number Diff line change
Expand Up @@ -24,19 +24,22 @@ class LastSignInHint
) {
private val prefs = context.getSharedPreferences(FILE_NAME, Context.MODE_PRIVATE)

/** The cosmetic last-sign-in details rendered on the returning-user card. */
data class Hint(
val displayName: String,
val email: String,
val photoUrl: String?,
)

/** Reads the stored hint, or null when no complete hint was persisted. */
fun read(): Hint? {
val displayName = prefs.getString(KEY_DISPLAY_NAME, null) ?: return null
val email = prefs.getString(KEY_EMAIL, null) ?: return null
val photoUrl = prefs.getString(KEY_PHOTO_URL, null)
return Hint(displayName, email, photoUrl)
}

/** Persists [hint] so the returning-user card survives sign-out. */
fun write(hint: Hint) {
prefs.edit {
putString(KEY_DISPLAY_NAME, hint.displayName)
Expand All @@ -45,6 +48,7 @@ class LastSignInHint
}
}

/** Wipes the stored hint (for example after an explicit account switch). */
fun clear() {
prefs.edit { clear() }
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,12 +9,15 @@ import kotlinx.coroutines.flow.Flow
/** Room access for the settled game-round ledger. */
@Dao
interface GameRoundDao {
/** Observes every settled round, newest first, for the History feed. */
@Query("SELECT * FROM game_round ORDER BY timestamp DESC")
fun observeRounds(): Flow<List<GameRoundEntity>>

/** Reads a single round by id for the round-detail / verification screen. */
@Query("SELECT * FROM game_round WHERE id = :id")
suspend fun getRound(id: String): GameRoundEntity?

/** Inserts a settled round, replacing any existing row with the same id. */
@Insert(onConflict = OnConflictStrategy.REPLACE)
suspend fun insertRound(round: GameRoundEntity)
}
Original file line number Diff line number Diff line change
Expand Up @@ -12,9 +12,11 @@ interface NotificationDao {
@Query("SELECT * FROM notification ORDER BY createdAt DESC")
fun observeAll(): Flow<List<NotificationEntity>>

/** Observes the unread count that drives the lobby bell badge. */
@Query("SELECT COUNT(*) FROM notification WHERE isRead = 0")
fun observeUnreadCount(): Flow<Int>

/** Total row count; used to decide whether the inbox needs seeding. */
@Query("SELECT COUNT(*) FROM notification")
suspend fun count(): Int

Expand All @@ -23,9 +25,11 @@ interface NotificationDao {
@Insert
suspend fun insertAll(items: List<NotificationEntity>): List<Long>

/** Marks the row with [id] read; returns the number of rows updated. */
@Query("UPDATE notification SET isRead = 1 WHERE id = :id")
suspend fun markRead(id: String): Int

/** Marks every unread row read; returns the number of rows updated. */
@Query("UPDATE notification SET isRead = 1 WHERE isRead = 0")
suspend fun markAllRead(): Int
}
Original file line number Diff line number Diff line change
Expand Up @@ -22,14 +22,19 @@ import androidx.room.RoomDatabase
exportSchema = true,
)
abstract class StackDatabase : RoomDatabase() {
/** DAO for the local app-settings row. */
abstract fun appSettingsDao(): AppSettingsDao

/** DAO for the notification inbox. */
abstract fun notificationDao(): NotificationDao

/** DAO for the cached news feed. */
abstract fun newsArticleDao(): NewsArticleDao

/** DAO for the wallet snapshot and its transaction ledger. */
abstract fun walletDao(): WalletDao

/** DAO for the settled game-round ledger. */
abstract fun gameRoundDao(): GameRoundDao

companion object {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,24 +10,31 @@ import kotlinx.coroutines.flow.Flow
/** Room access for the wallet snapshot and its transaction ledger. */
@Dao
interface WalletDao {
/** Observes the singleton wallet row; emits null until it is seeded. */
@Query("SELECT * FROM wallet WHERE id = 0")
fun observeWallet(): Flow<WalletEntity?>

/** Reads the singleton wallet row once, or null when it has not been seeded. */
@Query("SELECT * FROM wallet WHERE id = 0")
suspend fun getWallet(): WalletEntity?

/** Inserts or updates the singleton wallet row, returning its row id. */
@Upsert
suspend fun upsertWallet(wallet: WalletEntity): Long

/** Observes the full transaction ledger, newest first. */
@Query("SELECT * FROM wallet_transaction ORDER BY timestamp DESC")
fun observeTransactions(): Flow<List<WalletTransactionEntity>>

/** Counts the ledger rows; used to decide whether seeding is needed. */
@Query("SELECT COUNT(*) FROM wallet_transaction")
suspend fun transactionCount(): Int

/** Inserts a batch of ledger rows, replacing any with matching ids. */
@Insert(onConflict = OnConflictStrategy.REPLACE)
suspend fun insertTransactions(items: List<WalletTransactionEntity>): List<Long>

/** Inserts a single ledger row, replacing any with the same id. */
@Insert(onConflict = OnConflictStrategy.REPLACE)
suspend fun insertTransaction(item: WalletTransactionEntity): Long
}
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ data class WalletEntity(
val updatedAt: Long,
) {
companion object {
/** The wallet row is a singleton; its primary key is always this. */
const val SINGLETON_ID = 0
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import retrofit2.http.Query
* round-trips.
*/
interface NewsApiService {
/** Fetches articles matching [query] from the "/everything" endpoint. */
@GET("everything")
suspend fun fetchEverything(
@Query("q") query: String,
Expand All @@ -28,6 +29,10 @@ interface NewsApiService {
}
}

/**
* Top-level NewsAPI response. [status] is "ok" on success; [code] and
* [message] carry the error detail when it is "error".
*/
@JsonClass(generateAdapter = true)
data class NewsApiResponseDto(
val status: String,
Expand All @@ -37,6 +42,7 @@ data class NewsApiResponseDto(
val message: String? = null,
)

/** A single article in the NewsAPI response; most fields are nullable upstream. */
@JsonClass(generateAdapter = true)
data class NewsApiArticleDto(
val source: NewsApiSourceDto,
Expand All @@ -47,6 +53,7 @@ data class NewsApiArticleDto(
val publishedAt: String?,
)

/** The publisher block nested in each NewsAPI article. */
@JsonClass(generateAdapter = true)
data class NewsApiSourceDto(
val id: String?,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ class WalletRemoteDataSource
constructor(
private val firestore: FirebaseFirestore,
) {
/** Reads `wallets/{uid}`, returning null when the document is absent. */
suspend fun fetchWallet(uid: String): Wallet? {
val doc =
firestore
Expand All @@ -44,6 +45,7 @@ class WalletRemoteDataSource
)
}

/** Reads the user's `transactions` documents into ledger entries. */
suspend fun fetchTransactions(uid: String): List<WalletLedgerEntry> {
val snapshot =
firestore
Expand All @@ -65,6 +67,10 @@ class WalletRemoteDataSource
}
}

/**
* Writes a `pending` withdrawal document (the client half of EP-05);
* the Cloud Function later signs and confirms it on-chain.
*/
suspend fun createPendingWithdrawal(
uid: String,
id: String,
Expand Down
6 changes: 6 additions & 0 deletions app/src/main/java/com/plainstudio/stackcasino/di/AppModule.kt
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,12 @@ object AppModule {
@Singleton
fun provideIdGenerator(): IdGenerator = UuidIdGenerator()

/**
* Application-lifetime [CoroutineScope] for fire-and-forget work. A
* [SupervisorJob] keeps one failing child from cancelling its
* siblings, and [Dispatchers.IO] suits the best-effort remote writes
* this scope is meant for.
*/
@Provides
@Singleton
@AppScope
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ data class ChatTurn(
val text: String,
)

/** Who authored a [ChatTurn]: the player ([User]) or the assistant ([Nep]). */
enum class Role { User, Nep }

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ object BlackjackMath {
return sum
}

/** Whether the hand busts: its best total exceeds 21. */
fun isBust(cards: List<Int>): Boolean = total(cards) > BLACKJACK

/** A natural: exactly two cards totalling 21. */
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,10 @@ data class Wallet(
val currencyCode: String,
)

/** Direction of a wallet transaction: funds in ([Deposit]) or out ([Withdraw]). */
enum class WalletTxType { Deposit, Withdraw }

/** Lifecycle state of a wallet transaction in the ledger. */
enum class WalletTxStatus { Confirmed, Pending, Failed }

/** A single ledger entry in the wallet transaction history. */
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,10 @@ import kotlinx.coroutines.flow.Flow
* (validate + record a pending transaction, locking the funds).
*/
interface WalletRepository {
/** Cold stream of the wallet balances and deposit address. */
val wallet: Flow<Wallet>

/** Cold stream of the wallet transaction history, newest first. */
val transactions: Flow<List<WalletLedgerEntry>>

/** Seeds the wallet + sample ledger on first run if empty. Idempotent. */
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -40,4 +40,5 @@ data class ChatMessage(
val isError: Boolean = false,
)

/** Who authored a [ChatMessage]: the player or the Nep assistant. */
enum class Author { User, Nep }
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,10 @@ class AssistantViewModel
private val _uiState = MutableStateFlow<AssistantUiState>(AssistantUiState.Welcome)
val uiState: StateFlow<AssistantUiState> = _uiState.asStateFlow()

/**
* Appends the user turn, shows the typing indicator, and asks the
* repository for Nep's reply. Blank or over-budget text is ignored.
*/
fun sendMessage(text: String) {
val trimmed = text.trim()
if (trimmed.isEmpty() || trimmed.length > MAX_USER_MESSAGE_LENGTH) return
Expand Down Expand Up @@ -84,6 +88,7 @@ class AssistantViewModel
}
}

/** Resets the chat back to the welcome state. */
fun clear() {
_uiState.value = AssistantUiState.Welcome
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,10 @@ class LoginViewModel
)
val events: SharedFlow<LoginEvent> = _events.asSharedFlow()

/**
* Launches the Credential Manager sign-in flow. No-ops while a
* sign-in is already in flight so a double tap can't fire twice.
*/
fun signIn(activity: Activity) {
if (_uiState.value is LoginUiState.Loading) return
_uiState.value = LoginUiState.Loading
Expand All @@ -64,6 +68,7 @@ class LoginViewModel
signIn(activity)
}

/** Dismisses the error banner, returning to the resting state. */
fun dismissError() {
if (_uiState.value is LoginUiState.Error) {
_uiState.value = deriveRestingState()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -197,6 +197,7 @@ class BlackjackViewModel
gameRepository.settleBlackjack(settled, effectiveStake, payout, playerTotal, dealerTotal, label)
}

/** Resolves a finished hand to its (payout, result label) pair. */
private fun outcome(
playerBust: Boolean,
playerTotal: Int,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,10 @@ data class HistoryData(
val rounds: List<HistoryRound>,
)

/**
* Pre-formatted aggregates for the summary strip. [netLabel] is already
* sign-prefixed ("+$412.30" / "-$50.00") so the screen renders it as-is.
*/
data class HistorySummary(
val totalRounds: Int,
val winRatePercent: Int,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,10 @@ data class LobbyData(
val recentActivity: List<RecentRound>,
)

/**
* Header identity block. [greeting] is the time-of-day salutation
* ("Good Evening") rendered above the display name.
*/
data class UserSummary(
val displayName: String,
val greeting: String,
Expand All @@ -65,6 +69,7 @@ data class BalanceSummary(
val lockedSubtitle: String,
)

/** Current-session tallies shown under the balance hero (rounds, wins, losses). */
data class SessionStats(
val rounds: Int,
val wins: Int,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ class LobbyViewModel
constructor(
gameRepository: GameRepository,
) : ViewModel() {
/** The user's most recently played game, or null when none has been played yet. */
val lastPlayedGame: StateFlow<GameKey?> =
gameRepository.rounds
.map { rounds -> rounds.firstOrNull()?.game }
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,13 @@ class NewsDetailViewModel
}
}

/**
* Render state for the news detail screen.
*
* * [Loading] -> the cached lookup has not emitted yet.
* * [NotFound] -> no article matches the requested id in the cache.
* * [Loaded] -> the cached [NewsArticle] is ready to render.
*/
sealed interface NewsDetailUiState {
data object Loading : NewsDetailUiState

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -41,10 +41,12 @@ class NotificationsViewModel
viewModelScope.launch { repository.ensureSeeded() }
}

/** Marks the notification with [id] as read. */
fun markRead(id: String) {
viewModelScope.launch { repository.markRead(id) }
}

/** Marks every notification in the inbox as read. */
fun markAllRead() {
viewModelScope.launch { repository.markAllRead() }
}
Expand Down
Loading
Loading