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:
private class AuthorizationInterceptor() : Interceptor {override fun intercept(chain: Interceptor.Chain): Response {val request = chain.request().newBuilder().apply {TokenRepository.getToken()?.let { token ->addHeader("Authorization", token)}}.build()return chain.proceed(request)}}
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
:
val apolloClient = ApolloClient.Builder().serverUrl("https://apollo-fullstack-tutorial.herokuapp.com/graphql").okHttpClient(OkHttpClient.Builder().addInterceptor(AuthorizationInterceptor()).build()).build()
Add the BookTrip and CancelTrip mutations
Next to schema.graphqls
add a BookTrip.graphql
file:
mutation BookTrip($id:ID!) {bookTrips(launchIds: [$id]) {successmessage}}
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:
mutation CancelTrip($id:ID!) {cancelTrip(launchId: $id) {successmessage}}
Connect the mutations to your UI
Go back to LaunchDetails.kt
, and replace the TODO
s in onBookButtonClick
by executing the appropriate mutation based on whether the launch is booked:
private suspend fun onBookButtonClick(launchId: String, isBooked: Boolean, navigateToLogin: () -> Unit): Boolean {if (TokenRepository.getToken() == null) {navigateToLogin()return false}val mutation = if (isBooked) {CancelTripMutation(id = launchId)} else {BookTripMutation(id = launchId)}val response = apolloClient.mutation(mutation).execute()return when {response.exception != null -> {Log.w("LaunchDetails", "Failed to book/cancel trip", response.exception)false}response.hasErrors() -> {Log.w("LaunchDetails", "Failed to book/cancel trip: ${response.errors?.get(0)?.message}")false}else -> true}}
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:
// Book buttonval scope = rememberCoroutineScope()var isBooked by remember { mutableStateOf(data.launch?.isBooked == true) }Button(modifier = Modifier.padding(top = 32.dp).fillMaxWidth(),onClick = {scope.launch {val ok = onBookButtonClick(launchId = data.launch?.id ?: "",isBooked = isBooked,navigateToLogin = navigateToLogin)if (ok) {isBooked = !isBooked}}}) {Text(text = if (!isBooked) "Book now" else "Cancel booking")}
Let's also add a loading indicator and prevent the button from being clicked while the mutation is running:
// Book buttonvar loading by remember { mutableStateOf(false) }val scope = rememberCoroutineScope()var isBooked by remember { mutableStateOf(data.launch?.isBooked == true) }Button(modifier = Modifier.padding(top = 32.dp).fillMaxWidth(),enabled = !loading,onClick = {loading = truescope.launch {val ok = onBookButtonClick(launchId = data.launch?.id ?: "",isBooked = isBooked,navigateToLogin = navigateToLogin)if (ok) {isBooked = !isBooked}loading = false}}) {if (loading) {SmallLoading()} else {Text(text = if (!isBooked) "Book now" else "Cancel booking")}}
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.