Skip to content

Conversation

@theMr17
Copy link
Owner

@theMr17 theMr17 commented Apr 28, 2025

Summary by CodeRabbit

  • New Features

    • Introduced a complete login and authentication flow with GitHub OAuth, including login, setup, and notification screens.
    • Added deep linking support for handling OAuth callbacks.
    • Implemented a splash screen for improved app startup experience.
    • Added user-friendly error messages for network and persistence errors.
  • Improvements

    • Enhanced error handling and messaging throughout the authentication process.
    • Updated app theme and permissions for network access.
    • Improved state management and navigation using Jetpack Compose and Navigation.
    • Added persistent OAuth state management to secure the authentication process.
    • Included authorization token retrieval and storage with network error handling.
    • Integrated GitHub authentication intent creation with state tracking.
    • Refined HTTP client to include authorization headers dynamically.
  • Bug Fixes

    • Improved reliability of token and OAuth state persistence with robust error handling.
  • Tests

    • Added comprehensive UI and ViewModel tests for login and setup flows to ensure correct behavior in various scenarios.
    • Updated tests to use structured result wrappers and Truth assertions.
    • Converted HTTP client factory tests to use dependency injection with Hilt.
  • Chores

    • Updated dependencies and build configuration for better navigation, splash screen, and testing support.
    • Refactored build scripts to manage client credentials securely and exclude unnecessary resources.
    • Added utility functions for lifecycle-aware event observation and toast messaging.

Issue Reference

Screenshots

Essential Checklist

  • The PR title starts with "Fix #bugnum: " (If this PR fixes part of an issue, prefix the title with "Fix part of #bugnum: ...").
  • The PR does not contain any unnecessary code changes from Android Studio.
  • The PR is made from a branch that is not called "main" and is up-to-date with "main".

@coderabbitai
Copy link

coderabbitai bot commented Apr 28, 2025

Walkthrough

This change implements GitHub OAuth authentication and robust error handling, introduces a Compose Navigation architecture, and adds comprehensive UI, domain, and data layers for login and setup flows. It refactors persistence operations to use structured error types, enhances dependency injection, and provides new UI screens, navigation routes, and localized error messages.

Changes

Files/Groups Change Summary
app/build.gradle.kts, gradle/libs.versions.toml Added Compose Navigation, Hilt, splash screen, and test dependencies; updated dependency versions and packaging exclusions.
app/src/main/AndroidManifest.xml, app/src/main/res/values/splash.xml, app/src/main/res/values/strings.xml Added INTERNET permission, splash screen theme, intent filter for OAuth callback, and new string resources for errors and UI.
app/src/main/java/com/notifier/app/MainActivity.kt Refactored to use splash screen and Compose Navigation; integrated login, setup, and notification screens with deep linking.
app/src/main/java/com/notifier/app/auth/... (all subfolders) Implemented GitHub OAuth flow: domain models, DTOs, mappers, networking, presentation (state, events, actions, ViewModels, UI, navigation), and intent provider for OAuth.
app/src/main/java/com/notifier/app/core/data/persistence/DataStoreManager.kt, PreferenceKeys.kt, runDataStoreCatching.kt Refactored DataStoreManager to use Result-wrapped error handling; added OAuth state management and error categorization utility.
app/src/main/java/com/notifier/app/core/domain/util/PersistenceError.kt Introduced PersistenceError enum for persistence error categorization.
app/src/main/java/com/notifier/app/core/presentation/util/NetworkErrorToString.kt, PersistenceErrorToString.kt, showToast.kt, ObserveAsEvents.kt Added utilities for error-to-string conversion, toast display, and lifecycle-aware event observation in Compose.
app/src/main/java/com/notifier/app/core/presentation/BaseViewModel.kt Added generic base ViewModel class for state, event, and action management.
app/src/main/java/com/notifier/app/core/data/networking/HttpClientFactory.kt Refactored to a singleton class with injected DataStoreManager; adds Bearer token to requests.
app/src/main/java/com/notifier/app/di/ApiModule.kt Updated DI to provide HttpClient and RemoteAuthTokenDataSource as singletons.
app/src/main/java/com/notifier/app/notification/domain/NotificationDataSource.kt Generalized error type in interface from NetworkError to Error.
app/src/main/java/com/notifier/app/notification/presentation/NotificationRoute.kt, NotificationScreen.kt Added notification screen route and UI.
app/src/androidTest/java/com/notifier/app/... (all subfolders) Added and updated tests for DataStoreManager, HttpClientFactory, Login and Setup screens and ViewModels, using Hilt and Compose testing.

Sequence Diagram(s)

sequenceDiagram
    participant User
    participant App (MainActivity)
    participant LoginViewModel
    participant GitHubAuthIntentProvider
    participant Browser
    participant GitHub
    participant SetupViewModel
    participant DataStoreManager

    User->>App (MainActivity): Launches app
    App (MainActivity)->>LoginViewModel: Check login status
    LoginViewModel->>DataStoreManager: getAccessToken()
    DataStoreManager-->>LoginViewModel: Result(token or error)
    LoginViewModel-->>App (MainActivity): State: LOGGED_OUT or LOGGED_IN

    User->>App (MainActivity): Clicks "Login with GitHub"
    App (MainActivity)->>GitHubAuthIntentProvider: createGitHubAuthIntent()
    GitHubAuthIntentProvider->>DataStoreManager: setOAuthState(state)
    GitHubAuthIntentProvider-->>App (MainActivity): Intent (OAuth URL), state
    App (MainActivity)->>Browser: Launch OAuth URL
    Browser->>GitHub: User authenticates
    GitHub->>App (MainActivity): Redirect with code & state (deep link)
    App (MainActivity)->>SetupViewModel: getAuthToken(code, state)
    SetupViewModel->>DataStoreManager: getOAuthState()
    SetupViewModel->>GitHub: Exchange code for access token
    GitHub-->>SetupViewModel: access_token
    SetupViewModel->>DataStoreManager: setAccessToken(token)
    SetupViewModel-->>App (MainActivity): State: SUCCESS or FAILED
    App (MainActivity)->>User: Navigates to Home/Notification screen
Loading

Assessment against linked issues

Objective Addressed Explanation
Implement GitHub OAuth authentication flow (redirect, code exchange, token handling) (#12)
Use Compose Navigation for screen management, define routes, and set up NavHost (#16)
Refactor DataStoreManager for robust error handling with runDataStoreCatching, PersistenceError, and Result (#18)
Add utility functions to convert errors to user-friendly strings (#18)

Assessment against linked issues: Out-of-scope changes

Code Change Explanation
Generalized error type in NotificationDataSource from NetworkError to Error (app/src/main/java/com/notifier/app/notification/domain/NotificationDataSource.kt) This change is not directly related to authentication, navigation, or DataStoreManager error handling. It may be a preparatory or consistency update, but it's not required by the linked issues.

Possibly related PRs

Poem

In the garden of code, a rabbit did hop,
Adding OAuth and screens—oh, it just wouldn’t stop!
With errors now tamed and navigation anew,
Compose routes are blooming, the tests grew and grew.
GitHub logins now shine, the splash screen’s a delight—
This bunny’s been busy coding all night!
🐇✨


📜 Recent review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between ba8c99d and 3ecbbf6.

📒 Files selected for processing (1)
  • app/src/androidTest/java/com/notifier/app/auth/presentation/setup/SetupViewModelTest.kt (1 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
  • app/src/androidTest/java/com/notifier/app/auth/presentation/setup/SetupViewModelTest.kt
✨ Finishing Touches
  • 📝 Generate Docstrings

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share
🪧 Tips

Chat

There are 3 ways to chat with CodeRabbit:

  • Review comments: Directly reply to a review comment made by CodeRabbit. Example:
    • I pushed a fix in commit <commit_id>, please review it.
    • Explain this complex logic.
    • Open a follow-up GitHub issue for this discussion.
  • Files and specific lines of code (under the "Files changed" tab): Tag @coderabbitai in a new review comment at the desired location with your query. Examples:
    • @coderabbitai explain this code block.
    • @coderabbitai modularize this function.
  • PR comments: Tag @coderabbitai in a new PR comment to ask questions about the PR branch. For the best results, please provide a very specific query, as very limited context is provided in this mode. Examples:
    • @coderabbitai gather interesting stats about this repository and render them as a table. Additionally, render a pie chart showing the language distribution in the codebase.
    • @coderabbitai read src/utils.ts and explain its main purpose.
    • @coderabbitai read the files in the src/scheduler package and generate a class diagram using mermaid and a README in the markdown format.
    • @coderabbitai help me debug CodeRabbit configuration file.

Support

Need help? Create a ticket on our support page for assistance with any issues or questions.

Note: Be mindful of the bot's finite context window. It's strongly recommended to break down tasks such as reading entire modules into smaller chunks. For a focused discussion, use review comments to chat about specific files and their changes, instead of using the PR comments.

CodeRabbit Commands (Invoked using PR comments)

  • @coderabbitai pause to pause the reviews on a PR.
  • @coderabbitai resume to resume the paused reviews.
  • @coderabbitai review to trigger an incremental review. This is useful when automatic reviews are disabled for the repository.
  • @coderabbitai full review to do a full review from scratch and review all the files again.
  • @coderabbitai summary to regenerate the summary of the PR.
  • @coderabbitai generate docstrings to generate docstrings for this PR.
  • @coderabbitai generate sequence diagram to generate a sequence diagram of the changes in this PR.
  • @coderabbitai resolve resolve all the CodeRabbit review comments.
  • @coderabbitai configuration to show the current CodeRabbit configuration for the repository.
  • @coderabbitai help to get help.

Other keywords and placeholders

  • Add @coderabbitai ignore anywhere in the PR description to prevent this PR from being reviewed.
  • Add @coderabbitai summary to generate the high-level summary at a specific location in the PR description.
  • Add @coderabbitai anywhere in the PR title to generate the title automatically.

CodeRabbit Configuration File (.coderabbit.yaml)

  • You can programmatically configure CodeRabbit by adding a .coderabbit.yaml file to the root of your repository.
  • Please see the configuration documentation for more information.
  • If your editor has YAML language server enabled, you can add the path at the top of this file to enable auto-completion and validation: # yaml-language-server: $schema=https://coderabbit.ai/integrations/schema.v2.json

Documentation and Community

  • Visit our Documentation for detailed information on how to use CodeRabbit.
  • Join our Discord Community to get help, request features, and share feedback.
  • Follow us on X/Twitter for updates and announcements.

@theMr17 theMr17 changed the title Fix #12: @coderabbitai Fix #12, #16: @coderabbitai May 1, 2025
@theMr17 theMr17 changed the title Fix #12, #16: @coderabbitai Fix #12, #16, #18: @coderabbitai May 1, 2025
@theMr17 theMr17 marked this pull request as ready for review May 1, 2025 08:56
@coderabbitai coderabbitai bot changed the title Fix #12, #16, #18: @coderabbitai Fix #12, #16, #18: Add GitHub OAuth, error handling, and navigation with Jetpack Compose May 1, 2025
Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 14

🧹 Nitpick comments (17)
app/src/main/java/com/notifier/app/core/domain/util/PersistenceError.kt (1)

1-7: Good error classification for persistence operations.

The PersistenceError enum effectively categorizes the different types of errors that can occur during persistence operations. The three categories (IO, SERIALIZATION, UNKNOWN) cover the common failure scenarios when working with DataStore.

Consider adding a more detailed documentation comment above the enum to explain when each error type would be used and how they should be handled.

 package com.notifier.app.core.domain.util

+/**
+ * Represents errors that can occur during persistence operations.
+ * 
+ * - IO: Input/output errors when reading from or writing to storage
+ * - SERIALIZATION: Errors during serialization or deserialization of data
+ * - UNKNOWN: Unexpected or uncategorized errors
+ */
 enum class PersistenceError : Error {
     IO,
     SERIALIZATION,
     UNKNOWN,
 }
app/src/main/java/com/notifier/app/core/presentation/util/showToast.kt (1)

1-15: Well-documented utility function.

This is a clean implementation of a toast utility with good KDoc comments. The default duration parameter makes it more convenient to use.

Consider making this an extension function on Context for even more elegant usage:

 package com.notifier.app.core.presentation.util

 import android.content.Context
 import android.widget.Toast

 /**
- * Extension function to show a Toast message in the given context.
+ * Extension function to show a Toast message.
  *
- * @param context the context in which the Toast should be shown.
  * @param message the message to be shown in the Toast.
  * @param duration the duration for which the Toast should be shown. Defaults to Toast.LENGTH_LONG.
  */
-fun showToast(context: Context, message: String, duration: Int = Toast.LENGTH_LONG) {
+fun Context.showToast(message: String, duration: Int = Toast.LENGTH_LONG) {
-    Toast.makeText(context, message, duration).show()
+    Toast.makeText(this, message, duration).show()
 }
app/build.gradle.kts (1)

36-45: Good configuration for client credentials with sensible defaults.

The implementation correctly adds the CLIENT_ID and CLIENT_SECRET from properties with fallback to dummy values. This approach works well for both local development and CI environments.

For further security, consider adding a check or warning when using dummy values in release builds:

 release {
     isMinifyEnabled = false
     buildConfigField("String", "BASE_URL", "\"https://api.github.com/\"")
+    // Check if using real credentials for release build
+    val clientId = properties.getProperty("CLIENT_ID", "dummy_client_id")
+    if (clientId == "dummy_client_id") {
+        logger.warn("WARNING: Using dummy CLIENT_ID for release build!")
+    }
+    val clientSecret = properties.getProperty("CLIENT_SECRET", "dummy_client_secret")
+    if (clientSecret == "dummy_client_secret") {
+        logger.warn("WARNING: Using dummy CLIENT_SECRET for release build!")
+    }
     buildConfigField(
         "String",
         "CLIENT_ID",
-        "\"${properties.getProperty("CLIENT_ID", "dummy_client_id")}\""
+        "\"$clientId\""
     )
     buildConfigField(
         "String",
         "CLIENT_SECRET",
-        "\"${properties.getProperty("CLIENT_SECRET", "dummy_client_secret")}\""
+        "\"$clientSecret\""
     )

Also applies to: 50-59

app/src/main/java/com/notifier/app/core/presentation/util/NetworkErrorToString.kt (1)

7-17: Consider using a specific string resource for SERVER_ERROR

Currently, NetworkError.SERVER_ERROR is mapped to the generic "unknown error" string. For better user experience, consider creating a dedicated string resource for server errors:

val resId = when (this) {
    NetworkError.REQUEST_TIMEOUT -> R.string.error_request_timeout
    NetworkError.TOO_MANY_REQUESTS -> R.string.error_too_many_requests
    NetworkError.NO_INTERNET -> R.string.error_no_internet
-   NetworkError.SERVER_ERROR -> R.string.error_unknown
+   NetworkError.SERVER_ERROR -> R.string.error_server
    NetworkError.SERIALIZATION -> R.string.error_serialization
    NetworkError.UNKNOWN -> R.string.error_unknown
}

You would also need to add a new string resource in strings.xml:

<string name="error_server">The server encountered an error. Please try again later.</string>
app/src/main/res/values/strings.xml (1)

3-8: Well-written error messages with good UX

These error messages are clear, user-friendly, and follow good UX writing practices. They avoid technical jargon and provide appropriate guidance to users.

Consider adding one more string resource for server errors to complement the NetworkError.SERVER_ERROR case:

<string name="error_server">The server encountered an error. Please try again later.</string>
app/src/main/java/com/notifier/app/auth/presentation/login/LoginRoute.kt (1)

35-38: Consider passing the modifier to the LoginScreen

The LoginScreen composable accepts a modifier parameter, but you're not passing the modifier from LoginRoute to it. For better composability and reusability, consider forwarding the modifier:

 LoginScreen(
     state = state,
-    onAction = { action -> viewModel.onAction(action) }
+    onAction = { action -> viewModel.onAction(action) },
+    modifier = Modifier
 )
app/src/main/java/com/notifier/app/core/presentation/util/ObserveAsEvents.kt (1)

12-27: LGTM: Well-implemented lifecycle-aware event collector

This utility function is well-designed following Compose best practices:

  • Properly respects the Android lifecycle using repeatOnLifecycle
  • Correctly uses Dispatchers.Main.immediate for UI updates
  • Provides flexibility with optional keys for recomposition control

One small enhancement could be adding a third optional key parameter for additional flexibility:

fun <T> ObserveAsEvents(
    events: Flow<T>,
    key1: Any? = null,
    key2: Any? = null,
+   key3: Any? = null,
    onEvent: (T) -> Unit,
) {
    val lifecycleOwner = LocalLifecycleOwner.current
-   LaunchedEffect(lifecycleOwner.lifecycle, key1, key2) {
+   LaunchedEffect(lifecycleOwner.lifecycle, key1, key2, key3) {
        lifecycleOwner.repeatOnLifecycle(Lifecycle.State.STARTED) {
            withContext(Dispatchers.Main.immediate) {
                events.collect(onEvent)
            }
        }
    }
}
app/src/main/java/com/notifier/app/auth/presentation/login/LoginViewModel.kt (2)

13-25: Add class-level documentation for the ViewModel

Consider adding KDoc for the ViewModel class to describe its role in the authentication flow. This would help other developers understand how this component fits into the overall architecture.

+/**
+ * ViewModel for the login screen that manages UI state and handles user actions.
+ * Part of the GitHub OAuth authentication flow.
+ */
@HiltViewModel
class LoginViewModel @Inject constructor() : ViewModel() {

17-21: Consider adjusting the stateIn sharing timeout

The current 5-second timeout for stopping the state flow collection might be appropriate, but it's worth documenting why this specific value was chosen, especially since this is part of the authentication flow where timing can be important.

val state = _state
    .stateIn(
        viewModelScope,
-        SharingStarted.WhileSubscribed(stopTimeoutMillis = 5000L),
+        SharingStarted.WhileSubscribed(stopTimeoutMillis = 5000L), // Keep state alive for 5 seconds after last subscriber
        LoginState()
    )
app/src/main/java/com/notifier/app/core/data/persistence/runDataStoreCatching.kt (1)

11-37: Add logging for caught exceptions

Your error handling correctly maps exceptions to appropriate PersistenceError types. However, the static analysis tool has flagged "swallowed exceptions" on lines 29, 31, and 33 because you're not logging or preserving the original exception details.

Consider adding logging to capture the original exception details for debugging purposes:

import com.notifier.app.core.domain.util.PersistenceError
import com.notifier.app.core.domain.util.Result
import kotlinx.coroutines.ensureActive
import kotlinx.io.IOException
import kotlinx.serialization.SerializationException
import kotlin.coroutines.cancellation.CancellationException
import kotlin.coroutines.coroutineContext
+import android.util.Log

/**
 * Executes a suspend block safely, returning a [Result] that wraps either the success value
 * or a [PersistenceError] in case of exception.
 *
 * Use this to wrap DataStore operations that might fail (e.g., IO, serialization).
 *
 * @param block the suspend lambda to execute.
 * @return a [Result.Success] if the block executes without exception,
 * or a [Result.Error] with appropriate [PersistenceError].
 */
suspend inline fun <T> runDataStoreCatching(
    crossinline block: suspend () -> T,
): Result<T, PersistenceError> {
    return try {
        val result = block()
        Result.Success(result)
    } catch (e: CancellationException) {
        throw e // Let coroutine cancellation propagate
    } catch (e: IOException) {
+        Log.e("DataStore", "IO error during DataStore operation", e)
        Result.Error(PersistenceError.IO)
    } catch (e: SerializationException) {
+        Log.e("DataStore", "Serialization error during DataStore operation", e)
        Result.Error(PersistenceError.SERIALIZATION)
    } catch (e: Exception) {
        coroutineContext.ensureActive()
+        Log.e("DataStore", "Unknown error during DataStore operation", e)
        Result.Error(PersistenceError.UNKNOWN)
    }
}
🧰 Tools
🪛 detekt (1.23.7)

[warning] 29-29: The caught exception is swallowed. The original exception could be lost.

(detekt.exceptions.SwallowedException)


[warning] 31-31: The caught exception is swallowed. The original exception could be lost.

(detekt.exceptions.SwallowedException)


[warning] 33-33: The caught exception is swallowed. The original exception could be lost.

(detekt.exceptions.SwallowedException)

app/src/main/java/com/notifier/app/auth/data/networking/RemoteAuthTokenDataSource.kt (1)

24-34: Extract the GitHub OAuth endpoint URL to a constant

The GitHub OAuth token endpoint URL is hardcoded in the implementation. For improved maintainability and flexibility, consider extracting this URL to a constants file or configuration.

+private const val GITHUB_OAUTH_TOKEN_URL = "https://github.com/login/oauth/access_token"

override suspend fun getAuthToken(
    clientId: String,
    clientSecret: String,
    code: String,
): Result<AuthToken, NetworkError> {
    return safeCall<AuthTokenResponseDto> {
        httpClient.post(
-            urlString = "https://github.com/login/oauth/access_token"
+            urlString = GITHUB_OAUTH_TOKEN_URL
        ) {
            parameter("client_id", clientId)
            parameter("client_secret", clientSecret)
            parameter("code", code)
        }

Additionally, consider specifying the desired content type for the GitHub API. GitHub's OAuth endpoints often require specific content types like application/json.

app/src/main/java/com/notifier/app/auth/presentation/setup/SetupScreen.kt (1)

28-34: Add loading indicators for the loading states

The loading states (FETCHING_TOKEN and SAVING_TOKEN) only display text but don't include any visual loading indicators. Consider adding a progress indicator to improve user experience.

SetupStep.FETCHING_TOKEN -> {
+    Column(
+        horizontalAlignment = Alignment.CenterHorizontally,
+        verticalArrangement = Arrangement.spacedBy(8.dp)
+    ) {
+        CircularProgressIndicator()
         Text(text = "Connecting to GitHub...")
+    }
}

SetupStep.SAVING_TOKEN -> {
+    Column(
+        horizontalAlignment = Alignment.CenterHorizontally,
+        verticalArrangement = Arrangement.spacedBy(8.dp)
+    ) {
+        CircularProgressIndicator()
         Text(text = "Saving user information...")
+    }
}

Don't forget to add the necessary import for CircularProgressIndicator and Arrangement.spacedBy.

app/src/main/java/com/notifier/app/core/data/persistence/DataStoreManager.kt (2)

26-30: Enhanced error handling with Result type.

The getAccessToken method now properly uses the runDataStoreCatching utility to handle potential errors during DataStore operations, returning a typed Result that clearly communicates success or failure scenarios.

However, the return type could be more specific about the error type.

-suspend fun getAccessToken(): Result<String, Error> = runDataStoreCatching {
+suspend fun getAccessToken(): Result<String, PersistenceError> = runDataStoreCatching {

36-42: Consistent error handling pattern applied.

The setAccessToken method follows the same pattern as getAccessToken, providing consistent error handling throughout the DataStore operations.

Similarly to the previous method, the return type could be more specific:

-suspend fun setAccessToken(accessToken: String): Result<Unit, Error> = runDataStoreCatching {
+suspend fun setAccessToken(accessToken: String): Result<Unit, PersistenceError> = runDataStoreCatching {
app/src/main/java/com/notifier/app/auth/presentation/setup/SetupViewModel.kt (3)

53-55: Consider extracting BuildConfig dependencies for better testability.

Direct usage of BuildConfig makes unit testing more challenging. Consider injecting these values or using a configuration provider interface that can be mocked in tests.

+ // In a new file: AuthConfig.kt
+ interface AuthConfig {
+     val clientId: String
+     val clientSecret: String
+ }
+
+ class ProductionAuthConfig @Inject constructor() : AuthConfig {
+     override val clientId: String = BuildConfig.CLIENT_ID
+     override val clientSecret: String = BuildConfig.CLIENT_SECRET
+ }

// In SetupViewModel constructor
-class SetupViewModel @Inject constructor(
-    private val authTokenDataSource: AuthTokenDataSource,
-    private val dataStoreManager: DataStoreManager,
-) : ViewModel() {
+class SetupViewModel @Inject constructor(
+    private val authTokenDataSource: AuthTokenDataSource,
+    private val dataStoreManager: DataStoreManager,
+    private val authConfig: AuthConfig
+) : ViewModel() {

// In getAuthToken method
-            authTokenDataSource.getAuthToken(
-                clientId = BuildConfig.CLIENT_ID,
-                clientSecret = BuildConfig.CLIENT_SECRET,
-                code = code
-            )
+            authTokenDataSource.getAuthToken(
+                clientId = authConfig.clientId,
+                clientSecret = authConfig.clientSecret,
+                code = code
+            )

39-69: Consider adding logging for better debugging.

For improved debugging capabilities, consider adding logging statements at key points in the authentication flow, especially around error conditions.

+ import timber.log.Timber

// In getAuthToken method
  fun getAuthToken(code: String?) {
      if (code.isNullOrBlank()) {
+         Timber.d("Authentication failed: empty or null code")
          _state.update { it.copy(setupStep = SetupStep.FAILED) }
          return
      }

+     Timber.d("Fetching auth token with code")
      _state.update { it.copy(setupStep = SetupStep.FETCHING_TOKEN) }

      viewModelScope.launch {
          authTokenDataSource.getAuthToken(
              clientId = BuildConfig.CLIENT_ID,
              clientSecret = BuildConfig.CLIENT_SECRET,
              code = code
          ).onSuccess { authToken ->
+             Timber.d("Auth token received successfully")
              _state.update {
                  it.copy(
                      setupStep = SetupStep.SAVING_TOKEN,
                      authToken = authToken
                  )
              }
              saveAuthToken(authToken.accessToken)
          }.onError { error ->
+             Timber.e(error, "Failed to fetch auth token")
              _state.update { it.copy(setupStep = SetupStep.FAILED) }
              _events.send(SetupEvent.NetworkErrorEvent(error as NetworkError))
          }
      }
  }

75-84: Add logging to saveAuthToken method.

Similarly, consider adding logging to the saveAuthToken method for better debugging.

private fun saveAuthToken(token: String) {
    viewModelScope.launch {
+       Timber.d("Saving auth token to DataStore")
        dataStoreManager.setAccessToken(token).onSuccess {
+           Timber.d("Auth token saved successfully")
            _state.update { it.copy(setupStep = SetupStep.SUCCESS) }
        }.onError { error ->
+           Timber.e(error, "Failed to save auth token")
            _state.update { it.copy(setupStep = SetupStep.FAILED) }
            _events.send(SetupEvent.PersistenceErrorEvent(error as PersistenceError))
        }
    }
}
📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between c529437 and fb5a0cd.

📒 Files selected for processing (33)
  • app/build.gradle.kts (3 hunks)
  • app/src/androidTest/java/com/notifier/app/core/data/persistence/DataStoreManagerTest.kt (2 hunks)
  • app/src/main/AndroidManifest.xml (2 hunks)
  • app/src/main/java/com/notifier/app/MainActivity.kt (2 hunks)
  • app/src/main/java/com/notifier/app/auth/data/mappers/AuthTokenMapper.kt (1 hunks)
  • app/src/main/java/com/notifier/app/auth/data/networking/RemoteAuthTokenDataSource.kt (1 hunks)
  • app/src/main/java/com/notifier/app/auth/data/networking/dto/AuthTokenResponseDto.kt (1 hunks)
  • app/src/main/java/com/notifier/app/auth/domain/AuthToken.kt (1 hunks)
  • app/src/main/java/com/notifier/app/auth/domain/AuthTokenDataSource.kt (1 hunks)
  • app/src/main/java/com/notifier/app/auth/presentation/login/LoginAction.kt (1 hunks)
  • app/src/main/java/com/notifier/app/auth/presentation/login/LoginEvent.kt (1 hunks)
  • app/src/main/java/com/notifier/app/auth/presentation/login/LoginRoute.kt (1 hunks)
  • app/src/main/java/com/notifier/app/auth/presentation/login/LoginScreen.kt (1 hunks)
  • app/src/main/java/com/notifier/app/auth/presentation/login/LoginState.kt (1 hunks)
  • app/src/main/java/com/notifier/app/auth/presentation/login/LoginViewModel.kt (1 hunks)
  • app/src/main/java/com/notifier/app/auth/presentation/login/components/LoginButton.kt (1 hunks)
  • app/src/main/java/com/notifier/app/auth/presentation/setup/SetupEvent.kt (1 hunks)
  • app/src/main/java/com/notifier/app/auth/presentation/setup/SetupRoute.kt (1 hunks)
  • app/src/main/java/com/notifier/app/auth/presentation/setup/SetupScreen.kt (1 hunks)
  • app/src/main/java/com/notifier/app/auth/presentation/setup/SetupState.kt (1 hunks)
  • app/src/main/java/com/notifier/app/auth/presentation/setup/SetupViewModel.kt (1 hunks)
  • app/src/main/java/com/notifier/app/auth/presentation/util/createGitHubAuthIntent.kt (1 hunks)
  • app/src/main/java/com/notifier/app/core/data/persistence/DataStoreManager.kt (1 hunks)
  • app/src/main/java/com/notifier/app/core/data/persistence/runDataStoreCatching.kt (1 hunks)
  • app/src/main/java/com/notifier/app/core/domain/util/PersistenceError.kt (1 hunks)
  • app/src/main/java/com/notifier/app/core/presentation/util/NetworkErrorToString.kt (1 hunks)
  • app/src/main/java/com/notifier/app/core/presentation/util/ObserveAsEvents.kt (1 hunks)
  • app/src/main/java/com/notifier/app/core/presentation/util/PersistenceErrorToString.kt (1 hunks)
  • app/src/main/java/com/notifier/app/core/presentation/util/showToast.kt (1 hunks)
  • app/src/main/java/com/notifier/app/di/ApiModule.kt (1 hunks)
  • app/src/main/java/com/notifier/app/notification/domain/NotificationDataSource.kt (2 hunks)
  • app/src/main/res/values/strings.xml (1 hunks)
  • gradle/libs.versions.toml (3 hunks)
🧰 Additional context used
🧬 Code Graph Analysis (8)
app/src/main/java/com/notifier/app/auth/presentation/login/components/LoginButton.kt (2)
app/src/main/java/com/notifier/app/auth/presentation/util/createGitHubAuthIntent.kt (1)
  • createGitHubAuthIntent (15-22)
app/src/main/java/com/notifier/app/ui/theme/Theme.kt (1)
  • GitHubNotifierTheme (39-61)
app/src/main/java/com/notifier/app/auth/presentation/setup/SetupScreen.kt (1)
app/src/main/java/com/notifier/app/ui/theme/Theme.kt (1)
  • GitHubNotifierTheme (39-61)
app/src/main/java/com/notifier/app/auth/presentation/login/LoginScreen.kt (2)
app/src/main/java/com/notifier/app/auth/presentation/login/components/LoginButton.kt (1)
  • LoginButton (13-24)
app/src/main/java/com/notifier/app/ui/theme/Theme.kt (1)
  • GitHubNotifierTheme (39-61)
app/src/main/java/com/notifier/app/auth/presentation/login/LoginRoute.kt (2)
app/src/main/java/com/notifier/app/core/presentation/util/ObserveAsEvents.kt (1)
  • ObserveAsEvents (12-27)
app/src/main/java/com/notifier/app/auth/presentation/login/LoginScreen.kt (1)
  • LoginScreen (16-29)
app/src/main/java/com/notifier/app/auth/presentation/setup/SetupRoute.kt (3)
app/src/main/java/com/notifier/app/core/presentation/util/ObserveAsEvents.kt (1)
  • ObserveAsEvents (12-27)
app/src/main/java/com/notifier/app/core/presentation/util/showToast.kt (1)
  • showToast (13-15)
app/src/main/java/com/notifier/app/auth/presentation/setup/SetupScreen.kt (1)
  • SetupScreen (17-50)
app/src/main/java/com/notifier/app/auth/data/networking/RemoteAuthTokenDataSource.kt (1)
app/src/main/java/com/notifier/app/core/data/networking/safeCall.kt (1)
  • safeCall (22-37)
app/src/main/java/com/notifier/app/MainActivity.kt (2)
app/src/main/java/com/notifier/app/auth/presentation/login/LoginRoute.kt (1)
  • LoginRoute (16-39)
app/src/main/java/com/notifier/app/auth/presentation/setup/SetupRoute.kt (1)
  • SetupRoute (20-45)
app/src/main/java/com/notifier/app/core/data/persistence/DataStoreManager.kt (1)
app/src/main/java/com/notifier/app/core/data/persistence/runDataStoreCatching.kt (1)
  • runDataStoreCatching (21-37)
🪛 detekt (1.23.7)
app/src/main/java/com/notifier/app/core/data/persistence/runDataStoreCatching.kt

[warning] 29-29: The caught exception is swallowed. The original exception could be lost.

(detekt.exceptions.SwallowedException)


[warning] 31-31: The caught exception is swallowed. The original exception could be lost.

(detekt.exceptions.SwallowedException)


[warning] 33-33: The caught exception is swallowed. The original exception could be lost.

(detekt.exceptions.SwallowedException)

🔇 Additional comments (41)
app/src/main/java/com/notifier/app/auth/domain/AuthToken.kt (1)

3-7: Clean and concise domain model implementation.

This AuthToken data class correctly encapsulates the essential OAuth token information with immutable properties, following Kotlin best practices for domain models.

app/src/main/java/com/notifier/app/auth/data/mappers/AuthTokenMapper.kt (1)

6-10: Well-implemented mapper extension function.

The extension function properly maps the DTO to the domain model with a clean, concise implementation using named parameters for readability.

app/src/main/java/com/notifier/app/auth/presentation/login/LoginEvent.kt (1)

5-7: Appropriate use of sealed interface for login events.

The sealed interface pattern is well-suited for representing a closed set of events in the presentation layer. The Error implementation properly encapsulates network errors.

app/src/main/java/com/notifier/app/auth/presentation/login/LoginAction.kt (1)

3-5: Clean implementation of user action representation.

Using a sealed interface with a data object implementation is a good approach for modeling UI actions. This follows modern Kotlin patterns and supports a type-safe event-driven architecture.

app/src/androidTest/java/com/notifier/app/core/data/persistence/DataStoreManagerTest.kt (5)

4-4: Good addition of Result import.

The new import correctly supports the refactored API that now returns Result types.


32-39: Well-structured test update for the new Result-based API.

The test has been properly updated to check both that the result is a Success type and then verify the actual token value. The added comments also improve readability by clearly separating the save and retrieve steps.


44-46: Correct handling of Result in empty token test case.

The test appropriately checks for Result.Success before extracting the data.


51-56: Good update to empty string test with Result handling.

The test correctly verifies both the Result type and the token value.


64-75: Well-structured token overwrite test with clear step comments.

The test is properly updated to handle the Result wrapper and includes helpful comments that clarify the test steps.

app/build.gradle.kts (2)

1-2: Secure approach to handling client credentials.

Good implementation for loading client credentials from local.properties, which should not be committed to version control. The null-safe approach ensures the app can be built even if the file doesn't exist.

Also applies to: 27-32


91-91: Good addition of navigation dependencies.

The added dependencies for Jetpack Compose Navigation and its Hilt integration support the new navigation system mentioned in the AI summary.

Also applies to: 95-96

app/src/main/java/com/notifier/app/auth/presentation/login/LoginState.kt (1)

1-10: Well-designed state class for Compose

This data class follows best practices for a Jetpack Compose state holder:

  • Good use of @Immutable annotation for Compose performance optimizations
  • Appropriate default values for initial state
  • Clean and focused design with clear property names
app/src/main/java/com/notifier/app/core/presentation/util/PersistenceErrorToString.kt (1)

1-14: Good approach to error message handling

This extension function is well-implemented:

  • Properly maps domain errors to user-facing messages
  • Supports localization through string resources
  • Complete handling of all error types in the enum
  • Good separation of concerns between domain errors and UI presentation
app/src/main/AndroidManifest.xml (2)

5-5: INTERNET permission correctly added for OAuth networking.

This permission is required for the app to make network requests to GitHub's authentication endpoints.


21-30: OAuth callback intent filter properly configured.

The intent filter is correctly set up to handle deep links with the custom URI scheme "github-notifier://auth-callback", which enables the GitHub OAuth flow to redirect back to your app after authentication.

app/src/main/java/com/notifier/app/auth/presentation/setup/SetupEvent.kt (1)

6-9: Well-structured event interface for error handling.

Good use of sealed interface pattern for type-safe error events. The clear separation between network and persistence errors will facilitate precise error handling in the UI layer.

app/src/main/java/com/notifier/app/notification/domain/NotificationDataSource.kt (2)

3-3: Good generalization of the error import.

Replacing a specific error type with the more generic Error interface improves flexibility in the error handling system.


20-20: Well-aligned error handling approach.

Generalizing the return type from a specific error to the broader Error interface allows for more flexible error handling while maintaining type safety. The documentation is properly updated to reflect this change.

Also applies to: 24-24

app/src/main/java/com/notifier/app/auth/data/networking/dto/AuthTokenResponseDto.kt (1)

6-14: Well-structured DTO for GitHub OAuth token response.

The DTO is properly annotated with @Serializable and uses @SerialName to correctly map JSON field names to Kotlin property names. All properties are appropriately defined as non-nullable strings.

app/src/main/java/com/notifier/app/di/ApiModule.kt (2)

17-21: Good addition of @singleton annotation

Adding the @singleton annotation ensures that only one instance of HttpClient is created and reused throughout the application. This is important for HttpClient which manages connection pools and resources.


23-29: Proper implementation of dependency injection

The provider method correctly binds the RemoteAuthTokenDataSource implementation to the AuthTokenDataSource interface, following dependency inversion principles. This allows for easier testing and potential alternative implementations in the future.

app/src/main/java/com/notifier/app/auth/presentation/login/LoginRoute.kt (2)

13-14: LGTM: Well-structured navigation destination

The LoginScreen data object is properly marked as @serializable, which is necessary for navigation in apps using Compose Navigation with type-safe navigation destinations.


23-33: LGTM: Appropriate event handling

Good implementation of one-time event handling using ObserveAsEvents utility. The error toast shows user-friendly messages using the error.toString() extension.

app/src/main/java/com/notifier/app/auth/presentation/login/LoginScreen.kt (1)

31-44: LGTM: Well-implemented preview

The preview composable is well-structured with appropriate annotations for light/dark and dynamic color previews. Good practice to wrap the component in both the theme and Scaffold to simulate the real usage environment.

app/src/main/java/com/notifier/app/auth/presentation/setup/SetupState.kt (2)

6-10: LGTM: Well-structured immutable state

The SetupState class is properly marked as @immutable and has a clear structure with appropriate default values. This design promotes predictable state management.


12-24: LGTM: Well-documented enum states

Excellent use of documentation comments for each enum value, clearly explaining the purpose and progression of each step in the authentication setup process.

app/src/main/java/com/notifier/app/core/data/persistence/runDataStoreCatching.kt (1)

27-35: Well-implemented coroutine cancellation handling

The handling of CancellationException is correctly implemented. By rethrowing this specific exception, you ensure that coroutine cancellation properly propagates through the call stack. Additionally, the check for coroutineContext.ensureActive() before returning an unknown error is a good practice to prevent executing code in cancelled coroutines.

🧰 Tools
🪛 detekt (1.23.7)

[warning] 29-29: The caught exception is swallowed. The original exception could be lost.

(detekt.exceptions.SwallowedException)


[warning] 31-31: The caught exception is swallowed. The original exception could be lost.

(detekt.exceptions.SwallowedException)


[warning] 33-33: The caught exception is swallowed. The original exception could be lost.

(detekt.exceptions.SwallowedException)

app/src/main/java/com/notifier/app/auth/data/networking/RemoteAuthTokenDataSource.kt (1)

16-35: LGTM - Well-implemented datasource with proper error handling

The implementation correctly uses the safeCall utility to handle network errors and maps the response DTO to a domain model. The code is clean, follows dependency injection patterns, and correctly implements the interface.

app/src/main/java/com/notifier/app/auth/presentation/setup/SetupScreen.kt (1)

17-50: Good modular UI structure with state handling

The UI component is well-structured with clear separation of concerns. The use of a when statement to handle different states is appropriate and the preview composable for testing in different UI modes is a good practice.

app/src/main/java/com/notifier/app/MainActivity.kt (2)

12-20: Appropriate imports for navigation components.

The imports correctly include all necessary navigation dependencies for implementing the navigation architecture and deep linking functionality.


31-31: Good separation of concerns with MainAppContent.

Extracting the navigation setup into a separate composable function improves code organization and maintainability.

app/src/main/java/com/notifier/app/auth/presentation/setup/SetupRoute.kt (3)

14-18: Well-structured navigation parameters with nullability support.

The SetupScreen data class correctly handles both the authorization code and state parameters as nullable, which is appropriate for OAuth callbacks where these parameters may not always be present.


21-27: ViewModel is properly injected and state is collected correctly.

The code correctly uses Hilt for ViewModel injection and properly collects the state using collectAsStateWithLifecycle for lifecycle-aware state collection.


32-42: Effective error handling with contextual error messages.

The implementation correctly handles both persistence and network errors by displaying appropriate toast messages to the user, using the context-aware toString extension function for readable error messages.

gradle/libs.versions.toml (4)

16-18: Version keys follow consistent naming convention.

The renamed and added version keys follow camelCase convention for better readability and consistency.


36-36: Essential navigation dependency added.

The addition of the navigation-compose dependency is necessary for implementing the navigation architecture in the app.


47-50: Proper Dagger Hilt dependencies with consistent version references.

All Dagger Hilt dependencies now correctly reference the renamed version key, and the addition of hilt-navigation-compose supports the navigation-based architecture.


58-58: Dagger Hilt plugin updated to use renamed version key.

The plugin reference is correctly updated to use the renamed version key, maintaining consistency.

app/src/main/java/com/notifier/app/core/data/persistence/DataStoreManager.kt (2)

6-8: Appropriate imports for error handling.

The imports correctly include the necessary types for implementing the Result pattern for error handling.


22-25: Improved documentation with error handling details.

The documentation now clearly indicates the possible return types, including success and error scenarios.

app/src/main/java/com/notifier/app/auth/presentation/setup/SetupViewModel.kt (1)

22-38: Well-structured ViewModel with proper state management.

The code follows best practices for state management using StateFlow and events using Channel. The use of SharingStarted.WhileSubscribed with a timeout is particularly good for managing resources efficiently.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🔭 Outside diff range comments (1)
app/src/main/java/com/notifier/app/core/data/networking/HttpClientFactory.kt (1)

61-77: ⚠️ Potential issue

headers { … } is not valid inside HttpClientConfig; move the headers into defaultRequest

headers { … } belongs to HttpRequestBuilder, not the client config DSL, so this block will not compile.
Combine it with the existing defaultRequest block:

-        // Set default request headers and properties
-        defaultRequest {
-            contentType(ContentType.Application.Json)
-        }
-
-        val accessToken = runBlocking {
-            var retrievedToken = ""
-            dataStoreManager.getAccessToken().onSuccess {
-                retrievedToken = it
-            }
-            retrievedToken
-        }
-
-        headers {
-            append(HttpHeaders.Authorization, "Bearer $accessToken")
-            append("X-GitHub-Api-Version", "2022-11-28")
-        }
+        // Set default request headers and properties
+        val accessToken = runBlocking {
+            var retrievedToken = ""
+            dataStoreManager.getAccessToken().onSuccess {
+                retrievedToken = it
+            }
+            retrievedToken
+        }
+
+        defaultRequest {
+            contentType(ContentType.Application.Json)
+            header(HttpHeaders.Authorization, "Bearer $accessToken")
+            header("X-GitHub-Api-Version", "2022-11-28")
+        }
♻️ Duplicate comments (3)
app/src/main/java/com/notifier/app/auth/presentation/setup/SetupRoute.kt (1)

29-31: Handle the null / empty OAuth code before calling the ViewModel

This LaunchedEffect unconditionally forwards code to viewModel.getAuthToken(code).
If the deep-link failed and code is null or blank, the ViewModel immediately transitions to the FAILED state. Consider short-circuiting here and showing a user-friendly error (or navigating back) instead of letting the ViewModel handle an invalid call.

This was pointed out in a previous review and is still unresolved.

app/src/main/java/com/notifier/app/auth/presentation/setup/SetupViewModel.kt (2)

76-77: Unsafe cast may crash with non-network errors

Casting error as NetworkError will throw ClassCastException if a different error slips through. Use smart-casting:

-_events.send(SetupEvent.NetworkErrorEvent(error as NetworkError))
+when (error) {
+    is NetworkError -> _events.send(SetupEvent.NetworkErrorEvent(error))
+    else            -> _events.send(SetupEvent.NetworkErrorEvent(NetworkError.UNKNOWN))
+}

91-92: Same unsafe cast for persistence errors

Replicate the safer pattern used above to avoid a potential crash:

-_events.send(SetupEvent.PersistenceErrorEvent(error as PersistenceError))
+when (error) {
+    is PersistenceError -> _events.send(SetupEvent.PersistenceErrorEvent(error))
+    else                -> _events.send(SetupEvent.PersistenceErrorEvent(PersistenceError.UNKNOWN))
+}
🧹 Nitpick comments (8)
app/src/main/java/com/notifier/app/auth/presentation/login/LoginState.kt (1)

5-14: Minor: trailing comma inside single-parameter data class is unnecessary noise

The trailing comma after LoginStatus is legal but adds no value here and can cause superfluous diff churn on future edits.

-data class LoginState(
-    val status: LoginStatus = LoginStatus.LOADING,
-)
+data class LoginState(
+    val status: LoginStatus = LoginStatus.LOADING
+)
app/src/main/java/com/notifier/app/core/data/networking/HttpClientFactory.kt (1)

66-72: Blocking the main thread at startup may cause jank; consider a non-blocking token provider

runBlocking executes on whatever thread instantiates HttpClient (often the main/UI thread during Hilt graph creation).
If DataStore is still initialising, this can freeze the app for several hundred ms.

Prefer Ktor’s Auth/BearerAuth plugin (or DefaultRequest with a suspending token supplier) so the token is fetched off the main thread and can be refreshed automatically:

install(Auth) {
    bearer {
        loadTokens {
            dataStoreManager.getAccessToken()
                .getOrNull()
                ?.let { BearerTokens(it, it) } // refresh token logic later
        }
    }
}

This removes the blocking call and ensures HTTP requests pick up token updates after login/logout.

app/src/main/java/com/notifier/app/notification/presentation/NotificationRoute.kt (1)

9-12: Consider passing modifier parameter to NotificationScreen

The NotificationRoute composable doesn't pass any modifier to the NotificationScreen it's calling. This limits flexibility when you might need to apply modifiers from the navigation layer.

@Composable
fun NotificationRoute() {
-    NotificationScreen()
+    NotificationScreen(
+        modifier = Modifier
+    )
}
app/src/main/java/com/notifier/app/MainActivity.kt (1)

42-84: Well-structured navigation implementation with proper deep link handling

The navigation setup is well-implemented with:

  • Proper Scaffold and NavHost configuration
  • Type-safe navigation using route objects
  • Correct deep link handling for the OAuth callback
  • Clear separation between navigation and UI components

Consider refactoring the duplicate navigation logic to a shared function to avoid repetition.

@Composable
private fun MainAppContent() {
    Scaffold(modifier = Modifier.fillMaxSize()) { innerPadding ->
        val navController = rememberNavController()
+       
+       fun navigateToHomeScreen() {
+           navController.navigate(NotificationScreen) {
+               popUpTo(LoginScreen) {
+                   inclusive = true
+               }
+           }
+       }

        NavHost(
            navController = navController,
            startDestination = LoginScreen,
            modifier = Modifier.padding(innerPadding)
        ) {
            composable<LoginScreen> {
                LoginRoute(
-                   onNavigateToHomeScreen = {
-                       navController.navigate(NotificationScreen) {
-                           popUpTo(LoginScreen) {
-                               inclusive = true
-                           }
-                       }
-                   }
+                   onNavigateToHomeScreen = ::navigateToHomeScreen
                )
            }

            composable<SetupScreen>(
                deepLinks = listOf(
                    navDeepLink<SetupScreen>(
                        basePath = "github-notifier://auth-callback"
                    )
                )
            ) {
                val args = it.toRoute<SetupScreen>()
-               SetupRoute(code = args.code, onNavigateToHomeScreen = {
-                   navController.navigate(NotificationScreen) {
-                       popUpTo(LoginScreen) {
-                           inclusive = true
-                       }
-                   }
-               })
+               SetupRoute(
+                   code = args.code, 
+                   onNavigateToHomeScreen = ::navigateToHomeScreen
+               )
            }

            composable<NotificationScreen> {
                NotificationRoute()
            }
        }
    }
}
app/src/main/java/com/notifier/app/auth/presentation/login/LoginViewModel.kt (2)

24-31: checkAuthStatus() is executed for every collector – move it to init {}

Because onStart { … } is applied after the stateIn, each new subscriber to state will trigger checkAuthStatus(). That can execute unnecessary I/O multiple times and create inconsistent state races.

A simpler, single-shot alternative:

 private val _state = MutableStateFlow(LoginState())
 val state = _state
-    .onStart { checkAuthStatus() }
     .stateIn( … )

-init {
-    checkAuthStatus()
-}
+init {
+    checkAuthStatus()
+}

This guarantees the status check runs exactly once when the ViewModel starts.


33-34: Prefer Channel(capacity = Channel.BUFFERED) for one-off UI events

Channel() without parameters is UNLIMITED and can grow unbounded if, for any reason, events are emitted faster than the UI can consume them.
Switching to Channel(capacity = Channel.BUFFERED) provides back-pressure while still guaranteeing delivery order.

-private val _events = Channel<LoginEvent>()
+private val _events = Channel<LoginEvent>(capacity = Channel.BUFFERED)
app/src/main/java/com/notifier/app/auth/presentation/setup/SetupViewModel.kt (2)

61-66: Client secret embedded in APK – consider moving to a secure backend

BuildConfig.CLIENT_SECRET is shipped with the app, which makes it trivial to extract via APK inspection and compromise your GitHub OAuth app.
Best practice is to perform the code-exchange on a backend service so that the mobile app only holds the public client ID.


36-38: Bounded channel for events

Like in LoginViewModel, consider giving _events a bounded capacity:

-private val _events = Channel<SetupEvent>()
+private val _events = Channel<SetupEvent>(capacity = Channel.BUFFERED)

This prevents uncontrolled memory growth if the UI misses events.

📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between fb5a0cd and 3dae001.

📒 Files selected for processing (21)
  • app/build.gradle.kts (3 hunks)
  • app/src/main/AndroidManifest.xml (1 hunks)
  • app/src/main/java/com/notifier/app/MainActivity.kt (1 hunks)
  • app/src/main/java/com/notifier/app/auth/presentation/login/LoginAction.kt (1 hunks)
  • app/src/main/java/com/notifier/app/auth/presentation/login/LoginEvent.kt (1 hunks)
  • app/src/main/java/com/notifier/app/auth/presentation/login/LoginRoute.kt (1 hunks)
  • app/src/main/java/com/notifier/app/auth/presentation/login/LoginScreen.kt (1 hunks)
  • app/src/main/java/com/notifier/app/auth/presentation/login/LoginState.kt (1 hunks)
  • app/src/main/java/com/notifier/app/auth/presentation/login/LoginViewModel.kt (1 hunks)
  • app/src/main/java/com/notifier/app/auth/presentation/login/components/LoginButton.kt (1 hunks)
  • app/src/main/java/com/notifier/app/auth/presentation/setup/SetupAction.kt (1 hunks)
  • app/src/main/java/com/notifier/app/auth/presentation/setup/SetupEvent.kt (1 hunks)
  • app/src/main/java/com/notifier/app/auth/presentation/setup/SetupRoute.kt (1 hunks)
  • app/src/main/java/com/notifier/app/auth/presentation/setup/SetupScreen.kt (1 hunks)
  • app/src/main/java/com/notifier/app/auth/presentation/setup/SetupViewModel.kt (1 hunks)
  • app/src/main/java/com/notifier/app/core/data/networking/HttpClientFactory.kt (3 hunks)
  • app/src/main/java/com/notifier/app/di/ApiModule.kt (1 hunks)
  • app/src/main/java/com/notifier/app/notification/presentation/NotificationRoute.kt (1 hunks)
  • app/src/main/java/com/notifier/app/notification/presentation/NotificationScreen.kt (1 hunks)
  • app/src/main/res/values/splash.xml (1 hunks)
  • gradle/libs.versions.toml (3 hunks)
✅ Files skipped from review due to trivial changes (2)
  • app/src/main/res/values/splash.xml
  • app/src/main/java/com/notifier/app/notification/presentation/NotificationScreen.kt
🚧 Files skipped from review as they are similar to previous changes (8)
  • app/src/main/java/com/notifier/app/auth/presentation/login/components/LoginButton.kt
  • app/build.gradle.kts
  • app/src/main/java/com/notifier/app/auth/presentation/setup/SetupEvent.kt
  • app/src/main/java/com/notifier/app/di/ApiModule.kt
  • app/src/main/java/com/notifier/app/auth/presentation/setup/SetupScreen.kt
  • app/src/main/java/com/notifier/app/auth/presentation/login/LoginScreen.kt
  • app/src/main/AndroidManifest.xml
  • gradle/libs.versions.toml
🧰 Additional context used
🧬 Code Graph Analysis (3)
app/src/main/java/com/notifier/app/notification/presentation/NotificationRoute.kt (1)
app/src/main/java/com/notifier/app/notification/presentation/NotificationScreen.kt (1)
  • NotificationScreen (16-27)
app/src/main/java/com/notifier/app/auth/presentation/login/LoginRoute.kt (3)
app/src/main/java/com/notifier/app/core/presentation/util/ObserveAsEvents.kt (1)
  • ObserveAsEvents (12-27)
app/src/main/java/com/notifier/app/auth/presentation/util/createGitHubAuthIntent.kt (1)
  • createGitHubAuthIntent (15-22)
app/src/main/java/com/notifier/app/auth/presentation/login/LoginScreen.kt (1)
  • LoginScreen (18-48)
app/src/main/java/com/notifier/app/auth/presentation/setup/SetupRoute.kt (3)
app/src/main/java/com/notifier/app/core/presentation/util/ObserveAsEvents.kt (1)
  • ObserveAsEvents (12-27)
app/src/main/java/com/notifier/app/core/presentation/util/showToast.kt (1)
  • showToast (13-15)
app/src/main/java/com/notifier/app/auth/presentation/setup/SetupScreen.kt (1)
  • SetupScreen (17-51)
⏰ Context from checks skipped due to timeout of 90000ms (3)
  • GitHub Check: instrumented_tests (29)
  • GitHub Check: unit_tests
  • GitHub Check: build
🔇 Additional comments (6)
app/src/main/java/com/notifier/app/auth/presentation/login/LoginAction.kt (1)

3-5:

Details

❌ Incorrect review comment

Same experimental “data object” caveat applies here

See the remark in LoginEvent.kt. Prefer regular object or ensure the data-objects feature flag is enabled.

-sealed interface LoginAction {
-    data object OnLoginButtonClick : LoginAction
-    data object OnUserLoggedIn : LoginAction
+sealed interface LoginAction {
+    object OnLoginButtonClick : LoginAction
+    object OnUserLoggedIn : LoginAction
 }

🏁 Script executed:

#!/bin/bash
# Search for Kotlin plugin version in build scripts
echo "➡️ Kotlin plugin declarations:"
grep -R "kotlin(\"" -n . || true
grep -R "id \"org.jetbrains.kotlin" -n . || true

# Search for compiler flags related to DataObjects
echo "➡️ freeCompilerArgs:"
grep -R "freeCompilerArgs" -n . || true

echo "➡️ DataObjects feature flags or language flags:"
grep -R "DataObjects" -n . || true

Length of output: 381


🏁 Script executed:

#!/bin/bash
# Locate Gradle build files
echo "➡️ Gradle build files:"
find . -maxdepth 2 -type f -name "build.gradle*"

# Check for Kotlin plugin declarations in Groovy DSL
echo "➡️ Kotlin plugin (Groovy DSL):"
grep -R "id ['\"]org.jetbrains.kotlin" -n build.gradle* || true

# Check for Kotlin plugin (Kotlin DSL)
echo "➡️ Kotlin plugin (Kotlin DSL):"
grep -R "kotlin(\"jvm\"" -n build.gradle.kts || true

# Inspect all build.gradle(.kts) for compiler args
echo "➡️ freeCompilerArgs in build files:"
grep -R "freeCompilerArgs" -n build.gradle* || true

Length of output: 546


🏁 Script executed:

#!/bin/bash
# Scan app module's Gradle file for Kotlin plugin version and compiler settings
echo "➡️ Contents of app/build.gradle.kts:"
sed -n '1,200p' app/build.gradle.kts

echo "➡️ Kotlin plugin declarations in app/build.gradle.kts:"
grep -nE 'id ["'\'']org.jetbrains.kotlin|kotlin\(' app/build.gradle.kts || true

echo "➡️ Compiler arguments and opt-in/language settings in app/build.gradle.kts:"
grep -nE 'freeCompilerArgs|languageVersion|apiVersion|opt-in|@OptIn' app/build.gradle.kts || true

Length of output: 4232


🌐 Web query:

Does Kotlin require an experimental flag to use the “data object” feature (DataObjects)? Which Kotlin version made it stable?

💡 Result:

Kotlin initially required an experimental flag for the "data object" feature but has since made it stable in version 1.9.0.

When data objects were first introduced in Kotlin 1.7.20, they were an experimental feature[1][6][10]. To use them at that time, developers needed to enable the -language-version 1.9 compiler option[8][10]. In a Gradle project, this could be done by adding specific configuration to the build.gradle file:

tasks
.withType<org.jetbrains.kotlin.gradle.tasks.KotlinCompilationTask<*>>()
.configureEach {
  compilerOptions
  .languageVersion
  .set(
    org.jetbrains.kotlin.gradle.dsl.KotlinVersion.KOTLIN_1_9
  )
}

The data object feature was designed to allow developers to declare objects with singleton semantics while providing a clean toString() representation[8]. This was particularly useful in sealed class hierarchies, where data objects could be used alongside data classes to maintain symmetry[6].

In Kotlin 1.9.0, released on July 6, 2023, data objects became stable[6]. The official documentation states: "Data object declarations, which were introduced in Kotlin 1.8.20, are now Stable. This includes the functions added for symmetry with data classes: toString(), equals(), and hashCode()"[6].

Therefore, since Kotlin 1.9.0, developers no longer need to use experimental flags to use the data object feature in their code.

Citations:


🏁 Script executed:

#!/bin/bash
# Locate the version catalog file and print its content to determine Kotlin plugin version
echo "➡️ Searching for version catalog file (libs.versions.toml)..."
catalog=$(find . -maxdepth 3 -type f -name 'libs.versions.toml' | head -n1)

if [ -n "$catalog" ]; then
  echo "➡️ Found version catalog at: $catalog"
  echo "➡️ Displaying first 200 lines of the version catalog:"
  sed -n '1,200p' "$catalog"
else
  echo "❌ No version catalog file (libs.versions.toml) found"
fi

Length of output: 4609


No action required: Data objects are stable

Our project uses Kotlin plugin version 2.1.10 (≥ 1.9.0), where data object declarations are stable and don’t require an experimental flag. You can safely keep data object in LoginAction.kt as is.

Likely an incorrect or invalid review comment.

app/src/main/java/com/notifier/app/auth/presentation/setup/SetupAction.kt (1)

3-5: Well-defined sealed interface for action handling

The sealed interface pattern is an excellent choice for representing user actions in a type-safe way. This approach enables exhaustive when expressions and restricts the action hierarchy to only defined types, making the codebase more maintainable.

app/src/main/java/com/notifier/app/notification/presentation/NotificationRoute.kt (1)

6-7: Good use of serializable data object for navigation route

Using a serializable data object for the route follows good practices for type-safe navigation in Compose.

app/src/main/java/com/notifier/app/auth/presentation/login/LoginRoute.kt (2)

12-13: Appropriate use of serializable data object for navigation

Using a serializable data object for the login screen route enables type-safe navigation with the Compose Navigation component.


15-39: Well-structured route composable with proper separation of concerns

The LoginRoute composable is well-implemented with:

  • Proper state collection using collectAsStateWithLifecycle
  • Effective event handling with ObserveAsEvents
  • Clean separation between navigation logic and UI rendering
  • Appropriate dependency injection with Hilt

This structure provides good maintainability and testability.

app/src/main/java/com/notifier/app/MainActivity.kt (1)

31-31: Good use of splash screen API

Properly initializing the splash screen before other UI operations follows best practices for creating a smooth app startup experience.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

♻️ Duplicate comments (2)
app/src/main/java/com/notifier/app/MainActivity.kt (1)

58-98: LGTM! Well-implemented navigation with proper deep link handling

The navigation setup follows best practices:

  • Type-safe navigation with route objects
  • Proper deep link configuration for OAuth callback
  • Clear separation of navigation and screen components
  • Appropriate backstack management with popUpTo
app/src/main/java/com/notifier/app/auth/presentation/setup/SetupViewModel.kt (1)

39-121: Implement navigation after successful OAuth flow

The ViewModel implementation for handling OAuth flow, token exchange, and state management looks good. The navigation to home screen is properly implemented through the events channel.

🧹 Nitpick comments (2)
app/src/main/java/com/notifier/app/MainActivity.kt (1)

46-99: Consider extracting duplicated navigation logic

The navigation logic to NotificationScreen with backstack clearing is duplicated in both the LoginRoute and SetupRoute handlers.

Extract this into a private function to improve maintainability:

+private fun NavController.navigateToHomeAndClearBackstack() {
+    navigate(NotificationScreen) {
+        popUpTo(LoginScreen) {
+            inclusive = true
+        }
+    }
+}

...
// Then replace the two occurrences with:
-navController.navigate(NotificationScreen) {
-    popUpTo(LoginScreen) {
-        inclusive = true
-    }
-}
+navController.navigateToHomeAndClearBackstack()
app/src/main/java/com/notifier/app/auth/presentation/setup/SetupViewModel.kt (1)

77-98: Consider simplifying the validation logic flow

The validateOAuthState method has multiple early returns which can make the flow harder to follow and maintain.

Consider restructuring to reduce complexity:

private suspend fun validateOAuthState(receivedState: String): Boolean {
-    var savedState = ""
-    dataStoreManager.getOAuthState()
-        .onSuccess { savedState = it }
-        .onError {
-            failSetup()
-            return false
-        }
-
-    if (receivedState != savedState) {
-        failSetup()
-        return false
-    }
-
-    dataStoreManager.clearOAuthState()
-        .onError {
-            failSetup()
-            return false
-        }
-
-    return true
+    return when (val stateResult = dataStoreManager.getOAuthState()) {
+        is Result.Success -> {
+            val savedState = stateResult.value
+            if (receivedState != savedState) {
+                failSetup()
+                false
+            } else {
+                val clearResult = dataStoreManager.clearOAuthState()
+                if (clearResult.isFailure) {
+                    failSetup()
+                    false
+                } else {
+                    true
+                }
+            }
+        }
+        is Result.Failure -> {
+            failSetup()
+            false
+        }
+    }
}
📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 3dae001 and 62fe4ed.

📒 Files selected for processing (8)
  • app/src/main/java/com/notifier/app/MainActivity.kt (1 hunks)
  • app/src/main/java/com/notifier/app/auth/presentation/login/LoginRoute.kt (1 hunks)
  • app/src/main/java/com/notifier/app/auth/presentation/setup/SetupRoute.kt (1 hunks)
  • app/src/main/java/com/notifier/app/auth/presentation/setup/SetupViewModel.kt (1 hunks)
  • app/src/main/java/com/notifier/app/auth/presentation/util/GitHubAuthIntentProvider.kt (1 hunks)
  • app/src/main/java/com/notifier/app/core/data/networking/HttpClientFactory.kt (3 hunks)
  • app/src/main/java/com/notifier/app/core/data/persistence/DataStoreManager.kt (1 hunks)
  • app/src/main/java/com/notifier/app/core/data/persistence/PreferenceKeys.kt (1 hunks)
✅ Files skipped from review due to trivial changes (1)
  • app/src/main/java/com/notifier/app/core/data/persistence/PreferenceKeys.kt
🚧 Files skipped from review as they are similar to previous changes (3)
  • app/src/main/java/com/notifier/app/core/data/networking/HttpClientFactory.kt
  • app/src/main/java/com/notifier/app/auth/presentation/setup/SetupRoute.kt
  • app/src/main/java/com/notifier/app/core/data/persistence/DataStoreManager.kt
🧰 Additional context used
🧬 Code Graph Analysis (1)
app/src/main/java/com/notifier/app/MainActivity.kt (4)
app/src/main/java/com/notifier/app/ui/theme/Theme.kt (1)
  • GitHubNotifierTheme (39-61)
app/src/main/java/com/notifier/app/auth/presentation/login/LoginRoute.kt (1)
  • LoginRoute (14-39)
app/src/main/java/com/notifier/app/auth/presentation/setup/SetupRoute.kt (1)
  • SetupRoute (20-54)
app/src/main/java/com/notifier/app/notification/presentation/NotificationRoute.kt (1)
  • NotificationRoute (9-12)
⏰ Context from checks skipped due to timeout of 90000ms (2)
  • GitHub Check: instrumented_tests (29)
  • GitHub Check: build
🔇 Additional comments (2)
app/src/main/java/com/notifier/app/auth/presentation/login/LoginRoute.kt (1)

11-40: LGTM! Well-structured composable with proper event handling

The code follows best practices for Jetpack Compose with:

  • Clear separation of UI and navigation logic
  • Lifecycle-aware state collection
  • Event-based navigation handling
  • Clean parameter passing to child composables
app/src/main/java/com/notifier/app/auth/presentation/setup/SetupViewModel.kt (1)

137-147: Improved error handling with safer type checking

The error handling logic correctly uses the is operator to check the error type before casting, which is safer than using an unsafe cast.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

♻️ Duplicate comments (1)
app/src/main/java/com/notifier/app/auth/presentation/setup/SetupViewModel.kt (1)

184-187: Safe error handling implementation confirmed.

The persistence error handling also demonstrates the same safe type checking pattern, avoiding the unsafe casting issues mentioned in previous reviews.

This implementation properly addresses the previous review feedback about unsafe casting in error handling methods.

🧹 Nitpick comments (4)
app/src/androidTest/java/com/notifier/app/auth/presentation/login/LoginScreenTest.kt (1)

52-64: Consider adding test for null status edge case.

While this test correctly covers the LOGGED_IN state, the LoginScreen implementation also handles null status as equivalent to LOADING. Consider adding a test case for this edge case to ensure complete coverage.

Add an additional test method:

@Test
fun testLoginScreen_whenStatusIsNull_showsProgressAndMessage() {
    composeRule.setContent {
        LoginScreen(
            state = LoginState(status = null),
            onLoginButtonClick = {}
        )
    }

    composeRule
        .onNode(hasTestTag("CircularProgressIndicator"))
        .assertExists()
    composeRule
        .onNodeWithText("Verifying authentication status...")
        .assertIsDisplayed()
}
app/src/main/java/com/notifier/app/core/presentation/BaseViewModel.kt (2)

17-24: Consider eliminating redundant initialState usage in state property.

The initialState parameter is used twice - once to initialize mutableStateFlow and again in the stateIn call. Since mutableStateFlow is already initialized with initialState, the initialValue parameter in stateIn is redundant.

-    open val state: StateFlow<State>
-        get() = mutableStateFlow
-            .stateIn(
-                scope = viewModelScope,
-                started = SharingStarted.WhileSubscribed(stopTimeoutMillis = 5000L),
-                initialValue = initialState // Initial state before any flow emissions
-            )
+    open val state: StateFlow<State>
+        get() = mutableStateFlow
+            .stateIn(
+                scope = viewModelScope,
+                started = SharingStarted.WhileSubscribed(stopTimeoutMillis = 5000L),
+                initialValue = mutableStateFlow.value
+            )

18-19: Reconsider making state property open for inheritance.

The state property is marked as open, but its getter contains complex logic with stateIn configuration. Subclasses overriding this might need to replicate the same sharing behavior, leading to code duplication. Consider whether this flexibility is necessary.

If the sharing configuration needs to be consistent across all ViewModels, consider making this property final or providing a protected method for customizing the sharing configuration.

app/src/main/java/com/notifier/app/auth/presentation/setup/SetupViewModel.kt (1)

41-45: Remove redundant state property override.

The overridden state property duplicates the exact same logic as the base class. This override is unnecessary and adds maintenance overhead.

-    /**
-     * Public immutable state exposed to UI.
-     *
-     * Emits the current [SetupState] to the UI, which reflects progress through setup steps.
-     */
-    override val state = mutableStateFlow.stateIn(
-        scope = viewModelScope,
-        started = SharingStarted.WhileSubscribed(stopTimeoutMillis = 5000L),
-        initialValue = SetupState()
-    )

The base class already provides the same functionality through its state property.

📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between ef3ad69 and cd217c8.

📒 Files selected for processing (6)
  • app/src/androidTest/java/com/notifier/app/auth/presentation/login/LoginScreenTest.kt (1 hunks)
  • app/src/main/java/com/notifier/app/auth/presentation/login/LoginScreen.kt (1 hunks)
  • app/src/main/java/com/notifier/app/auth/presentation/login/LoginViewModel.kt (1 hunks)
  • app/src/main/java/com/notifier/app/auth/presentation/setup/SetupRoute.kt (1 hunks)
  • app/src/main/java/com/notifier/app/auth/presentation/setup/SetupViewModel.kt (1 hunks)
  • app/src/main/java/com/notifier/app/core/presentation/BaseViewModel.kt (1 hunks)
✅ Files skipped from review due to trivial changes (1)
  • app/src/main/java/com/notifier/app/auth/presentation/login/LoginScreen.kt
🚧 Files skipped from review as they are similar to previous changes (2)
  • app/src/main/java/com/notifier/app/auth/presentation/login/LoginViewModel.kt
  • app/src/main/java/com/notifier/app/auth/presentation/setup/SetupRoute.kt
🧰 Additional context used
🧬 Code Graph Analysis (2)
app/src/androidTest/java/com/notifier/app/auth/presentation/login/LoginScreenTest.kt (1)
app/src/main/java/com/notifier/app/auth/presentation/login/LoginScreen.kt (1)
  • LoginScreen (36-65)
app/src/main/java/com/notifier/app/auth/presentation/setup/SetupViewModel.kt (1)
app/src/main/java/com/notifier/app/core/presentation/BaseViewModel.kt (1)
  • sendEvent (31-33)
⏰ Context from checks skipped due to timeout of 90000ms (3)
  • GitHub Check: instrumented_tests (29)
  • GitHub Check: unit_tests
  • GitHub Check: build
🔇 Additional comments (9)
app/src/androidTest/java/com/notifier/app/auth/presentation/login/LoginScreenTest.kt (3)

12-14: LGTM!

The test setup with createComposeRule() is correct for Compose UI testing.


16-31: Good test coverage for loading state.

The test correctly verifies both the progress indicator and the status message. The use of hasTestTag() for finding the CircularProgressIndicator is a good practice.


33-50: Excellent callback testing approach.

The test properly verifies both the UI display and the button click interaction using a local variable to track the callback execution. This is a solid testing pattern for user interactions.

app/src/main/java/com/notifier/app/core/presentation/BaseViewModel.kt (2)

26-27: Well-designed event handling pattern.

The use of a buffered Channel for one-time events is an excellent architectural choice. It ensures events are delivered reliably and prevents UI state confusion that could occur with StateFlow-based events.


31-33: Robust event emission implementation.

The sendEvent method properly uses viewModelScope.launch to ensure events are sent asynchronously and cancelled when the ViewModel is cleared. This prevents potential memory leaks and ensures proper lifecycle management.

app/src/main/java/com/notifier/app/auth/presentation/setup/SetupViewModel.kt (4)

170-177: Excellent improvement in error handling safety.

The error handling has been significantly improved from the previous implementation. Using type checking with if (error is NetworkError) instead of unsafe casting is much safer and prevents potential ClassCastException.

This addresses the concerns raised in previous reviews about unsafe casting.


67-91: Well-structured OAuth authentication flow.

The getAuthToken method demonstrates excellent error handling and state management:

  • Proper input validation for null/blank parameters
  • Sequential async operations with proper error propagation
  • State updates at appropriate points in the flow
  • Clean separation of concerns with helper methods

99-120: Robust OAuth state validation with comprehensive error handling.

The state validation logic properly handles multiple failure scenarios:

  • Persistence errors when retrieving saved state
  • State mismatch validation
  • Cleanup operations with error handling

The early return pattern keeps the code readable and prevents nested error handling.


20-30: Excellent documentation quality.

The KDoc provides comprehensive information about the ViewModel's responsibilities, parameters, and usage. This level of documentation greatly improves maintainability and developer experience.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 0

🧹 Nitpick comments (2)
app/src/androidTest/java/com/notifier/app/auth/presentation/setup/SetupScreenTest.kt (2)

44-68: Consider using a more robust callback verification approach.

While the current boolean flag approach works well, consider using a mock or capturing the actual action parameter for more precise verification:

 @Test
 fun testSetupScreen_whenSuccess_showsSuccessMessageAndContinueButton_clickTriggersCallback() {
-    var continueClicked = false
+    var capturedAction: SetupAction? = null

     composeRule.setContent {
         SetupScreen(
             state = SetupState(setupStep = SetupStep.SUCCESS),
             onAction = {
-                if (it == SetupAction.OnContinueButtonClick) {
-                    continueClicked = true
-                }
+                capturedAction = it
             }
         )
     }

     composeRule
         .onNodeWithText("Connected successfully!")
         .assertIsDisplayed()

     composeRule
         .onNodeWithText("Continue")
         .assertIsDisplayed()
         .performClick()

-    assertThat(continueClicked).isTrue()
+    assertThat(capturedAction).isEqualTo(SetupAction.OnContinueButtonClick)
 }

This approach provides more precise verification and would catch issues if the wrong action type was passed.


70-82: Consider adding retry interaction test for failed state.

The FAILED state test currently only verifies the error message display. Consider adding a test that verifies user interaction in the failed state (e.g., if there's a retry button or other interactive elements that should be available when setup fails).

📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between eaf8dc1 and 85b58b6.

📒 Files selected for processing (1)
  • app/src/androidTest/java/com/notifier/app/auth/presentation/setup/SetupScreenTest.kt (1 hunks)
🧰 Additional context used
🧬 Code Graph Analysis (1)
app/src/androidTest/java/com/notifier/app/auth/presentation/setup/SetupScreenTest.kt (1)
app/src/main/java/com/notifier/app/auth/presentation/setup/SetupScreen.kt (1)
  • SetupScreen (33-67)
⏰ Context from checks skipped due to timeout of 90000ms (3)
  • GitHub Check: unit_tests
  • GitHub Check: instrumented_tests (29)
  • GitHub Check: build
🔇 Additional comments (1)
app/src/androidTest/java/com/notifier/app/auth/presentation/setup/SetupScreenTest.kt (1)

1-83: Excellent UI test coverage for SetupScreen states!

This test class provides comprehensive coverage of all SetupStep states with clear, well-structured tests that properly verify the UI behavior. The test assertions correctly match the actual UI implementation from SetupScreen.kt.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 0

♻️ Duplicate comments (1)
app/src/androidTest/java/com/notifier/app/auth/presentation/login/LoginScreenTest.kt (1)

1-11: Missing imports are still unresolved.

The imports for LoginState, LoginStatus, and TEST_TAG_CIRCULAR_PROGRESS_INDICATOR are still missing as identified in the previous review.

🧹 Nitpick comments (2)
app/src/androidTest/java/com/notifier/app/auth/presentation/login/LoginScreenTest.kt (2)

16-32: Consider using string resources instead of hard-coded text.

The test uses a hard-coded string "Verifying authentication status…" for assertion. Consider using the same string resource that the actual UI uses to maintain consistency and avoid test brittleness due to text changes.

 composeRule
-    .onNodeWithText("Verifying authentication status…")
+    .onNodeWithText(context.getString(R.string.logging_in_loading_message))
     .assertIsDisplayed()

34-50: Duplicate test logic - consider parameterized test.

This test method has identical logic to the previous test for the null status case. Both null and LOADING status show the same UI elements according to the LoginScreen implementation.

Consider using a parameterized test to reduce code duplication:

-@Test
-fun testLoginScreen_whenLoading_showsProgressAndMessage() {
-    // Same implementation as null test
-}
+@ParameterizedTest
+@ValueSource(strings = ["null", "LOADING"])
+fun testLoginScreen_whenStatusIsNullOrLoading_showsProgressAndMessage(statusType: String) {
+    val status = when(statusType) {
+        "null" -> null
+        "LOADING" -> LoginStatus.LOADING
+        else -> null
+    }
+    // Test implementation
+}
📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 2421855 and ab617e5.

📒 Files selected for processing (2)
  • app/src/androidTest/java/com/notifier/app/auth/presentation/login/LoginScreenTest.kt (1 hunks)
  • app/src/androidTest/java/com/notifier/app/auth/presentation/setup/SetupScreenTest.kt (1 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
  • app/src/androidTest/java/com/notifier/app/auth/presentation/setup/SetupScreenTest.kt
🧰 Additional context used
🧬 Code Graph Analysis (1)
app/src/androidTest/java/com/notifier/app/auth/presentation/login/LoginScreenTest.kt (1)
app/src/main/java/com/notifier/app/auth/presentation/login/LoginScreen.kt (1)
  • LoginScreen (40-69)
⏰ Context from checks skipped due to timeout of 90000ms (2)
  • GitHub Check: instrumented_tests (29)
  • GitHub Check: build
🔇 Additional comments (2)
app/src/androidTest/java/com/notifier/app/auth/presentation/login/LoginScreenTest.kt (2)

52-69: Well-structured interaction test with proper state verification.

The test correctly verifies both the UI state (button display) and the interaction behavior (callback invocation). The use of a boolean flag to track the callback execution is a good testing pattern.


71-83: Good coverage for the success state.

The test appropriately verifies the logged-in state displays the correct success message. Consider using string resources for consistency as mentioned in earlier comments.

@theMr17 theMr17 changed the title Fix #12, #16, #18: Add GitHub OAuth, error handling, and navigation with Jetpack Compose Fix #12, #16, #18: Add GitHub OAuth, error handling, and navigation May 31, 2025
@theMr17
Copy link
Owner Author

theMr17 commented May 31, 2025

@coderabbitai review

@coderabbitai
Copy link

coderabbitai bot commented May 31, 2025

✅ Actions performed

Review triggered.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🧹 Nitpick comments (3)
app/src/androidTest/java/com/notifier/app/auth/presentation/setup/SetupViewModelTest.kt (3)

91-94: Simplify the state collection.

The .map { it } transformation is redundant and can be removed for cleaner code.

-            val stateStatuses = viewModel.state
-                .take(2)
-                .map { it }
-                .toList()
+            val stateStatuses = viewModel.state
+                .take(2)
+                .toList()

114-117: Apply the same simplification here.

Remove the redundant .map { it } transformation.

-            val stateStatuses = viewModel.state
-                .take(2)
-                .map { it }
-                .toList()
+            val stateStatuses = viewModel.state
+                .take(2)
+                .toList()

141-144: Apply the same simplification here as well.

Remove the redundant .map { it } transformation.

-            val stateStatuses = viewModel.state
-                .take(2)
-                .map { it }
-                .toList()
+            val stateStatuses = viewModel.state
+                .take(2)
+                .toList()
📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between ffa69e8 and 40adaa8.

📒 Files selected for processing (2)
  • app/src/androidTest/java/com/notifier/app/auth/presentation/login/LoginViewModelTest.kt (1 hunks)
  • app/src/androidTest/java/com/notifier/app/auth/presentation/setup/SetupViewModelTest.kt (1 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
  • app/src/androidTest/java/com/notifier/app/auth/presentation/login/LoginViewModelTest.kt
⏰ Context from checks skipped due to timeout of 90000ms (2)
  • GitHub Check: instrumented_tests (29)
  • GitHub Check: build
🔇 Additional comments (4)
app/src/androidTest/java/com/notifier/app/auth/presentation/setup/SetupViewModelTest.kt (4)

1-17: LGTM! Well-structured test imports and setup.

The imports are comprehensive and appropriate for testing coroutines, mocking, and assertions. Good use of testing libraries.


18-30: LGTM! Proper test setup with dependency injection.

The mock setup correctly isolates the view model's dependencies for focused unit testing.


32-37: LGTM! Good baseline test for initial state.

Testing the initial state ensures the view model starts in the expected condition.


40-77: LGTM! Comprehensive input validation coverage.

These tests thoroughly cover edge cases for null and blank input parameters, ensuring robust validation.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 0

🧹 Nitpick comments (2)
app/src/main/java/com/notifier/app/auth/presentation/setup/SetupViewModel.kt (2)

86-107: Consider refactoring to a more functional approach.

The current pattern of using a mutable variable savedState and updating it in the onSuccess callback could be refactored to be more functional and eliminate the mutable state.

Consider this more functional approach:

-private suspend fun validateOAuthState(receivedState: String): Boolean {
-    var savedState = ""
-    dataStoreManager.getOAuthState()
-        .onSuccess { savedState = it }
-        .onError {
-            failSetup()
-            return false
-        }
-
-    if (receivedState != savedState) {
-        failSetup()
-        return false
-    }
-
-    dataStoreManager.clearOAuthState()
-        .onError {
-            failSetup()
-            return false
-        }
-
-    return true
-}
+private suspend fun validateOAuthState(receivedState: String): Boolean {
+    return dataStoreManager.getOAuthState()
+        .fold(
+            onSuccess = { savedState ->
+                if (receivedState == savedState) {
+                    dataStoreManager.clearOAuthState()
+                        .fold(
+                            onSuccess = { true },
+                            onFailure = { failSetup(); false }
+                        )
+                } else {
+                    failSetup()
+                    false
+                }
+            },
+            onFailure = { failSetup(); false }
+        )
+}

54-58: Consider providing more specific error handling for invalid OAuth parameters.

While the null/blank validation is correct, consider logging or providing more specific error information when OAuth parameters are invalid, as this could help with debugging OAuth flow issues.

fun getAuthToken(code: String?, receivedState: String?) {
    if (code.isNullOrBlank() || receivedState.isNullOrBlank()) {
+        // Consider logging: "Invalid OAuth parameters: code=${code?.isNotBlank()}, state=${receivedState?.isNotBlank()}"
        failSetup()
        return
    }
📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 132ef54 and 18192d4.

📒 Files selected for processing (4)
  • app/src/androidTest/java/com/notifier/app/auth/presentation/login/LoginViewModelTest.kt (1 hunks)
  • app/src/androidTest/java/com/notifier/app/auth/presentation/setup/SetupViewModelTest.kt (1 hunks)
  • app/src/main/java/com/notifier/app/auth/presentation/setup/SetupViewModel.kt (1 hunks)
  • app/src/main/java/com/notifier/app/core/presentation/BaseViewModel.kt (1 hunks)
🚧 Files skipped from review as they are similar to previous changes (3)
  • app/src/androidTest/java/com/notifier/app/auth/presentation/login/LoginViewModelTest.kt
  • app/src/androidTest/java/com/notifier/app/auth/presentation/setup/SetupViewModelTest.kt
  • app/src/main/java/com/notifier/app/core/presentation/BaseViewModel.kt
🧰 Additional context used
🧬 Code Graph Analysis (1)
app/src/main/java/com/notifier/app/auth/presentation/setup/SetupViewModel.kt (1)
app/src/main/java/com/notifier/app/core/presentation/BaseViewModel.kt (1)
  • sendEvent (75-77)
⏰ Context from checks skipped due to timeout of 90000ms (2)
  • GitHub Check: instrumented_tests (29)
  • GitHub Check: build
🔇 Additional comments (2)
app/src/main/java/com/notifier/app/auth/presentation/setup/SetupViewModel.kt (2)

156-177: Good improvement in error handling safety.

The error handling methods now use safe type checking with if (error is Type) instead of unsafe casting, which addresses the previous review concerns about potential ClassCastException. This is a much safer approach.


18-33: Excellent documentation and architecture.

The KDoc is comprehensive and clearly explains the ViewModel's responsibilities. The dependency injection setup with Hilt is properly configured, and the inheritance from BaseViewModel follows good architectural patterns.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🧹 Nitpick comments (5)
app/src/test/java/com/notifier/app/auth/presentation/login/LoginViewModelTest.kt (5)

28-44: Verify ViewModel interaction and consider test robustness.

The test logic is correct, but consider these improvements:

  1. Verify mock interactions: The test doesn't verify that getAccessToken() was actually called
  2. Test brittleness: Taking exactly 3 emissions assumes the ViewModel won't add intermediate states

Consider adding interaction verification:

+import io.mockk.coVerify

 @Test
 fun testAuthStatus_tokenIsValid_setsStateToLoggedInEventually() = runTest {
     coEvery {
         dataStoreManager.getAccessToken()
     } returns Result.Success("dummy_valid_token")

     val stateStatuses = viewModel.state
         .take(3)
         .map { it.status }
         .toList()

     assertThat(stateStatuses).containsExactly(
         null,
         LoginStatus.LOADING,
         LoginStatus.LOGGED_IN
     ).inOrder()
+    
+    coVerify(exactly = 1) { dataStoreManager.getAccessToken() }
 }

46-62: Apply same improvements as previous test.

This test has the same structure and potential improvements as the valid token test - consider adding interaction verification and being mindful of the fixed emission count.


64-80: Apply same improvements as previous tests.

Consistent with the previous authentication status tests - consider adding mock verification and documentation about the expected emission sequence.


82-88: Consider testing event consumption and preventing multiple emissions.

The test correctly verifies the navigation event, but consider these enhancements:

  1. Event uniqueness: Verify that only one event is emitted
  2. Event consumption: Test that events are properly consumed
 @Test
 fun testOnAction_onLoginButtonClick_emitsNavigateToGitHubAuthEvent() = runTest {
     viewModel.onAction(LoginAction.OnLoginButtonClick)

-    val event = viewModel.events.first()
-    assertThat(event).isEqualTo(LoginEvent.NavigateToGitHubAuth)
+    val events = viewModel.events.take(1).toList()
+    assertThat(events).containsExactly(LoginEvent.NavigateToGitHubAuth)
 }

90-96: Apply same event testing improvements.

Consider the same enhancements as the previous action test to ensure event uniqueness and proper testing of the emission behavior.

📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 9d01787 and e86b940.

📒 Files selected for processing (3)
  • app/src/main/java/com/notifier/app/auth/presentation/login/LoginViewModel.kt (1 hunks)
  • app/src/test/java/com/notifier/app/auth/presentation/login/LoginViewModelTest.kt (1 hunks)
  • app/src/test/java/com/notifier/app/auth/presentation/setup/SetupViewModelTest.kt (1 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
  • app/src/main/java/com/notifier/app/auth/presentation/login/LoginViewModel.kt
⏰ Context from checks skipped due to timeout of 90000ms (3)
  • GitHub Check: instrumented_tests (29)
  • GitHub Check: unit_tests
  • GitHub Check: build
🔇 Additional comments (6)
app/src/test/java/com/notifier/app/auth/presentation/login/LoginViewModelTest.kt (2)

1-16: LGTM: Clean imports and proper test dependencies.

The import statements are well-organized and include appropriate testing libraries (Google Truth, MockK, Coroutines Test) for comprehensive unit testing.


17-26: LGTM: Standard test setup pattern.

The test class follows proper initialization patterns with MockK and lateinit variables for clean test setup.

app/src/test/java/com/notifier/app/auth/presentation/setup/SetupViewModelTest.kt (4)

1-18: Imports and dependencies look comprehensive.

Good use of testing libraries including Truth for assertions, mockk for mocking, and coroutine testing utilities.


20-32: Test setup follows proper patterns.

The @Before setup correctly initializes mocks and the ViewModel under test.


34-79: Input validation tests provide good coverage.

These tests properly validate the handling of null and blank inputs for both code and receivedState parameters.


142-170: Async event testing is implemented correctly.

Good use of async and await to test concurrent state changes and event emissions.

@theMr17
Copy link
Owner Author

theMr17 commented Jun 5, 2025

@coderabbitai review

@coderabbitai
Copy link

coderabbitai bot commented Jun 5, 2025

✅ Actions performed

Review triggered.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@theMr17 theMr17 merged commit f3d5054 into main Jun 6, 2025
4 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

2 participants