This post is part four of a series adapted from Practical KMP, my book on building production Kotlin Multiplatform apps for Android and iOS. Part two built the authentication remote store and modelled its response.
Implementing the Sign-up Endpoint
Now that we’ve created the models for receiving back an authentication response, it’s time for us to implement the API request for this sign-up flow. In this part, we’ll focus on building this API call inside of an interfaced authentication repository, providing clients with the ability to trigger the sign-up flow through their application and support user registration.
When it comes to modelling this implementation, we’re going to provide access to this through an interface. This separation allows us to define a clear contract with clients for the authentication functions that we support, as well as make it easier to test any places where these interfaces are utilised by swapping out these interfaces for mocks or fake implementations.
Next we’ll go ahead and declare the sign-up contract for our FirebaseAuthenticationRepository interface. This will be called by the client and implemented by our authentication service. We’ll create a new suspending function, signUp, adding 3 arguments that will be used to execute the sign-up operation.
- apiKey — the API key used to be able to communicate with Firebase
- email — the user’s email address that they have provided for sign-up
- password — the user’s password that they have provided for sign-up
All of these parameters are required as part of the sign-up flow and have been marked as non-null in our contract. When requesting to perform the sign-up operation, all of these fields will need to be provided from the UI layer implementation.
interface FirebaseAuthenticationRepository {
suspend fun signUp(
apiKey: String,
email: String,
password: String
): AuthenticationResult
}
Now that we have this interface in place, we’re going to go ahead and have our AuthenticationRemote class implement it. This class will serve as the implementation of our authentication contract, handling the actual communication with the Firebase authentication service.
class AuthenticationRemote(
client: HttpClientEngine = HttpClient().engine
) : FirebaseAuthenticationRepository {
...
}
Doing so allows us to enforce the contract of our FirebaseAuthenticationRepository, which means we will need to implement the signUp function that we defined in our contract.
class AuthenticationRemote(
client: HttpClientEngine = HttpClient().engine
) : FirebaseAuthenticationRepository {
override suspend fun signUp(
apiKey: String,
email: String,
password: String
): AuthenticationResult {
}
}
With this in place we can now go ahead and start to build out the implementation of our signUp function. The code that we run to communicate with the Firebase API may throw an exception, so to handle this, we’re going to start by defining a try/catch block that’ll house our implementation. When an exception occurs here, we’ll simply return an empty instance of the AuthenticationResult, ensuring that our function always returns a valid response even when things go wrong.
override suspend fun signUp(
apiKey: String,
email: String,
password: String
): AuthenticationResult {
return try {
} catch (e: Exception) {
AuthenticationResult()
}
}
This ensures that instead of our networking library crashing the client application, the error is caught and returned to the calling point using the same modelling that it’ll expect in the happy path.
Next, we’re going to trigger the API request to the signUp endpoint using the client instance that we have inside of our remote class. For this request we’re going to be making a POST request to the Firebase API, so we’ll use the post function on this client reference.
When calling this function we’ll need to pass the URL for the endpoint that we want to call, so we’ll use the base constant that we previously defined in this class, followed by appending :signUp to point to the URL path for performing the sign-up operation. This will give us a complete URL of https://identitytoolkit.googleapis.com/v1/accounts:signUp.
From this request, we’ll use the body function to return the result of the operation.
override suspend fun signUp(
apiKey: String,
email: String,
password: String
): AuthenticationResult {
return try {
client.post("$authenticationBase:signUp") {
}.body()
} catch (e: Exception) {
AuthenticationResult()
}
}
When making this post request, we’ll need to provide a few pieces of information. We’ll start here by defining the content-type of the request using the contentType function and providing the FormUrlEncoded value, which allows for the data we are sending in the request to be correctly encoded.
override suspend fun signUp(
apiKey: String,
email: String,
password: String
): AuthenticationResult {
return try {
client.post("$authenticationBase:signUp") {
contentType(FormUrlEncoded)
}.body()
} catch (e: Exception) {
AuthenticationResult()
}
}
Next, we’ll need to add the API key to our request, which is done in the form of a query parameter. While we could have appended this to our URL, adding it separately makes this part of our code more readable and keeps a clear separation between the URL and additional properties. Here we’ll use the url function and its builder to append a new parameter to the request. For this, the Firebase API requires this to be sent using the name key, coupled with the API key for our application.
override suspend fun signUp(
apiKey: String,
email: String,
password: String
): AuthenticationResult {
return try {
client.post("$authenticationBase:signUp") {
contentType(FormUrlEncoded)
url { parameters.append("key", apiKey) }
}.body()
} catch (e: Exception) {
AuthenticationResult()
}
}
Finally, we need to pass the email and password to be used for account creation. For this we’ll need to use the FormDataContent class, followed by the Parameters class to build the required input data structure for our request.
Using the email and password keys, we’ll set each of these properties using the values that are provided to our function as arguments. The Firebase API expects these specific field names, so it’s important that we use the exact strings "email" and "password" when appending our form data. Here we’ll also pass a returnSecureToken argument so that the authentication token is returned to us within the response of the request.
override suspend fun signUp(
apiKey: String,
email: String,
password: String
): AuthenticationResult {
return try {
client.post("$authenticationBase:signUp") {
contentType(FormUrlEncoded)
url { parameters.append("key", apiKey) }
setBody(
FormDataContent(
Parameters.build {
append("email", email)
append("password", password)
append("returnSecureToken", "true")
}
)
)
}.body()
} catch (e: Exception) {
AuthenticationResult()
}
}
With this in place, we now have a signUp function implemented that will allow a client to trigger a request to create an account using a provided email and password. The result of this API request will be returned in the form of the AuthenticationResult, either containing the successful authentication data from the API response or an empty result in the case of an exception that may be caught within our function.
This post is adapted from Practical KMP, my book on building real Kotlin Multiplatform apps for Android and iOS. Grab a copy for the sign-in, token refresh and account management endpoints too.