Skip to content

Commit 236bbcb

Browse files
dadachiclaude
andcommitted
Implement CodedError system with NATA-XXXX error codes
Add platform-specific error codes (NATA prefix) for user-facing error messages. Error types in common/errors/ package. - Add CodedError interface, AppError, NfcError, SubscriptionError - Make ApiException implement CodedError (NATA-2001, NATA-2002) - Move error types to common/errors/ package - Add Throwable.codedDescription extension - Update all ViewModels to use codedDescription - Add unit tests for all error types Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent 8170d3d commit 236bbcb

32 files changed

Lines changed: 263 additions & 100 deletions

app/src/main/kotlin/com/nativeapptemplate/nativeapptemplatefree/MainActivityViewModel.kt

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import androidx.lifecycle.ViewModel
55
import androidx.lifecycle.viewModelScope
66
import com.nativeapptemplate.nativeapptemplatefree.MainActivityUiState.Loading
77
import com.nativeapptemplate.nativeapptemplatefree.MainActivityUiState.Success
8+
import com.nativeapptemplate.nativeapptemplatefree.common.errors.codedDescription
89
import com.nativeapptemplate.nativeapptemplatefree.data.login.LoginRepository
910
import com.nativeapptemplate.nativeapptemplatefree.model.CompleteScanResult
1011
import com.nativeapptemplate.nativeapptemplatefree.model.CompleteScanResultType
@@ -125,8 +126,8 @@ class MainActivityViewModel @Inject constructor(
125126
try {
126127
loginRepository.setCompleteScanResult(completeScanResult)
127128
} catch (exception: Exception) {
128-
val message = exception.message
129-
completeScanResult.message = message ?: "Unknown Error"
129+
val message = exception.codedDescription
130+
completeScanResult.message = message
130131
completeScanResult.completeScanResultType = CompleteScanResultType.Failed
131132

132133
loginRepository.setCompleteScanResult(completeScanResult)
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
package com.nativeapptemplate.nativeapptemplatefree.common.errors
2+
3+
sealed class ApiException(message: String, cause: Throwable? = null) :
4+
Exception(message, cause), CodedError {
5+
6+
class ApiError(
7+
val code: Int,
8+
val apiMessage: String,
9+
) : ApiException("$apiMessage [Status: $code]") {
10+
override val errorCode: String = "NATA-2001"
11+
override val errorDescription: String = "$apiMessage [Status: $code]"
12+
}
13+
14+
class UnprocessableError(
15+
val rawMessage: String,
16+
cause: Throwable? = null,
17+
) : ApiException("Not processable error($rawMessage).", cause) {
18+
override val errorCode: String = "NATA-2002"
19+
override val errorDescription: String = "Processing error: $rawMessage"
20+
}
21+
}
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
package com.nativeapptemplate.nativeapptemplatefree.common.errors
2+
3+
sealed class AppError(
4+
override val errorCode: String,
5+
override val errorDescription: String,
6+
) : Exception(errorDescription), CodedError {
7+
8+
class Unexpected(detail: String? = null) : AppError(
9+
errorCode = "NATA-1001",
10+
errorDescription = "Unexpected error" + if (detail != null) ": $detail" else "",
11+
)
12+
}
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
package com.nativeapptemplate.nativeapptemplatefree.common.errors
2+
3+
interface CodedError {
4+
val errorCode: String
5+
val errorDescription: String
6+
val formattedDescription: String
7+
get() = "[$errorCode] $errorDescription"
8+
}
9+
10+
val Throwable.codedDescription: String
11+
get() = (this as? CodedError)?.formattedDescription ?: message ?: "Unknown Error"
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
package com.nativeapptemplate.nativeapptemplatefree.common.errors
2+
3+
sealed class NfcError(
4+
override val errorCode: String,
5+
override val errorDescription: String,
6+
) : Exception(errorDescription), CodedError {
7+
8+
class ScanFailed(detail: String? = null) : NfcError(
9+
errorCode = "NATA-3001",
10+
errorDescription = "NFC scan operation failed" + if (detail != null) ": $detail" else "",
11+
)
12+
}
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
package com.nativeapptemplate.nativeapptemplatefree.common.errors
2+
3+
sealed class SubscriptionError(
4+
override val errorCode: String,
5+
override val errorDescription: String,
6+
) : Exception(errorDescription), CodedError {
7+
8+
class RestoreFailed(detail: String? = null) : SubscriptionError(
9+
errorCode = "NATA-6001",
10+
errorDescription = "Failed to restore purchases" + if (detail != null) ": $detail" else "",
11+
)
12+
13+
class SubscriptionRequired : SubscriptionError(
14+
errorCode = "NATA-6002",
15+
errorDescription = "User needs an active subscription",
16+
)
17+
}

app/src/main/kotlin/com/nativeapptemplate/nativeapptemplatefree/data/login/LoginRepositoryImpl.kt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,11 @@
11
package com.nativeapptemplate.nativeapptemplatefree.data.login
22

33
import androidx.annotation.VisibleForTesting
4+
import com.nativeapptemplate.nativeapptemplatefree.common.errors.ApiException
45
import com.nativeapptemplate.nativeapptemplatefree.datastore.NatPreferencesDataSource
56
import com.nativeapptemplate.nativeapptemplatefree.model.*
67
import com.nativeapptemplate.nativeapptemplatefree.model.LoggedInShopkeeper
78
import com.nativeapptemplate.nativeapptemplatefree.model.Login
8-
import com.nativeapptemplate.nativeapptemplatefree.network.ApiException
99
import com.nativeapptemplate.nativeapptemplatefree.network.Dispatcher
1010
import com.nativeapptemplate.nativeapptemplatefree.network.NatDispatchers
1111
import com.nativeapptemplate.nativeapptemplatefree.network.emitApiResponse

app/src/main/kotlin/com/nativeapptemplate/nativeapptemplatefree/network/ApiException.kt

Lines changed: 0 additions & 23 deletions
This file was deleted.

app/src/main/kotlin/com/nativeapptemplate/nativeapptemplatefree/network/ApiResponseExtensions.kt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
package com.nativeapptemplate.nativeapptemplatefree.network
22

3+
import com.nativeapptemplate.nativeapptemplatefree.common.errors.ApiException
34
import com.nativeapptemplate.nativeapptemplatefree.model.NativeAppTemplateApiError
45
import com.skydoves.sandwich.ApiResponse
56
import com.skydoves.sandwich.message

app/src/main/kotlin/com/nativeapptemplate/nativeapptemplatefree/ui/app_root/AcceptPrivacyViewModel.kt

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ package com.nativeapptemplate.nativeapptemplatefree.ui.app_root
22

33
import androidx.lifecycle.ViewModel
44
import androidx.lifecycle.viewModelScope
5+
import com.nativeapptemplate.nativeapptemplatefree.common.errors.codedDescription
56
import com.nativeapptemplate.nativeapptemplatefree.data.login.LoginRepository
67
import dagger.hilt.android.lifecycle.HiltViewModel
78
import kotlinx.coroutines.flow.MutableStateFlow
@@ -41,10 +42,10 @@ class AcceptPrivacyViewModel @Inject constructor(
4142

4243
booleanFlow
4344
.catch { exception ->
44-
val message = exception.message
45+
val message = exception.codedDescription
4546
_uiState.update {
4647
it.copy(
47-
message = message ?: "Unknown Error",
48+
message = message,
4849
isLoading = false,
4950
)
5051
}

0 commit comments

Comments
 (0)