# Error Handling # Android SDK: Error Handling Best practices and patterns for handling errors in the Digital Card Engine SDK, including network failures, authentication issues, and MCD-specific errors. ## Error Types ### 1. CardProviderException Custom SDK exception for API and authentication errors. ```kotlin sealed class CardProviderException(message: String) : Exception(message) { class AuthenticationError(message: String) : CardProviderException(message) class NetworkError(message: String) : CardProviderException(message) class ValidationError(message: String) : CardProviderException(message) class ServerError(message: String) : CardProviderException(message) } ``` ### 2. MCD Errors Errors from the MeaWallet MCD SDK when fetching secure card data. ```kotlin // MCD onFailure callback override fun onFailure(mcdError: McdError) { // mcdError.code: Error code // mcdError.name: Error type name // mcdError.message: Description // mcdError.requestId: Request identifier for debugging } ``` ### 3. Standard Exceptions * `IllegalStateException` - SDK not initialized, invalid state * `IllegalArgumentException` - Invalid parameters * `IOException` - Network connectivity issues * `SecurityException` - Permission issues *** ## Common Error Scenarios ### Authentication Errors **Cause**: Invalid, expired, or missing authentication token. **Symptoms**: * 401 Unauthorized responses * "Auth token cannot be empty or blank" * "Auth token appears to be too short" **Solution**: ```kotlin import com.paymentology.ei_card_provider_sdk.EiCardProviderSDK class TokenManager(private val sdk: DigitalCardEngineSDK) { suspend fun refreshTokenIfNeeded() { try { val cards = sdk.getCards() // Success - token valid } catch (e: CardProviderException.AuthenticationError) { // Token expired - fetch new one val newToken = fetchFreshTokenFromBackend() EiCardProviderSDK.updateAuthToken(newToken) // Retry operation val cards = sdk.getCards() } } private suspend fun fetchFreshTokenFromBackend(): String { // Your backend call to get new token return myBackendApi.refreshToken() } } ``` ### Network Errors **Cause**: No internet connection, timeout, DNS failure. **Symptoms**: * `SocketTimeoutException` * `UnknownHostException` * `ConnectException` **Solution**: ```kotlin import androidx.compose.runtime.* import kotlinx.coroutines.delay @Composable fun CardListWithRetry(sdk: DigitalCardEngineSDK) { var retryCount by remember { mutableStateOf(0) } var errorMessage by remember { mutableStateOf(null) } LaunchedEffect(retryCount) { try { val cards = sdk.getCards() errorMessage = null } catch (e: IOException) { errorMessage = "Network error. Please check your connection." } catch (e: Exception) { errorMessage = "Error: ${e.message}" } } if (errorMessage != null) { ErrorView( message = errorMessage!!, onRetry = { retryCount++ } ) } else { UILibrary.CardListView(sdk = sdk, /* ... */) } } ``` ### MCD Errors (Secure Card Data) **Cause**: Missing TOTP secret, card not selected, MCD configuration issues. **Symptoms**: * MCD error codes in logs * "No selected card" errors * Secure fields not displaying **Common MCD Error Codes**: | Code | Description | Solution | | ------ | ---------------- | -------------------------------------- | | `1001` | Invalid card ID | Verify `giftCardId` is correct | | `1002` | Invalid secret | Check TOTP secret generation | | `1003` | Card not found | Ensure card exists and user has access | | `2001` | Network error | Check connectivity, retry | | `3001` | Invalid response | Contact support | **Solution**: ```kotlin import android.util.Log lifecycleScope.launch { try { // Ensure card is selected sdk.selectCard(card) // Fetch secure card images val images = sdk.getCardImages() if (images != null) { panImageView.setImageBitmap(images["pan"]) cvvImageView.setImageBitmap(images["cvv"]) } } catch (e: IllegalStateException) { when { e.message?.contains("No selected card") == true -> { Log.e("MCD", "Card not selected - call sdk.selectCard() first") showError("Card selection error") } e.message?.contains("MCD error") == true -> { Log.e("MCD", "MCD SDK error: ${e.message}") showError("Unable to load secure card details") } else -> { Log.e("MCD", "Unexpected error: ${e.message}") showError("An error occurred") } } } } ``` ### Initialization Errors **Cause**: SDK not initialized before use. **Symptoms**: * "SDK not initialized" errors * Null pointer exceptions * Crashes on API calls **Solution**: ```kotlin class MyApp : Application() { lateinit var sdk: DigitalCardEngineSDK private var isInitialized = false override fun onCreate() { super.onCreate() initializeSDK() } private fun initializeSDK() { try { sdk = DigitalCardEngineSDK() val config = DceConfiguration(/* ... */) sdk.initialize( context = this, env = Environment.TEST, dceConfiguration = config ) isInitialized = true } catch (e: Exception) { Log.e("App", "Failed to initialize SDK: ${e.message}") // Handle critical error - maybe show error screen } } fun getSdk(): DigitalCardEngineSDK { if (!isInitialized) { throw IllegalStateException("SDK not initialized") } return sdk } } ``` *** ## Error Handling Patterns ### 1. Try-Catch Pattern Use for synchronous operations or within coroutines. ```kotlin import kotlinx.coroutines.launch lifecycleScope.launch { try { val cards = sdk.getCards() updateUI(cards) } catch (e: CardProviderException.AuthenticationError) { handleAuthError() } catch (e: CardProviderException.NetworkError) { showRetryOption() } catch (e: Exception) { logError(e) showGenericError() } } ``` ### 2. Result Pattern Use for cleaner error propagation. ```kotlin sealed class Result { data class Success(val data: T) : Result() data class Error(val exception: Exception) : Result() } suspend fun fetchCardsResult(): Result> { return try { val cards = sdk.getCards() Result.Success(cards) } catch (e: Exception) { Result.Error(e) } } // Usage when (val result = fetchCardsResult()) { is Result.Success -> updateUI(result.data) is Result.Error -> handleError(result.exception) } ``` ### 3. State Pattern (Compose) Use for UI state management. ```kotlin sealed class UiState { object Loading : UiState() data class Success(val data: T) : UiState() data class Error(val message: String) : UiState() } class CardListViewModel(private val sdk: DigitalCardEngineSDK) : ViewModel() { var uiState by mutableStateOf>>(UiState.Loading) private set fun loadCards() { uiState = UiState.Loading viewModelScope.launch { uiState = try { val cards = sdk.getCards() UiState.Success(cards) } catch (e: Exception) { UiState.Error(e.message ?: "Unknown error") } } } } // Usage in Compose @Composable fun CardListScreen(viewModel: CardListViewModel) { when (val state = viewModel.uiState) { is UiState.Loading -> LoadingIndicator() is UiState.Success -> CardList(state.data) is UiState.Error -> ErrorView(state.message) { viewModel.loadCards() } } } ``` *** ## Retry Strategies ### Exponential Backoff ```kotlin import kotlinx.coroutines.delay import kotlin.math.pow suspend fun retryWithBackoff( maxRetries: Int = 3, initialDelayMs: Long = 1000, factor: Double = 2.0, block: suspend () -> T ): T { var currentDelay = initialDelayMs var lastException: Exception? = null repeat(maxRetries) { attempt -> try { return block() } catch (e: Exception) { lastException = e if (attempt < maxRetries - 1) { Log.w("Retry", "Attempt ${attempt + 1} failed, retrying in ${currentDelay}ms") delay(currentDelay) currentDelay = (currentDelay * factor).toLong() } } } throw lastException ?: Exception("Max retries exceeded") } // Usage lifecycleScope.launch { try { val cards = retryWithBackoff { sdk.getCards() } updateUI(cards) } catch (e: Exception) { showError("Failed after multiple retries: ${e.message}") } } ``` ### Conditional Retry ```kotlin fun shouldRetry(exception: Exception): Boolean { return when (exception) { is IOException -> true // Network errors is CardProviderException.NetworkError -> true is CardProviderException.AuthenticationError -> false // Don't retry auth errors else -> false } } suspend fun retryIfAppropriate( maxRetries: Int = 3, block: suspend () -> T ): T { var lastException: Exception? = null repeat(maxRetries) { attempt -> try { return block() } catch (e: Exception) { lastException = e if (!shouldRetry(e) || attempt >= maxRetries - 1) { throw e } delay(1000L * (attempt + 1)) } } throw lastException ?: Exception("Max retries exceeded") } ``` *** ## Logging Best Practices ### Structured Logging ```kotlin import android.util.Log object SdkLogger { private const val TAG = "DigitalCardEngine" fun logApiCall(endpoint: String, params: Map? = null) { Log.d(TAG, "API Call: $endpoint ${params?.let { "with $it" } ?: ""}") } fun logApiSuccess(endpoint: String, duration: Long) { Log.d(TAG, "API Success: $endpoint (${duration}ms)") } fun logApiError(endpoint: String, error: Exception) { Log.e(TAG, "API Error: $endpoint - ${error.message}", error) } fun logMcdError(error: McdError) { Log.e(TAG, "MCD Error [${error.code}]: ${error.name} - ${error.message} (reqId: ${error.requestId})") } } // Usage try { SdkLogger.logApiCall("getCards") val startTime = System.currentTimeMillis() val cards = sdk.getCards() val duration = System.currentTimeMillis() - startTime SdkLogger.logApiSuccess("getCards", duration) } catch (e: Exception) { SdkLogger.logApiError("getCards", e) throw e } ``` *** ## User-Friendly Error Messages ```kotlin fun getUser FriendlyMessage(exception: Exception): String { return when (exception) { is CardProviderException.AuthenticationError -> "Your session has expired. Please log in again." is CardProviderException.NetworkError -> "Unable to connect. Please check your internet connection and try again." is CardProviderException.ServerError -> "Our servers are experiencing issues. Please try again later." is IOException -> "Connection problem. Please check your network and retry." is IllegalStateException -> when { exception.message?.contains("No selected card") == true -> "Card selection error. Please try again." exception.message?.contains("MCD error") == true -> "Unable to display secure card information." else -> "An unexpected error occurred." } else -> "Something went wrong. Please try again." } } // Usage @Composable fun ErrorView(exception: Exception, onRetry: () -> Unit) { Column( modifier = Modifier.padding(16.dp), horizontalAlignment = Alignment.CenterHorizontally ) { Text( text = getUserFriendlyMessage(exception), style = MaterialTheme.typography.bodyLarge ) Spacer(modifier = Modifier.height(16.dp)) Button(onClick = onRetry) { Text("Try Again") } } } ``` *** ## Complete Example ```kotlin import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import kotlinx.coroutines.launch class CardViewModel(private val sdk: DigitalCardEngineSDK) : ViewModel() { var state by mutableStateOf(CardState.Loading) private set sealed class CardState { object Loading : CardState() data class Success(val cards: List) : CardState() data class Error(val message: String, val canRetry: Boolean) : CardState() } init { loadCards() } fun loadCards() { state = CardState.Loading viewModelScope.launch { try { val cards = retryWithBackoff(maxRetries = 2) { sdk.getCards() } state = CardState.Success(cards) } catch (e: CardProviderException.AuthenticationError) { // Try to refresh token try { val newToken = refreshToken() EiCardProviderSDK.updateAuthToken(newToken) loadCards() // Retry after token refresh } catch (tokenError: Exception) { state = CardState.Error( message = "Authentication failed. Please log in again.", canRetry = false ) } } catch (e: IOException) { state = CardState.Error( message = "Network error. Check your connection.", canRetry = true ) } catch (e: Exception) { Log.e("CardViewModel", "Unexpected error", e) state = CardState.Error( message = "An error occurred: ${e.message}", canRetry = true ) } } } private suspend fun refreshToken(): String { // Your token refresh logic throw NotImplementedError("Implement token refresh") } } @Composable fun CardScreen(viewModel: CardViewModel) { when (val state = viewModel.state) { is CardViewModel.CardState.Loading -> { Box(Modifier.fillMaxSize(), contentAlignment = Alignment.Center) { CircularProgressIndicator() } } is CardViewModel.CardState.Success -> { CardList(cards = state.cards) } is CardViewModel.CardState.Error -> { ErrorView( message = state.message, canRetry = state.canRetry, onRetry = if (state.canRetry) { { viewModel.loadCards() } } else null ) } } } ``` *** ## Troubleshooting Checklist **SDK Initialization Issues**: * [ ] SDK initialized in Application.onCreate() * [ ] Valid token provided * [ ] Correct environment selected (DEV/TEST/PROD) * [ ] Internet permission in manifest **Authentication Errors**: * [ ] Token is not expired * [ ] Token format is correct * [ ] Token refresh logic implemented * [ ] Backend issuing valid tokens **Network Errors**: * [ ] Device has internet connectivity * [ ] Firewall not blocking requests * [ ] Correct API endpoints configured * [ ] SSL certificates valid **MCD Errors**: * [ ] MCD artifact added to dependencies * [ ] MCD credentials configured in gradle.properties * [ ] Card selected before fetching images * [ ] TOTP secret available and valid * [ ] Correct MCD environment for your build *** ## See Also * [Initialize](initialize-1.md) - Proper SDK initialization * [Card List](card-list-1.md) - Handling errors in card lists * [Card Details](card-details.md) - MCD error handling * [Data Models](data-models.md) - Understanding SDK data structures