Turbo Native - Native Authentication Part 3 - Android Client
Tags: android, turbo, ios, rails
Published: 2023-08-21 10:33:11

---
title: Turbo Native - Native Authentication Part 3 - Android Client
published: true
date: 2023-07-17 00:00:00 UTC
tags: android, turbo, ios, rails
canonical_url: https://williamkennedy.ninja/turbo/2023/07/17/turbo-native-auth-android/
---

In this series’s previous two blog posts, we covered setting up a Rails app and an iOS app. In this article, we’re going to do the same for Android. The code can be seen [here](https://github.com/williamkennedy/turbo_auth_android/tree/main).

The approach for the Turbo Android app is the same as the Turbo iOS app.  
When a user is unauthenticated, we return a 401 status code. We intercept that error and display a native sign-in flow in our iOS app.

## Initial Setup

First, we can set up our app as per this [blog post](https://williamkennedy.ninja/android/2023/05/10/up-and-running-with-turbo-android-part-1/). However, instead of setting up No Activity, set up an Empty Activity, which will install all the Jetpack compose libraries.

We then add our Turbo library and the okhttp3 library in the Gradle file.

```
// hotwire
implementation "dev.hotwire:turbo:7.0.0-rc18"

// okhttp3
implementation("com.squareup.okhttp3:okhttp:4.10.0")

```

After that, press the sync button to install all our dependencies.

## Create a SignInFragment

With everything set up, we next need to trigger a native flow every time a user visits /sign\_in or a page that returns a 401 unauthorised status.

The first thing we will do is create our `SignInFragment`.

```
private const val AUTH_TOKEN_KEY = "auth_token"
private const val SHARED_PREFS_NAME = "turbo_native_auth"

@TurboNavGraphDestination(uri = "turbo://fragment/sign_in")
class SignInFragment : TurboFragment(), TurboNavDestination {

  override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View {
    return ComposeView(requireContext()).apply {
      setContent {
        AppTheme {
          SignInForm()
        }
      }
    }
  }

  @OptIn(ExperimentalMaterial3Api::class)
    @Composable
    fun SignInForm() {
      var email by remember { mutableStateOf("") }
      var password by remember { mutableStateOf("") }

      val context = LocalContext.current

        Column(
            modifier = Modifier
            .padding(16.dp)
            .fillMaxWidth(),
            horizontalAlignment = Alignment.CenterHorizontally,
            verticalArrangement = Arrangement.Center
            ) {
          TextField(
              value = email,
              onValueChange = { newText ->
              email = newText.trimEnd()
              },
              label = { Text("Email") },
              modifier = Modifier.fillMaxWidth()
              )
            Spacer(modifier = Modifier.height(8.dp))
            TextField(
                value = password,
                onValueChange = { password = it },
                label = { Text("Password") },
                visualTransformation = PasswordVisualTransformation(),
                modifier = Modifier.fillMaxWidth()
                )
            Spacer(modifier = Modifier.height(16.dp))
            Button(
                onClick = { performSignIn(context, email, password) },
                modifier = Modifier.align(Alignment.End)
                ) {
              Text("Sign In")
            }
        }
    }
}

```

We then add this to our list of registered fragments in our `MainSessionNavHost`.

```
val BASE_URL = "http://10.0.2.2:3005"
val SIGN_IN_URL = "${BASE_URL}/sign_in"
val PATH_CONFIGURATION_URL = "${BASE_URL}/turbo/android/path_configuration"
val API_SIGN_IN_URL = "${BASE_URL}/api/v1/sessions"

class MainSessionNavHost : TurboSessionNavHostFragment() {
  override var sessionName = "main"
    override var startLocation = BASE_URL

    override val registeredFragments: List<KClass<out Fragment>>
    get() = listOf(
        WebFragment::class,
        WebModalFragment::class,
        SignInFragment::class
        )

    override val registeredActivities: List<KClass<out AppCompatActivity>>
    get() = listOf()

    override val pathConfigurationLocation: TurboPathConfiguration.Location
    get() = TurboPathConfiguration.Location(
        assetFilePath = "json/path_configuration.json",
        remoteFileUrl = PATH_CONFIGURATION_URL
        )
}

```

## Update Path Configuration

Now, we update our `path_configuration` file so our native fragment gets displayed. Just like turbo-ios, turbo-android relies heavily on path configuration. We can get a file from the server and save one locally. For this project, our path configuration file looks like the following:

```
{
  "settings": {
    "screenshot_enabled": true
  },
    "rules": [
    {
      "patterns": [
        ".*"
      ],
      "properties": {
        "context": "default",
        "uri": "turbo://fragment/web",
        "pull_refresh_enabled": true
      }
    },
    {
      "patterns": [
        "/new$",
      "/edit$"
      ],
      "properties": {
        "context": "modal",
        "uri": "turbo://fragment/web/modal/sheet",
        "pull_to_refresh_enabled": false
      }
    },
    {
      "patterns": [
        "/sign_in$"
      ],
      "properties": {
        "context": "default",
        "uri": "turbo://fragment/sign_in",
        "pull_refresh_enabled": false
      }
    }
  ]
}

```

Just like that, every time we navigate to `sign_in`, we display a native sign-in form. However, it doesn’t do much yet. We still need to be able to send requests and save the cookies and auth token.

## Native Sign in

For our iOS application, we saved our authentication token to the keychain to be held securely and retrieved in other parts of the application via a getter method.

For the Android application, we will use the [Shared Preferences](https://developer.android.com/training/data-storage/shared-preferences) api.

```
private fun getAuthToken(context: Context): String? {
  val sharedPreferences = context.getSharedPreferences(SHARED_PREFS_NAME, Context.MODE_PRIVATE)
    return sharedPreferences.getString(AUTH_TOKEN_KEY, null)
}

```

This method retrieves the AuthToken if we need to make an API request.

We also need to be able to write the token to our shared preferences.

```
private fun saveAuthToken(context: Context, authToken: String?) {
  val sharedPreferences = context.getSharedPreferences(SHARED_PREFS_NAME, Context.MODE_PRIVATE)
    sharedPreferences.edit().putString(AUTH_TOKEN_KEY, authToken).apply()
}

```

Finally, we have to save our cookies.

```
private fun saveCookies(cookies: List<String>) {
  val cookieManager = CookieManager.getInstance()

    for (cookie in cookies) {
      parse(API_SIGN_IN_URL.toHttpUrlOrNull()!!, cookie)?.let {
        cookieManager.setCookie(API_SIGN_IN_URL, it.toString())
      }
    }
}

```

Now we can add all call all these methods from our `performSignIn` method in the `SignInFragment`:

```
private fun performSignIn(context: Context, email: String, password: String) {
  val client = OkHttpClient()

    val requestBody = FormBody.Builder()
    .add("email", email)
    .add("password", password)
    .build()

    val request = Request.Builder()
    .url(API_SIGN_IN_URL)
    .post(requestBody)
    .build()

    client.newCall(request).enqueue(object : Callback {
        override fun onFailure(call: Call, e: IOException) {
        // Handle network failure or API error
        }

        override fun onResponse(call: Call, response: Response) {
        if (response.isSuccessful) {
        val authToken = response.header("X-Session-Token")
        val cookies = response.headers("Set-Cookie")

        saveAuthToken(context, authToken)
        saveCookies(cookies)

        // execute on the main thread
        requireActivity().runOnUiThread {
        navigateUp()
        sessionNavHostFragment.reset()
        }

        } else {
        // Raise error
        }
        }
    })
}

```

Since this is an async request, you must ask Turbo Android to navigate back to the previous page.

```
// execute on the main thread
requireActivity().runOnUiThread {
  navigateUp()
    sessionNavHostFragment.reset()
}

```

## Handling a 401 error.

So now we should be able to spin up our Android app and sign in and out.

The next thing we need to do is handle the 401 unauthorised errors.

In our `WebFragment`, we can override the onVisitErrorReceived method and navigate to our sign-in fragment.

```
override fun onVisitErrorReceived(location: String, errorCode: Int) {
  when (errorCode) {
    401 -> navigate(SIGN_IN_URL, TurboVisitOptions(action = REPLACE))
      else -> super.onVisitErrorReceived(location, errorCode)
  }
}

```

## Unauthorised modal

In our example app, when we navigate to `/new`, a WebModalFragment is displayed. I could not figure out how to dismiss it and then navigate to the `SignInFragment`, so I handled the case where there was a 401 error.

By overriding the `createErrorView`, we can inject our own.

```
@TurboNavGraphDestination(uri = "turbo://fragment/web/modal/sheet")
class WebModalFragment: TurboWebBottomSheetDialogFragment(), TurboNavDestination {

  @SuppressLint("InflateParams")
    override fun createErrorView(statusCode: Int): View {
      when (statusCode) {
        401 -> return layoutInflater.inflate(R.layout.turbo_auth_error, null)
          else -> return super.createErrorView(statusCode)
      }
    }
}

```

The customer error layout informs the user that they are not logged in, so they can dismiss and log in. Not a perfect flow, but it works for now.

Turbo Android is an excellent library for quickly building hybrid apps. As you can see, utilising native for specific scenarios is straightforward. It’s simply a matter of creating and adding the fragment to our list of registered fragments.