-
Notifications
You must be signed in to change notification settings - Fork 1
Fix #12, #16, #18: Add GitHub OAuth, error handling, and navigation #14
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Conversation
WalkthroughThis 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
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
Assessment against linked issues
Assessment against linked issues: Out-of-scope changes
Possibly related PRs
Poem
📜 Recent review detailsConfiguration used: CodeRabbit UI 📒 Files selected for processing (1)
🚧 Files skipped from review as they are similar to previous changes (1)
✨ Finishing Touches
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. 🪧 TipsChatThere are 3 ways to chat with CodeRabbit:
SupportNeed 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)
Other keywords and placeholders
CodeRabbit Configuration File (
|
… and provide default values for CLIENT_ID and CLIENT_SECRET
…e clarity and consistency
…asses for improved clarity
There was a problem hiding this 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_ERRORCurrently,
NetworkError.SERVER_ERRORis 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 UXThese 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_ERRORcase:<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 LoginScreenThe 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 collectorThis 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 ViewModelConsider 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 thestateInsharing timeoutThe 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 exceptionsYour error handling correctly maps exceptions to appropriate
PersistenceErrortypes. 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 constantThe 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 statesThe loading states (
FETCHING_TOKENandSAVING_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
CircularProgressIndicatorandArrangement.spacedBy.app/src/main/java/com/notifier/app/core/data/persistence/DataStoreManager.kt (2)
26-30: Enhanced error handling with Result type.The
getAccessTokenmethod now properly uses therunDataStoreCatchingutility 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
setAccessTokenmethod follows the same pattern asgetAccessToken, 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
BuildConfigmakes 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
📒 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
AuthTokendata 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
Errorimplementation 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 ComposeThis data class follows best practices for a Jetpack Compose state holder:
- Good use of
@Immutableannotation 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 handlingThis 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
Errorinterface 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
Errorinterface 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
@Serializableand uses@SerialNameto 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 annotationAdding 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 injectionThe 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 destinationThe 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 handlingGood 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 previewThe 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 stateThe 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 statesExcellent 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 handlingThe handling of
CancellationExceptionis correctly implemented. By rethrowing this specific exception, you ensure that coroutine cancellation properly propagates through the call stack. Additionally, the check forcoroutineContext.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 handlingThe implementation correctly uses the
safeCallutility 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 handlingThe UI component is well-structured with clear separation of concerns. The use of a
whenstatement 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
SetupScreendata 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
collectAsStateWithLifecyclefor 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
toStringextension 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.
app/src/main/java/com/notifier/app/auth/presentation/util/createGitHubAuthIntent.kt
Outdated
Show resolved
Hide resolved
app/src/main/java/com/notifier/app/auth/presentation/login/components/LoginButton.kt
Show resolved
Hide resolved
app/src/main/java/com/notifier/app/auth/presentation/login/components/LoginButton.kt
Show resolved
Hide resolved
app/src/main/java/com/notifier/app/auth/presentation/login/LoginScreen.kt
Show resolved
Hide resolved
app/src/main/java/com/notifier/app/auth/presentation/setup/SetupRoute.kt
Show resolved
Hide resolved
app/src/main/java/com/notifier/app/auth/presentation/setup/SetupRoute.kt
Outdated
Show resolved
Hide resolved
app/src/main/java/com/notifier/app/auth/presentation/setup/SetupViewModel.kt
Show resolved
Hide resolved
app/src/main/java/com/notifier/app/auth/presentation/setup/SetupViewModel.kt
Show resolved
Hide resolved
…oken from DataStoreManager
…ccept onClick parameter
There was a problem hiding this 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 insideHttpClientConfig; move the headers intodefaultRequest
headers { … }belongs toHttpRequestBuilder, not the client config DSL, so this block will not compile.
Combine it with the existingdefaultRequestblock:- // 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 thenull/ empty OAuth code before calling the ViewModelThis
LaunchedEffectunconditionally forwardscodetoviewModel.getAuthToken(code).
If the deep-link failed andcodeisnullor blank, the ViewModel immediately transitions to theFAILEDstate. 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 errorsCasting
error as NetworkErrorwill throwClassCastExceptionif 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 errorsReplicate 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 noiseThe trailing comma after
LoginStatusis 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
runBlockingexecutes on whatever thread instantiatesHttpClient(often the main/UI thread during Hilt graph creation).
IfDataStoreis still initialising, this can freeze the app for several hundred ms.Prefer Ktor’s
Auth/BearerAuthplugin (orDefaultRequestwith 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 NotificationScreenThe 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 handlingThe 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 toinit {}Because
onStart { … }is applied after thestateIn, each new subscriber tostatewill triggercheckAuthStatus(). 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: PreferChannel(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 toChannel(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_SECRETis 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 eventsLike in
LoginViewModel, consider giving_eventsa 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
📒 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 regularobjector 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 . || trueLength 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* || trueLength 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 || trueLength 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.9compiler 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(), andhashCode()"[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:
- 1: https://www.youtube.com/watch?v=ovAqcwFhEGc
- 2: Kotlin/KEEP#317
- 3: https://kotlinlang.org/api/core/kotlin-stdlib/kotlin.experimental/
- 4: https://kotlinlang.org/api/core/kotlin-stdlib/kotlin.reflect/-experimental-associated-objects/
- 5: https://dart.dev/tools/experiment-flags
- 6: https://kotlinlang.org/docs/whatsnew19.html
- 7: https://www.youtube.com/watch?v=0JRPA0tt9og
- 8: https://book.kotlincn.net/text/whatsnew1820.html
- 9: https://appcircle.io/blog/kotlin-data-objects
- 10: https://kotlinlang.org/docs/whatsnew1720.html
🏁 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" fiLength of output: 4609
No action required: Data objects are stable
Our project uses Kotlin plugin version 2.1.10 (≥ 1.9.0), where
data objectdeclarations are stable and don’t require an experimental flag. You can safely keepdata objectinLoginAction.ktas 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 handlingThe 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 routeUsing 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 navigationUsing 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 concernsThe
LoginRoutecomposable 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 APIProperly initializing the splash screen before other UI operations follows best practices for creating a smooth app startup experience.
app/src/main/java/com/notifier/app/auth/presentation/login/LoginEvent.kt
Show resolved
Hide resolved
app/src/main/java/com/notifier/app/auth/presentation/setup/SetupRoute.kt
Show resolved
Hide resolved
There was a problem hiding this 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 handlingThe 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 flowThe 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 logicThe navigation logic to
NotificationScreenwith backstack clearing is duplicated in both theLoginRouteandSetupRoutehandlers.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 flowThe
validateOAuthStatemethod 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
📒 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 handlingThe 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 checkingThe error handling logic correctly uses the
isoperator to check the error type before casting, which is safer than using an unsafe cast.
app/src/main/java/com/notifier/app/auth/presentation/util/GitHubAuthIntentProvider.kt
Show resolved
Hide resolved
app/src/main/java/com/notifier/app/auth/presentation/util/GitHubAuthIntentProvider.kt
Show resolved
Hide resolved
…tests for loading and login states
…nsistency in tests
There was a problem hiding this 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_INstate, theLoginScreenimplementation also handlesnullstatus as equivalent toLOADING. 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
initialStateparameter is used twice - once to initializemutableStateFlowand again in thestateIncall. SincemutableStateFlowis already initialized withinitialState, theinitialValueparameter instateInis 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
stateproperty is marked asopen, but its getter contains complex logic withstateInconfiguration. 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
finalor 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
stateproperty 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
stateproperty.
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
📒 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 theCircularProgressIndicatoris 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
sendEventmethod properly usesviewModelScope.launchto 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
getAuthTokenmethod 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.
app/src/androidTest/java/com/notifier/app/auth/presentation/login/LoginScreenTest.kt
Show resolved
Hide resolved
There was a problem hiding this 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
📒 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
SetupStepstates with clear, well-structured tests that properly verify the UI behavior. The test assertions correctly match the actual UI implementation fromSetupScreen.kt.
There was a problem hiding this 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, andTEST_TAG_CIRCULAR_PROGRESS_INDICATORare 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
📒 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.
|
@coderabbitai review |
✅ Actions performedReview triggered.
|
There was a problem hiding this 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
📒 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.
app/src/androidTest/java/com/notifier/app/auth/presentation/setup/SetupViewModelTest.kt
Show resolved
Hide resolved
…ve coroutine handling
…andling in SetupViewModel
There was a problem hiding this 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
savedStateand updating it in theonSuccesscallback 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
📒 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.
There was a problem hiding this 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:
- Verify mock interactions: The test doesn't verify that
getAccessToken()was actually called- 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:
- Event uniqueness: Verify that only one event is emitted
- 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
📒 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
@Beforesetup 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
codeandreceivedStateparameters.
142-170: Async event testing is implemented correctly.Good use of
asyncandawaitto test concurrent state changes and event emissions.
app/src/test/java/com/notifier/app/auth/presentation/setup/SetupViewModelTest.kt
Show resolved
Hide resolved
app/src/test/java/com/notifier/app/auth/presentation/setup/SetupViewModelTest.kt
Show resolved
Hide resolved
…implify error handling" This reverts commit e86b940.
…ons and error events
|
@coderabbitai review |
✅ Actions performedReview triggered.
|
Summary by CodeRabbit
New Features
Improvements
Bug Fixes
Tests
Chores
Issue Reference
DataStoreManager#18Screenshots
Essential Checklist