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.

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.

// 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:

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:

import androidx.compose.runtime.*
import kotlinx.coroutines.delay

@Composable
fun CardListWithRetry(sdk: DigitalCardEngineSDK) {
    var retryCount by remember { mutableStateOf(0) }
    var errorMessage by remember { mutableStateOf<String?>(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:

CodeDescriptionSolution
1001Invalid card IDVerify giftCardId is correct
1002Invalid secretCheck TOTP secret generation
1003Card not foundEnsure card exists and user has access
2001Network errorCheck connectivity, retry
3001Invalid responseContact support

Solution:

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:

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.

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.

sealed class Result<out T> {
    data class Success<T>(val data: T) : Result<T>()
    data class Error(val exception: Exception) : Result<Nothing>()
}

suspend fun fetchCardsResult(): Result<List<CardEntity>> {
    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.

sealed class UiState<out T> {
    object Loading : UiState<Nothing>()
    data class Success<T>(val data: T) : UiState<T>()
    data class Error(val message: String) : UiState<Nothing>()
}

class CardListViewModel(private val sdk: DigitalCardEngineSDK) : ViewModel() {
    
    var uiState by mutableStateOf<UiState<List<CardEntity>>>(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

import kotlinx.coroutines.delay
import kotlin.math.pow

suspend fun <T> 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

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 <T> 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

import android.util.Log

object SdkLogger {
    private const val TAG = "DigitalCardEngine"
    
    fun logApiCall(endpoint: String, params: Map<String, Any>? = 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

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

import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import kotlinx.coroutines.launch

class CardViewModel(private val sdk: DigitalCardEngineSDK) : ViewModel() {
    
    var state by mutableStateOf<CardState>(CardState.Loading)
        private set
    
    sealed class CardState {
        object Loading : CardState()
        data class Success(val cards: List<CardEntity>) : 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