Last Week

Login page is beautified. Basic input validation is set up. When any error is detected, the login screen is reset and a snackbar message is displayed.

Example of real-time input validation 1 Example of real-time input validation 2

What does it mean in English?

George Carlin once uttered the wisest of words: “Imagine an average stupid person and then realize that half of the population is stupider than that.” Even if your users are all saints and none seeks to destroy your app with malice, you still need to carefully handle the fruits of their stupidity.

We perform input validation to ensure that a user is entering what we expect in the text field. If we don’t do it, at the very least the app would crash when we ask a function to digest a malformed argument. Combined with input sanitation, we remove the possibility for the user to perform an injection attack where our app is asked to do something it’s not supposed to.

Tools used:

Nerdy Details

How did I validate my user input?

On the login screen I capture two pieces of information: email, and one-time passcode. Both pieces of information are sent to Supabase’s authentication service, which is hardened against attacks. While it is sound practice to not trust the backend and perform input sanitation regardless, here I will only focus on input validation.

We first create a simple function using regex for pattern matching.

fun isValidEmail(email: String): Boolean {
    val pattern = Regex("[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\\.[a-zA-Z]{2,}")
    return pattern.matches(email)
}

We throw an error if this pattern matching function fails.

fun validateEmail(email: String) {
    if (!isValidEmail(email)) {
        throw IllegalArgumentException("Invalid email format.")
    }
}

In the repository implementation, right before I make the Supabase API call, I validate the email.

override suspend fun signInUser(email: String): Result<Unit> = makeApiCall(dispatcher) {
    validateEmail(email)
    // repo sign-in code
}

As a reminder from last week, the makeApiCall() is a helper function that simplifies coroutine launch and error handling.

suspend fun <T> makeApiCall(
    dispatcher: CoroutineDispatcher,
    call: suspend () -> T
): Result<T> = runCatching {
    withContext(dispatcher) {
        call.invoke()
    }
}

At the call site in ViewModel, we ask an error handling function to take care of the validation errors thrown.

private fun signInUser(email: String) {
    viewModelScope.launch {
        UserRepository.signInUser(email = email)
            .onSuccess {
                // do something
            }
            .onFailure { throwable ->
                // do something
                handleSignInError(throwable)
            }
    }
}

Recall that we threw an IllegalArgumentException.

private fun handleSignInError(throwable: Throwable) {
    when (throwable) {
        is IllegalArgumentException -> {
            // handle input validation errors
        }
        else -> {
            // handle other errors
        }
    }
}

The actual handling logic for when input validation fails is a part of the larger Model-View-Intent (MVI) design pattern, which I will discuss in a future update.

Next Week

  • Investigate Supabase Postgres Database.
  • Create first schema and tables and populate by making a call from app.
  • Set up the initial stages for the data layer for Postgres database.