Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 12 additions & 0 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,18 @@ MVVM layered architecture following [Android Modern App Architecture](https://de
- **Navigation routes**: Defined as extension functions on `NavGraphBuilder` (e.g., `shopListView()`, `navigateToShopDetail()`). Routes use type-safe navigation.
- **Proto DataStore**: User preferences and NFC scan state are persisted via Protocol Buffers (lite).

## Error Handling (CodedError System)
All errors should use the `CodedError` interface. Error codes use the `NATA-XXXX` prefix (NativeAppTemplate Android).

| Range | Type | Description |
|-------|------|-------------|
| NATA-1xxx | App/general errors | Unexpected errors, catch-all |
| NATA-2xxx | API/network errors | HTTP request failures, parsing errors |
| NATA-3xxx | NFC/scan errors | NFC tag read/write/scan failures |

- New error types must implement `CodedError`
- Use `codedDescription` (not `message` or `localizedMessage`) in all user-facing error messages — this prepends `[NATA-XXXX]` for `CodedError` types

## Testing

- Tests use JUnit 4 with `kotlinx.coroutines.test` and Robolectric.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import com.nativeapptemplate.nativeapptemplatefree.MainActivityUiState.Loading
import com.nativeapptemplate.nativeapptemplatefree.MainActivityUiState.Success
import com.nativeapptemplate.nativeapptemplatefree.common.errors.codedDescription
import com.nativeapptemplate.nativeapptemplatefree.data.login.LoginRepository
import com.nativeapptemplate.nativeapptemplatefree.model.CompleteScanResult
import com.nativeapptemplate.nativeapptemplatefree.model.CompleteScanResultType
Expand Down Expand Up @@ -125,8 +126,8 @@ class MainActivityViewModel @Inject constructor(
try {
loginRepository.setCompleteScanResult(completeScanResult)
} catch (exception: Exception) {
val message = exception.message
completeScanResult.message = message ?: "Unknown Error"
val message = exception.codedDescription
completeScanResult.message = message
completeScanResult.completeScanResultType = CompleteScanResultType.Failed

loginRepository.setCompleteScanResult(completeScanResult)
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
package com.nativeapptemplate.nativeapptemplatefree.common.errors

sealed class ApiException(message: String, cause: Throwable? = null) :
Exception(message, cause), CodedError {

class ApiError(
val code: Int,
val apiMessage: String,
) : ApiException("$apiMessage [Status: $code]") {
override val errorCode: String = "NATA-2001"
override val errorDescription: String = "$apiMessage [Status: $code]"
}

class UnprocessableError(
val rawMessage: String,
cause: Throwable? = null,
) : ApiException("Not processable error($rawMessage).", cause) {
override val errorCode: String = "NATA-2002"
override val errorDescription: String = "Processing error: $rawMessage"
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
package com.nativeapptemplate.nativeapptemplatefree.common.errors

sealed class AppError(
override val errorCode: String,
override val errorDescription: String,
) : Exception(errorDescription), CodedError {

class Unexpected(detail: String? = null) : AppError(
errorCode = "NATA-1001",
errorDescription = "Unexpected error" + if (detail != null) ": $detail" else "",
)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
package com.nativeapptemplate.nativeapptemplatefree.common.errors

interface CodedError {
val errorCode: String
val errorDescription: String
val formattedDescription: String
get() = "[$errorCode] $errorDescription"
}

val Throwable.codedDescription: String
get() = (this as? CodedError)?.formattedDescription ?: message ?: "Unknown Error"
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
package com.nativeapptemplate.nativeapptemplatefree.common.errors

sealed class NfcError(
override val errorCode: String,
override val errorDescription: String,
) : Exception(errorDescription), CodedError {

class ScanFailed(detail: String? = null) : NfcError(
errorCode = "NATA-3001",
errorDescription = "NFC scan operation failed" + if (detail != null) ": $detail" else "",
)
}
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
package com.nativeapptemplate.nativeapptemplatefree.data.login

import androidx.annotation.VisibleForTesting
import com.nativeapptemplate.nativeapptemplatefree.common.errors.ApiException
import com.nativeapptemplate.nativeapptemplatefree.datastore.NatPreferencesDataSource
import com.nativeapptemplate.nativeapptemplatefree.model.*
import com.nativeapptemplate.nativeapptemplatefree.model.LoggedInShopkeeper
import com.nativeapptemplate.nativeapptemplatefree.model.Login
import com.nativeapptemplate.nativeapptemplatefree.network.ApiException
import com.nativeapptemplate.nativeapptemplatefree.network.Dispatcher
import com.nativeapptemplate.nativeapptemplatefree.network.NatDispatchers
import com.nativeapptemplate.nativeapptemplatefree.network.emitApiResponse
Expand Down

This file was deleted.

Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package com.nativeapptemplate.nativeapptemplatefree.network

import com.nativeapptemplate.nativeapptemplatefree.common.errors.ApiException
import com.nativeapptemplate.nativeapptemplatefree.model.NativeAppTemplateApiError
import com.skydoves.sandwich.ApiResponse
import com.skydoves.sandwich.message
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package com.nativeapptemplate.nativeapptemplatefree.ui.app_root

import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import com.nativeapptemplate.nativeapptemplatefree.common.errors.codedDescription
import com.nativeapptemplate.nativeapptemplatefree.data.login.LoginRepository
import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.flow.MutableStateFlow
Expand Down Expand Up @@ -41,10 +42,10 @@ class AcceptPrivacyViewModel @Inject constructor(

booleanFlow
.catch { exception ->
val message = exception.message
val message = exception.codedDescription
_uiState.update {
it.copy(
message = message ?: "Unknown Error",
message = message,
isLoading = false,
)
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package com.nativeapptemplate.nativeapptemplatefree.ui.app_root

import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import com.nativeapptemplate.nativeapptemplatefree.common.errors.codedDescription
import com.nativeapptemplate.nativeapptemplatefree.data.login.LoginRepository
import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.flow.MutableStateFlow
Expand Down Expand Up @@ -41,10 +42,10 @@ class AcceptTermsViewModel @Inject constructor(

booleanFlow
.catch { exception ->
val message = exception.message
val message = exception.codedDescription
_uiState.update {
it.copy(
message = message ?: "Unknown Error",
message = message,
isLoading = false,
)
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package com.nativeapptemplate.nativeapptemplatefree.ui.app_root
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import com.nativeapptemplate.nativeapptemplatefree.NatConstants
import com.nativeapptemplate.nativeapptemplatefree.common.errors.codedDescription
import com.nativeapptemplate.nativeapptemplatefree.data.login.SignUpRepository
import com.nativeapptemplate.nativeapptemplatefree.model.SendResetPassword
import com.nativeapptemplate.nativeapptemplatefree.utils.Utility.validateEmail
Expand Down Expand Up @@ -49,10 +50,10 @@ class ForgotPasswordViewModel @Inject constructor(

booleanFlow
.catch { exception ->
val message = exception.message
val message = exception.codedDescription
_uiState.update {
it.copy(
message = message ?: "Unknown Error",
message = message,
isLoading = false,
)
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package com.nativeapptemplate.nativeapptemplatefree.ui.app_root
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import com.nativeapptemplate.nativeapptemplatefree.NatConstants
import com.nativeapptemplate.nativeapptemplatefree.common.errors.codedDescription
import com.nativeapptemplate.nativeapptemplatefree.data.login.SignUpRepository
import com.nativeapptemplate.nativeapptemplatefree.model.SendConfirmation
import com.nativeapptemplate.nativeapptemplatefree.utils.Utility.validateEmail
Expand Down Expand Up @@ -49,10 +50,10 @@ class ResendConfirmationInstructionsViewModel @Inject constructor(

booleanFlow
.catch { exception ->
val message = exception.message
val message = exception.codedDescription
_uiState.update {
it.copy(
message = message ?: "Unknown Error",
message = message,
isLoading = false,
)
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import android.util.Log
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import com.nativeapptemplate.nativeapptemplatefree.NatConstants
import com.nativeapptemplate.nativeapptemplatefree.common.errors.codedDescription
import com.nativeapptemplate.nativeapptemplatefree.data.login.LoginRepository
import com.nativeapptemplate.nativeapptemplatefree.model.LoggedInShopkeeper
import com.nativeapptemplate.nativeapptemplatefree.model.Login
Expand Down Expand Up @@ -49,10 +50,10 @@ class SignInEmailAndPasswordViewModel @Inject constructor(

loggedInShopkeeperFlow
.catch { exception ->
val message = exception.message
val message = exception.codedDescription
_uiState.update {
it.copy(
message = message ?: "Unknown Error",
message = message,
isLoading = false,
)
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package com.nativeapptemplate.nativeapptemplatefree.ui.app_root
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import com.nativeapptemplate.nativeapptemplatefree.NatConstants
import com.nativeapptemplate.nativeapptemplatefree.common.errors.codedDescription
import com.nativeapptemplate.nativeapptemplatefree.data.login.SignUpRepository
import com.nativeapptemplate.nativeapptemplatefree.model.SignUp
import com.nativeapptemplate.nativeapptemplatefree.model.TimeZones
Expand Down Expand Up @@ -56,10 +57,10 @@ class SignUpViewModel @Inject constructor(

loggedInShopkeeperFlow
.catch { exception ->
val message = exception.message
val message = exception.codedDescription
_uiState.update {
it.copy(
message = message ?: "Unknown Error",
message = message,
isLoading = false,
)
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import androidx.lifecycle.SavedStateHandle
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import androidx.navigation.toRoute
import com.nativeapptemplate.nativeapptemplatefree.common.errors.codedDescription
import com.nativeapptemplate.nativeapptemplatefree.data.login.LoginRepository
import com.nativeapptemplate.nativeapptemplatefree.model.CompleteScanResult
import com.nativeapptemplate.nativeapptemplatefree.model.CompleteScanResultType
Expand Down Expand Up @@ -53,8 +54,8 @@ class DoScanViewModel @Inject constructor(
try {
loginRepository.setShowTagInfoScanResult(showTagInfoScanResult)
} catch (exception: Exception) {
val message = exception.message
showTagInfoScanResult.message = message ?: "Unknown Error"
val message = exception.codedDescription
showTagInfoScanResult.message = message
showTagInfoScanResult.showTagInfoScanResultType = ShowTagInfoScanResultType.Failed

loginRepository.setShowTagInfoScanResult(showTagInfoScanResult)
Expand All @@ -73,8 +74,8 @@ class DoScanViewModel @Inject constructor(
try {
loginRepository.setCompleteScanResult(completeScanResult)
} catch (exception: Exception) {
val message = exception.message
completeScanResult.message = message ?: "Unknown Error"
val message = exception.codedDescription
completeScanResult.message = message
completeScanResult.completeScanResultType = CompleteScanResultType.Failed

loginRepository.setCompleteScanResult(completeScanResult)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package com.nativeapptemplate.nativeapptemplatefree.ui.scan

import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import com.nativeapptemplate.nativeapptemplatefree.common.errors.codedDescription
import com.nativeapptemplate.nativeapptemplatefree.data.item_tag.ItemTagRepository
import com.nativeapptemplate.nativeapptemplatefree.data.login.LoginRepository
import com.nativeapptemplate.nativeapptemplatefree.model.CompleteScanResult
Expand Down Expand Up @@ -86,10 +87,10 @@ class ScanViewModel @Inject constructor(
)
}
}.catch { exception ->
val message = exception.message
val message = exception.codedDescription
_uiState.update {
it.copy(
message = message ?: "Unknown Error",
message = message,
isLoading = false,
)
}
Expand All @@ -109,10 +110,10 @@ class ScanViewModel @Inject constructor(

itemTagFlow
.catch { exception ->
val message = exception.message
val message = exception.codedDescription
val showTagInfoScanResult = uiState.value.showTagInfoScanResult
showTagInfoScanResult.showTagInfoScanResultType = ShowTagInfoScanResultType.Failed
showTagInfoScanResult.message = message ?: "Unknown Error"
showTagInfoScanResult.message = message

loginRepository.setShowTagInfoScanResult(showTagInfoScanResult)

Expand Down Expand Up @@ -160,10 +161,10 @@ class ScanViewModel @Inject constructor(

itemTagFlow
.catch { exception ->
val message = exception.message
val message = exception.codedDescription
val completeScanResult = uiState.value.completeScanResult
completeScanResult.completeScanResultType = CompleteScanResultType.Failed
completeScanResult.message = message ?: "Unknown Error"
completeScanResult.message = message

loginRepository.setCompleteScanResult(completeScanResult)

Expand Down Expand Up @@ -214,10 +215,10 @@ class ScanViewModel @Inject constructor(

itemTagFlow
.catch { exception ->
val message = exception.message
val message = exception.codedDescription
val completeScanResult = uiState.value.completeScanResult
completeScanResult.completeScanResultType = CompleteScanResultType.Failed
completeScanResult.message = message ?: "Unknown Error"
completeScanResult.message = message

loginRepository.setCompleteScanResult(completeScanResult)

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package com.nativeapptemplate.nativeapptemplatefree.ui.settings
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import com.nativeapptemplate.nativeapptemplatefree.NatConstants
import com.nativeapptemplate.nativeapptemplatefree.common.errors.codedDescription
import com.nativeapptemplate.nativeapptemplatefree.data.login.AccountPasswordRepository
import com.nativeapptemplate.nativeapptemplatefree.model.UpdatePasswordBody
import com.nativeapptemplate.nativeapptemplatefree.model.UpdatePasswordBodyDetail
Expand Down Expand Up @@ -53,10 +54,10 @@ class PasswordEditViewModel @Inject constructor(

booleanFlow
.catch { exception ->
val message = exception.message
val message = exception.codedDescription
_uiState.update {
it.copy(
message = message ?: "Unknown Error",
message = message,
isLoading = false,
)
}
Expand Down
Loading
Loading