10. Authenticate your operations


In this section, you will book a flight 🚀! Booking a flight requires being authenticated to the server so the correct person is sent to space! To do that, and since Apollo Kotlin is using OkHttp to handle HTTP requests, you will use an OkHttp Interceptor to add headers to your GraphQL requests.

Add the interceptor

In Apollo.kt, add the AuthorizationInterceptor class:

Kotlin
app/src/main/kotlin/com/example/rocketreserver/Apollo.kt
1private class AuthorizationInterceptor() : Interceptor {
2  override fun intercept(chain: Interceptor.Chain): Response {
3    val request = chain.request().newBuilder()
4        .apply {
5          TokenRepository.getToken()?.let { token ->
6            addHeader("Authorization", token)
7          }
8        }
9        .build()
10    return chain.proceed(request)
11  }
12}

This interceptor appends an "Authorization: $token" HTTP header to requests if the token is not null.

Use the interceptor

Create a custom OkHttpClient that will use this interceptor and pass it to the ApolloClient:

Kotlin
app/src/main/kotlin/com/example/rocketreserver/Apollo.kt
1val apolloClient = ApolloClient.Builder()
2    .serverUrl("https://apollo-fullstack-tutorial.herokuapp.com/graphql")
3    .okHttpClient( // highlight-line
4        OkHttpClient.Builder() // highlight-line
5            .addInterceptor(AuthorizationInterceptor()) // highlight-line
6            .build() // highlight-line
7    ) // highlight-line
8    .build()

Add the BookTrip and CancelTrip mutations

Next to schema.graphqls add a BookTrip.graphql file:

GraphQL
app/src/main/graphql/BookTrip.graphql
1mutation BookTrip($id:ID!) {
2  bookTrips(launchIds: [$id]) {
3    success
4    message
5  }
6}

Notice how the bookTrips field takes a list as argument but the mutation itself only take a single variable.

Also add the CancelTrip.graphql file. This mutation doesn't use lists:

GraphQL
app/src/main/graphql/CancelTrip.graphql
1mutation CancelTrip($id:ID!) {
2  cancelTrip(launchId: $id) {
3    success
4    message
5  }
6}

Connect the mutations to your UI

Go back to LaunchDetails.kt, and replace the TODOs in onBookButtonClick by executing the appropriate mutation based on whether the launch is booked:

Kotlin
app/src/main/java/com/example/rocketreserver/LaunchDetails.kt
1private suspend fun onBookButtonClick(launchId: String, isBooked: Boolean, navigateToLogin: () -> Unit): Boolean {
2    if (TokenRepository.getToken() == null) {
3        navigateToLogin()
4        return false
5    }
6    val mutation = if (isBooked) {
7        CancelTripMutation(id = launchId)
8    } else {
9        BookTripMutation(id = launchId)
10    }
11    val response = try {
12        apolloClient.mutation(mutation).execute()
13    } catch (e: ApolloException) {
14        Log.w("LaunchDetails", "Failed to book/cancel trip", e)
15        return false
16    }
17
18    if (response.hasErrors()) {
19        Log.w("LaunchDetails", "Failed to book/cancel trip: ${response.errors?.get(0)?.message}")
20        return false
21    }
22    return true
23}

Now back to the LaunchDetails function, declare a coroutine scope to be able to call the suspend onBookButtonClick. Also, let's remember isBooked and change the button's text accordingly:

Kotlin
app/src/main/java/com/example/rocketreserver/LaunchDetails.kt
1// Book button
2val scope = rememberCoroutineScope() // highlight-line
3var isBooked by remember { mutableStateOf(data.launch?.isBooked == true) } // highlight-line
4Button(
5    modifier = Modifier
6        .padding(top = 32.dp)
7        .fillMaxWidth(),
8    onClick = {
9        scope.launch {
10            val ok = onBookButtonClick( // highlight-line
11                launchId = data.launch?.id ?: "", // highlight-line
12                isBooked = isBooked, // highlight-line
13                navigateToLogin = navigateToLogin // highlight-line
14            ) // highlight-line
15            if (ok) { // highlight-line
16                isBooked = !isBooked // highlight-line
17            } // highlight-line
18        }
19    }
20) {
21    Text(text = if (!isBooked) "Book now" else "Cancel booking") // highlight-line
22}

Let's also add a loading indicator and prevent the button from being clicked while the mutation is running:

Kotlin
app/src/main/java/com/example/rocketreserver/LaunchDetails.kt
1// Book button
2var loading by remember { mutableStateOf(false) } // highlight-line
3val scope = rememberCoroutineScope()
4var isBooked by remember { mutableStateOf(data.launch?.isBooked == true) }
5Button(
6    modifier = Modifier
7        .padding(top = 32.dp)
8        .fillMaxWidth(),
9    enabled = !loading, // highlight-line
10    onClick = {
11        loading = true // highlight-line
12        scope.launch {
13            val ok = onBookButtonClick(
14                launchId = data.launch?.id ?: "",
15                isBooked = isBooked,
16                navigateToLogin = navigateToLogin
17            )
18            if (ok) {
19                isBooked = !isBooked
20            }
21            loading = false // highlight-line
22        }
23    }
24) {
25    if (loading) { // highlight-line
26        SmallLoading() // highlight-line
27    } else { // highlight-line
28        Text(text = if (!isBooked) "Book now" else "Cancel booking")
29    } // highlight-line
30}

Book your trip!

Compile and run your app. You can now book and cancel your trips! The button will change based on whether a trip has been booked or not.

In the next section, you will write your first subscription and be notified in real time when someone books a flight.

Edit on GitHub

Forums