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